Decorator Arguments Template

Next, Pyc. 13 should be extended to a full Decorator Arguments Template. This includes that the callable modified by the decorator behaves beyond the pure syntax also behaves semantically like the original object – just in the sense of Python’s duck typing:

Because function.__name__, function.__doc__, etc. returns the name, description, etc. of the original function, but the same queries to the decorated function return the name, description, etc. of the decorator, this original information must still be transferred by the decorator from the original object to the modified object when the decorator is instantiated. For this purpose there is the update_wrapper function in functools.

In addition, using Union from the typing library meaningful types will be annotated in the template.

All together then results in the Decorator Arguments Template.

Pyc. 14 Decorator Arguments Template
from functools import update_wrapper
from typing import Union

class Decorator:

    def __init__(self,
                 substitute: Union[type, callable, None] = None,
                 *args: object,
                 **kwargs: object) -> None:
        # Collect all named parameters
        self._substitute = substitute

        # --- Decorator Arguments Template (1/2)
        if self._substitute is not None:
            # Decoration without parameter(s)
            self._args = args         # Positional params to substitute
            self._kwargs = kwargs     # Keyword    params to substitute
            self._deco_args = ()      # Opt: no positional deco. params
            self._deco_kwargs = {}    # Opt: no keyword    deco. params
            update_wrapper(self, self._substitute, updated=())

            pass                      # Some code               (*)
        else:
            # Decoration with parameter(s)
            self._deco_args = args    # Positional decorator params
            self._deco_kwargs = kwargs# Keyword decorator params

            pass                      # Some code               (*)

    def __call__(self, *args: object, **kwargs: object) -> object:
        # --- Decorator Arguments Template (2/2)
        if self._substitute is None:
            # Decoration with parameter(s)
            self._substitute = args[0]# Assign the substitute
            self._args = args[1:]     # Positional params to substitute
            self._kwargs = kwargs     # Keyword    params to substitute
            update_wrapper(self, self._substitute, updated=())

            pass                      # Some code               (*)

            return self               # Finally, return completed instance
        else:
            # *** Decorator Code ***
                                      # Some code using: self._substitute,
            pass                      # self._args, self._kwargs and
                                      # self._deco_args, self._deco_kwargs
            return result             # Return some result

It is obvious that some parameters should not be changed afterwards, e.g. substitute. Therefore, for example, self._substitute became a weak private variable indicating possible conflict potential. It would be worth considering encapsulating critical parameters in a suitable property based on strong private getter/setter methods and variables.

After the instantiation of the decorator, each call to the decorated callable within the __call__ method runs into the Decorator Code section of the else branch because the query self._substitute is None always evaluates to False. This minor additional expense is more than compensated for by the benefits of implementing the Decorator Unification Protocol.

However, if the four code sections symbolized by pass # Some code in Pyc. 14 become more extensive in a more elaborate implementation, it may be useful to separate them into appropriate methods or functions.

But often the first three pass # some code sections, marked with an extra (*) in Pyc. 14, are omitted, because when the callable is initialized, mostly only data is collected that will be used later in the Decorator Code section, i.e. __call__ decomposes into a __post_init__ and the real decorator part. In such situations the above Decorator Arguments Template can be converted into a simpler Decorator Arguments Methods Template.

Pyc. 15 Decorator Arguments Methods Template
from functools import update_wrapper
from typing import Union

class Decorator:

    def __init__(self,
                 substitute: Union[type, callable, None] = None,
                 *args: object,
                 **kwargs: object) -> None:
        # Collect all named parameters
        self._substitute = substitute

        # --- Decorator Arguments Template (1/2)
        if self._substitute is not None:
            # Decoration without parameter(s)
            self._args = args         # Positional params to substitute
            self._kwargs = kwargs     # Keyword    params to substitute
            self._deco_args = ()      # Opt: no positional deco. params
            self._deco_kwargs = {}    # Opt: no keyword    deco. params
            update_wrapper(self, self._substitute, updated=())

            # Instantiation is done, assign the decorator
            self._call_method = self.decorator
        else:
            # Decoration with parameter(s)
            self._deco_args = args    # Positional decorator params
            self._deco_kwargs = kwargs# Keyword decorator params

            # Instantiation not yet completed, assign the post init
            self._call_method = self.__post_init__

    def __post_init__(self, *args: object, **kwargs: object) -> object:
        # --- Decorator Arguments Template (2/2)
        # Decoration with parameter(s)
        self._substitute = args[0]    # Assign the substitute
        self._args = args[1:]         # Positional params to substitute
        self._kwargs = kwargs         # Keyword    params to substitute
        update_wrapper(self, self._substitute, updated=())

        # Instantiation is done, assign the decorator
        self._call_method = self.decorator

        return self                   # Finally, return completed instance

    def __call__(self, *args: object, **kwargs: object) -> object:
        # Delegation
        return self._call_method(*args, **kwargs)

    def decorator(self, *args: object, **kwargs: object) -> object:
        # *** Decorator Code ***
                                      # Some code using: self._substitute,
        pass                          # self._args, self._kwargs and
                                      # self._deco_args, self._deco_kwargs
        return result                 # Return some result

In this variant from Pyc. 15, the redirection in the __call__ delegate now results in a slight extra effort. But in this case you have a clean separation between the multi-level data collection and the encapsulated data processing, and if necessary even the option to include different decorator methods via decorator parameters.

To illustrate the use of the Decorator Arguments Methods Template from Pyc. 15, here is another concrete example of a typical wrapper decorator that prints a configurable text before and after the execution of the original function when the modified function is called.

