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.
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.
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.
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.
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:
@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.
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.
© Copyright 2020-, Martin Abel, eVation. All Rights Reserved. |