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__ - використовується для обробки доступу до атрибутів, що не існують в об'єкті при доступі за допомогою точкової нотації obj.<attribute> - __setattr__, __delattr__ - викликаються, якщо ми пробуємо взаємодіяти з атрибутом, якого немає в об'єкті - __getattribute__ - викликається при спробі отримати значення атрибуту. Якщо цей метод перевизначений, стандартний механізм пошуку значень атрибутів не буде задіяний. - __dict__ - сховище атрибутів, визначених користувачем. Пошук в ньому проводиться під час виконання і при пошуку враховується __dict__ класу обєкту і базових класів. - __bases__ - список прямих батьків - __iter__ - повертає ітератор - __eq__ - перевірка на рівність з іншим об'єктом - __add__ - додавання до іншого об'єкта - __call__ - дозволяє екземпляру класу поводитися як функція. Це означає, що об’єкт класу можна викликати як функцію з допомогою дужок - __getitem__ - метод, який викликається, коли викликається при доступі за допомогою квадратних дужок myobject[key], передаючи ключ або індекс (значення в квадратних дужках) як параметр. По послідовностях (списки, кортежі, рядки) можна ітеруватись, оскільки вони, реалізовуюсть метод __getitem__ і __len__ - __len__ - повертає довжину послідовності - __enter__, __exit__ - потрібні для реалізації менеджера контексту

Найкращий спосіб правильно реалізувати ці методи (і перевірити набір методів, які потрібно реалізувати разом) — імплементувати базовий абстрактний клас, визначений в модулі collections.abc (https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes). Ці інтерфейси надають методи, які потрібно реалізувати, а також подбають про правильне створення типу (коли функція isinstance() викликається для об’єкта).

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

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

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

Яка різниця між __getattr__ та __getattribute__?

Метод __getattr__ використовується для обробки доступу до атрибутів, які відсутні в об'єкті. Використовується для реалізації динамічного доступу до атрибутів.

За допомогою магічного методу __getattr__ можна контролювати спосіб отримання атрибутів з об’єктів. Коли ми викликаємо атрибут <myobject>.<myattribute>, Python шукатиме <myattribute> у словнику об’єкта, викликаючи на ньому __getattribute__. Якщо атрибут не знайдено (об’єкт не має атрибута, який ми шукаємо), то викликається додатковий метод, __getattr__, передаючи назву атрибута (myattribute) як параметр. Тобто, __getattr__ викликається тільки в тому випадку, коли атрибут не існує, або якщо не знайдений в об'єкті. Якщо атрибут існує, то Python повертає його значення без виклику __getattr__.

class DynamicAttributes:
    def __getattr__(self, name):  # This method will be called if the attribute does not exist
        if name == "special_attribute":
            return "This is a dynamically created attribute"
        else:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

obj = DynamicAttributes()

print(obj.special_attribute)  # Accessing a non-existent attribute will call __getattr__
"This is a dynamically created attribute"

print(obj.some_other_attribute)  # Accessing another non-existent attribute will raise an AttributeError

Для того, щоб getattr() правильно працював з __getattr__, він має викидати AttributeError. В іншому випадку getattr не поверне значення по замовчуванню, якщо атрибут відсутній.

getattr(obj, "something", "default")  # "default"

Важливо пам'ятати, що метод __getattr__ не викликається для атрибутів, які існують в об'єкті. Якщо ви хочете перехоплювати доступ до всіх атрибутів, включаючи ті, що вже існують, потрібно використовувати метод __getattribute__.

Метод __qualname__

__qualname__ (qualified name) використовується для того, щоб вказати повне ім'я об'єкта (зокрема, функції, класу чи методу) у контексті його вкладеності. Це допомагає ідентифікувати об'єкт у його ієрархії.

Наприклад, якщо є вкладені класи або методи, __qualname__ покаже повну структуру, де знаходиться цей об'єкт (на відміну від __name__, який дає лише коротке ім'я об'єкта).

class Outer:
    class Inner:
        def method(self):
            pass

print(Outer.Inner.method.__qualname__)  # Outer.Inner.method

