Decorator Factory in Python
When studying the open source project ‘langchain’, I came across an intriguing function: xor_args(), found in the file ‘langchain/utils.py’. This function is an example of a Decorator Factory, a more advanced concept in Python.
def xor\_args(\*arg\_groups: Tuple\[str, ...\]) -> Callable:
"""Validate specified keyword args are mutually exclusive."""
def decorator(func: Callable) -> Callable:
def wrapper(\*args: Any, \*\*kwargs: Any) -> Callable:
"""Validate exactly one arg in each group is not None."""
counts = \[
sum(1 for arg in arg\_group if kwargs.get(arg) is not None)
for arg\_group in arg\_groups
\]
invalid\_groups = \[i for i, count in enumerate(counts) if count != 1\]
if invalid\_groups:
invalid\_group\_names = \[", ".join(arg\_groups\[i\]) for i in invalid\_groups\]
raise ValueError(
"Exactly one argument in each of the following"
" groups must be defined:"
f" {', '.join(invalid\_group\_names)}"
)
return func(\*args, \*\*kwargs)
return wrapper
return decorator
Firstly, let’s understand what a Decorator is in Python. A decorator is a function that modifies or enhances the behavior of another function. For instance, consider this example:
@uppercase
def say_hello():
return "Hello World!"
Here, @uppercase is a decorator which alters the function ‘say_hello()’, resulting in the string “HELLO WORLD!”. This decorator is implemented via an enclosed wrapper function as shown below:
def uppercase(function):
def wrapper():
func = function()
string_uppercase = func.upper()
return string_uppercase
return wrapper
However, this decorator has a limitation: it doesn’t allow passing parameters into the decorated function. This is where Decorator Factories come in.
A Decorator Factory differs from a decorator in that it returns a decorator, not just a function. Let’s modify the ‘uppercase’ decorator to accept parameters:
def uppercase(username):
def decorating(fn):
def wrapper():
func = function()
string_uppercase = func.upper()
return f"{string_uppercase}, {username}"
return wrapper
return decorating
Applying the decorator factory to ‘say_hello()’ would look like this.
@uppercase("David")
def say_hello():
return "Hello World!"
The function ‘say_hello()’ will now return the string “HELLO WORLD! David”.
Having understood the Decorator Factory in Python, let’s now revisit the function ‘xor_args()’.
xor_args(*arg_groups: Tuple[str, ...]) -> Callable:- This is the definition of the decorator factory functionxor_args. It takes one or more tuples of strings as its input arguments. Each tuple represents a group of keyword arguments.def decorator(func: Callable) -> Callable:- This is the definition of the decorator. It will take a functionfuncas input.def wrapper(*args: Any, **kwargs: Any) -> Callable:- This is the definition of a wrapper function inside the decorator. This function will replace or wrap the original functionfunc.- In the
wrapperfunction, it checks whether exactly one argument in each group is notNone. Thecountslist contains the number of non-None arguments in each group, computed by the list comprehension and thesumfunction. - If there is a group where not exactly one argument is non-None, it raises a
ValueErrorwith a message indicating which groups are invalid. - If there is no invalid group, it calls the original function
funcwith the provided arguments and keyword arguments and returns the result.
By using this Decorator Factory, we can easily enforce complex conditions on the arguments of a function, ensuring that our code is both efficient and easy to read.