Skip to content

Class and Object

Класи, об'єкти

Клас

Клас - це структура даних в Python, яка дозволяє об'єднувати дані та функції, що з ними пов'язані, в одному об'єкті.

  • Клас визначає атрибути (змінні) та методи (функції), які можна використовувати для маніпуляції цими атрибутами.
  • Клас є шаблоном для створення об'єктів, тобто, він описує структуру об'єкта.
  • Класи дозволяють використовувати концепцію наслідування для створення нових класів, які можуть успадковувати атрибути та методи від інших класів.
  • В Python, всі речі є об'єктами, і класи є способом створення цих об'єктів. Клас створює об'єкт свого власного типу.
  • Об'єкти зберігаються в хіпі - heap. А референси - в стеку.
  • Змінна - референс - посилання на об'єкт.

Від чого успадковуються класи і метакласи?

Summary

Класи успадковуються від базового класу object, а метакласи - від базового метакласу type.

MyClass.__base__ == object           # MyClass is a subclass of object.
MyMetaClass.__base__ == type         # MyMetaClass is a subclass of type.

Всі класи в Python успадковуються від базового класу object. Це означає, що object є базовим класом для всіх класів у Python 3.

class MyClass(object):
   pass

Але можна не вказувати object, оскільки він є базовим класом за замовчуванням

class MyClass:
   pass

Метаклас - це клас, який контролює створення і поведінку інших класів.

Метакласи в Python успадковуються від базового метакласу type.

Приклад створення метакласу, який додає атрибут до всіх класів

class AddAttributeMeta(type):
   def __init__(cls, name, bases, attrs):      
       attrs['status'] = 'new'  # Add attribute 'status' to the class
       super().__init__(name, bases, attrs)

class MyClass(metaclass=AddAttributeMeta):
   pass

obj = MyClass()
print(obj.status)  # "new"

Що робить метод __init__?

Метод __init__ є конструктором класу в Python. Він автоматично викликається при створенні нового об'єкта або екземпляра класу і використовується для ініціалізації початкових значень атрибутів цього об'єкта. У методі __init__ можна вказати параметри, які передаються при створенні об'єкта, і встановити їх як атрибути об'єкта. Це дозволяє підготувати об'єкт до подальшої роботи, встановити початкові стани та значення.

class Car:
    def __init__(self, color, brand):
        self.color = color
        self.brand = brand

Що таке __new__ і чим він відрізняється від __init__? У якій послідовності вони виконуються

Summary

Основна різниця між цими двома методами полягає в тому, що __new__ обробляє створення об'єкта, а __init__ обробляє його ініціалізацію.

__new__ викликається автоматично при виклику імені класу (при створенні екземпляра), тоді як __init__ викликається кожного разу, коли екземпляр класу повертається __new__, передаючи повернений екземпляр у __init__ як параметр self. Тому навіть якщо зберегти екземпляр глобально/статично та повертати його кожного разу з __new__, для нього все одно кожного разу буде викликатися __init__.

З цього випливає, що спочатку викликається __new__, а потім __init__.

a = A()
1) a = object.__new__(A)
2) object.__init__(a)

Що таке self в Python?

У Python self - це ключове слово, яке використовується для визначення екземпляра або об'єкта класу. Тобто це посилання на поточний об'єкт або екземпляр класу. Використовується для доступу до атрибутів та методів об'єкта всередині класу. Ім'я self є конвенційним та може бути будь-яким, але зазвичай використовується саме self для читабельності коду.

У методах класу, наприклад у методі __init__, self вказує на об'єкт, для якого викликається цей метод. У всіх інших методах він також передає посилання на об'єкт, який викликав цей метод, що дозволяє працювати з атрибутами та методами цього об'єкта. Іде першим параметром.

Це допомагає уникнути плутанини між локальними змінними та атрибутами класу, і дозволяє зручно та однозначно звертатися до властивостей об'єкта всередині його методів.

self потрібно передати першим параметром, бо усі методи знаходяться в класі, а не в інстансі. Без нього клас не буде знати, до кого застосовувати метод.

Як отримати список атрибутів об'єкта

Функція dir повертає список рядків - полів об'єкта. Поле __dict__ містить словник виду {поле -> значення} (або ж викликати vars). dir() повертає список імен, включаючи всі доступні атрибути, включаючи вбудовані, тоді як vars() повертає лише атрибути, які були визначені в самому об'єкті.

