On Mon, Mar 16, 2015 at 10:51 PM, Steven D'Aprano <steve+comp.lang.pyt...@pearwood.info> wrote: > Chris Angelico wrote: > Just for the record, it's not just name bindings that make a generator > behave as a coroutine. E.g.: > > py> def co(): > ... print( (yield 1) + 2 ) > ... > py> a = co() > py> next(a) > 1 > py> a.send(98) > 100 > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > StopIteration
Yep, I agree. It's not the name binding, it's whether the yield expression's result is instantly discarded vs used in some way. > It's surprising to me that you can send() into a non-coroutine and have it > ignored. I'm not even sure that is documented behaviour or just an accident > of implementation. I agree conceptually; but there are two different consistencies here, and they clash, my lords, they clash! One is that the API for a coroutine includes send() but the API for a non-coroutine generator doesn't, and that trying to send() into the latter should raise an error. (Side point: What error? I'd accept AttributeError, but since it's possible to mix sendable and nonsendable yields, it might be better to have it raise at the time of the call - that is, when you send a non-None value into a generator that's just going to discard the value, it raises on arrival. In that case, possibly ValueError? GeneratorStyleError?) And on the other hand, we have a very strong Python convention that any expression can be used as a statement without altering the effect of that expression in any way. Assigning something to a name that you never use, or passing it as an argument to a no-op function, should not suddenly change the behaviour of anything. >> That said, though, it would be quite reasonable for a *linter* to warn >> you about sending into a generator that never uses sent values. With a >> good type inference system (not sure if MyPy is sufficient here), it >> would be possible to distinguish between those two functions and >> declare that the first one has the return type "sendable_generator" >> and the second one "nonsendable_generator", and give a warning if you >> send into the latter. But I don't think that's the language's job. > > I do :-) > > I think it would be hard for a linter to do this. It can't just do a > type-check, because the types of generator-iterators and > generator-coroutines are the same. It would need to actually analyse the > source code, AST, or byte-code. That's doable, but the compiler already > does that, and compiles different code for the two cases: > > py> from dis import dis > py> def g1(): > ... yield 1 > ... > py> def g2(): > ... x = yield 1 > ... > py> dis(g1) > 2 0 LOAD_CONST 1 (1) > 3 YIELD_VALUE > 4 POP_TOP > 5 LOAD_CONST 0 (None) > 8 RETURN_VALUE > py> dis(g2) > 2 0 LOAD_CONST 1 (1) > 3 YIELD_VALUE > 4 STORE_FAST 0 (x) > 7 LOAD_CONST 0 (None) > 10 RETURN_VALUE > > > so it should be easy for the compiler to recognise a yield used as if it > were a statement versus one used as an expression, and take appropriate > steps. But maybe there are subtle corner cases I haven't thought of. That code isn't actually all that different, though. What you have is this: 1) Load up a value 2) Yield, which pops the value off, yields it, and pushes the sent value or None 3a) in g1: Discard the value of the expression that we've just finished 3b) in g2: Store the value of that expression into x 4) Load None, and return it. Steps 1, 3, and 4 would all be identical if you replace the "yield 1" with, say, "print(1)". It'll pop the 1 off, do something, and leave a result on the stack. The difference between assignment and non-assignment comes later. The risk I see here is that a simple optimization might suddenly make a semantic change to something. Imagine this function: def gen(): while True: x = (yield 1) if x == 5: break Okay, so it cares about sent values. Well and good. Then we change it so it doesn't always care: def gen(state): while True: x = (yield 1) if state and x == 5: break Ahh but look! That's a mode-switch parameter. We should break that out into two functions. def gen_stoppable(): while True: x = (yield 1) if x == 5: break def gen_unstoppable(): while True: x = (yield 1) Now, according to the current rules, you can change that last line to just "yield 1" without the two functions differing in API. There's no sent value that will stop the second one, but it will accept and ignore sent values, just as the first one does. (Imagine that's some sort of password, and the second generator is for the case where the account is locked and there IS no password.) But according to your rules, changing it to no longer assign the yielded value somewhere would make a semantic and API change to the function. That seems, to me, at least as surprising as the case of send() working when the value's going to be ignored. Either way, it's not going to be ideal. I do hear you about the problem of sending when you shouldn't be able to... but I'm not convinced the cure isn't worse than the disease. ChrisA -- https://mail.python.org/mailman/listinfo/python-list