На этом шаге мы рассмотрим способ решения этой задачи.
Вы написали функцию или метод, который использует *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) >>>
На следующем шаге мы рассмотрим принуждение к использованию соглашений о кодировании в классах.