Є також геттери та сеттери для отримання доступу до атрибутів.

<list> = dir(<object>)                     # Names of object's attributes (incl. methods).
<dict> = vars(<object>)                    # Dict of writable attributes. Also <obj>.__dict__.
<bool> = hasattr(<object>, '<attr_name>')  # Checks if getattr() raises an AttributeError.
value  = getattr(<object>, '<attr_name>')  # Raises AttributeError if attribute is missing.
setattr(<object>, '<attr_name>', value)    # Only works on objects with '__dict__' attribute.
delattr(<object>, '<attr_name>')           # Same. Also `del <object>.<attr_name>`.

Що таке магічні методи та для чого вони потрібні

Summary

Магічні методи (дандр методи, Magic methods, dundr methods) - це методи, імена яких починаються і закінчуються подвійним підкресленням. Магічні тому, що майже ніколи не викликаються явно. Їх викликають вбудовані функції або синтаксичні конструкції. Ці методи дозволяють налаштовувати поведінку об’єктів класу при взаємодії з іншими об’єктами або при виконанні певних операцій. Тобто вони дозволяють класам мати або перевизначити певну поведінку при виконанні різних операцій.

Наприклад, функція len() викликає метод __len__() переданого об'єкта. Метод __add__(self, other) автоматично викликається при використанні оператора + для додавання.

Найвживаніші магічні методи - __init__ - конструктор класу. Приймає екземпляр класу. - __new__ - приймає не instance, а об'єкт класу. Ініт вже приймає інстанс. Створює даний об'єкт, після цього на нього може відбуватись ініціалізація - __class__ - визначає клас або тип, екземпляром якого являється об'єкт. Клас і тип - різні назви одного і того ж - type(x) == x.__class__ - __name__ - імя класу - __str__, __repr__ - повертають рядкове представлення обєкту. repr - для девелопера, str - для прінта - __getattr__, __setattr__, __delattr__ - викликаються, якщо ми пробуємо знайти атрибут, якого немає - __getattribute__ - викликається при спробі отримати значення атрибуту. Якщо цей метод перевизначений, стандартний механізм пошуку значень атрибутів не буде задіяний. - __dict__ - сховище атрибутів, визначених користувачем. Пошук в ньому проводиться під час виконання і при пошуку враховується __dict__ класу обєкту і базових класів. - __bases__ - список прямих батьків - __iter__ - повертає ітератор - __eq__ - перевірка на рівність з іншим об'єктом - __add__ - додавання до іншого об'єкта - __call__ - дозволяє екземпляру класу поводитися як функція. Це означає, що об’єкт класу можна викликати як функцію з допомогою дужок

Яка різниця між __str__ та __repr__?

__str__ та __repr__- повертають рядкове представлення об'єкту.

  • repr() (representation) - повертає рядок, що представляє об'єкт у вигляді, зручному для розробника. Це те, що ми бачимо, коли об'єкт відображається на консолі Python або налагодженні. Щоб вивести для виводу об'єкт в його "представленні" (__repr__) у рядках форматування (f-strings), використовується спеціальний форматний ключ !r: f"Object: {obj!r}"
  • str() (string) - повертає рядок, що представляє об'єкт у вигляді зручному для користувача. Це те, що друкує функція print(). Якщо не реалізований - то повертає __repr__.

Як у класі посилатися на батьківський клас (super())

Функція super() використовується як метод для створення об'єкта-проксі який дозволяє доступитися до методів батьківського класу. Цей об'єкт-проксі забезпечує механізм динамічного зв'язування і дозволяє викликати методи батьківського класу з поточного класу та отримувати доступ до його атрибутів.

Конкретний об'єкт, який повертає super(), залежить від контексту виклику. Він представляє наступний клас у ланцюжку успадкування (методи якого можуть бути викликані) після поточного класу.

При виклику методу через super(), контекст виклику передається батьківському методу, що дозволяє коректно обробляти дані відповідно до правил успадкування.

class NextClass(FirstClass):
    def __init__(self, x):
        super().__init__()
        self.x = x

Найчастіше super() використовується для магічних методів. Наприклад, щоб викликати __init__() батьківського класу.

Чи можливе множинне наслідування

