Шаг 166.
Python: сборник рецептов. Метапрограммирование. Написание декораторов, которые добавляют аргументы обернутым функциям

    На этом шаге мы рассмотрим особенности решения этой задачи.

Задача

    Вы хотите написать декоратор, который добавляет дополнительный аргумент в сигнатуру вызова обертываемой функции. Однако добавленный аргумент не может перекрываться с существующими условиями вызова функции.

Решение

    Дополнительные аргументы могут быть внедрены в сигнатуру вызова путем использования обязательных именованных аргументов. Рассмотрите такой декоратор:

>>> from functools import wraps
>>> def optional_debug(func):
	@wraps(func)
	def wrapper(*args, debug=False, **kwargs):
		if debug:
			print('Calling', func.__name__)
		return func(*args, **kwargs)
	return wrapper

>>> 
>>> @optional_debug
def spam(a, b, c):
	print(a, b, c)

	
>>> spam(1, 2, 3)
1 2 3
>>> spam(1, 2, 3, debug=True)
Calling spam
1 2 3
>>> 


Обсуждение

    Добавление аргументов в сигнатуру обернутой функции - не самое распространенное применение декораторов. Однако это может оказаться полезным приемом для избежания некоторых шаблонов повторения кода. Например, если у вас есть такой код:

def a(x, debug=False):
    if debug:
        print('Calling a')
    . . .

def b(x, y, z, debug=False):
    if debug:
        print('Calling b')
    . . .

def c(x, y, debug=False):
    if debug:
        print('Calling c')
    . . .
то вы можете отрефакторить его вот так:
@optional_debug
def a(x):
    . . .

@optional_debug
def b(x, y, z):
    . . .

@optional_debug
def c(x, y):
    . . .

    Реализация этого рецепта базируется на том факте, что обязательные именованные аргументы легко добавить в функции, которые также принимают параметры *args и **kwargs. При использовании обязательного именованного аргумента он выделяется в специальный случай и удаляется из последующих вызовов, которые используют только оставшиеся позиционные и именованные аргументы.

    Здесь есть хитрость, которая касается потенциального конфликта имен между добавленным аргументом и аргументами оборачиваемой функции. Например, если декоратор @optional_debug был применен к функции, которая уже имела аргумент debug, тогда все сломается. Если это вызывает опасения, то можно добавить дополнительную проверку:

>>> from functools import wraps
>>> import inspect
>>> def optional_debug(func):
	if 'debug' in inspect.getargspec(func).args:
		raise TypeError('debug argument already defined')
	@wraps(func)
	def wrapper(*args, debug=False, **kwargs):
		if debug:
			print('Calling', func.__name__)
		return func(*args, **kwargs)
	return wrapper

>>> 

    Последнее уточнение касается правильного управления сигнатурами функций. Проницательный программист поймет, что сигнатура обернутых функций будет неправильной. Например:

>>> @optional_debug
def add(x,y):
	return x+y

>>> import inspect
>>> print(inspect.signature(add))
(x, y)
>>> 

    Это может быть исправлено с помощью такой модификации:

>>> from functools import wraps
>>> import inspect
>>> def optional_debug(func):
	if 'debug' in inspect.getargspec(func).args:
		raise TypeError('debug argument already defined')
	@wraps(func)
	def wrapper(*args, debug=False, **kwargs):
		if debug:
			print('Calling', func.__name__)
		return func(*args, **kwargs)
	sig = inspect.signature(func)
	parms = list(sig.parameters.values())
	parms.append(inspect.Parameter('debug', inspect.Parameter.KEYWORD_ONLY, 
                           default=False))
	wrapper.__signature__ = sig.replace(parameters=parms)
	return wrapper

>>> 
>>> @optional_debug
def add(x,y):
	return x+y

>>> print(inspect.signature(add))
(x, y, *, debug=False)
>>> add(2, 3)
5
>>> 

    На следующем шаге мы рассмотрим использование декораторов для исправления определений классов.




Предыдущий шаг Содержание Следующий шаг