[Python-ideas] Proposal for new-style decorators

Christophe Schlick cschlick at gmail.com
Tue Apr 26 19:10:03 CEST 2011


I am really sorry for splitting the text. My first post was in one
piece but got blocked by the moderators. I didn't want to create some
ridiculous suspense at the end of the first part. Here is the second
part...

---

Part 2 - The new-style syntax for decorators:

Here is the code of the same decorators as in Part 1 above, but the
proposed syntax. The first decorator ('new_style_repeat_fix') is
created without parameters, while the second one
('new_style_repeat_var') uses arguments 'n' and 'trace' with the same
role as previously:

#---
@decorator
def new_style_repeat_fix(self, *args, **keys):
  """docstring for decorating function"""
  print "apply %r on %r" % (self.deco.__name__, self.func.__name__)
  for n in range(3): self.func(*args, **keys)

@decorator(n=3, trace=True)
def new_style_repeat_var(self, *args, **keys):
  """docstring for decorating function"""
  if self.trace:
    print "apply %r on %r" % (self.deco.__name__, self.func.__name__)
  for n in range(self.n): self.func(*args, **keys)
#---

When examining the new-style syntax, one can notice that there are
basically four characteristics that distinguish NSD from OSD:

* Each NSD is decorated by a newly-introduced callable class called
'decorator' (code provided in Part 3) by using one of two possible
decorating forms. The first form '@decorator' is employed for
decorators that do not need any parameter. The second form
'@decorator(arg1=val1, arg2=val2...)' is employed to specify a
sequence of named arguments (combined with their default values) that
are passed to the decorator using the standard notation for keyword
arguments.

* The role of the 'decorator' class is to generate the decorated
function (i.e. the inner nested function with the classic OSD syntax)
and to broadcast it to the decorating function as its first argument
'self'. When the second decorating form is used, all keyword arguments
used in '@decorator(...)' are automatically injected as
meta-attributes in this decorated function 'self'. In the example
above, the two decorator arguments 'n' and 'trace' are available
within the code of 'new_style_repeat_var' as 'self.n' and 'self.trace'
respectively. This mechanism avoids one level of nested functions used
in standard OSD.

* In addition to these decorator arguments, the decorating process
also injects two other meta-attributes in the decorated function:
'self.deco' represents a reference to the decorating function, while
'self.func' represents a reference to the undecorated function. If
there are several chained decorators, the mechanism is made recursive
(more on this later). Note that this implies a slight name
restriction: neither 'deco' nor 'func' can be used as the name of a
parameter passed to the decorator, as this would generate collision in
the corresponding namespace. An alternative might be to consider the
two references as "special" attributes and rename them as
'self.__deco__' and 'self.__func__' respectively. I have no clear
opinion about the pros/cons of the two alternatives.

* Finally, note that each NSD has the same 3-argument signature:
(self, *args, **keys). The first argument 'self' has been explained
above. The two others 'args' and 'keys' respectively represent the set
of positional and keyword arguments, as usual. However, all the values
in either 'args' or 'keys' are not meant to be used by the decorating
function, but always directly passed to the undecorated function. This
means that the statement 'self.func(*args, **keys)' will always appear
somewhere in the code of an NSD. Following this mechanism in the
decorating function avoids the other level of nested functions used in
standard OSD, and guarantees that flat functions are always
sufficient.

Once the NSD have been defined with the new syntax, they can be used
to decorate functions using the standard @-notation, either for single
or multiple decoration. For instance:

#---
@new_style_repeat_fix
def testA(first=0, last=0):
  """docstring for undecorated function"""
  print "testA: first=%s last=%s" % (first, last)

@new_style_repeat_var(n=5) # 'n' is changed, 'trace' keeps default value
def testB(first=0, last=0):
  """docstring for undecorated function"""
  print "testB: first=%s last=%s" % (first, last)

@new_style_repeat_var # both 'n' and 'trace' keep their default values
@new_style_repeat_fix
@new_style_repeat_var(n=5, trace=False) # both 'n' and 'trace' are changed
def testC(first=0, last=0):
  """docstring for undecorated function"""
  print "testC: first=%s last=%s" % (first, last)
#---

When applying a decorator without arguments, or when *all* its
arguments use their default values, the parenthesis after the
decorator name may be dropped. In other words, '@mydeco' and
'@mydeco()' are totally equivalent, whether 'mydeco' takes arguments
or not. This solves a non-symmetric behavior of standard OSD that has
always bothered me: '@old_style_repeat_fix' works but
'@old_style_repeat_fix()' does not, and inversely
'@old_style_repeat_var()' works but '@old_style_repeat_var' does not.

Note also that in the case of chained decorators, each decoration
level stores its own set of parameters, so there is no conflict when
applying the same decorator several times on the same function, as
done with 'new_style_repeat_var' on 'testC'.

Now let's play a bit with some introspection tools:

#---
>>> testA
<function <deco>testA...>

>>> testB
<function <deco>testB...>

>>> testC
<function <deco><deco><deco>testC...>
#---

To explicitely expose the decoration process, a '<deco>' substring is
added as a prefix to the '__name__' attribute for each decorated
function (more precisely, there is one '<deco>' for each level of
decoration, as can be seen with 'testC'). So, each time a '<deco>'
prefix is encountered, the user knows that the reference to the
corresponding undecorated function (resp. decorating function) is
available through the meta-attribute '.func' (resp. '.deco'). When
calling 'help' on a decorated function, this principle is clearly
displayed, and the user can thus easily obtain useful information,
including correct name/signature/docstring:

#---
>>> help(testA)
<deco>testA(*args, **keys)
    use help(testA.func) to get genuine help

>>> testA.func, testA.deco
(<function testA...>, <function new_style_repeat_fix...>)

>>> help(testA.func)
testA(first=0, last=0)
    docstring for undecorated function

>>> help(testA.deco)
new_style_repeat_fix(self, *args, **keys)
    docstring for decorating function
#---

In the case of chained decorators, the same principle holds
recursively. As can be seen in the example below, all information
relative to a multi-decorated function (including all decorator
arguments used at any decoration level) can be easily fetched by
successive applications of the '.func' suffix:

#---
>>> help(testC)
<deco><deco><deco>testC(*args, **keys)
    use help(testC.func.func.func) to get genuine help

>>> testC.func, testC.deco, testC.n, testC.trace
(<function <deco><deco>testC...>, <function new_style_repeat_var...>, 3, True)

>>> testC.func.func, testC.func.deco
(<function <deco>testC...>, <function new_style_repeat_fix...>)

>>> testC.func.func.func, testC.func.func.deco, testC.func.func.n, testC.func.func.trace
(<function testC...>, <function new_style_repeat_var...>, 5, False)

>>> help(testC.func.func.func)
testC(first=0, last=0)
    docstring for undecorated function
#---

------
to be continued in Part 3...

CS



More information about the Python-ideas mailing list