На этом шаге мы рассмотрим решение этой задачи.
Вы хотите иметь возможность включить принудительную проверку типов аргументов функции.
Перед тем как показывать код решения, напомним, что цель этого рецепта - получить средства для принудительной проверки правильности типов входных аргументов функции. Вот короткий пример, который иллюстрирует идею:
>>> @typeassert(int, int) def add(x, y): return x + y >>> add(2, 3) 5 >>> add(2, 'hello') Traceback (most recent call last): File "<pyshell#7>", line 1, in <module> add(2, 'hello') File "<pyshell#2>", line 18, in wrapper raise TypeError( TypeError: Argument y must be <class 'int'> >>>
Теперь приведем реализацию декоратора @typeassert:
>>> from inspect import signature >>> from functools import wraps >>> def typeassert(*ty_args, **ty_kwargs): def decorate(func): # Если мы в оптимизированном режиме, отключаем проверку типов if not __debug__: return func # Отображаем имена аргументов функции на предоставленные типы sig = signature(func) bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments @wraps(func) def wrapper(*args, **kwargs): bound_values = sig.bind(*args, **kwargs) # Принудительно проверяем типы предоставленных аргументов ассертами for name, value in bound_values.arguments.items(): if name in bound_types: if not isinstance(value, bound_types[name]): raise TypeError( 'Argument {} must be {}'.format(name, bound_types[name]) ) return func(*args, **kwargs) return wrapper return decorate >>>
Вы обнаружите, что этот декоратор достаточно гибок и позволяет указать типы для всех (или для подмножества) аргументов функции. Более того, типы могут быть указаны позиционно или с помощью именованных аргументов. Вот пример:
>>> @typeassert(int, z=int) def spam(x, y, z=42): print(x, y, z) >>> spam(1, 2, 3) 1 2 3 >>> spam(1, 'hello', 3) 1 hello 3 >>> spam(1, 'hello', 'world') Traceback (most recent call last): File "<pyshell#12>", line 1, in <module> spam(1, 'hello', 'world') File "<pyshell#2>", line 18, in wrapper raise TypeError( TypeError: Argument z must be <class 'int'> >>>
В этом рецепте приведен пример продвинутого декоратора, который вводит несколько важных и полезных концепций.
Во-первых, одна из особенностей декораторов в том, что они применяются только один раз, во время определения функции. В некоторых случаях вы можете захотеть отключить функциональность, добавленную декоратором. Чтобы сделать это, просто заставьте ваш декоратор вернуть необернутую функцию. В решении приведенный ниже фрагмент кода возвращает неизмененную функцию, если значение глобальной переменной __debug__ установлено на False (как и в том случае, когда интерпретатор Python запускается в оптимизированном режиме с параметрами -O или -OO): . . .
def decorate(func): # Если мы в оптимизированном режиме, отключаем проверку типов if not __debug__: return func . . .
Следующая тонкость написания декораторов в том, что это подразумевает изучение и работу с аргументной сигнатурой оборачиваемой функции. Оптимальный инструмент для этого - функция inspect.signature(). Она позволяет вам извлечь информацию о сигнатуре из вызываемого объекта. Например:
>>> from inspect import signature >>> def spam(x, y, z=42): pass >>> sig = signature(spam) >>> print(sig) (x, y, z=42) >>> sig.parameters mappingproxy(OrderedDict([('x', <Parameter "x">), ('y', <Parameter "y">), ('z', <Parameter "z=42">)])) >>> sig.parameters['z'].name 'z' >>> sig.parameters['z'].default 42 >>> sig.parameters['z'].kind <_ParameterKind.POSITIONAL_OR_KEYWORD: 1> >>>
В первой части нашего декоратора мы используем метод сигнатур bind_partial(), чтобы выполнить частичную привязку предоставленных типов к именам аргументов. Вот пример того, как это работает:
>>> bound_types = sig.bind_partial(int, z=int) >>> bound_types <BoundArguments (x=<class 'int'>, z=<class 'int'>)> >>> bound_types.arguments OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)]) >>>
На примере этой частичной привязки вы заметите, что недостающие аргументы просто игнорируются (то есть нет привязки для аргумента у). Однако наиболее важная часть привязки - это создание упорядоченного словаря bound_types.arguments. Этот словарь отображает имена аргументов на предоставленные значения в том же порядке, что и сигнатура функции. В случае нашего декоратора это отображение содержит ассерты типов, которые мы будем принудительно проверять.
В функции-обертке, созданной декоратором, используется метод sig.bind(). bind() похож на bind_partial(), за исключением того, что он не позволяет пропускать аргументы. Вот как это работает:
>>> bound_values = sig.bind(1, 2, 3)
>>> bound_values.arguments
OrderedDict([('x', 1), ('y', 2), ('z', 3)])
>>>
Используя это отображение, относительно легко обеспечить требуемые проверки:
>>> for name, value in bound_values.arguments.items(): if name in bound_types.arguments: if not isinstance(value, bound_types.arguments[name]): raise TypeError() >>>
В этом решении есть тонкий аспект: ассерты не применяются к непредоставленным аргументам со значениями по умолчанию. Например, этот код работает, хотя значение items по умолчанию имеет "неправильный" тип:
>>> @typeassert(int, list) def bar(x, items=None): if items is None: items = [] items.append(x) return items >>> bar(2) [2] >>> bar(2, 3) Traceback (most recent call last): File "<pyshell#37>", line 1, in <module> bar(2, 3) File "<pyshell#2>", line 18, in wrapper raise TypeError( TypeError: Argument items must be <class 'list'> >>> bar(4, [1, 2, 3]) [1, 2, 3, 4] >>>
Последнюю точку в обсуждении этого приема проектирования ставит такой вопрос: использовать аргументы декораторов или аннотации функций? Почему бы, например, не написать вот такой декоратор, который будет "обращать внимание" на аннотации:
@typeassert def spam(x:int, y, z:int=42): print(x, y, z)
Возможная причина не использовать аннотации в том, что к каждому аргументу функции можно прикрепить только одну аннотацию. Поэтому если аннотации используются для проверки типов, они уже не могут быть использованы ни для чего другого. Также в этом случае декоратор @typeassert не будет работать с функциями, которые используют аннотации для других целей. Путем использования аргументов декоратора, как показано в решении, декоратор получает более общее назначение и может быть использован с любой функцией - даже с теми, которые используют аннотации.
Дополнительную информацию об объектах сигнатур функций можно получить в PEP 362, а также в документации модуля inspect.
На следующем шаге мы рассмотрим определение декораторов как части класса.