On Tue, May 25, 2021 at 12:30 AM Steven D'Aprano firstname.lastname@example.org wrote:
Your proposal appears to be:
@decorator(expression) targetname # transformed into: targetname = decorator("targetname", expression)
But in class and function decorator contexts, the equivalent syntax is equivalent to:
@decorator("spam this") def func(): pass # transformed to: def func(): pass func = decorator("spam this")(func)
except that in CPython it is not actually implemented as two separate steps like that.
But 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.)
Yes, you are correct that the current only use of a decorator, if it takes arguments, first executes the decorator factory and then passes in the function object. This proposal is more restrictive, and always calls the decorator once, whether parentheses follow the decorator name or not. I suppose the same could be done for variable decorating. It would look something like:
@namedtuple("x y z") Point
would translate to
Point = namedtuple("x y z")("Point")
only namedtuple doesn't work that way, so a new decorator factory would have to be created
@namedtuple_declare("x y z") Point
where namedtuple_declare worked something like this?
@classmethod def namedtuple_declare(field_names, *, rename=False, defaults=None, module=None): def delayed_namedtuple(typename): return namedtuple(typename, field_names, rename=rename, defaults=defaults, module=module) return delayed_namedtuple
Which maybe would turn out to be useful as you could pre-bake the decorator function. Others may feel differently but I don't find that as compelling as the original restricted proposal.
# now possible, but what does this get you over different instances of the same # class? point_tuple = namedtuple_declare("x y z") @point_tuple Origin @point_tuple Inflection @point_tuple Point
Of course even following the function decorators, the most basic uses would still act the same.
@str GREEN @symbols x
However, a lot of the "for free" uses I outlined would no longer be possible, and new factory functions would have to be added to the standard lib to facilitate creating a variable decorator for these use cases.
I realize the original proposal acts differently than current decorators, it's not currently valid to have the @ on the same line as the def, so until now it was inherently a multi-line statement but I was proposing a one-line only statement. The use of @ is not necessary to the overall proposal. I chose it because current users of python would understand that somewhere there is a function of the same name that is "getting in the way" of the binding. Also it visually draws attention to itself when scanning down the left hand side unlike some other options such as "as" which has historically been the only use of bind to the right.
To my mind, the only interesting part of this proposal is access to the binding target name. If we remove that from the proposal, variable decorators are nothing more than function calls, and we already can do that:
variable = decorator("spam this")
Yes, this was the only reason I had to bring this proposal forward. Although it could be argued that function decorators are still only function calls, they have proven to be popular and IMO create cleaner reading code.
Here's a counter-proposal: we have a special symbol which is transformed at compile-time to the left hand assignment target as a string. Let's say we make that special expression `@@` or the googly-eyes symbol.
Very interesting idea. I will say that turning this proposal back into something to be used in assignment does expand it reach, by how much would have to be decided, and can make for more confusion. You bring up spam = eggs = cheese = func(arg, @@) but what about spam = (eggs := func(@@, flags)) I think I know what the @@ is supposed to turn into. At least I do if @@ is valid in assignment expressions. If it is not, did I remember that it always binds to the assignment statement name?
I will say that this offers more flexibility in that the binding name can be reused anywhere on the lhs, although I don't think it's common to have these sorts of factories where the first argument isn't the name. I think it does place a larger cognitive burden on the developer (very small, but still larger IMO). They will still have to remember what the symbol is for name replacement and be willing to type that in instead. Will python users reach for this in the simple case? RED = "RED" is maybe faster to get out of my fingers than pausing and writing RED = @@. Will some scripts start to look like this?
PATH = os.getenv("PATH") VERY_LONG_USER_OPTION_TO_TYPE = os.getenv(@@) OTHEROPTIONHARDTOREAD = os.getenv(@@) PYTHONHOME = os.getenv("PYTHONHONE") # oops!
This is maybe overly critical, and I realize there is nothing stopping decorators from ending up just like this, but I think that if a developer starts out writing an assignment statement they might just finish it without reaching for @@. Starting a new line with a decorator is a distinct way of binding names.
Some things would be more intuitive though, especially around dicts and general uses of square brackets.
header = message.get(@@) @message.get header
verbose = @@ in options @operator.contains(options) verbose
RED = @@
# transforms to `RED = 'RED'`
... If there's no target, it resolves to None: ... Chained assignments transform to a tuple of target names: ... Sequence unpacking assignment gets transformed as a single comma-seperated string:
This feels like a lot of rules to keep straight, and a lot of verbose checking required for factory functions that want to support all three (four?! I would also be in favor of SyntaxError) possibilities.
Target resolution is performed at compile-time, not runtime. There's no
global variable called "@@". That means that this won't work:
code = compile("type(@@, bases, namespaces)", '', 'single') # above transforms to `type(None, bases, namespace)` myclass = eval(code)
But I think that restriction is fine.
A very interesting counter. Lots to consider here, appreciate the detailed reply!