Так, в класах в Python можна вказати більше одного батька для похідного класу. На відміну від, наприклад, Java.

Що таке MRO

Summary

MRO (Method Resolution Order) - порядок вирішення методу. Це алгоритм пошуку методу в разі, якщо у класу є два або більше батьків.

При успадкуванні класів нового стилю застосовується правило MRO (порядок вирішення методів), тобто лінійний обхід дерева класів, при цьому вкладений елемент успадкування стає доступним у атрибуті __mro__ даного класу. Такий алгоритм називається C3-лініаризація. Наслідування за правилом MRO здійснюється приблизно за наступним порядком.

  1. Перерахування всіх класів, успадкованих екземпляром, за правилом пошуку DFLR для класичних класів, причому клас включається в результат пошуку стільки разів, скільки він зустрічається при обході.
  2. Перегляд у отриманому списку дублікатів класів, з яких видаляються всі, крім останнього (останнього справа) дубліката в списку.

Обхід у глибину та зліва направо - DFLR 1. Спочатку екземпляр 2. Потім його клас 3. Далі всі суперкласи його класу з обходом спочатку у глибину, а потім зліва направо 4. Використовується перше знайдене входження.

Упорядкування за правилом MRO застосовується при успадкуванні та виклику вбудованої функції super(), яка завжди викликає наступний клас за правилом MRO (відносно точки виклику).

Приклад успадкування в не-ромбовидних ієрархічних деревах

class D:          attr = 3      #  D:3   E:2
class B(D):       pass          #   |     |
class E:          attr = 2      #   B    C:1
class C(E):       attr = 1      #    \   /
class A(B, C):    pass          #      A
X = A()                         #      |
print(X.attr)                   #      X

>>> DFLR = [X, A, B, D, C, E]
>>> MRO = [X, A, B, D, C, E, object]
>>> Outputs string "3"

Приклад успадкування в ромбовидних ієрархічних деревах

class D:          attr = 3      #     D:3   
class B(D):       pass          #    /   \
class C(D):       attr = 1      #   B   C:1
class A(B, C):    pass          #    \   /
X = A()                         #      A
print(X.attr)                   #      |
...                             #      X

>>> DFLR = [X, A, B, D, C, D]
>>> MRO = [X, A, B, C, D, object] (keeps only the last duplicate D)
>>> Outputs string "1"

Що таке проблема ромба (Diamond problem)

При ромбовидному успадкуванні потрібно визначити, який метод класу слід викликати. У Python вирішується за допомогою MRO.

Приклад успадкування в ромбовидних ієрархічних деревах

class D:          attr = 3      #     D:3   
class B(D):       pass          #    /   \
class C(D):       attr = 1      #   B   C:1
class A(B, C):    pass          #    \   /
X = A()                         #      A
print(X.attr)                   #      |
...                             #      X

>>> DFLR = [X, A, B, D, C, D]
>>> MRO = [X, A, B, C, D, object] (keeps only the last duplicate D)
>>> Outputs string "1"

Що таке міксіни?

Міксін (mix-in, примішання) - це шаблон проектування в ООП, коли до ланцюжка успадкування додається невеликий допоміжний клас.

Наприклад, є клас

class NowMixin:
    def now(self):
        return datetime.datetime.utcnow()

Тоді будь-який клас, успадкований з цим міксіном, матиме метод now().

У назвах міксінів зазвичай додають слово Mixin, оскільки не існує жодного механізму для розрізнення повноцінного класу і міксіна. Міксін технічно є звичайним класом.

Чому рівний вираз object() == object()

Завжди неправда, оскільки за замовчуванням об'єкти порівнюються за полем id (адресою в пам'яті), якщо тільки не перевизначений метод __eq__.

__slots__

Класи зберігають поля та їх значення у прихованому словнику __dict__. Оскільки словник є змінною структурою, можна додавати та видаляти поля класу в будь-який момент. Параметр __slots__ у класі жорстко фіксує набір полів класу. Слоти використовуються, коли клас може мати дуже багато полів, наприклад, в деяких ORM або коли критична продуктивність, оскільки доступ до слоту відбувається швидше, ніж пошук у словнику, або коли в процесі виконання програми створюється мільйони екземплярів класу, застосування __slots__ дозволить економити пам'ять.

Слоти активно використовуються в бібліотеці requests.

