Decorators for variables

tldr: Using three method declarations or chaining method calls is ugly, why not allow variables and attributes to be decorated too? Currently the way to create variables with custom get/set/deleters is to use the @property decorator or use property(get, set, del, doc?), and this must be repeated per variable. If I were able to decorate multiple properties with decorators like @not_none or something similar, it would take away a lot of currently used confusing code. Feedback is appreciated. ----------- The current ways to define the getter, setter and deleter methods for a variable or attribute are the following: @property def name(): """ docstring """ ... code @name.setter def name(): ... code @name.deleter def name(): ... code and var = property(getter, setter, deleter, docstring) These two methods are doable when you only need to change access behaviour changes on only one variable or property, but the more variables you want to change access to, the longer and more bloated the code will get. Adding multiple restrictions on a variable will for instance look like this: var = decorator_a(decorator_b(property(value))) or @property def var(self): return decorator_a.getter(decorator_b..getter(self._value)) ... etc or even this @decorator_a @decorator_b def var(self): pass I propose the following syntax, essentially equal to the syntax of function decorators: @decorator var = some_value which would be the same as var = decorator(some_value) and can be chained as well: @decorator @decorator_2 var = some_value which would be var = decorator(decorator_2(some_value)) or similarly var = decorator(decorator_2()) var = some_value The main idea behind the proposal is that you can use the decorator as a standardized way to create variables that have the same behaviour, instead of havng to do that using methods. I think that a lot can be gained by specifying a decorator that can decorate variables or properties. Note that many arguments will be the same as for function decorators (PEP 0318), but then applied to variable/property/attribute declaration.

On Fri, Apr 1, 2016 at 8:18 AM, Matthias welp <boekewurm@gmail.com> wrote:
I don't see the relationship between this paragraph and the rest of your proposal. How does a decorator in place of an explicit function call prevent repetition?
What about augmented assignment? Should this work? @float var += 20 And would that be equivalent to: @float var = var + 20 Or: var = var + float(20) Also, what about attributes and items? @decorator x.attr = value @another_decorator d['foo'] = value
By "methods" you mean "function composition", right? Otherwise I don't understand what methods have got to do with this.

If you were to define a variable you currently could use the @property for each variable, which could take up to 3 declarations of the same name per use of the pattern. Using a decorator might take that down to only 1 extra line.
What about augmented assignment? Should this work?
The steps it would go through were these: 1. the value of the statement is calculated. e.g. val + 20 in the first case given. 2. the decorator is applied on that value 3. the return value from the decorator is then assigned to the variable. This is, again, very similar to the way function decorators work, and a short-handed method to make property access more transparent to the programmer.
Also, what about attributes and items?
I have not yet thought about that, as they are not direct scope variables. The idea was there to decorate the attribute or variable at the moment it would get defined, not per se after definition.
instead of having to do that using methods
By "methods" you mean "function composition", right?
Sorry for my terminology, I meant function calls, but function composition is indeed what would happen effectively.

On Fri, Apr 1, 2016 at 9:46 AM, Matthias welp <boekewurm@gmail.com> wrote:
An example of the transformation that you intend would help here. If you're intending this as a @property replacement then as far as I can see you still need to write up to three functions that define the property's behavior.

On Sat, Apr 2, 2016 at 1:18 AM, Matthias welp <boekewurm@gmail.com> wrote:
The whole point of decorator syntax for classes and functions is that their definitions take many lines, and the decoration belongs as part of the function signature or class definition. At the top of a function block is a line which specifies the function name and any arguments, and then you have the docstring. Similarly with classes - name, superclasses, metaclass, docstring. All up the top. Placing the decorator above that allows for an extremely convenient declarative syntax that keeps all that information together. Also, the decorator syntax replaces the redundant names: def functionname(): ... functionname = decorator(functionname) where the function first gets defined using its name, and then gets rebound (which involves looking up the name and then assigning the result back) - three separate uses of the name. In contrast, you're asking for syntax to help you modify an expression. Expressions already don't need the decorator syntax, because we can replace this: var = some_value var = decorator(var) with this: var = decorator(some_value) as in your example. Decorator syntax buys us nothing above this. ChrisA

