
On Thu, May 27, 2021 at 12:33:20PM -0400, Ricky Teachey wrote:
On Thu, May 27, 2021 at 10:25 AM Steven D'Aprano <steve@pearwood.info> wrote:
Okay. Without reading the source code, does this code snippet use the old `__call__` protocol or the new `__decoration_call__` protocol?
@flambé class Banana_Surprise: pass
Invoking @flambé causes:
flambé.__decoration_call__(Banana_Surprise, "Banana_Surprise")
...to be used, if flambé has that method.
Right: you can't tell from the code you are looking at whether flambé is being called as a new or old style decorator. You have to dig into the definition of flambé to work out the meaning of the code. On the one hand we have the current decorator protocol, where the object flambé is called with a single argument, the class Banana_Surprise. I can only think of three cases where something similar occurs, but the differences are critical. - Binary operators can call either or both of two reflected dunders, such as `__add__` and `__radd__`. - Iteration normally calls `__next__`, but it can fall back on the old sequence protocol that relies on `__getitem__`. - Augmented assignment calls the augmented assignment method, such as `__iadd__`, but may fall back on the regular `+` operator. In all of these cases, the intended semantics are the same and the arguments passed are (almost) the same. E.g. normally we would use the same signatures for each of the operators: def __add__(self, other): ... def __radd__(self, other): ... def __iadd__(self, other): ... and if fact for many purposes, add and radd will be the same function object. The old sequence protocol is a minor exception, because `__getitem__` requires an argument (it is called with 0, 1, 2, 3, ... until IndexError occurs). But that's almost an implementation detail. The fact that `__next__` takes no explicit enumeration, but `__getitem__` does, can be ignored when we think about the high-level concept of iteration. Conceptionally, at a high-level, the sequence and iterator protocols operate the same way: call the dunder repeatedly until an exception occurs and the details of which dunder and which exception don't matter. Iteration is iteration. But here, the details *do* matter. Old classic decorators and this proposed new decorator syntax do not share a high-level concept. There is a certain relationship in the sense that both the new and old protocols involve calling a function, but the details are very different: - old decorator protocol is a normal function call with a single argument; - new protocol uses a different method, with two arguments, one of which is implicit. Old decorator protocol is about *decorating* a function or class object with new functionality (wrapping it in a wrapper function, say, or modifying it in place). New decorator protocol and variable decoration doesn't decorate anything: there's nothing to decorate when used with a bare name: @decorate var which presumably evaluates to: var = decorate.__decorate_call__('var') So we're not decorating an object. There's no object to decorate! With a binding: @decorate var = expression var = decorate.__decorate_call__('var', expression) Again, conceptually we're not decorating anything. We're just reusing the `@` syntax for something that is not conceptually related to old style decorators. This is just a syntax for *implicitly* getting access to the left hand side assignment target name as a string. [...]
I just don't know whether or not the decorator `flambé` receives the name or not.
If you wrote the decorator, you know.
If you didn't, why does it matter if it is being passed? Why do you need to know that?
Because I don't know the meaning of the code. Am I decorating an object or doing something with the target name? I can't interpret the code until I know what protocol was supported by the decorator object. Let me give you an analogy: the `with` statement has a context manager protocol that uses an `__enter__` and `__exit__` pair of methods. With statements are awesome! They're so awesome that we should use the exact same "with" syntax for Pascal-style implicit attribute access, using a different protocol. https://docs.python.org/3/faq/design.html#why-doesn-t-python-have-a-with-sta... with myobject: do_something() with myotherobj: do_something() There is no visible difference between the two pieces of code, but one is an old style enter/exit context manager, the other is our hypothetical new style with syntax that does something completely different and calls different dunder methods. One of them is calling the global do_something and one is calling the object's do_something method. Which is which? Code that is different should look different. -- Steve