Як у класі посилатися на батьківський клас (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, оскільки не існує жодного механізму для розрізнення повноцінного класу і міксіна. Міксін технічно є звичайним класом.

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

Абстрактні класи та методи в 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

Чому рівний вираз 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

Під капотом це реалізується за допомогою дескрипторів. Оскільки атрибуту __dict__ для зберігання значень змінних екземпляра немає, Python замість цього створює дескриптор для кожного слоту та зберігає значення там. Це має побічний ефект: ми не можемо змішувати атрибути класу з атрибутами екземпляра (наприклад, використання атрибута класу як значення за замовчуванням для атрибута екземпляра неможливо, тому що значення будуть перезаписані).

Атрибути. В чому сенс _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

Оскільки функції реалізують протокол дескриптора (реалізують метод __get__), перед викликом методу спочатку викликається метод __get__(). Це перетворює функцію на метод, що означає прив'язку викликаного до екземпляра об'єкту, з яким він збирається працювати.

Це можна імітувати за допомогою MethodType із модуля types. Перший параметр цього класу має бути callable, а другий — це об'єкт, до якого прив'язується ця функція. Щось подібне до цього використовують об'єкти функцій у Python, щоб вони могли працювати як методи, коли їх визначають всередині класу. У цьому прикладі абстракція MyClass намагається імітувати об'єкт функції, оскільки в реальному інтерпретаторі це реалізовано на C.

from types import MethodType

class Method:
    def __init__(self, name):
        self.name = name

    def __call__(self, instance, arg1, arg2):
        print(f"{self.name}: {instance} called with {arg1} and {arg2}")

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return MethodType(self, instance)

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

І @staticmethod, і @classmethod реалізовані за допомогою дескриптора.

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

Функція __get__ забезпечує, що жодні параметри не прив'язуються, окрім тих, які визначені самою функцією. Іншими словами, вона скасовує прив'язку, здійснену методом __get__() для функцій, які роблять self першим параметром цієї функції.

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

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

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

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

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

Якщо потрібно ще щось перекласти чи пояснити, дайте знати! 😊

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

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

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

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

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

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

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

Що таке властивість (@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__(). Це використовується лише у випадках, коли дескриптору потрібно знати або клас, у якому він був створений, або назву змінної класу, якій він був призначений.

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

Дескриптори використовуються у вбудованих механізмах Python

  • Декоратор @property є дескриптором, який реалізує повний протокол дескриптора для визначення своїх дій get, set і delete.
  • Методи класів (@classmethod) та статичні методи (@staticmethod) є дескрипторами.
  • Функції реалізують метод __get__, тому вони можуть працювати як методи, коли визначені всередині класу.
  • __slots__ використовує дескриптори для оптимізації пам'яті.

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

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

Тобто у випадку дескрипторів, коли об'єкт визначено як атрибут класу (а він є дескриптором), коли клієнт запитує цей атрибут, замість отримання самого об’єкта (поведінка за замовчуванням), отримуємо результат виклику магічного методу __get__.

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

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

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

Links

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

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

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

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

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

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

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

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

Методи дескрипторів – це спеціальні методи класу, які дозволяють контролювати доступ до атрибутів об'єкту через механізм дескрипторів. Дескриптори використовуються в Python для реалізації властивостей (property), в розробці фреймворків.

  • __get__(self, instance, owner) – викликається під час доступу до атрибуту.
    • Перший параметр, instance, посилається на об'єкт, з якого викликається дескриптор.
    • Параметр owner є посиланням на клас цього екземпляру. Хоча клас можна взяти безпосередньо з екземпляру (owner = instance.__class__), можлива ситуація, коли дескриптор викликається з класу, а не з екземпляру. Відповідно, тоді значення екземпляра є None, але іноді треба виконати деяку обробку і в цьому випадку.
    • Загалом, якщо не потрібно спеціально працювати з параметром owner, найпоширеніший підхід — просто повернути сам дескриптор, коли instance дорівнює None. Це пояснюється тим, що коли користувачі викликають дескриптор із класу, вони, ймовірно, очікують отримати сам дескриптор, що є логічним. Але, звичайно, це дійсно залежить від конкретної ситуації.
  • __set__(self, instance, value) – викликається при встановленні значення атрибута - client.descriptor = "value".
    • Якщо client.descriptor не реалізує __set__(), тоді value повністю замінить дескриптор.
    • Те, що зазвичай розміщується у властивостях @property, можна абстрагувати в дескриптор і використовувати багаторазово. У цьому випадку метод __set__() робить те, що робить @property.setter.
    • В методі __set__ потрібно звертатись безпосередньо до атрибута __dict__ екземпляра.
    • Не можна використовувати setattr() або вираз присвоєння безпосередньо на дескрипторі всередині методу __set__, оскільки це викличе нескінченну рекурсію. Метод (__set__) викликається, коли щось призначається атрибуту, який є дескриптором. Тому, використання setattr() знову викличе цей дескриптор, що, у свою чергу, викличе його знову, і так далі. Це призведе до нескінченної рекурсії.
  • __delete__(self, instance) – викликається під час видалення атрибута - del client.descriptor.
  • __set_name__(self, owner, name) – викликається один раз під час створення класу, що використовує дескриптор. Дозволяє дескрипторам дізнатися ім'я атрибута, до якого вони прив'язані в класі. Доданий в Python 3.6.
    • Ця назва атрибута використовується для читання з та запису до __dict__ у методах __get__ та __set__, відповідно.
    • До версії Python 3.6 дескриптор не міг автоматично отримати цю назву, тому найбільш загальний підхід полягав у тому, щоб просто передавати її явно при ініціалізації об'єкта. Це вимагало дублювати назву щоразу, коли потрібно було використати дескриптор для нового атрибута.
class PositiveNumber:
    def __get__(self, instance, owner):
        return instance.__dict__.get(self.name, 0)

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Value must be positive")
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = name  # Store attribute name for correct instance dict access

class Product:
    price = PositiveNumber()

    def __init__(self, price):
        self.price = price  # Calls __set__ method of the descriptor

product = Product(100)
print(product.price)  # Calls __get__ method

product.price = -50  # Raises ValueError

Проблеми, які можуть виникнути з дескрипторами

  • Дескриптори можуть викликати нескінченну рекурсію. Наприклад, при використанні setattr() або прямого доступу до атрибутів.
class Descriptor:
    def __set__(self, instance, value):
        setattr(instance, 'value', value)  # Using setattr here causes infinite recursion

class MyClass:
    attr = Descriptor()

obj = MyClass()
obj.attr = 42  # This will raise a RecursionError

Щоб уникнути цієї проблеми, варто напряму звертатись до __dict__:

class Descriptor:
    def __set__(self, instance, value):
        instance.__dict__['value'] = value  # Directly accessing __dict__ avoids recursion

class MyClass:
    attr = Descriptor()

obj = MyClass()
obj.attr = 42  # Works correctly
  • У випадках, коли дескриптор працює з атрибутами екземпляра через __dict__, можуть виникати конфлікти з іншими механізмами, які змінюють або контролюють доступ до атрибутів об'єкта.
  • Якщо дескриптор записує значення напряму у __dict__ екземпляра, це може обійти контроль, передбачений у методах __set__ або __delete__.

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

class SharedDataDescriptor:
    def __init__(self, initial_value):
        self.value = initial_value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.value

    def __set__(self, instance, value):
        self.value = value

class ClientClass:
    descriptor = SharedDataDescriptor("first value")

>>> client1 = ClientClass()
>>> client1.descriptor
'first value'

>>> client2 = ClientClass()
>>> client2.descriptor
'first value'

>>> client2.descriptor = "value for client 2"
>>> client2.descriptor
'value for client 2'

>>> client1.descriptor
'value for client 2'
  • Якщо дескриптор зберігає посилання на екземпляр класу, це може перешкоджати його коректному видаленню збирачем сміття. Наприклад коли потрібно, щоб об'єкт дескриптора самостійно відстежував значення для кожного екземпляра у внутрішньому відображенні (mapping) та повертати значення з цього відображення. Однак, оскільки клас-клієнт має посилання на дескриптор, а тепер дескриптор буде зберігати посилання на об'єкти, які його використовують, це створить циклічні залежності, і, як результат, ці об'єкти ніколи не будуть знищені збирачем сміття, оскільки вони посилаються один на одного. Щоб вирішити цю проблему, словник повинен бути слабким, як це визначено в модулі weakref. Це вирішує проблеми, але має деякі аспекти, які варто врахувати - об'єкти більше не зберігають свої атрибути - замість цього їх зберігає дескриптор (наприклад, виклик vars(client) не поверне повні дані). Та це вимагає, щоб об'єкти були хешованими.
from weakref import WeakKeyDictionary

class DescriptorClass:
    def __init__(self, initial_value):
        self.value = initial_value
        self.mapping = WeakKeyDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.mapping.get(instance, self.value)

    def __set__(self, instance, value):
        self.mapping[instance] = value