On Fri, Apr 1, 2016 at 10:18 AM Matthias welp <boekewurm@gmail.com> wrote:
tldr: Using three method declarations or chaining method calls is ugly, why not allow variables and attributes to be decorated too?
Focusing on attributes, not variables in general. Check out how Django dealt with this ( https://docs.djangoproject.com/en/1.9/topics/db/models/#quick-example). And SQLAlchemy (http://flask-sqlalchemy.pocoo.org/2.1/models/). Do their solutions satisfy?

The thing I'm missing in those solutions is how it isn't chainable. If I would want something that uses access logging (like what Django and SQLAlchemy are doing), and some 'mixin' for that variable to prevent cycle protection, then that would be hard. The only other way is using function composition, and that can lead to statements that are too long to read comfortably.

On Fri, Apr 01, 2016 at 07:49:41PM +0200, Matthias welp wrote:
I'm not sure how function composition is harder to read than decorator syntax: @logging @require(int) @ensure(str) @validate @spam @eggs x = 999 versus: x = logging( require(int)( ensure(str)( validate( spam( eggs( 999 )))))) is not that different. Sure, you have a few extra brackets, but you have fewer @ symbols. And if you're going to do that sort of thing a lot, you just need a couple of helper functions: def compose(f, g): # Return a new function which applies f(g(args)). def composed(*args, **kw): return f(g(*args, **kwargs)) return composed def helper(use_logging=False, requires=None, ensures=None, use_validate=False, use_spam=False, use_eggs=False): funcs = [] if use_logging: funcs.append(logging) if requires: funcs.append(require(requires)) if ensures: funcs.append(ensure(ensures)) if use_validate: funcs.append(validate) # likewise for spam and eggs if not funcs: # Identity function returns whatever it is given. return lambda arg: arg else: f = funcs[0] for g in funcs[1:]: f = compose(g) return f all_validation = helper(True, int, str, True, True, True) x = all_validation(999) -- Steve

On Fri, Apr 1, 2016 at 8:18 AM, Matthias welp <boekewurm@gmail.com> wrote:
I don't see the relationship between this paragraph and the rest of your proposal. How does a decorator in place of an explicit function call prevent repetition?
What about augmented assignment? Should this work? @float var += 20 And would that be equivalent to: @float var = var + 20 Or: var = var + float(20) Also, what about attributes and items? @decorator x.attr = value @another_decorator d['foo'] = value
By "methods" you mean "function composition", right? Otherwise I don't understand what methods have got to do with this.

If you were to define a variable you currently could use the @property for each variable, which could take up to 3 declarations of the same name per use of the pattern. Using a decorator might take that down to only 1 extra line.
What about augmented assignment? Should this work?
The steps it would go through were these: 1. the value of the statement is calculated. e.g. val + 20 in the first case given. 2. the decorator is applied on that value 3. the return value from the decorator is then assigned to the variable. This is, again, very similar to the way function decorators work, and a short-handed method to make property access more transparent to the programmer.
Also, what about attributes and items?
I have not yet thought about that, as they are not direct scope variables. The idea was there to decorate the attribute or variable at the moment it would get defined, not per se after definition.
instead of having to do that using methods
By "methods" you mean "function composition", right?
Sorry for my terminology, I meant function calls, but function composition is indeed what would happen effectively.

On Fri, Apr 1, 2016 at 9:46 AM, Matthias welp <boekewurm@gmail.com> wrote:
An example of the transformation that you intend would help here. If you're intending this as a @property replacement then as far as I can see you still need to write up to three functions that define the property's behavior.

On Sat, Apr 2, 2016 at 1:18 AM, Matthias welp <boekewurm@gmail.com> wrote:
The whole point of decorator syntax for classes and functions is that their definitions take many lines, and the decoration belongs as part of the function signature or class definition. At the top of a function block is a line which specifies the function name and any arguments, and then you have the docstring. Similarly with classes - name, superclasses, metaclass, docstring. All up the top. Placing the decorator above that allows for an extremely convenient declarative syntax that keeps all that information together. Also, the decorator syntax replaces the redundant names: def functionname(): ... functionname = decorator(functionname) where the function first gets defined using its name, and then gets rebound (which involves looking up the name and then assigning the result back) - three separate uses of the name. In contrast, you're asking for syntax to help you modify an expression. Expressions already don't need the decorator syntax, because we can replace this: var = some_value var = decorator(var) with this: var = decorator(some_value) as in your example. Decorator syntax buys us nothing above this. ChrisA

On Fri, Apr 1, 2016 at 10:18 AM Matthias welp <boekewurm@gmail.com> wrote:
tldr: Using three method declarations or chaining method calls is ugly, why not allow variables and attributes to be decorated too?
Focusing on attributes, not variables in general. Check out how Django dealt with this ( https://docs.djangoproject.com/en/1.9/topics/db/models/#quick-example). And SQLAlchemy (http://flask-sqlalchemy.pocoo.org/2.1/models/). Do their solutions satisfy?

The thing I'm missing in those solutions is how it isn't chainable. If I would want something that uses access logging (like what Django and SQLAlchemy are doing), and some 'mixin' for that variable to prevent cycle protection, then that would be hard. The only other way is using function composition, and that can lead to statements that are too long to read comfortably.

On Fri, Apr 01, 2016 at 07:49:41PM +0200, Matthias welp wrote:
I'm not sure how function composition is harder to read than decorator syntax: @logging @require(int) @ensure(str) @validate @spam @eggs x = 999 versus: x = logging( require(int)( ensure(str)( validate( spam( eggs( 999 )))))) is not that different. Sure, you have a few extra brackets, but you have fewer @ symbols. And if you're going to do that sort of thing a lot, you just need a couple of helper functions: def compose(f, g): # Return a new function which applies f(g(args)). def composed(*args, **kw): return f(g(*args, **kwargs)) return composed def helper(use_logging=False, requires=None, ensures=None, use_validate=False, use_spam=False, use_eggs=False): funcs = [] if use_logging: funcs.append(logging) if requires: funcs.append(require(requires)) if ensures: funcs.append(ensure(ensures)) if use_validate: funcs.append(validate) # likewise for spam and eggs if not funcs: # Identity function returns whatever it is given. return lambda arg: arg else: f = funcs[0] for g in funcs[1:]: f = compose(g) return f all_validation = helper(True, int, str, True, True, True) x = all_validation(999) -- Steve
participants (6)
-
Chris Angelico
-
Greg Ewing
-
Ian Kelly
-
Matthias welp
-
Michael Selik
-
Steven D'Aprano