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 <[email protected]>
> 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-statement-for-attribute-assignments
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
_______________________________________________
Python-ideas mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at
https://mail.python.org/archives/list/[email protected]/message/NOKCTEKOJ7BQIKODKV4TX7W5WUL5P3Y7/
Code of Conduct: http://python.org/psf/codeofconduct/