On Wed, May 26, 2021 at 7:54 AM Steven D'Aprano <steve(a)pearwood.info> wrote:
> On Wed, May 26, 2021 at 01:33:07PM +0200, Stéfane Fermigier wrote:
>
> > Last point that I like in the decorator syntax: it's
> >
> > I can compose N different decorators and keep the intent obvious:
> >
> > @acl(READ, WRITE)
> > @constraint(10 < _ < 100)
> > @not_null
> > @indexed
> > @depends_on(whatever)
> > @inject
> > @...
> > first_name: str
>
> Hmm, that's a good point.
>
> On the other hand, it would be terribly confusing if the same syntax:
>
> @decorate
>
> had radically different meaning depending on whether it was followed by
> a class/function or a bare name.
>
>
> --
> Steve
>
....and previously Steve also said:
On Tue, May 25, 2021 at 3:28 AM Steven D'Aprano <steve(a)pearwood.info> wrote:
> On Mon, May 24, 2021 at 06:36:47PM -0700, micro codery wrote:
>
> > Basically this would add syntax to python that would transform
> > @decorator("spam this") variable
> > into
> > variable = decorator("variable", "spam this")
>
> That is confusingly different from decorator syntax in other contexts....
>
> @decorator("spam this")
> def func(): pass
>
> # transformed to:
>
> def func(): pass
> func = decorator("spam this")(func)
>
> ...the critical difference is that the argument "spam this" should be
> passed to the decorator *factory*, which then returns the actual
> decorator that gets applied to the variable. (Or function/ class in the
> case of regulator decorator syntax.)
Well, in actuality even if it were implemented the way you described, it is
still going to be very different--- I might even say radically different.
These two ideas of a decorator syntax result are not the same:
RESULT A: function decorator
# func = decorator("spam")(func)
RESULT B: variable decorator
# name = decorator("spam")("name")
...because func is passed as an object, but "name" a string representing
the name of the object. Two very different things.
For this reason I think I would agree even more so that the differences in
the decorator behavior would be an extremely significant point of confusion.
This got me to thinking: what if access to the variable name were provided
by another means, and ONLY when the decorator syntax is employed?
So then I started to write this big long email and it kind of got out of
hand. Hopefully it isn't a disaster.
FIRST LAYER: RICK'S (LIKELY HALF-BAKED) COUNTER PROPOSAL
Maybe employment of decorator syntax could OPTIONALLY trigger a new dunder
method-- here I'll just call it __decoration_call__-- with the signature:
def __decoration_call__(self, obj: Any, by_name: str) -> Any: ...
Before I describe what I intend by this, first I will stipulated that what
I am proposing here should only be implemented so that the behavior of all
currently existing decorators (i.e., all callables) would remain exactly as
it does today.
So, for any existing callable with the name decorator, this:
@decorator("spam this")
def func(): ...
...continues to mean this, just as it does today:
def func(): ...
func = decorator("spam this")(func)
My idea is to optionally allow any callable object to write a
__decoration_call__ method that gets called in lieu of the __call__ method
when the callable object is employed using decorator syntax. When this
happens, the decorated named is supplied- not counting self- as the first
argument (e.g., by_name), which contains the str value of the name the
decorator was applied to.
Let's explain using code examples.
In actuality, unless I'm wrong (I might be; not an expert) current
decorator syntax is really sugar for:
def func(): ...
func = decorator.__call__("spam this").__call__(func)
My proposal is to make it such that:
@decorator
def func(): ...
...*can result* in this:
def func(): ...
func = decorator.__decoration_call__( func, "func")
And also so that this:
@decorator("spam this")
def func(): ...
...*can result* in this:
def func(): ...
func = decorator.__call__("spam this").__decoration_call__(func, "func")
I say "*can result*" because this has the following limitations:
1. occurs only when the callable object is employed as a decorator
2. occurs only when __decoration_call__ exists (and if it doesn't, just
revert back to the usual __call__(func_object, obj))
Here is an example to further illustrate the suggested behavior. It is not
intended to demonstrate a useful example. You could write a callable object
like this:
class Decorator:
def __call__(self, obj):
print("spam")
return obj
def __decoration_call__(self , obj, by_name):
print("eggs")
print(by_name)
return obj
decorator = Decorator()
And the behavior would be like this:
def func(): ...
func = decorator(func)
# Console output:
# 'spam'
@decorator
def func(): ...
# Console output:
# 'eggs'
# 'func'
In order to preserve current behavior, the built-in <class 'function'> type
would not grow its own new __decoration_call__ method. So in the case of
existing normal functions, and also in the case of existing custom Callable
objects, the fallback would be to just use __call__ as normal.
In this way, all of the proposal is optional. But as illustrated above, the
new dunder can be implemented by any customized Callable class.
I think __decoration_call__ by itself could provide a lot of interesting
possibilities for existing class and function decoration. But my main
motivation for this is as a different way of implementing the variable
decorator idea proposed by Jeremiah.
SECOND LAYER: VARIABLE DECORATORS
With __decoration_call__ in place, we could do a lot of interesting things
if we take the additional step of broadening existing decorator syntax such
that you can decorate not just functions and classes but also variables
(similar the variable decorator proposal recently proposed by Jeremiah).
But first let's define the variable decorator syntax behavior.
Using the same decorator object I have defined above, and borrowing
Jeremiah's original example, the new syntax would transform this:
@decorator variable
...into this:
variable = decorator.__decoration_call__(None, "variable")
EXPLANATION OF None: in the example above there is no assignment on the
decorated line, and so there is no object to pass to the dunder at that
point . Because of this, the first argument, which is usually the object
being decorated, is supplied as None in the call to __decoration_call__ .
Only the name being assigned to the as-yet uncreated object exists.
And the new syntax would transform this:
@decorator variable = "spam"
...into this:
variable = decorator.__decoration_call__(variable, "variable")
With the behavior of this variable decoration defined as shown above, here
are some of the interesting possibilities I was considering.
One possibility is avoiding the pitfalls of name repetition in the RAD
framework context previously mentioned by Stéfane Fermigier in Jeremiah's
variable decorator thread (see his post for examples). It could see other
frameworks, like web frameworks, making use if this too.
(Here I will repeat some of the possibilities suggested by Jeremiah in his
first post) The standard library and other library functions could, as
needed, make use of __decoration_call__ as they see fit. I could see
factories like namedtuple, make_dataclass, several typing factories, and
Enum, implementing __decoration_call__ so you can say things like:
@namedtuple Point = "x y z"
@make_dataclass Point = [("x", int), ("y", int), ("z", int)]
@typing.TypeVar T = (str, bytes)
@typing.NewType UserId = int
@enum.Enum Colors = "RED GREEN BLUE"
Admittedly, these examples are not all that compelling on their own, but I
think they read really well and they would allow you to avoid retyping a
name (and potentially making a mistake in retyping it), so I think I really
like it.
___
SIDEBAR
I also note that the above examples use a different idiom than suggested by
Jeremiah. He has suggested these:
@namedtuple("x y z") Point
@make_dataclass([("x", int), ("y", int), ("z", int)]) Point
@typing.TypeVar( (str, bytes) ) T
@typing.NewType(int) UserId
@enum.Enum("RED GREEN BLUE") Colors
...but using my proposal I do not see a way to make this idiom easily work
with the existing factories. I am not sure which of these two idioms people
would prefer. I can see pros and cons for both idioms. If people preferred
the second idiom, new decorator objects could be added for it.
___
One interesting possibility might be to modify inspect.getsource so that it
can retrieve the RHS of the line on which it appears*:
@inspect.getsource my_var = 1/2
print(my_var)
# Result
# '@inspect.getsource my_var = 1/2'
* NOTE: I am admittedly hand-waving this. Maybe there are reasons getsource
would not be able to do this?
Using the rhs string from this new version of getsource, it would be a
pretty simple matter for a symbolic math library like sympy to replace the
float value resulting from 1/2 in my_var with a symbolic math value:
@sympy.sympify my_var = 1/2
# Result is not 0.5, but:
# 1/2
...or create a Fraction (using a modified Fraction type):
@Fraction my_var = 1/2
# Result is not 0.5, but:
# Fraction(1, 2)
..or a Decimal (using a modified Decimal type):
@Decimal my_var = 0.5
# Result is not 0.5, but:
# Decimal('0.5')
MAYBE A DEFAULT DUNDER AFTER ALL...?
One final possibility: I have said above we would not supply any default
__decoration_call__ method, and instead fallback on __call__. But it might
be beneficial to supply a default __decoration_call__ with the following
implementation:
def __decoration_call__(self, func, by_name):
if func is None:
return self(by_name)
return self(func)
This would allow things like this, "out of the box":
@typing.NewType Color = str
@Color GREEN
...which becomes:
Color = typing.NewType.__decoration_call__(str, "Color")
GREEN = Color.__decoration_call__(None, "GREEN")
In the above, Color is just a stand-in for str. If there is a default
__decoration_call__ method as I wrote above, then:
Color.__decoration_call__(None, "GREEN")
...is actually just:
str.__decoration_call__(None, "GREEN")
..and the default implementation just returns:
str("GREEN")
...and assigns it to the name, GREEN.
Guess I'll leave it at that. Let's see how this is received.
---
Ricky.
"I've never met a Kentucky man who wasn't either thinking about going home
or actually going home." - Happy Chandler