Pyc. 16 Simple example wrapper
from functools import update_wrapper
from typing import Union

class SomeWrapper:
    def __init__(self,
                 substitute: Union[type, callable, None] = None,
                 *args: object,
                 message_before: str = "ENTER",
                 message_after: str = "LEAVE",
                 **kwargs: object) -> None:
        # Collect all named parameters
        self._substitute = substitute
        self._message_before = message_before
        self._message_after = message_after

        # --- Decorator Arguments Template (1/2)
        if self._substitute is not None:
            # Decoration without parameter(s)
            self._args = args         # Positional params to substitute
            self._kwargs = kwargs     # Keyword    params to substitute
            self._deco_args = ()      # Opt: no positional deco. params
            self._deco_kwargs = {}    # Opt: no keyword    deco. params
            update_wrapper(self, self._substitute, updated=())

            # Instantiation is done, assign the decorator
            self._call_method = self.decorator
        else:
            # Decoration with parameter(s)
            self._deco_args = args    # Positional decorator params
            self._deco_kwargs = kwargs# Keyword decorator params

            # Instantiation not yet completed, assign the post init
            self._call_method = self.__post_init__

    def __post_init__(self, *args: object, **kwargs: object) -> object:
        # --- Decorator Arguments Template (2/2)
        # Decoration with parameter(s)
        self._substitute = args[0]    # Assign the substitute
        self._args = args[1:]         # Positional params to substitute
        self._kwargs = kwargs         # Keyword    params to substitute
        update_wrapper(self, self._substitute, updated=())

        # Instantiation is done, assign the decorator
        self._call_method = self.decorator

        return self                   # Finally, return completed instance

    def __call__(self, *args: object, **kwargs: object) -> object:
        # Delegation
        return self._call_method(*args, **kwargs)

    def decorator(self, *args: object, **kwargs: object) -> object:
        # *** Decorator Code ***

        # Action *before* original code
        print(f"{self.__name__}: {self._message_before}")

        # Delegation to original code
        if args or kwargs:
            result = self._substitute(*args, **kwargs)
        else:
            result = self._substitute(*self._args, **self._kwargs)

        # Action *after* original code
        print(f"{self.__name__}: {self._message_after}")

        return result               # Result from original code

The methods __init__, __post_init__ and __call__ were taken unchanged from the template Pyc. 15 and only in __init__ the two highlighted lines for the collection of the additional named parameters message_before and message_after were added. Finally the wrapper code is concentrated in the decorator method.

This amount of generic code in Pyc. 16 allows typical wrapper decorators such as above SomeWrapper to be derived from the base decorator BaseDecorator defined in decoratory.basic and primarily to override its decorator method and, if necessary, to override its method __init__ to collect additional named parameters. In this way, only the highlighted code in Pyc. 16 still needs to be written.

Pyc. 17 Inherited simple example wrapper
from decoratory.basic import BaseDecorator
from typing import Union

class SomeWrapper(BaseDecorator):
    def __init__(self,
                 substitute: Union[type, callable, None] = None,
                 *args: object,
                 message_before: str = "ENTER",
                 message_after: str = "LEAVE",
                 **kwargs: object) -> None:

        super().__init__(substitute, *args, **kwargs)

        # Collect all *additional* named parameters
        self._message_before = message_before
        self._message_after = message_after

    def decorator(self, *args: object, **kwargs: object) -> object:
        # *** Decorator Code ***

        # Action *before* original code
        print(f"{self.__name__}: {self._message_before}")

        # Delegation to original code
        if args or kwargs:
            result = self._substitute(*args, **kwargs)
        else:
            result = self._substitute(*self._args, **self._kwargs)

        # Action *after* original code
        print(f"{self.__name__}: {self._message_after}")

        return result               # Result from original code

If SomeWrapper is used for a static decoration with customized keyword parameter values of an example add printer function and then executed, the following printed output is produced:

Pyc. 18 Static decoration with SomeWrapper
@SomeWrapper(message_before="Calc started...",
             message_after="... Calc done!")
def add(a: int, b: int):
    print(f"Result: {a + b}")           # Print added numbers

# Execute the decorated add function
add(2, 3)                               # add: Calc started...
                                        # Result: 5
                                        # add: ... Calc done!

As expected, the decorated function prints a given enter and leave message before and after the actual result of the undecorated function.

Since the original add function does not define default values for its two parameters, a parameter-free call to add() would raise a TypeError. To avoid such situations, tailored default values can be set or overwritten via the extra parameters of a dyamic decoration, without having to change the original function.

Pyc. 19 Dynamic decoration with SomeWrapper
def add(a: int, b: int):
    print(f"Result: {a + b}")           # Print added numbers

add = SomeWrapper(add, 1, b=2)          # Tailored defaults a=1, b=2

# Execute the decorated add function
add()                                   # add: ENTER    (deco  default)
                                        # Result: 3     (extra default)
                                        # add: LEAVE    (deco  default)

This parameter substitution is done in the decorator code by if .. else .. distinction, where it is decided whether current parameters are present and can be used, or whether the extra parameters coming from the initialization of the decorator are taken as function arguments. For the sake of clarity, the principle of all or nothing is applied here, i.e. defaults are defined for all parameters and only used if no current parameters are transmitted. There is no mixing of current and default parameters. So, a call add(7) with an incomplete parameter set is explicitly not supported and again would raise a TypeError. However, it is up to everyone to adapt this behavior to own expectations.

For more sophisticated solutions, e.g. see Generic Wrapper in the following section Decorator Implementations.


◄ 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.