Note: I PROMISE this is a better idea than my last 20 bad ones(raise_if, shlex extra argument, etc.) I'll use an example to illustrate the idea first. Let's use a completely non-realistic and contrived example. Say you have a lexer that's really slow. Now, this lexer might be a function that uses generators, i.e.: def mylexer(input): while input: ... if xyz: yield SomeToken() Now, if we have a parser that uses that lexer, it always has to wait for the lexer to yield the next token *after* it already parsed the current token. That can be somewhat time consuming. Caching iterators are based on the idea: what if the iterator is running at the same time as the function getting the iterator elements? Or, better yet, it's an iterator wrapper that takes an iterator and continues to take its elements while the function that uses the iterator is running? This is easily accomplished using multiprocessing and pipes. Since that was somewhat vague, here's an example: def my_iterator(): for i in range(0,5): time.sleep(0.2) yield i for item in my_iterator(): time.sleep(0.5) print(item) Now with a normal iterator, the flow is like this: - Wait for my_iterator to return an element(0.2s) - Wait 0.5s and print the element(0.5s) In total, that takes 0.7s per element. What a waste! What if the iterator was yielding elements at the same time as the for loop was using them? Well, for every for loop iteration, the iterator could generate ~2.2 elements. That's what a caching iterator does. It runs both at the same time using multiprocessing. It's thread safe as long as the iterator doesn't depend on whatever is using it. An example: def my_iterator(): for i in range(0,5): time.sleep(0.2) yield i for item in itertools.CachingIterator(my_iterator()): # this is the only change time.sleep(0.5) print(item) Now the flow is like this: - Wait for my_iterator to return the very first element. - While that first element is looped over, continue recieving elements from my_iterator(), storing them in an intermediate space(similar to a deque). - When the loop is completed, take the next element from the intermediate space and loop over it - While that element is looped over, continue recieving elements... ...and so forth. That way, time isn't wasted waited for the loop to finish. I have a working implementation. Although there is a very slight overhead, in the above example, about 0.4s is still saved. There could also be an lmap function, which just does this: def lmap(f,it): yield from map(f,CachingIterator(it)) Thoughts? -- Ryan If anybody ever asks me why I prefer C++ to C, my answer will be simple: "It's becauseslejfp23(@#Q*(E*EIdc-SEGFAULT. Wait, I don't think that was nul-terminated."
I believe the new asyncio package in 3.4 solves this problem (and more!). Brendan ________________________________ From: Python-ideas [python-ideas-bounces+moloney=ohsu.edu@python.org] on behalf of Ryan Gonzalez [rymg19@gmail.com] Sent: Tuesday, February 25, 2014 3:08 PM To: python-ideas Subject: [Python-ideas] Caching iterators Note: I PROMISE this is a better idea than my last 20 bad ones(raise_if, shlex extra argument, etc.) I’ll use an example to illustrate the idea first. Let’s use a completely non-realistic and contrived example. Say you have a lexer that’s really slow. Now, this lexer might be a function that uses generators, i.e.: def mylexer(input): while input: ... if xyz: yield SomeToken() Now, if we have a parser that uses that lexer, it always has to wait for the lexer to yield the next token after it already parsed the current token. That can be somewhat time consuming. Caching iterators are based on the idea: what if the iterator is running at the same time as the function getting the iterator elements? Or, better yet, it’s an iterator wrapper that takes an iterator and continues to take its elements while the function that uses the iterator is running? This is easily accomplished using multiprocessing and pipes. Since that was somewhat vague, here’s an example: def my_iterator(): for i in range(0,5): time.sleep(0.2) yield i for item in my_iterator(): time.sleep(0.5) print(item) Now with a normal iterator, the flow is like this: * Wait for my_iterator to return an element(0.2s) * Wait 0.5s and print the element(0.5s) In total, that takes 0.7s per element. What a waste! What if the iterator was yielding elements at the same time as the for loop was using them? Well, for every for loop iteration, the iterator could generate ~2.2 elements. That’s what a caching iterator does. It runs both at the same time using multiprocessing. It’s thread safe as long as the iterator doesn’t depend on whatever is using it. An example: def my_iterator(): for i in range(0,5): time.sleep(0.2) yield i for item in itertools.CachingIterator(my_iterator()): # this is the only change time.sleep(0.5) print(item) Now the flow is like this: * Wait for my_iterator to return the very first element. * While that first element is looped over, continue recieving elements from my_iterator(), storing them in an intermediate space(similar to a deque). * When the loop is completed, take the next element from the intermediate space and loop over it * While that element is looped over, continue recieving elements… …and so forth. That way, time isn’t wasted waited for the loop to finish. I have a working implementation. Although there is a very slight overhead, in the above example, about 0.4s is still saved. There could also be an lmap function, which just does this: def lmap(f,it): yield from map(f,CachingIterator(it)) Thoughts? -- Ryan If anybody ever asks me why I prefer C++ to C, my answer will be simple: "It's becauseslejfp23(@#Q*(E*EIdc-SEGFAULT. Wait, I don't think that was nul-terminated."
On Tue, Feb 25, 2014 at 05:08:44PM -0600, Ryan Gonzalez wrote:
Note:
I PROMISE this is a better idea than my last 20 bad ones(raise_if, shlex extra argument, etc.)
Don't make promises you can't keep. (Sorry, that was a cheap shot, but you practically begged for somebody to make it :-)
I'll use an example to illustrate the idea first. Let's use a completely non-realistic and contrived example. Say you have a lexer that's really slow. Now, this lexer might be a function that uses generators, i.e.: [...] Now, if we have a parser that uses that lexer, it always has to wait for the lexer to yield the next token *after* it already parsed the current token. That can be somewhat time consuming.
This is the nature of serial processing. Normally responsibility for moving to parallel processing (whether threads or processes or something else) is handled by the user, not the standard library. Very little in the std lib is calculated in parallel.
Caching iterators are based on the idea: what if the iterator is running at the same time as the function getting the iterator elements? Or, better yet, it's an iterator wrapper that takes an iterator and continues to take its elements while the function that uses the iterator is running? This is easily accomplished using multiprocessing and pipes.
Since that was somewhat vague, here's an example:
def my_iterator(): for i in range(0,5): time.sleep(0.2) yield i for item in my_iterator(): time.sleep(0.5) print(item)
So you have a delay in calculating each value in the iterator, and a longer delay using each value. If you perform those sequentially, it takes 0.7 seconds per item; if you could perform them perfectly in parallel, only 0.5 seconds.
Now with a normal iterator, the flow is like this:
- Wait for my_iterator to return an element(0.2s) - Wait 0.5s and print the element(0.5s)
In total, that takes 0.7s per element. What a waste! What if the iterator was yielding elements at the same time as the for loop was using them? Well, for every for loop iteration, the iterator could generate ~2.2 elements. That's what a caching iterator does. It runs both at the same time using multiprocessing.
You could use threads if the processes are IO bound rather than CPU bound. Since threads are less expensive than processing, surely that choice should have to be up to the user? In either case, one disadvantage of a cache is that values are no longer being calculated lazily (i.e. on request). Because the iterator runs faster than the consumer, rather than working in lock-step you end up generating more values than you need: each iteration sees the iterator generate 2.2 elements and the consumer use 1. By the time the consumer has used ten thousand values, the iterator has generated twenty-two thousand values, and twelve thousand remain in the cache. While tempting, the caller needs to be aware that such a caching system: (1) uses potentially unbounded amounts of memory; (2) is potentially harmful if calculating the values has side-effects; (3) it can lead to "lost" data if the caller access the underlying iterator without going through the cache; and (4) it is wasteful if the consumer stops early and never uses all the values. (CPU cycles are cheap, but they aren't free.) None of these invalidate the basic idea, but they do limit the applicability of it. [...]
Now the flow is like this:
- Wait for my_iterator to return the very first element. - While that first element is looped over, continue recieving elements from my_iterator(), storing them in an intermediate space(similar to a deque).
The data structure you want is a Queue. The std lib has a thread-safe Queue implementation, in the queue module.
I have a working implementation. Although there is a very slight overhead, in the above example, about 0.4s is still saved.
I think you should publish this implementation as a recipe on the ActiveState website, and see what feedback you get there. Once it is proven to be useful in practice, rather than just theoretically useful, then it could be considered for the std lib. http://code.activestate.com/recipes/ -- Steven
On Wed, Feb 26, 2014 at 11:01 AM, Steven D'Aprano <steve@pearwood.info> wrote:
While tempting, the caller needs to be aware that such a caching system:
(1) uses potentially unbounded amounts of memory;
Easy fix: Limit the size of the queue. Just like with pipes between processes, the producer will block trying to push more data into the queue until the consumer's taken some out. Of course, then you have to figure out what's the right queue size. In many cases the safest and simplest might well be zero, aka current behaviour.
(2) is potentially harmful if calculating the values has side-effects;
(3) it can lead to "lost" data if the caller access the underlying iterator without going through the cache; and
Deal with these two by making it something you have to explicitly request. In that way, it's no different from itertools.tee() - once you tee an iterator, you do not touch the underlying one at all.
(4) it is wasteful if the consumer stops early and never uses all the values. (CPU cycles are cheap, but they aren't free.)
Also partly solved by the queue size limit (don't let it run free forever). That said, though, I don't actually know of any place where I would want this facility where I wouldn't already be working with, say, a socket connection, or a queue, or something else that buffers. ChrisA
On 2/25/2014 7:25 PM, Chris Angelico wrote:
On Wed, Feb 26, 2014 at 11:01 AM, Steven D'Aprano <steve@pearwood.info> wrote:
While tempting, the caller needs to be aware that such a caching system:
(1) uses potentially unbounded amounts of memory;
Easy fix: Limit the size of the queue.
multiprocessing.Queue is a process shared near clone of queue.Queue and has one optional arg -- maxsize. It is implemented with a pipe, locks, and semaphores.
Just like with pipes between processes, the producer will block trying to push more data into the queue until the consumer's taken some out.
This is the default behavior of Queue.put.
Of course, then you have to figure out what's the right queue size.
42 ;-)
In many cases the safest and simplest might well be zero, aka current behaviour.
(2) is potentially harmful if calculating the values has side-effects;
(3) it can lead to "lost" data if the caller access the underlying iterator without going through the cache; and
If the iterator is in another process only connected by a pipe, it cannot be accessed otherwise than through the Queue.
Deal with these two by making it something you have to explicitly request. In that way, it's no different from itertools.tee() - once you tee an iterator, you do not touch the underlying one at all.
(4) it is wasteful if the consumer stops early and never uses all the values. (CPU cycles are cheap, but they aren't free.)
Also partly solved by the queue size limit (don't let it run free forever).
-- Terry Jan Reedy
I was reading a blog post here: http://stupidpythonideas.blogspot.co.nz/2014/02/fixing-lambda.html where the author points out that there are a number of different problems that the various enhanced-lambda proposals are trying to solve, which are best addressed by different solutions. Here's a suggestion for one of them. Suppose we could write things like: sorted(things, key(x) = x.date) Button("Do it!", on_click() = fire_the_ducks()) It only addresses the case of passing a function using a keyword argument, but I think it would make for very readable code in those cases. And it doesn't use any colons! -- Greg
On Wed, Feb 26, 2014 at 11:17 AM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Here's a suggestion for one of them. Suppose we could write things like:
sorted(things, key(x) = x.date)
Button("Do it!", on_click() = fire_the_ducks())
It only addresses the case of passing a function using a keyword argument, but I think it would make for very readable code in those cases. And it doesn't use any colons!
Gut feeling: Do not like something that puts magic into one specific place. Apart from the * and ** unpacking tools, there's nothing special about a function's arguments that lets you use special syntax for creating a function, or anything else. One of Python's strengths is that a given expression results in a given value, and that's true regardless of where that value's going. Having these two do very different things is confusing: Button("Do it!", on_click() = fire_the_ducks()) Button("Do it!", fire_the_ducks()) One of them is a keyword argument (spaced in violation of PEP8, incidentally; not sure if that adds to the confusion by making it look like assignment when it isn't), and the other is a positional argument. Everywhere else in Python, that would be the whole difference. Now, apparently, one of them calls a function now and passes its return value, and the other creates a lambda that'll call the function later. Oh, and also: Why create a lambda that just calls one named function? Are you expecting the name to be rebound? Otherwise, just pass fire_the_ducks as the on-click callback. ChrisA
From: Greg Ewing <greg.ewing@canterbury.ac.nz> Sent: Tuesday, February 25, 2014 4:17 PM
I was reading a blog post here:
http://stupidpythonideas.blogspot.co.nz/2014/02/fixing-lambda.html
where the author points out that there are a number of different problems that the various enhanced-lambda proposals are trying to solve, which are best addressed by different solutions.
Here's a suggestion for one of them. Suppose we could write things like:
sorted(things, key(x) = x.date)
Button("Do it!", on_click() = fire_the_ducks())
Of course that particular case would be better written as: Button("Do it!", on_click = fire_the_ducks) But it becomes more useful if you do anything else: Button("Do it!", on_click() = fire_the_ducks(42)) At first glance, I think this is nice, but there's a nagging feeling that it may be a bit magical. Maybe if I think through what the compiler will do with it, I can resolve that feeling. (Obviously real users aren't going to care how it gets parsed and compiled, but if it's simple and clear enough, that implies that it can also be simple and clear to a human reader. Not perfectly, but… anyway, let's try it.) I'll do that at the end.
It only addresses the case of passing a function using
a keyword argument
It also doesn't look quite as nice when the function comes first, but I think it's still pretty nice: itertools.takewhile(predicate(x)=x<5, iterable=spam) And functions that take a function and then *args, like map, would be tricky, but then it's fine if this doesn't work nicely in every possible case for passing a function around. Also, this _could_ be extended to work in all cases where call expressions raise a SyntaxError, although I don't know that it _should_ be. For example, people who for whatever reason prefer to write "f = lambda x: …" instead of "def f(x): return …" today would probably love being able to write "f(x) = …", but I don't really want to encourage those people…
but I think it would make for very readable code in those cases. And it doesn't use any colons!
I don't understand the problem with the colons, but never mind that; I agree that it's very readable. And the real benefit to me is that it doesn't require any new and potentially weird syntax, like Nick's magic ? parameter, for the one-argument case. On the other hand, could this add any confusion? Today, we have: Button("Do it!", on_click=fire_the_ducks) # good Button("Do it!", on_click=fire_the_ducks()) # bad, passes call result as function Button("Do it!", on_click=lambda: fire_the_ducks()) # good Button("Do it!", on_click=lambda: fire_the_ducks) # bad, passes function returning function We'd be adding: Button("Do it!", on_click()=fire_the_ducks()) # good Button("Do it!", on_click()=fire_the_ducks) # bad, passes function returning function Would that last case add to the novices' confusion? And now, the parsing: First, in the grammar (see 6.3.4 Calls), you have to expand the left side of keyword_item. The simplest idea is: keyword_item = identifier [ "(" [parameter-list] ")" ] "=" expression Then, the keyword AST node expands to take all the same args-related attributes of the Lambda node: keyword(arg="on_click", args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwargs=None, value=Call(…)) Then, at compile time, if args is None, you just compile arg and value as today and push them on the stack; if it's not (even an empty list), you instead compile args, vararg, …, value together as a function, just as you would a Lambda node except that "body" is called "value",then push the arg and that function on the stack. That doesn't feel right. On the other hand, there's a simpler way to do it: keyword_item ::= simple_keyword_item | functional_keyword_item simple_keyword_item = identifier "=" expression functional_keyword_item = identifier "(" [parameter_list] ")" "=" expression Now, simple_keyword_item parses to the same keyword AST node as today, and functional_keyword_item also parses into a normal keyword node, which has a normal Lambda node as a value, built from the parameter_list and expression the exact same way as in a lambda expression. That seems pretty clear and simple, but now at the AST level there's no way to distinguish between "key(x)=x.date" and "key=lambda x: x.date". Is that acceptable? Last, there's always a hybrid: create a new KeyLambda node that has the same attributes as Lambda and compiles the same but can be distinguished from it by type, and maybe even a funckeyword that's identical to keyword as well. Then, no magic, and no irreversible parse either.
I do not feel that this is worth it for the specific case. Having a more generic "better syntax for lambdas" will do everything that this does, + add less heterogeneity to the language, + is useful in other places too. On Wed, Feb 26, 2014 at 11:44 AM, Andrew Barnert <abarnert@yahoo.com> wrote:
From: Greg Ewing <greg.ewing@canterbury.ac.nz>
Sent: Tuesday, February 25, 2014 4:17 PM
I was reading a blog post here:
http://stupidpythonideas.blogspot.co.nz/2014/02/fixing-lambda.html
where the author points out that there are a number of different problems that the various enhanced-lambda proposals are trying to solve, which are best addressed by different solutions.
Here's a suggestion for one of them. Suppose we could write things like:
sorted(things, key(x) = x.date)
Button("Do it!", on_click() = fire_the_ducks())
Of course that particular case would be better written as:
Button("Do it!", on_click = fire_the_ducks)
But it becomes more useful if you do anything else:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice, but there's a nagging feeling that it may be a bit magical. Maybe if I think through what the compiler will do with it, I can resolve that feeling. (Obviously real users aren't going to care how it gets parsed and compiled, but if it's simple and clear enough, that implies that it can also be simple and clear to a human reader. Not perfectly, but... anyway, let's try it.) I'll do that at the end.
It only addresses the case of passing a function using
a keyword argument
It also doesn't look quite as nice when the function comes first, but I think it's still pretty nice:
itertools.takewhile(predicate(x)=x<5, iterable=spam)
And functions that take a function and then *args, like map, would be tricky, but then it's fine if this doesn't work nicely in every possible case for passing a function around.
Also, this _could_ be extended to work in all cases where call expressions raise a SyntaxError, although I don't know that it _should_ be. For example, people who for whatever reason prefer to write "f = lambda x: ..." instead of "def f(x): return ..." today would probably love being able to write "f(x) = ...", but I don't really want to encourage those people...
but I think it would make for very readable code in those cases. And it doesn't use any colons!
I don't understand the problem with the colons, but never mind that; I agree that it's very readable. And the real benefit to me is that it doesn't require any new and potentially weird syntax, like Nick's magic ? parameter, for the one-argument case.
On the other hand, could this add any confusion? Today, we have:
Button("Do it!", on_click=fire_the_ducks) # good
Button("Do it!", on_click=fire_the_ducks()) # bad, passes call result as function
Button("Do it!", on_click=lambda: fire_the_ducks()) # good
Button("Do it!", on_click=lambda: fire_the_ducks) # bad, passes function returning function
We'd be adding:
Button("Do it!", on_click()=fire_the_ducks()) # good
Button("Do it!", on_click()=fire_the_ducks) # bad, passes function returning function
Would that last case add to the novices' confusion?
And now, the parsing:
First, in the grammar (see 6.3.4 Calls), you have to expand the left side of keyword_item.
The simplest idea is:
keyword_item = identifier [ "(" [parameter-list] ")" ] "=" expression
Then, the keyword AST node expands to take all the same args-related attributes of the Lambda node:
keyword(arg="on_click", args=[], vararg=None, kwonlyargs=[], kw_defaults=[], kwargs=None, value=Call(...))
Then, at compile time, if args is None, you just compile arg and value as today and push them on the stack; if it's not (even an empty list), you instead compile args, vararg, ..., value together as a function, just as you would a Lambda node except that "body" is called "value",then push the arg and that function on the stack.
That doesn't feel right. On the other hand, there's a simpler way to do it:
keyword_item ::= simple_keyword_item | functional_keyword_item simple_keyword_item = identifier "=" expression functional_keyword_item = identifier "(" [parameter_list] ")" "=" expression
Now, simple_keyword_item parses to the same keyword AST node as today, and functional_keyword_item also parses into a normal keyword node, which has a normal Lambda node as a value, built from the parameter_list and expression the exact same way as in a lambda expression.
That seems pretty clear and simple, but now at the AST level there's no way to distinguish between "key(x)=x.date" and "key=lambda x: x.date". Is that acceptable?
Last, there's always a hybrid: create a new KeyLambda node that has the same attributes as Lambda and compiles the same but can be distinguished from it by type, and maybe even a funckeyword that's identical to keyword as well. Then, no magic, and no irreversible parse either. _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
Andrew Barnert wrote:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice, but there's a nagging feeling that it may be a bit magical. Maybe if I think through what the compiler will do with it, I can resolve that feeling.
It's quite simple, it's just syntactic sugar for Button("Do it!", on_click = lambda: fire_the_ducks(42))
We'd be adding:
Button("Do it!", on_click()=fire_the_ducks()) # good
Button("Do it!", on_click()=fire_the_ducks) # bad, passes function returning function
Would that last case add to the novices' confusion?
Each bad case has a corresponding bad case using lambda, so I don't think we'd be adding any more badness overall.
And it doesn't use any colons!
I don't understand the problem with the colons
Some people don't like the idea of colons inside expressions. Personally I don't mind, as long as it's done tastefully (e.g. in conjunction with keywords).
And now, the parsing:
First, in the grammar (see 6.3.4 Calls), you have to expand the left side of keyword_item.
The grammar in the Language Reference is not quite the same as the grammar used by CPython. The parser actually already allows a general expession on the left of the =, and then sorts it out later in the compilation process. So it should be relaively easy to implement.
but now at the AST level there's no way to distinguish between "key(x)=x.date" and "key=lambda x: x.date". Is that acceptable?
Yes, because there isn't meant to be any semantic difference between them. -- Greg
From: Greg Ewing <greg.ewing@canterbury.ac.nz> Sent: Wednesday, February 26, 2014 2:16 PM
Andrew Barnert wrote:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice, but there's a nagging feeling that it may be a bit magical. Maybe if I think through what the compiler will do with it, I can resolve that feeling.
It's quite simple, it's just syntactic sugar for
Button("Do it!", on_click = lambda: fire_the_ducks(42))
And now, the parsing:
First, in the grammar (see 6.3.4 Calls), you have to expand the left side of keyword_item.
The grammar in the Language Reference is not quite the same as the grammar used by CPython. The parser actually already allows a general expession on the left of the =, and then sorts it out later in the compilation process. So it should be relaively easy to implement.
Yes, the grammar in the reference doesn't document what the first stage of the CPython parser does, but it does document what comes out at the AST level, which is the part that should be the same across implementations, and the first thing that's visible from within the interpreter, right? But yeah, within the CPython grammar: argument: test [comp_for] | test '=' test # Really [keyword '='] test
but now at the AST level there's no way
to distinguish between "key(x)=x.date" and "key=lambda x: x.date". Is that acceptable?
Yes, because there isn't meant to be any semantic difference between them.
Sure, but given that ASTs are exposed for introspection, is it expected that you should be able to distinguish two cases by their ASTs? I could see, say, MacroPy wanting to. (I could also see that not being important enough to care about, of course.)
On Wed, Feb 26, 2014 at 11:44:51AM -0800, Andrew Barnert wrote:
But it becomes more useful if you do anything else:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice,
At first glance, it looks like you are setting the on_click argument to the result of fire_the_ducks(42). This proposed syntax is going to be *really easy* for people to misinterpret when they see it in use. And not just novices -- I think this will be syntax that just begs to be misinterpreted when reading code, and misused when writing it. I think that having special syntax for anonymous function only inside function calls with keyword arguments is a violation of the Zen of Python (see the one about special cases) and the Principle Of Least Surprise. It's really a bad idea to have syntax for a "shorter lambda" that works here: f(arg=***whatever***) but not in these: f(***whatever***) [len, zip, map, ***whatever***, some_function] result = ***whatever***(arg) One of the best things about Python is it's internal consistency. It is remarkably free of special case syntax that works in one place but not in others. Let's keep it that way. -- Steven
From: Steven D'Aprano <steve@pearwood.info> Sent: Wednesday, February 26, 2014 8:19 PM
On Wed, Feb 26, 2014 at 11:44:51AM -0800, Andrew Barnert wrote:
But it becomes more useful if you do anything else:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice,
At first glance, it looks like you are setting the on_click argument to the result of fire_the_ducks(42).
It's also worth noting that this is exactly the kind of case where Nick's proposal also looks nice: Button("Do it!", on_click=:fire_the_ducks(42)) The test is in cases where Nick's syntax looks weird, either because it takes an argument, or because it's more symbols than letters: Button("Do it!", on_click=lambda btn: btn.fire_the_ducks(42)) Button("Do it!", on_click(btn)=btn.fire_the_ducks(42)) Button("Do it!", on_click=:?.fire_the_ducks(42)) You can try adding whitespace to the Nick version of last example anywhere you want, and it's still going to hurt to look at. But the Greg version looks actually clearer here than in the no-argument case.
It's really a bad idea to have syntax for a "shorter lambda"
that works here:
f(arg=***whatever***)
but not in these:
I don't think that's _necessarily_ a problem. After all, it's not a problem that you can filter a for loop in a comprehension but not anywhere else, is it? And it's not like there would be any confusion. If someone wants to know "how do I write a lambda function in a list", the answer is just "write lambda: spam(x)", not "you can't do it". However…
f(***whatever***)
That one seems like the easiest to dismiss—after all, you can, and should, use keyword arguments whenever it improves clarity—but it turns out to be the most serious problem. First, many of the key higher-order functions, even after the recent arg clinic work, still don't take keyword arguments—like almost everything in itertools. Second, many of these functions take a predicate first, meaning you have to keyword everything if you want to keyword that. And at that point, I think you lose all the benefits. Compare: takewhile(lambda x: x<5, a) takewhile(predicate(x)=x<5, iterable=a) In fact, even in cases where there aren't later parameters, compare: defaultdict(lamdba: []) defaultdict(default_factory()=[]) I think in both cases, the second one gains little or nothing in clarity in exchange for its verbosity. So, it's not really true that you can always just use a keyword. Which means the benefit of the feature overall may be too limited. I'm digging through examples, and so far, sorting keys and Tkinter callbacks are the only cases I've found that are improved (even assuming you accept the basic premise, which I know you personally don't).
On Thu, Feb 27, 2014 at 5:16 PM, Andrew Barnert <abarnert@yahoo.com> wrote:
It's really a bad idea to have syntax for a "shorter lambda"
that works here:
f(arg=***whatever***)
but not in these:
I don't think that's _necessarily_ a problem. After all, it's not a problem that you can filter a for loop in a comprehension but not anywhere else, is it?
I do. spam = ***whatever1*** ***whatever2*** spam ***whatever3*** ***whatever2*** (***whatever1***) ***whatever3*** Regardless of the exact values of the whatevers, these should be equivalent (apart from the fact that the first one forces evaluation of whatever1, while the second might not). You can't, however, put a filter onto spam, so that comparison with the comprehension isn't fair. There should be absolutely no difference between: f(arg=***whatever***) and: spam=***whatever*** f(arg=spam) (Okay, okay, the bytecode will differ, and the second one evaluates f after working out the argument value, and so on. I mean in normal usage.) Breaking that expectation would confuse a lot of people. It's problematic because it's still legal - if it threw a SyntaxError, it would at least be visible, but it doesn't: spam=fire_the_ducks(42) f(onclick()=spam) ChrisA
Chris Angelico wrote:
It's problematic because it's still legal - if it threw a SyntaxError, it would at least be visible, but it doesn't:
spam=fire_the_ducks(42) f(onclick()=spam)
That's equivalent to spam = fire_the_ducks(42) f(onclick = lambda: spam) which is not a syntax error either, but it's just as wrong, and I'm not convinced that it's a harder mistake to make. For what it's worth, the following *could* be made to work: spam() = fire_the_ducks(42) f(onclick = spam) -- Greg
On Thu, Feb 27, 2014 at 6:32 PM, Greg Ewing <greg.ewing@canterbury.ac.nz> wrote:
Chris Angelico wrote:
It's problematic because it's still legal - if it threw a SyntaxError, it would at least be visible, but it doesn't:
spam=fire_the_ducks(42) f(onclick()=spam)
That's equivalent to
spam = fire_the_ducks(42) f(onclick = lambda: spam)
which is not a syntax error either, but it's just as wrong, and I'm not convinced that it's a harder mistake to make.
But one of them is breaking an expression out and giving it a name, and the other is not. Currently, in this expression: f(arg=g(h)) you can replace f, g, and h with any expressions that result in those same values, and the function call will be exactly the same. But breaking out the expression part of a parenthesized call is suddenly very different. Yes, the same can be seen with lambda, but there it's a colon-based subexpression, and those are very obviously different (like how an if/for/except statement behaves differently from a simple block of code). ChrisA
On 27 Feb 2014 17:33, "Greg Ewing" <greg.ewing@canterbury.ac.nz> wrote:
Chris Angelico wrote:
It's problematic because it's still legal - if it threw a SyntaxError, it would at least be visible, but it doesn't:
spam=fire_the_ducks(42) f(onclick()=spam)
That's equivalent to
spam = fire_the_ducks(42) f(onclick = lambda: spam)
which is not a syntax error either, but it's just as wrong, and I'm not convinced that it's a harder mistake to make.
For what it's worth, the following *could* be made to work:
spam() = fire_the_ducks(42) f(onclick = spam)
Let's talk about that for a moment. It would be a matter of making this: NAME(ARGSPEC) = EXPR syntactic sugar for this: def NAME(ARGSPEC): return EXPR Not what you would call a big win. Cheers, Nick.
-- Greg
_______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
On 02/27/2014 04:17 AM, Nick Coghlan wrote:
For what it's worth, the following *could* be made to work:
spam() = fire_the_ducks(42) f(onclick = spam)
Let's talk about that for a moment. It would be a matter of making this:
NAME(ARGSPEC) = EXPR
syntactic sugar for this:
def NAME(ARGSPEC): return EXPR
Not what you would call a big win.
A good potential use of lambda is in constructing a dispatch table from a dictionary litteral or constructor. select = {key1:callable, key2:callable, ...} result = select[k]() select = dict(key1=callable, key2=callable, ...) result = select[k]() And there is also the case of it working in a ternary if... result = (callable if k else callable)() #k is True or False I think these are good test cases for evaluating usability and readability. It would be nice if it worked well for each of these. Cheers, Ron
Now that someone mentioned dispatch tables, another possibility would be to support assignment to tables and monkey-patching directly, with a syntax like def obj.method(*args): ... # __name__ = "method" (the attribute name) def table[key](*args): ... # __name__ = ??? (perhaps "table[key]"?) (and also any other "lvalue".) Antony 2014-02-27 7:05 GMT-08:00 Ron Adam <ron3200@gmail.com>:
On 02/27/2014 04:17 AM, Nick Coghlan wrote:
For what it's worth, the following *could* be made to work:
spam() = fire_the_ducks(42) f(onclick = spam)
Let's talk about that for a moment. It would be a matter of making this:
NAME(ARGSPEC) = EXPR
syntactic sugar for this:
def NAME(ARGSPEC): return EXPR
Not what you would call a big win.
A good potential use of lambda is in constructing a dispatch table from a dictionary litteral or constructor.
select = {key1:callable, key2:callable, ...} result = select[k]()
select = dict(key1=callable, key2=callable, ...) result = select[k]()
And there is also the case of it working in a ternary if...
result = (callable if k else callable)() #k is True or False
I think these are good test cases for evaluating usability and readability. It would be nice if it worked well for each of these.
Cheers, Ron
_______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
On 27 February 2014 18:27, Antony Lee <antony.lee@berkeley.edu> wrote:
Now that someone mentioned dispatch tables, another possibility would be to support assignment to tables and monkey-patching directly, with a syntax like
def obj.method(*args): ... # __name__ = "method" (the attribute name) def table[key](*args): ... # __name__ = ??? (perhaps "table[key]"?) (and also any other "lvalue".)
I would very much support this. It's actually odd that you *can't* do it, considering you *can* do class A: ... a = A() l = [0] for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ... However, there are disadvantages: potentially ambiguous grammar (very) small chance of misuse could hurt introspection Further, I don't imagine I'd use it frequently. Just enough, though.
On Fri, Feb 28, 2014 at 5:48 AM, Joshua Landau <joshua@landau.ws> wrote:
On 27 February 2014 18:27, Antony Lee <antony.lee@berkeley.edu> wrote:
Now that someone mentioned dispatch tables, another possibility would be to support assignment to tables and monkey-patching directly, with a syntax like
def obj.method(*args): ... # __name__ = "method" (the attribute name) def table[key](*args): ... # __name__ = ??? (perhaps "table[key]"?) (and also any other "lvalue".)
I would very much support this. It's actually odd that you *can't* do it, considering you *can* do
class A: ... a = A() l = [0]
for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ...
However, there are disadvantages:
potentially ambiguous grammar (very) small chance of misuse could hurt introspection
Further, I don't imagine I'd use it frequently. Just enough, though.
Remember, def creates a function with a name. If there's no obvious name to attach to the function, it'd be better to either: def functionname(...): .... table[key] = functionname or: table[key] = lambda(...): ... to either set a name, or not set a name, as the case may be. ChrisA
Chris Angelico wrote:
Remember, def creates a function with a name.
It doesn't *have* to do that, though. If there is no obvious name, it could use a generic fallback name, like lambda does.
def functionname(...): .... table[key] = functionname
I don't see how that's intrinsically better.
or:
table[key] = lambda(...): ...
to either set a name, or not set a name, as the case may be.
I don't see having a name vs. not having a name as the most important criterion for deciding whether to use a def or a lambda. -- Greg
On 27 February 2014 18:56, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 5:48 AM, Joshua Landau <joshua@landau.ws> wrote:
On 27 February 2014 18:27, Antony Lee <antony.lee@berkeley.edu> wrote:
Now that someone mentioned dispatch tables, another possibility would be to support assignment to tables and monkey-patching directly, with a syntax like
def obj.method(*args): ... # __name__ = "method" (the attribute name) def table[key](*args): ... # __name__ = ??? (perhaps "table[key]"?) (and also any other "lvalue".)
I would very much support this. It's actually odd that you *can't* do it, considering you *can* do [some other things]
Remember, def creates a function with a name. If there's no obvious name to attach to the function, it'd be better to either:
def functionname(...): .... table[key] = functionname
or:
table[key] = lambda(...): ...
to either set a name, or not set a name, as the case may be.
But there *is* a name. The thing is, that name might have most meaning in context. I personally don't see why from a reader's point of view, callbacks["jump"] is a worse name than jump_callback IMHO, I would just force the introspection name to be the expression in full, with the small exception of trimming *direct* dot-access; (such that the qualified name is never worse). Therefore we could have the names going from For callbacks["jump"]: jump_callback → callbacks["jump"] For myobj.myfunc (unchanged): myfunc → myfunc For myobj.callbacks[...].myfunc: ellipsis_myfunc_callback → callbacks[...].myfunc (I realise this example is contrived; it's to lower the number of examples) and the qualified names: For callbacks["jump"]: jump_callback → callbacks["jump"] For myobj.myfunc: myfunc → myobj.myfunc For myobj.callbacks[...].myfunc: ellipsis_myfunc_callback → myobj.callbacks[...].myfunc To me, the naming "problem" is actually a positive.
On Thu, Feb 27, 2014 at 06:48:25PM +0000, Joshua Landau wrote:
On 27 February 2014 18:27, Antony Lee <antony.lee@berkeley.edu> wrote:
Now that someone mentioned dispatch tables, another possibility would be to support assignment to tables and monkey-patching directly, with a syntax like
def obj.method(*args): ... # __name__ = "method" (the attribute name) def table[key](*args): ... # __name__ = ??? (perhaps "table[key]"?) (and also any other "lvalue".)
I would very much support this. It's actually odd that you *can't* do it, considering you *can* do
class A: ... a = A() l = [0]
for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ...
I wonder whether that is deliberate feature, or an accident of the way the syntax works. It does seem to be pretty useless though -- why are you using an attribute or list as a loop variable? As far as setting a method on an instance like that, if you're defining methods, 99.9% of the time they ought to be defined on the class, not on the instance. For the remaining one time in a thousand, it is nearly always sufficient to either embed the assignment inside __init__, or use a temporary, external, function: class Whatever: def __init__(self, value): def func(arg): return arg + value self.func = func Things like this are unusual enough that they don't need -- in fact, *shouldn't* have -- dedicated syntax to support them. That syntax simply complicates the interpreter, makes it harder for people to learn the language, adds more decisions for the programmer to make, and just for a marginal (if any) benefit. The list example is slightly better, but not enough to allow it. If you could populate the entire list as a single expression, that might be interesting: functions = [ def spam(): ..., def eggs(): ..., def cheese(): ..., ] but the syntax suggested here doesn't let you do that. You still have a separate statement for each item. All it buys you is saving *one line* per assignment: def spam(): ... functions[0] = spam or two if you absolutely must then unbind the name spam afterwards del spam (but normally I wouldn't bother).
However, there are disadvantages:
potentially ambiguous grammar (very) small chance of misuse could hurt introspection
Further, I don't imagine I'd use it frequently. Just enough, though.
I'd like to see some non-contrived use-cases for where you think you would actually use this. -- Steven
On Fri, Feb 28, 2014 at 8:14 AM, Steven D'Aprano <steve@pearwood.info> wrote:
class A: ... a = A() l = [0]
for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ...
I wonder whether that is deliberate feature, or an accident of the way the syntax works. It does seem to be pretty useless though -- why are you using an attribute or list as a loop variable?
I would say it's no accident that the for loop can accept any lvalue. That's how we can do this, which I'm sure you'll agree is *extremely* useful: for x,y in [(1,2), (3,4), (5,6)]: ... Assigning to the tuple works beautifully there (especially with enumerate or zip). Being able to assign to other complex targets is a bonus that you'll probably never come across (I can imagine someone might have a use for "for self.blah in ...", but can't concoct any right now), and something there's no point in forbidding, but I'm glad complex targets in general are accepted. It does allow stupid stuff, though. Check this out: lst=[10,20,30,40,50] # Any source list dest=[None]*len(lst) for i,dest[i] in enumerate(lst): pass assert dest == lst I suppose in theory there might be a use for that, but if you want to talk about things that accidentally work, I'd say this form of list copy would have to be one of them :) ChrisA (Why do I always come up with the stupidest ideas?)
On Feb 27, 2014, at 13:24, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 8:14 AM, Steven D'Aprano <steve@pearwood.info> wrote:
class A: ... a = A() l = [0]
for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ...
I wonder whether that is deliberate feature, or an accident of the way the syntax works. It does seem to be pretty useless though -- why are you using an attribute or list as a loop variable?
I would say it's no accident that the for loop can accept any lvalue. That's how we can do this, which I'm sure you'll agree is *extremely* useful:
for x,y in [(1,2), (3,4), (5,6)]: ...
Assigning to the tuple works beautifully there (especially with enumerate or zip). Being able to assign to other complex targets is a bonus that you'll probably never come across (I can imagine someone might have a use for "for self.blah in ...", but can't concoct any right now)
Something I've seen in real code (not _good_ code, but actually deployed): for self.index in range(len(self.values)): if self.values[self.index] == spam: break else: self.index = None Apparently someone didn't want to catch the exception from list.index. (Or, more likely, they were trying to write C code in Python.)
Last option's way more likely. I made that mistake about 2000 times moving over from C++. Andrew Barnert <abarnert@yahoo.com> wrote:
On Feb 27, 2014, at 13:24, Chris Angelico <rosuav@gmail.com> wrote:
On Fri, Feb 28, 2014 at 8:14 AM, Steven D'Aprano <steve@pearwood.info> wrote:
class A: ... a = A() l = [0]
for a.x in [1, 2, 3]: ... for l[0] in [1, 2, 3]: ...
I wonder whether that is deliberate feature, or an accident of the way the syntax works. It does seem to be pretty useless though -- why are you using an attribute or list as a loop variable?
I would say it's no accident that the for loop can accept any lvalue. That's how we can do this, which I'm sure you'll agree is *extremely* useful:
for x,y in [(1,2), (3,4), (5,6)]: ...
Assigning to the tuple works beautifully there (especially with enumerate or zip). Being able to assign to other complex targets is a bonus that you'll probably never come across (I can imagine someone might have a use for "for self.blah in ...", but can't concoct any right now)
Something I've seen in real code (not _good_ code, but actually deployed):
for self.index in range(len(self.values)): if self.values[self.index] == spam: break else: self.index = None
Apparently someone didn't want to catch the exception from list.index. (Or, more likely, they were trying to write C code in Python.) _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
-- Sent from my Android phone with K-9 Mail. Please excuse my brevity.
Steven D'Aprano wrote:
All it buys you is saving *one line* per assignment:
def spam(): ... functions[0] = spam
The motivation isn't to save a line, it's to make the code clearer, like the way decorators make the use of staticmethod and classmethod clearer. -- Greg
On Wed, Feb 26, 2014 at 10:16:13PM -0800, Andrew Barnert wrote:
From: Steven D'Aprano <steve@pearwood.info>
It's really a bad idea to have syntax for a "shorter lambda" that works here:
f(arg=***whatever***)
but not in these:
I don't think that's _necessarily_ a problem. After all, it's not a problem that you can filter a for loop in a comprehension but not anywhere else, is it?
I'll note that the ability to write "for x in seq if condition(x)" is a frequently requested enhancement to for-loops. Special case syntax, as suggested here, means that the user has to learn a whole lot of special cases. Instead of learning: "you can create <this object> with <this syntax>" you have to learn: "you can create <this object> with <this syntax> but only under <these circumstances>" which increases the burden of learning the language. Python is relatively easy to learn and remember because it is so consistent: if a piece of syntax works here, it almost always works there as well: Q: Can I create a function using lambda inside a list? A: Yes, you can create a function using lambda anywhere an expression is legal. Q: Can I create a function using lambda inside a dict? A: Yes, you can create a function using lambda in a dict. Q: How about inside a function call parens? A: Expressions are legal inside parens, so yes you can use lambda when calling a function. Q: How about inside a function call using a keyword parameter? A: Yes, whether the parameter is given by keyword or not makes no difference. Q: What about inside a set? Bet it doesn't work inside sets. A: Yes, it works inside sets. Q: How about tuples? A: Yes, it works inside tuples. Q. What, even on Fridays? There are very few exceptions to this consistency, and most of them can be sorted out by using parens. You nearly always can copy a valid expression from one part of a .py file, paste it into a similar but not identical context, and be very confident that it will be syntactically valid in the new context as well. -- Steven
Steven D'Aprano wrote:
It's really a bad idea to have syntax for a "shorter lambda" that works here:
f(arg=***whatever***)
but not in these:
f(***whatever***) [len, zip, map, ***whatever***, some_function] result = ***whatever***(arg)
The last one could be made to work as well, then it would be less of a special case. It would be a specialised form of assignment that's optimised for binding a name to a function, similar to def except that the body is an expression rather than a statement. -- Greg
On 27 February 2014 04:19, Steven D'Aprano <steve@pearwood.info> wrote:
On Wed, Feb 26, 2014 at 11:44:51AM -0800, Andrew Barnert wrote:
But it becomes more useful if you do anything else:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice,
At first glance, it looks like you are setting the on_click argument to the result of fire_the_ducks(42). This proposed syntax is going to be *really easy* for people to misinterpret when they see it in use. And not just novices -- I think this will be syntax that just begs to be misinterpreted when reading code, and misused when writing it.
I think that having special syntax for anonymous function only inside function calls with keyword arguments is a violation of the Zen of Python (see the one about special cases) and the Principle Of Least Surprise. It's really a bad idea to have syntax for a "shorter lambda" that works here:
f(arg=***whatever***)
but not in these:
f(***whatever***) [len, zip, map, ***whatever***, some_function] result = ***whatever***(arg)
I don't follow. I know it's different, but we have f(*args) but not [*args] And we have start, *mid, end = [1, 2, 3] but not (lambda start, *mid, end: ...)(1, 2, 3) and we have (lambda x: ...)(*[...]) but not foo = *[...] ... *wink¹* It's not silly to think that things are context-sensitive in a context-sensitive language. Personally this proposal makes sense and I honestly don't see the confusion that's being stated. It makes things significantly prettier and more readable, for one. Further, for consistency one can define: matrix.transform((x, y)=(y, x)) as an "anonymous" version, compiling to matrix.transform(lambda x, y: (y, x)) Please admit that this is way, way prettier. PS: One problem. "f( (a) = ... )" is currently valid. So is "f( (((((a))))) = ... )". Why the hell is this so? "(a) = 2" is valid, but so is "(a, b) = (2, 3)", whereas "f( (a, b) = (2, 3) )" is not. ¹http://www.python.org/dev/peps/pep-0448/
Please admit that this is way, way prettier.
If it's just a matter of prettyness, why not just alias lambda as λ Then we can have matrix.transform(λ x, y: (y, x)) I think it looks way prettier than either one: it doesn't have the verbosity of the lambda version, and doesn't have the weird contextuality of the = version. On Thu, Feb 27, 2014 at 3:38 PM, Joshua Landau <joshua@landau.ws> wrote:
On 27 February 2014 04:19, Steven D'Aprano <steve@pearwood.info> wrote:
On Wed, Feb 26, 2014 at 11:44:51AM -0800, Andrew Barnert wrote:
But it becomes more useful if you do anything else:
Button("Do it!", on_click() = fire_the_ducks(42))
At first glance, I think this is nice,
At first glance, it looks like you are setting the on_click argument to the result of fire_the_ducks(42). This proposed syntax is going to be *really easy* for people to misinterpret when they see it in use. And not just novices -- I think this will be syntax that just begs to be misinterpreted when reading code, and misused when writing it.
I think that having special syntax for anonymous function only inside function calls with keyword arguments is a violation of the Zen of Python (see the one about special cases) and the Principle Of Least Surprise. It's really a bad idea to have syntax for a "shorter lambda" that works here:
f(arg=***whatever***)
but not in these:
f(***whatever***) [len, zip, map, ***whatever***, some_function] result = ***whatever***(arg)
I don't follow. I know it's different, but we have
f(*args) but not [*args]
And we have
start, *mid, end = [1, 2, 3] but not (lambda start, *mid, end: ...)(1, 2, 3)
and we have
(lambda x: ...)(*[...]) but not foo = *[...]
... *wink¹*
It's not silly to think that things are context-sensitive in a context-sensitive language. Personally this proposal makes sense and I honestly don't see the confusion that's being stated. It makes things significantly prettier and more readable, for one.
Further, for consistency one can define:
matrix.transform((x, y)=(y, x))
as an "anonymous" version, compiling to
matrix.transform(lambda x, y: (y, x))
Please admit that this is way, way prettier.
PS: One problem. "f( (a) = ... )" is currently valid. So is "f( (((((a))))) = ... )". Why the hell is this so? "(a) = 2" is valid, but so is "(a, b) = (2, 3)", whereas "f( (a, b) = (2, 3) )" is not.
¹http://www.python.org/dev/peps/pep-0448/ _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
On 27 February 2014 23:47, Haoyi Li <haoyi.sg@gmail.com> wrote:
Please admit that this is way, way prettier.
If it's just a matter of prettyness, why not just alias lambda as λ
As much as this makes sense, and ignoring the downsides of non-ASCII characters¹, I have some comments: I disagree about λ being prettier, much as one writes "myfunc (x, y) = (y, x)" in Haskell instead of "myfunc = \(x, y) -> (y, x)"². This reminds me that we've had this argument shot down before, so chances are I'm defending a dead horse here. ¹ You might notice that I do so in my EMails, but only because Alt-Gr (on Linux) makes specific graphemes easy to reach. ² I don't actually know Haskell.
On 28/02/2014 12:38 p.m., Joshua Landau wrote:
Further, for consistency one can define:
matrix.transform((x, y)=(y, x)) Please admit that this is way, way prettier.
Hmm, I'm not so sure. That looks more like it should mean matrix.transform(x = y, y = x) i.e. an unpacking assignment to keyword args. -- Greg
participants (14)
-
Andrew Barnert -
Antony Lee -
Brendan Moloney -
Chris Angelico -
Greg -
Greg Ewing -
Haoyi Li -
Joshua Landau -
Nick Coghlan -
Ron Adam -
Ryan -
Ryan Gonzalez -
Steven D'Aprano -
Terry Reedy