Мінуси: не можна присвоїти класу поле, якого немає в слотах. Не працюють методи __getattr__ і __setattr__. Рішення: включити в __slots__ елемент __dict__.

class Person:
    __slots__ = ('name', 'age')

    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person("John", 30)

try:
    person.address = "New York"  # This will raise an AttributeError
except AttributeError as e:
    print("Error:", e)


person.name = "Alice"  # Setting values for attributes defined in __slots__
person.age = 25

Атрибути. В чому сенс _value, __value

Атрибути - це змінні класу. Можуть бути атрибути класу або атрибути інстансу. Атрибути класу спільні для всіх інстансів.

3 типи атрибутів - Публічні - public - a = 1 - не мають обмежень за доступом і можуть бути доступні з будь-якого місця програми. - Захищені - protected - _a = 1 - змінна чи метод не призначені для використання поза методами класу, однак атрибут доступний по цьому імені. - Приватні - private - __a = 1 - ззовні недоступний по цьому імені, тільки всередині. Але все-одно буде доступний під ім'ям a._ClassName__a.

Public Атрибути і методи, доступні ззовні класу.

Privat Поле класу з одним ведучим підкресленням вказує на те, що параметр використовується тільки всередині класу. При цьому воно доступне для звернення ззовні. Це обмеження доступу тільки на рівні угоди.

class Foo(object):
    def __init__(self):
        self._bar = 42

Foo()._bar
>>> 42

Сучасні IDE, наприклад PyCharm, виділять звернення до поля з підкресленням, але це не призведе до помилки в процесі виконання.

Protected Поля з подвійним підкресленням доступні всередині класу, але недоступні ззовні та недоступні нащадкам. Це досягається наступним прийомом: інтерпретатор присвоює таким полям імена у вигляді _<ClassName>__<fieldName>. Описаний механізм називається name mangling або name decoration.

class Parent():
    def __init__(self):
        self.__foo = 42

class Child(Parent): 
    def __init__(self): 
        super().__init__()

Parent().__foo
>>> AttributeError: 'Parent' object has no attribute '__foo'
Parent()._Parent__foo
>>> 42
Child()._Parent__foo
>>> 42

Що таке качина типізація

Summary

Качина типізація (Duck typing) - вид динамічної типізації, коли межі використання об'єкту визначаються його поточним набором методів і властивостей, на відміну від успадкування від певного класу.

Іншими словами, вважається, що об'єкт реалізує інтерфейс, якщо він містить всі методи цього інтерфейсу, незалежно від зв'язків у ієрархії успадкування та належності до якого-небудь конкретного класу.

Синоніми - неявна типізація, латентна типізація. Застосовуваної в таких мовах програмування - Perl, Smalltalk, Python, Objective-C, Ruby, JavaScript, Groovy, ColdFusion, Boo, Lua, Go, C#.

Назва терміна походить від англійського «duck test» («качиний тест»), який в оригіналі звучить так: «If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck». («Якщо воно виглядає як качка, плаває як качка і крякає як качка, то це напевно і є качка»).

Що таке перевантаження оператора (overloading)?

Overloading (перевантаження) оператора - це можливість змінювати поведінку вбудованих операторів (+, -, *) в своєму класі . Це досягається за допомогою спеціальних методів в класах, які визначають, як оператори мають взаємодіяти з об'єктами цього класу.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        sum_x = self.x + other.x
        sum_y = self.y + other.y
        return Vector(sum_x, sum_y)

v1 = Vector(1, 2)
v2 = Vector(3, 4)

result = v1 + v2
print("({0}, {1})".format(result.x, result.y))  # (4, 6)

У цьому прикладі метод __add__ перевантажено для класу Vector, що дозволяє об'єктам цього класу використовувати оператор + для додавання векторів.

Як створити клас з допомогою type()

type(name, bases, dict, **kwds)
class X:  # using class keyword
    ...

class Y(X):
    a = 1

Y = type('Y', (X,), dict(a=1))  # using type()

Перший аргумент вказує ім'я класу, другий - кортеж базових класів - (X,), а третій - словник атрибутів класу.

Методи. @staticmethod, @classmethod

Метод - це функція, яка викликається від інстансу класу. Метод зберігаються в класі, а не інстансі. Використовує параметр self, який представляє екземпляр класу.

