Implementation of the Decorator Unification Protocol¶
Hereafter, implementations of Decorator
will be done as a callable
object, so usage of a Decorator
class with a __call__
method.
In the simplest form Pyc. 2 or Pyc. 3
without decorator parameters the class Decorator
needs an initializer with
a matching signature.
class Decorator:
def __init__(self, substitute=None, *args, **kwargs):
self.substitute = substitute
pass # Some init code ...
The call Decorator(function)
in Pyc. 3 leads to the
instantiation of the class Decorator
and via the execution of the
initializer __init__
the value substitute = function
is captured and
all remaining positional parameters go to *args
, all keyword
parameters end up in **kwargs
.
substitute
is mandatory and always the first positional parameter
and is declared with a default value None
. All other positional and
keyword parameters are optional, but if present, with respect to the
second requirement of the Decorator Unification Protocol they must be
declared with a default value, e.g.
class SomeWrapper:
def __init__(self, substitute=None, # Mandatory first pos. arg.
position=1, *args, # Optional arg with default
keyword='value', **kwargs): # Optional kwarg with default
self.substitute = substitute
self.position = position # Stuff using position
self.keyword = keyword # Stuff using keyword
pass # Some init code ...
After instantiation, the Decorator
instance is reassigned to the original
function
identifier. In order for this modified function
to be
callable, the Decorator
instance must be a callable object, i.e.
Decorator
must implement the __call__
method.
The
__call__
method performs the decorator code!
Since the Decorator
class must be able to serve various functions with
arbitrary signatures, __call__
must be defined with fully generic
signature.
class Decorator:
def __init__(self, substitute=None, *args, **kwargs):
self.substitute = substitute
pass # Some init code ...
def __call__(self, *args, **kwargs):
pass # Perform decorator code
In the next step, decorator arguments are added as in Pyc. 4.
Applying the decorator definition from Pyc. 11 would
then possibly lead to the assignment substitute = deco_args[0]
, which in
general is of course wrong and must therefore be intercepted.
When Decorator
is instantiated using the __init__
method, it must be
recognized whether the parameters passed are the correct initialization
parameters (substitute, *args, **kwargs)
or the decorator parameters
(*deco_args, **deco_kwargs)
, the two must be distinguishable from within
the initializer. This distinguishing criterion is defined via an
Arguments Unification Contract and can be structured in different ways, more
or less general. The more specific and detailed the contract, the broader the
variability of decorator arguments, and vice versa. Typical contracts are:
- Keyword Contract
This common contract prohibits the usage of positional
*deco_args
parameters, and all decorator parameters must be passed as keyword parameters**deco_kwargs
. This way, when passing pure keyword parameters to the initializer, no assignment is made to the first positional parametersubstitute
, and it retains its defaultNone
value. A check forself.substitute is None
can then be used in__init__
as well as in subsequent__call__
calls to detect which parameter set was submitted.- Value Contract
The value contract allows both
*deco_args
and**deco_kwargs
parameters but requires a specific value fordeco_args[0]
, typicallyNone
, to identify decorator arguments and to ensure thatsubstitute = None
remains unchanged.- Type Contract
The type contract relies on the correct type of the value captured into
substitute
: a correct substitute must be either acallable
or atype
or possibly one of its instances. As long as it can be ensured thatdeco_args[0]
does not accept one of these correct types forsubstitute
, this contract will also work.
Value Contract and Type Contract can be implemented as described above, but they provide a false sense of security, since misuse cannot be ruled out. In contrast, here, the Keyword Contract is not implementable due to the third requirement from the Decorator Unification Protocol, i.e. it remains as a pure recommendation, but is less error-prone due to its intuitive usability and the associated good programming style. And so:
The Value Contract is implemented, but is used as a Keyword Contract |
This leads to the following code:
class Decorator:
def __init__(self, substitute=None, *args, **kwargs):
self.substitute = substitute
if self.substitute is not None:
# Arguments (substitute, *args, **kwargs) captured
pass # Decoration without parameter(s)
else:
# Arguments (**deco_kwargs) captured
pass # Decoration with parameter(s)
def __call__(self, *args, **kwargs):
pass # Perform decorator code
With this code it is now possible to differentiate in the initializer between
a call to @Decorator(some_key=some_value) def function
with a decorator
parameter and @Decorator def function
without parameters:
In the latter case
Decorator(function)
is called, as the first positional variablefunction
is assigned tosubstitute
, and theif
branch is entered, in which the initalization process can be completed.In the first case
Decorator(some_key=some_value)
is called, i.e.substitute=None
remains unchanged and in theelse
branch the decorator parameters can be processed. However, this does not complete the initialization ofDecorator
, because the second argument level(function)
has not yet been processed. This happens in the immediately following method call__call__(*args, **kwargs)
, whereargs[0] = function
is captured.__call__
must now decide whether it was called to execute the actual decorator code, or to complete the unfinished initialization ofDecorator
. The latter is the case exactly whenself.substitute
still contains the valueNone
. In this case__call__
completes the initialization by assigning thefunction
according toself.substitute = args[0]
and returns the fully initialized objectself
at the end of this processing. All subsequent calls to__call__
then always lead to the actual decorator code.
class Decorator:
def __init__(self, substitute=None, *args, **kwargs):
self.substitute = substitute
if self.substitute is not None:
# Arguments (substitute, *args, **kwargs) captured
pass # Decoration without parameter(s)
else:
# Arguments (**deco_kwargs) captured
pass # Decoration with parameter(s)
def __call__(self, *args, **kwargs):
if self.substitute is None:
# Arguments (function) captured
self.substitute = args[0]
pass # Decoration with parameter(s)
return self # Finally, return completed instance
else:
pass # Perform decorator code
The signature of __call__
must be completely generic, since arbitrary
transfer parameters must be processable for the execution of the decorator
code. Furthermore, the signatures of __call__
and __init__
must be
compatible to the extent that both must be equally capable of handling the
argument structure (function, *args, **kwargs)
. This in turn prohibits
__init__
from accepting only keyword parameters, which prevents the
direct implementation of the Keyword Contract mentioned above.
This is the payoff for being able to meet the third requirement of the
Decorator Unification Protocol.
Now
@Decorator() def function
is interpreted as@Decorator()(function)
: In__init__
the valuesubstitute = None
remains unchanged, and in__call__
the assignmentself.substitute = args[0]
results inself.substitute = function
; the same result as with decorator@Decorator def function
without brackets.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |