Шаг 171.
Python: сборник рецептов. Метапрограммирование. Принудительная установка аргументной сигнатуры при использовании *args и **kwargs

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

Задача

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

Решение

    Для любой задачи, где вы хотите манипулировать сигнатурами вызова функций, вы должны использовать связанные с сигнатурами возможности из модуля inspect. Особенный интерес представляют два класса - Signature и Parameter. Вот интерактивный сеанс создания сигнатуры функции:

>>> from inspect import Signature, Parameter
>>> # Создаем сигнатуру для func(x, y=42, *, z=None)
>>> parms = [Parameter('x', Parameter.POSITIONAL_OR_KEYWORD),
	  Parameter('y', Parameter.POSITIONAL_OR_KEYWORD, default=42),
	  Parameter('z', Parameter.KEYWORD_ONLY, default=None)]
>>> sig = Signature(parms)
>>> print(sig)
(x, y=42, *, z=None)
>>>

    Когда вы получаете объект сигнатуры, вы легко можете связать его с *args и **kwargs, используя метод сигнатур bind(), как показано в этом простом примере:

>>> def func(*args, **kwargs):
	bound_values = sig.bind(*args, **kwargs)
	for name, value in bound_values.arguments.items():
		print(name, value)

>>> # Поэкспериментируем со значениями
>>> func(1, 2, z=3)
x 1
y 2
z 3
>>> func(1)
x 1
>>> func(1, z=3)
x 1
z 3
>>> func(y=2, x=1)
x 1
y 2
>>> func(1, 2, 3, 4)
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    func(1, 2, 3, 4)
  File "<pyshell#7>", line 2, in func
    bound_values = sig.bind(*args, **kwargs)
  File "C:\Python38\lib\inspect.py", line 3025, in bind
    return self._bind(args, kwargs)
  File "C:\Python38\lib\inspect.py", line 2951, in _bind
    raise TypeError(
TypeError: too many positional arguments
>>> func(y=2)
Traceback (most recent call last):
  File "<pyshell#13>", line 1, in <module>
    func(y=2)
  File "<pyshell#7>", line 2, in func
    bound_values = sig.bind(*args, **kwargs)
  File "C:\Python38\lib\inspect.py", line 3025, in bind
    return self._bind(args, kwargs)
  File "C:\Python38\lib\inspect.py", line 2940, in _bind
    raise TypeError(msg) from None
TypeError: missing a required argument: 'x'
>>> 

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

    Ниже приведен более конкретный пример принудительного использования сигнатур функций. В этом коде базовый класс определяет __init__() максимально широкого назначения, но подклассы должны предоставить ожидаемую сигнатуру.

>>> from inspect import Signature, Parameter
>>> def make_sig(*names):
    parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
             for name in names]
    return Signature(parms)

>>> class Structure:
    __signature__ = make_sig()
    def __init__(self, *args, **kwargs):
        bound_values = self.__signature__.bind(*args, **kwargs)
        for name, value in bound_values.arguments.items():
            setattr(self, name, value)

>>> # Пример использования
>>> class Stock(Structure):
    __signature__ = make_sig('name', 'shares', 'price')

>>> class Point(Structure):
    __signature__ = make_sig('x', 'y')

>>> 

    Вот пример работы класса Stock:

>>> import inspect
>>> print(inspect.signature(Stock))
(name, shares, price)
>>> s1 = Stock('ACME', 100, 490.1)
>>> s2 = Stock('ACME', 100)
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    s2 = Stock('ACME', 100)
  File "<pyshell#17>", line 4, in __init__
    bound_values = self.__signature__.bind(*args, **kwargs)
  File "C:\Python38\lib\inspect.py", line 3025, in bind
    return self._bind(args, kwargs)
  File "C:\Python38\lib\inspect.py", line 2940, in _bind
    raise TypeError(msg) from None
TypeError: missing a required argument: 'price'
>>> s3 = Stock('ACME', 100, 490.1, shares=50)
Traceback (most recent call last):
  File "<pyshell#26>", line 1, in <module>
    s3 = Stock('ACME', 100, 490.1, shares=50)
  File "<pyshell#17>", line 4, in __init__
    bound_values = self.__signature__.bind(*args, **kwargs)
  File "C:\Python38\lib\inspect.py", line 3025, in bind
    return self._bind(args, kwargs)
  File "C:\Python38\lib\inspect.py", line 2964, in _bind
    raise TypeError(
TypeError: multiple values for argument 'shares'
>>> 


Обсуждение

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

    В последнем примере данного решения имеет смысл создать объекты сигнатур через использование кастомных метаклассов. Вот альтернативная реализация, которая показывает, как это сделать:

>>> from inspect import Signature, Parameter
>>> def make_sig(*names):
    parms = [Parameter(name, Parameter.POSITIONAL_OR_KEYWORD)
             for name in names]
    return Signature(parms)

>>> class StructureMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        clsdict['__signature__'] = make_sig(*clsdict.get('_fields', []))
        return super().__new__(cls, clsname, bases, clsdict)

>>> class Structure(metaclass=StructureMeta):
    _fields = []
    def __init__(self, *args, **kwargs):
        bound_values = self.__signature__.bind(*args, **kwargs)
        for name, value in bound_values.arguments.items():
            setattr(self, name, value)

>>> # Пример
>>> class Stock(Structure):
    _fields = ['name', 'shares', 'price']

>>> class Point(Structure):
    _fields = ['x', 'y']

>>> 

    При определении собственных сигнатур часто бывает полезно сохранить сигнатуру в специальном атрибуте __signature__, как показано выше. Если вы сделаете это, то код, использующий модуль inspect для интроспекции, увидит сигнатуру и сообщит о ней как об условии вызова. Например:

>>> import inspect
>>> print(inspect.signature(Stock))
(name, shares, price)
>>> print(inspect.signature(Point))
(x, y)
>>>

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




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