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.

Pyc. 9 Decorator without decorator parameters
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.

Pyc. 10 Decorator without decorator parameters but with extra parameters and default values
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.

Pyc. 11 Callable Decorator without decorator parameters
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 parameter substitute, and it retains its default None value. A check for self.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 for deco_args[0], typically None, to identify decorator arguments and to ensure that substitute = 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 a callable or a type or possibly one of its instances. As long as it can be ensured that deco_args[0] does not accept one of these correct types for substitute, 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:

Pyc. 12 Decorator with/without decorator parameters (1st approach)
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 variable function is assigned to substitute, and the if 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 the else branch the decorator parameters can be processed. However, this does not complete the initialization of Decorator, because the second argument level (function) has not yet been processed. This happens in the immediately following method call __call__(*args, **kwargs), where args[0] = function is captured. __call__ must now decide whether it was called to execute the actual decorator code, or to complete the unfinished initialization of Decorator. The latter is the case exactly when self.substitute still contains the value None. In this case __call__ completes the initialization by assigning the function according to self.substitute = args[0] and returns the fully initialized object self at the end of this processing. All subsequent calls to __call__ then always lead to the actual decorator code.

Pyc. 13 Decorator with/without decorator parameters (2nd approach)
 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 value substitute = None remains unchanged, and in __call__ the assignment self.substitute = args[0] results in self.substitute = function; the same result as with decorator @Decorator def function without brackets.


◄ prev

up

next ►

Legal Notice

Privacy Policy

Cookie Consent

Sphinx 7.2.6 & Alabaster 0.7.12

© Copyright 2020-, Martin Abel, eVation. All Rights Reserved.