__class_getitem__ проти __getitem__
Оновлено: 28.04.2023
Зазвичай підписка об’єкта з використанням квадратних дужок викликає метод екземпляра __getitem__(), визначений у класі об’єкта. Проте, якщо об’єкт, на який підписується, сам є класом, замість нього можна викликати метод класу __class_getitem__(). __class_getitem__() має повертати об’єкт GenericAlias, якщо він правильно визначений.
Представлений у вигляді expression obj[x], інтерпретатор Python виконує щось на зразок наступного процесу, щоб вирішити, чи слід __getitem__() або __class_getitem__() бути викликаним:
from inspect import isclass
def subscribe(obj, x):
"""Return the result of the expression 'obj[x]'"""
class_of_obj = type(obj)
# If the class of obj defines __getitem__,
# call class_of_obj.__getitem__(obj, x)
if hasattr(class_of_obj, '__getitem__'):
return class_of_obj.__getitem__(obj, x)
# Else, if obj is a class and defines __class_getitem__,
# call obj.__class_getitem__(x)
elif isclass(obj) and hasattr(obj, '__class_getitem__'):
return obj.__class_getitem__(x)
# Else, raise an exception
else:
raise TypeError(
f"'{class_of_obj.__name__}' object is not subscriptable"
)
У Python усі класи самі є екземплярами інших класів. Клас класу відомий як metaclass цього класу, і більшість класів мають клас type як метаклас. type не визначає __getitem__(), тобто такі вирази, як list[int], dict[str, float] і tuple[str, bytes] все призводить до виклику __class_getitem__():
>>> # list has class "type" as its metaclass, like most classes:
>>> type(list)
<class 'type'>
>>> type(dict) == type(list) == type(tuple) == type(str) == type(bytes)
True
>>> # "list[int]" calls "list.__class_getitem__(int)"
>>> list[int]
list[int]
>>> # list.__class_getitem__ returns a GenericAlias object:
>>> type(list[int])
<class 'types.GenericAlias'>
Однак, якщо клас має спеціальний метаклас, який визначає __getitem__(), підписка на клас може призвести до іншої поведінки. Приклад цього можна знайти в модулі enum:
>>> from enum import Enum
>>> class Menu(Enum):
... """A breakfast menu"""
... SPAM = 'spam'
... BACON = 'bacon'
...
>>> # Enum classes have a custom metaclass:
>>> type(Menu)
<class 'enum.EnumMeta'>
>>> # EnumMeta defines __getitem__,
>>> # so __class_getitem__ is not called,
>>> # and the result is not a GenericAlias object:
>>> Menu['SPAM']
<Menu.SPAM: 'spam'>
>>> type(Menu['SPAM'])
<enum 'Menu'>