class MyClass:
    def instance_method(self):
        return f"Instance method called. Attribute: {self.attribute}"

obj = MyClass()
obj.attribute = 5
print(obj.instance_method())  # Output: Instance method called. Attribute: 5

3 типи методів - метод інстансу - тільки від інстансу. Якщо від класу - помилка. Перший параметр - інстанс self. Цей метод має доступ до атрибутів і методів екземпляра. - метод класу @classmethod - першим параметром приймає клас cls. Може викликатись як від інстансу, так і від класу. - статичний метод @staticmethod

@staticmethod Статичний метод не використовує self або cls. Він не має доступу до атрибутів екземпляра або класу. Це по суті функція всередині класу.

Коли використовувати: - Коли метод не потребує доступу до екземпляра або класу. - Коли метод логічно зв'язаний з класом, але не використовує його атрибути або методи.

class MyClass:
    @staticmethod
    def static_method():
        return "Static method called"

print(MyClass.static_method())  # Output: Static method called

@classmethod Метод класу приймає параметр cls, який представляє сам клас. Це дозволяє методам класу доступ до атрибутів і методів класу.

Коли використовувати: - Коли метод повинен працювати з класом як з об'єктом. - Коли потрібно створити метод, що працює з загальними для всіх екземплярів даними.

class MyClass:
    class_attribute = "Class attribute"

    @classmethod
    def class_method(cls):
        return f"Class method called. Attribute: {cls.class_attribute}"

print(MyClass.class_method())  # Output: Class method called. Attribute: Class attribute

Що таке властивість (@property)?

Summary

Property (властивість) - це спеціальний метод класу, який дозволяє контролювати доступ до атрибутів об'єкта, коли вони читаються, записуються або видаляються. Property використовується для забезпечення контрольованого доступу до даних об'єкта. Реалізується з допомогою декоратора @property.

Property дозволяє викликати метод як атрибут і всередині заховати складну логіку (точкова нотація). Або коли змінюється логіка всередині, щоб не міняти API.

Метод property() приймає на вхід методи get, set и delete, і повертає об'єкти класу property.

property(fget=None, fset=None, fdel=None, doc=None)
class Circle:
    def __init__(self, radius):
        self._radius = radius  # Attribute with an underscore is a protected attribute


    @property  # Getter to retrieve the radius value
    def radius(self):  
        return self._radius


    @radius.setter  # Setter to set the radius value
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

    @radius.deleter
    def radius(self):
        del self._radius


    @property  # Getter to compute the area of the circle
    def area(self):
        return 3.14159 * self._radius * self._radius

circle = Circle(5)

print(circle.radius)  # Output: 5
circle.radius = 7
print(circle.radius)  # Output: 7
print(circle.area)    # Output: 153.93845

Декоратори @property і @radius.setter створюють property для атрибута radius. Property radius дозволяє отримувати і змінювати значення радіусу об'єкта circle.

Приблизна реалізація property() за допомогою протоколу дескрипторів

class Property:  
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):  
        self.fget = fget  
        self.fset = fset  
        self.fdel = fdel  
        self.__doc__ = doc  

    def __get__(self, obj, objtype=None):  
        if obj is None:  
            return self  

        if self.fget is None:  
            raise AttributeError  

        return self.fget(obj)  

    def __set__(self, obj, value):  
        if self.fset is None:  
            raise AttributeError  

        self.fset(obj, value)  

    def __delete__(self, obj):  
        if self.fdel is None:  
            raise AttributeError  

        self.fdel(obj)

Що таке дескриптор

Summary

Дескриптор - будь-який об'єкт, який визначає методи __get__(), __set__() або __delete__(). Якщо хоча б один із цих методів визначений, об'єкт стає дескриптором.

Додатково дескриптори можуть мати метод __set_name__(). Це використовується лише у випадках, коли дескриптору потрібно знати або клас, у якому він був створений, або назву змінної класу, якій він був призначений.

Дескриптор дозволяє контролювати доступ, присвоєння і видалення атрибутів об'єктів, замість використання звичайних методів доступу.

Дескриптори працюють лише тоді, коли використовуються як змінні класу. Якщо їх поставити в інстанс, вони не мають ефекту.

