Эта глава в большей мере посвящена метаклассам — особым конструкторам классов в Python. Здесь же подробно рассматривается упоминавшееся где-то давно сопоставление шаблону. Просто потому что больше этого делать негде. Также здесь затрагиваются основы работы с аннотациями, подробно этот вопрос обсуждается в отдельной главе про статическую типизацию.
Быстрый поиск
Метаклассы
Перед тем, как погрузиться в изучение метаклассов, рассмотрим специальный метод __init_subclass__ — он позволяет при создании класса-наследника передать ему дополнительные параметры:
>>> class Isub:
... def __init_subclass__(self, *args, **kwargs):
... print(args, kwargs)
... super().__init_subclass__(*args, **kwargs)
...
>>> class C(Isub):
... pass
...
() {}
>>> class Isub:
... def __init_subclass__(self, **kwargs):
... print(kwargs)
... super().__init_subclass__()
...
>>> class C(Isub, parameters="QQ"):
... pass
...
{'parameters': 'QQ'}
>>>
Этот метод по умолчанию является классовым, но никак не помечается (@classmethod)
Теперь про сами метаклассы. Зададимся вопросом: с помощью чего можно сделать класс? Можно явно его описать, можно вызвать функцию-генератор (мы уже рассматривали type), сделать декоратор для одного класса и получать другой класс (при этом можно непосредственно создавать новый класс, а можно просто патчить текущий). Также, как мы только что изучили, можно породить производный класс и встраиванием __init_subclass__ его улучшать.
Метакласс — это своего рода универсальная заглушка, которая своей сутью, созданием классов и метаклассов (да, рекурсия здесь работает), решает бесконечную диллему первосоздателя. В обычной жизни метакласс почти наверное не нужен для полноценного программирования. Но его возможности дают дополнительные ходы в создании объектов.
Например, рассмотрим функцию type, которая, вообще говоря, класс, и, если быть совсем точным¸ метакласс.
>>> type
<class 'type'>
>>> type(type)
<class 'type'>
>>>
>>> C = type('C', (), {'A': 123})
>>> C
<class '__main__.C'>
>>>
Что будет, если мы создадим производный от type класс? Получим другой конструктор классов — другой метакласс. Для его применения необходимо указать его в специальном параметре metaclass при описании класса, по умолчанию там зафиксировано значение type.
>>> class overtype(type):
... def __init__(self, *args, **kwargs):
... print(args, kwargs)
... super().__init__(*args, **kwargs)
...
>>> C = overtype('C', (), {'A': 123})
('C', (), {'A': 123}) {}
>>>
>>> class C(metaclass=overtype):
... A = 100500
...
('C', (), {'__module__': '__main__', '__qualname__': 'C', '__firstlineno__': 1, 'A': 100500, '__static_
attributes__': ()}) {}
>>>
Структура метакласса
Протокол метакласса включает в себя 4 основных метода:
from pprint import pprint
class ctype(type):
@classmethod
def __prepare__(metacls, name, bases, **kwds):
pprint(("prepare", name, bases, kwds))
return super().__prepare__(name, bases, **kwds)
def __new__(metacls, name, parents, ns, **kwds):
pprint(("new", metacls, name, parents, ns, kwds))
return super().__new__(metacls, name, parents, ns)
def __init__(cls, name, parents, ns, **kwds):
pprint(("init", cls, parents, ns, kwds))
return super().__init__(name, parents, ns)
def __call__(cls, *args, **kwargs):
pprint(("call", cls, args, kwargs))
return super().__call__(*args, **kwargs)
class C(int, metaclass=ctype, parameter="See me"):
field = 42
print("Create an instance:")
c = C("100500", base=16)
print(c, c % 256, type(c), type(c % 256))
- Классовый метод
__prepare__срабатывает самым первым и создаёт пространство имён будущего класса; - Статический метод
__new__создаёт экземпляр метакласса, т.е. сам класс; - Метод
__init__вызывается в момент описания нашего класса и срабатывает как инициализатор для класса; - Метод
__call__описывает привычное действие создания экземпляра самого класса;
C __prepare__ и __new__ нужно быть особо внимательными: это строго классовый и статический методы, причём для первого явное указания обязательно, а для второго — нет.
>>> from pprint import pprint
...
... class ctype(type):
... @classmethod
... def __prepare__(metacls, name, bases, **kwds):
... pprint(("prepare", name, bases, kwds))
... return super().__prepare__(name, bases, **kwds)
...
... def __new__(metacls, name, parents, ns, **kwds):
... pprint(("new", metacls, name, parents, ns, kwds))
... return super().__new__(metacls, name, parents, ns)
...
... def __init__(cls, name, parents, ns, **kwds):
... pprint(("init", cls, parents, ns, kwds))
... return super().__init__(name, parents, ns)
...
... def __call__(cls, *args, **kwargs):
... pprint(("call", cls, args, kwargs))
... return super().__call__(*args, **kwargs)
...
... class C(int, metaclass=ctype, parameter="See me"):
... field = 42
...
... print("Create an instance:")
... c = C("100500", base=16)
... print(c, c % 256, type(c), type(c % 256))
...
('prepare', 'C', (<class 'int'>,), {'parameter': 'See me'})
('new',
<class '__main__.ctype'>,
'C',
(<class 'int'>,),
{'__firstlineno__': 21,
'__module__': '__main__',
'__qualname__': 'C',
'__static_attributes__': (),
'field': 42},
{'parameter': 'See me'})
('init',
<class '__main__.C'>,
(<class 'int'>,),
{'__firstlineno__': 21,
'__module__': '__main__',
'__qualname__': 'C',
'__static_attributes__': (),
'field': 42},
{'parameter': 'See me'})
Create an instance:
('call', <class '__main__.C'>, ('100500',), {'base': 16})
1049856 0 <class '__main__.C'> <class 'int'>
>>>
Примеры использования метаклассов
На практике метаклассы используются для описания каких=то специальных свойств на уровне работы самих классов.
Первый пример — ненаследуемый класс. В момент создания класса проверяется наличие метакласса в MRO создаваемого класса (в параметр parent приезжает именно mro-последовательность родителей без текущего класса [его же ещё не существует]), при его наличии создания блокируется.
>>> class Final(type):
... def __new__(metacls, cls, parents, namespace):
... for parent in parents:
... if isinstance(parent, Final):
... raise TypeError(f"No inheritancefrom {parent}")
... return super().__new__(metacls, cls, parents, namespace)
...
>>> class E(metaclass=Final):
... a = 1
...
>>> class D(E):
... pass
...
Traceback (most recent call last):
File "<python-input-26>", line 1, in <module>
class D(E):
pass
File "<python-input-24>", line 5, in __new__
raise TypeError(f"No inheritancefrom {parent}")
TypeError: No inheritancefrom <class '__main__.E'>
>>>
Ещё один классический пример использования метаклассов — Синглтон, класс с единственным экземпляром. Проверка осуществляется непосредственно перед созданием экземпляра класса, при наличии экземпляра просто присылается ссылка на него.
>>> class Singleton(type):
... _instance = None
... def __call__(cls, *args, **kwargs):
... if cls._instance is None:
... cls._instance = super().__call__(*args, **kwargs)
... return cls._instance
...
>>> class A(metaclass=Singleton):
... B = 100500
...
>>> a = A()
>>> a.B
100500
>>> a
<__main__.A object at 0x7fe3d0aa1400>
>>> b = A()
>>> b.B
100500
>>> b.B = 42
>>> a.B
42
>>> b
<__main__.A object at 0x7fe3d0aa1400>
>>> a is b
True
>>>
Оператор Match-case
От метаклассов переходим к Match-case. Формальное его название — «Структурное сопоставление шаблону». Синтаксис шаблонов сильно отличается от Python, поскольку в match-case используется другой вид парсера — PEG-парсеру вместо LL —, это позволяет отделять контексты в шаблонах.
Классические стуктуры мы уже рассматривали: match-case отлично заменяет последовательности if-elif-else конструкций для проверки множества условий на вид данных:
>>> cmd = "help command"
>>> cmd.split()
['help', 'command']
>>> if cmd.split() == ['help']:
... print("help")
... elif cmd.split() == ['help', 'command']:
... print('smth more')
... # …
...
smth more
>>>
>>> match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case _:
... print("UNKNOWN")
...
Command help
>>> cmd = 'PAWSHFPA'
>>> match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case _:
... print("UNKNOWN")
...
UNKNOWN
>>>
Напомним, что в шаблонах поддерживаются связанные переменные. Более того, поскольку дял match-case не создаётся отдельного пространства имён (Python вообще не делает их без явного указания или строгих случаев, как в функциях, например), переменные продолжают жить после match-case:
>>> cmd = 'help me!'
... match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case _:
... print("UNKNOWN")
...
help on me!
>>> command
'me!'
>>>
Переменные могут быть множественными, т.е. поддерживать упаковку и распаковку объектов:
>>> cmd = 'help me please!'
... match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case _:
... print("UNKNOWN")
...
UNKNOWN
>>> cmd = 'help me please!'
... match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds]:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
Hepl on ['me', 'please!']
>>>
Принцип поиска шаблона — до первого совпадения. При этом для вариантов одного уровня можно задавать альтернативы, для них используется перегруженный оператор побитового ИЛИ:
>>> cmd = 'usage'
... match cmd.split():
... case ['help']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds]:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
UNKNOWN
>>> cmd = 'usage'
... match cmd.split():
... case ['help' | 'usage']:
... print('HELP')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds]:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
HELP
>>>
Для альтернатив также могут использоваться связанные переменные. Для этого используется дополнительное ключевое слово as:
>>> cmd = 'usage'
... match cmd.split():
... case ['help' | 'usage' as topic]:
... print(f'HELP = {topic}')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds]:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
HELP = usage
>>>
К шаблонам можно добавлять фильтры:
>>> cmd = 'help go go'
... match cmd.split():
... case ['help' | 'usage' as topic]:
... print(f'HELP = {topic}')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds] if len(cmds) < 3:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
Hepl on ['go', 'go']
>>> cmd = 'help go go go'
... match cmd.split():
... case ['help' | 'usage' as topic]:
... print(f'HELP = {topic}')
... case ['help', 'command']:
... print('Command help')
... case ['help', command]:
... print(f'help on {command}')
... case ['help', *cmds] if len(cmds) < 3:
... print('Hepl on', cmds)
... case _:
... print("UNKNOWN")
...
UNKNOWN
>>>
В Match-case предусмотрено сопоставление структур и типов согласно параметрам создания объектов (то есть можно задать просто класс, а можно указать, какие поля в нём должны проверяться для более точного совпадения):
>>> match v := eval(input()):
... case int(n):
... print(f'integer {n}')
... case float():
... print(f'Float {v}')
...
123
integer 123
>>> match v := eval(input()):
... case int(n):
... print(f'integer {n}')
... case float():
... print(f'Float {v}')
...
123.123
Float 123.123
>>> # это про структуру класса, его первое поле мы закрываем связанной переменной
>>>
При этом мы можем также указывать связанные переменные, например, для параметров из структуры. n из прошлого примера именно такая.
>>> class C:
... A = 123
...
>>> c = C()
>>> c.A = 100500
>>>
>>> match c:
... case C(A=100500):
... print('Y')
...
Y
>>> c.A = 42
>>> match c:
... case C(A=100500):
... print('Y')
...
>>>
Можно подвязываться как поимённо, так и позиционно
>>> from collections import namedtuple
... C = namedtuple("C", "a b")
... for c in C(2, 3), C(1, 2), C(2, 1), C(42, 100500), C(-1, -1):
... match c:
... case C(2, 3): # Позиционное перечисление
... print(C, "with 2 and 3")
... case C(a=1, b=V) | C(a=V, b=1): # Поимённое перечисление, одна переменная связана
... print(C, "with 1 and", V)
... case C(42): # Позиционное задание только одного поля
... print("Special", C)
... case C(A, b=B): # Одна переменная связана позиционно, другая — именем
... print("Any", C, "with", A, "and", B)
...
<class '__main__.C'> with 2 and 3
<class '__main__.C'> with 1 and 2
<class '__main__.C'> with 1 and 2
Special <class '__main__.C'>
Any <class '__main__.C'> with -1 and -1
>>>
Можно искать по шаблонам словари, однако при этом будет браться минимальный блок словаря, удовлетворяющий требованиям:
>>> d = {1:2, 3:4, 5:6}
... match d:
... case {3:4}:
... print("3:4")
...
3:4
>>> d = {1:2, 3:4, 5:7}
... match d:
... case {3:4, **tail}:
... print("3:4", tail)
...
3:4 {1: 2, 5: 7}
>>>
Аннотации
Зададим небольшое вводное описание аннотаций. Их возникновение, в первую очередь, связано с главным достоинством и недостатком Python — с неявной типизацией. Duck Typing помогает быстро писать короткий код, не требуя перечислений всех типов. Использование функций становится использованием алгоритмов, а не преобразователей типов. Неявная динамическая строгая типизация помогает делать код читаемым и маленьким, однако все ошибки с типами становятся RunTime-ошибками. Статический анализ невозможно добавить без описания типов, и проверять приходится прямо на ходу. Аннотации — это синтаксис питона, который позволяет сообщать подсказки аргументов и возвращаемых значений функций, а также полей классов.
>>> class C:
... A: int
... B: float = 1.2
... def __init__(self, a: int, b: float) -> None:
... self.A, self.B = a, b
... def sum(self) -> float:
... return self.A + self.B
...
>>> c = C('#WS', 'POI')
>>> c.sum()
'#WSPOI'
>>>
Просмотреть аннотации объектов можно с помощью специального модуля inspect, в котором есть выделенные для этого функции:
>>> import inspect
>>> inspect.get_annotations(C)
{'A': <class 'int'>, 'B': <class 'float'>}
>>> ann = inspect.get_annotations(C)
>>> ann['A']
<class 'int'>
>>> ann['B']
<class 'float'>
>>> ann = inspect.get_annotations(C.sum)
>>> ann
{'return': <class 'float'>}
>>> inspect.get_annotations(C.__init__)
{'a': <class 'int'>, 'b': <class 'float'>, 'return': None}
При этом наличие аннотации не означает наличие объекта или требование строгого следования ему. Это, буквально, лишь подсказка; логика использования её для непосредственной проверки ложится на плечи внешних систем: отдельных анализаторов, работающих поверх (MyPy); просто специальных алгоритмов разработчика и др.
>>> C.B
1.2
>>> C.A
Traceback (most recent call last):
File "<python-input-87>", line 1, in <module>
C.A
AttributeError: type object 'C' has no attribute 'A'
>>>
Более того, аннотациями может быть вообще любой питоновский объект. Это просто хранилище, с которым сверяются алгоритмы, которые должны это делать
>> class C:
... a: "askfiha" = 12
... b: 42 = 'QQ'
...
>>> inspect.get_annotations(C)
{'a': 'askfiha', 'b': 42}
>>>