Implementation of the Decorator Unification Protocol¶
Hereafter, implementations of Decorator will be done as a callable
object, so usage of a Decorator class including 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), both options 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_argsparameters, 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 defaultNonevalue. A check forself.substitute is Nonecan 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_argsand**deco_kwargsparameters but requires a specific value fordeco_args[0], typicallyNone, to identify decorator arguments and to ensure thatsubstitute = Noneremains unchanged.- Type Contract
The type contract relies on the correct type of the value captured into
substitute: a correct substitute must be either acallableor atypeor 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 variablefunctionis assigned tosubstitute, and theifbranch is entered, in which the initalization process can be completed.In the first case
Decorator(some_key=some_value)is called, i.e.substitute=Noneremains unchanged and in theelsebranch 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] = functionis 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.substitutestill contains the valueNone. In this case__call__completes the initialization by assigning thefunctionaccording toself.substitute = args[0]and returns the fully initialized objectselfat 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 functionis interpreted as@Decorator()(function): In__init__the valuesubstitute = Noneremains unchanged, and in__call__the assignmentself.substitute = args[0]results inself.substitute = function; the same result as with decorator@Decorator def functionwithout brackets.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |
|||