Стандартна поведінка при доступі до атрибутів – отримання, встановлення та видалення атрибута зі словника об'єкта. Наприклад, a.x має такий ланцюжок пошуку атрибуту: a.__dict__['x'], потім у type(a).__dict__['x'], і далі у базових класах type(a). Якщо шукане значення є об'єктом, що визначає один із методів дескриптора, тоді Python може замінити поведінку за замовчуванням і замість цього викликати метод дескриптора. Де це відбувається в ланцюжку пріоритетів, залежить від того, які методи дескриптора були визначені.

Є два види дескрипторів - дескриптор даних - дескриптор не даних

class DescriptorExample:
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError("Value must be an integer.")
        instance._value = value

class MyClass:
    def __init__(self, value):
        self._value = value
    value = DescriptorExample()

obj = MyClass(42)
print(obj.value)  # 42
obj.value = 100
print(obj.value)  # 100
obj.value = "string"  # throws ValueError

Links

Види дескрипторів

Є два види дескрипторів - дескриптор даних - дескриптор не даних

Якщо об’єкт визначає __set__() або __delete__(), він вважається дескриптором даних. Дескриптори, які визначають лише __get__(), називаються дескрипторами не даних (вони часто використовуються для методів, але можливі й інші способи використання).

Функції в Python являються дескрипторами (дескрипторами не даних) - так як реалізовують __get__()

>>> def foo(x):  
...   return x * 2  
...  
>>> '__get__' in dir(foo)  
True

Дескриптори даних і не даних відрізняються тим, як обчислюються перевизначення щодо записів у словнику екземпляра. Якщо в словнику екземпляра є запис із таким самим іменем, як і дескриптор даних, дескриптор даних має пріоритет. Якщо в словнику екземпляра є запис із таким же ім’ям, що й дескриптор не даних, пріоритет має словниковий запис.

Щоб створити дескриптор даних лише для читання, визначте __get__() і __set__() з __set__(), що викликає AttributeError під час виклику. Щоб зробити його дескриптором даних, достатньо визначити метод __set__(), що викликає винятки.

Абстрактні класи та методи

Абстрактні класи та методи в Python забезпечують механізм визначення шаблону для класів-нащадків, змушуючи їх реалізовувати певний набір методів. Вони є ключовим елементом для забезпечення інтерфейсу та дотримання принципів об'єктно-орієнтованого програмування, таких як поліморфізм.

Абстрактний клас — це клас, який не можна створити напряму. Він використовується як базовий клас для інших. У Python абстрактні класи визначаються за допомогою модуля abc (Abstract Base Classes).

Абстрактний метод — це метод, який визначений у базовому абстрактному класі, але не має реалізації. Класи-нащадки зобов’язані реалізувати всі абстрактні методи, інакше їх не можна створити. Для визначення абстрактного методу використовується декоратор @abstractmethod. Абстрактні методи оголошуються всередині абстрактного класу.

Абстрактні класи та методи допомагають створювати інтерфейси та структури для класів, які мають схожу поведінку.

from abc import ABC, abstractmethod

class Shape(ABC):  # Abstract class
    @abstractmethod
    def area(self):
        """Calculate the area of the shape."""
        pass

    @abstractmethod
    def perimeter(self):
        """Calculate the perimeter of the shape."""
        pass

class Rectangle(Shape):  # Subclass implementing the abstract methods
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

shape = Shape()  # Raises TypeError: Can't instantiate abstract class
rect = Rectangle(4, 5)
print("Area:", rect.area())  # Output: Area: 20
print("Perimeter:", rect.perimeter())  # Output: Perimeter: 18

Для чого метод __subclasshook__ ?

Метод __subclasshook__ використовується для перевірки, чи є клас підкласом певного класу. Цей метод є частиною механізму метакласів і надає можливість контролювати спадкування класів.

Коли викликається метод issubclass(cls, C), де cls - поточний клас, а C - потенційний батьківський клас, Python спочатку перевіряє, чи є метод __subclasshook__ в cls. Якщо такий метод існує, він викликається з двома аргументами: класом cls і класом C.

Метод __subclasshook__ повинен повернути True, якщо клас cls є підкласом класу C.

Метод __subclasshook__ можна використовувати для встановлення спеціальних вимог щодо спадкування. Наприклад, можна використовувати його, щоб перевірити, чи має підклас певні атрибути або методи, які необхідні для коректної роботи з певним інтерфейсом або механізмом.