__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'>