ООП Python
Принципы ООП
- Абстракция
- Наследование
- Инкапсуляция
- Полиморфизм
Наследование - способ создания класса. Его суть заключается в том, что функциональность нового класса наследуются от уже существующего класса. Новый класс называется производным (дочерним). Существующий — базовым (родительским). Наследование реализует отношение генерализации/специализации (Generalization/Specialization). Это означает, что производный класс (потомок) является (is-a) частным случаем базового класса (родителя). Он расширяет или уточняет функциональность базового класса, но не меняет его фундаментальную сущность. Производный класс представляет частный случай базового (например, Грузовик является Автомобилем). Попытка наследовать Автомобиль от Колеса для переиспользования метода «ехать» семантически неверна, так как автомобиль не является разновидностью колеса. Корректным паттерном здесь выступает композиция, которая выражает отношение «имеет» (has-a): автомобиль имеет четыре колеса и, координируя их работу, реализует собственное поведение «поехать», делегируя вызовы своим компонентам.
Инкапсуляция - в Python это прежде всего организационный принцип, который позволяет собрать связанные переменные (данные, состояние) и функции (методы, поведение) в рамках одного класса. Это создает четкую и понятную структуру, где класс отвечает за свою собственную логику и данные. Мы можем также ограничить доступ к методам и переменным, что предотвратит модификацию данных. В Python нет строгой приватности, как в других языках; вместо этого используются соглашения и механизм "изменения имени" (__fly) для приватных (private) методов и атрибутов. Приватные методы доступны только внутри класса (с двойным подчеркиванием в имени __do), а защищённые (protected) методы (с одним подчёркиванием _play) служат только для соглашения, что их не следует использовать извне. Отличие Private (приватного) от Protected (защищенного) заключается в том, что в Private используется name mangling — имя меняется интерпретатором для "защиты", т.е. невозможно напрямую обратиться по такому имени вне класса. Как обратиться к Protected методу вне класса и наследников? Protected — это только соглашение, а не ограничение языка.
class MyClass:
def __init__(self):
self.public_field = "публичное"
self._protected_field = "защищённое"
self.__private_field = "приватное"
def public_method(self):
return "публичный метод"
def _protected_method(self):
return "защищённый метод"
def __private_method(self):
return "приватный метод"
obj = MyClass()
print(obj._protected_field)
print(obj._protected_method())
В наследниках
class Child(MyClass):
def use_protected(self):
return self._protected_method()
Как обратится к Private методу вне класса? Напрямую — нельзя.
obj = MyClass()
# obj.__private_method() # ❌ Ошибка: AttributeError
Есть ли способ обойти приватность и получить доступ к приватным методам? Есть — через name mangling. Python не делает поля по-настоящему приватными, а просто меняет имя.
obj = MyClass()
# Можно вызвать приватный метод так:
print(obj._MyClass__private_method()) # ✅ Работает!
# И даже получить доступ к полю:
print(obj._MyClass__private_field) # ✅ Вернёт "приватное"
Это нарушает инкапсуляцию и не рекомендуется в production-коде.
Полиморфизм - это возможность применять одно и то же действие (метод или функцию) к объектам разных типов, и каждый объект отреагирует на это действие своим собственным, специфическим образом. Один интерфейс — множество реализаций. Мы можем использовать одинаковые команды для разных объектов, не задумываясь о их внутреннем устройстве, так как каждый объект "знает" (своя реализация внутри), как именно ему выполнить эту команду.
Функция len() в Python — идеальный пример полиморфизма. Мы можем использовать одну и ту же функцию для совершенно разных объектов:
- Для строки len("Привет") вернет количество символов
- Для списка len([1, 2, 3]) вернет количество элементов
- Для словаря len({"a": 1, "b": 2}) вернет количество пар ключ-значение
При этом нам не нужно знать, как именно считается длина для каждого типа данных — мы просто используем единый интерфейс len(), а каждый объект самостоятельно определяет, что для него значит "длина" и как ее вычислить.
Оператор + в Python, когда одна и та же операция выполняется по-разному в зависимости от типов объектов:
- Для чисел 5 + 3 → сложение (результат: 8)
- Для строк "hello" + "world" → конкатенация (результат: "helloworld")
- Для списков [1, 2] + [3, 4] → объединение (результат: [1, 2, 3, 4])
Абстракция - выделение существенных характеристик объекта, которые отличают его от всех других видов объектов, при одновременном игнорировании несущественных деталей реализации. Когда вы используете педаль газа, вам важно знать что она делает (автомобиль ускоряется), а не как это происходит (впрыск топлива, работа цилиндров, трансмиссии). Все сложные механизмы скрыты — вы взаимодействуете с простым интерфейсом (педалью), который абстрагирует всю техническую сложность. Таким образом, абстракция позволяет работать с объектами на концептуальном уровне, скрывая ненужные детали реализации.
Что такое self в методах класса?
self — это ссылка на текущий экземпляр класса (на самого себя).
Как работает
class Person:
def __init__(self, name): # self — новый объект
self.name = name # сохраняем имя В ЭТОТ объект
def hello(self): # self — конкретный объект
print(f"Привет, я {self.name}")
p = Person("Анна") # self внутри = p
p.hello() # self внутри = p → "Привет, я Анна"
Что происходит на самом деле
p.hello() # Python превращает в:
Person.hello(p) # self = p (объект передается первым аргументом)
Главные правила
- Первый параметр любого метода экземпляра —
self - Не нужно передавать при вызове — Python подставляет автоматически
- Через self обращаемся к атрибутам и методам объекта
Итоги
self— это сам объект, от которого вызван метод- Нужен для доступа к атрибутам и другим методам
- Без
selfметод не знает, с каким именно объектом работать - Можно назвать по-другому, но все используют
self
Магические методы
Магические методы (иногда также называемые специальными методами или dunder-методами от "double underscore methods" — то есть "методы с двойным подчеркиванием", то есть начинаются и заканчиваются двумя подчеркиваниями (например, __init__()). Это специальные методы, которые необходимы для реализации базовых операций над объектами. Благодаря этим методам объекты могут реализовывать, поддерживать и взаимодействовать с базовыми конструкциями языка, а именно:
- итерирование
- коллекции
- доступ к атрибутам
- перегрузка операторов
- контекстные менеджеры
- вызов методов
- создание и уничтожение объектов
Магические они потому, что почти никогда не вызываются явно. Их вызывают встроенные функции или синтаксические конструкции. Например, функция len() вызывает метод __len__() переданного объекта. Метод __add__(self, other) вызывается автоматически при сложении оператором +.
Какие есть магические методы и для чего они используются?
-
__init__(self, ...): Вызывается при создании нового экземпляра класса и используется для инициализации его атрибутов. -
__repr__(self): Возвращает формальное строковое представление объекта, которое должно быть максимально точным и однозначным. В идеале эта строка должна быть валидным Python-кодом, который можно использовать для воссоздания объекта. Используется в первую очередь для отладки разработчиками. -
__str__(self): Возвращает неформальное строковое представление объекта, ориентированное на удобочитаемость. Это то, что видят пользователи при вызове print() или str(). Если__str__не определён, Python использует__repr__как запасной вариант. -
__len__(self): Возвращает длину объекта. Это используется, когда объект поддерживает операцию получения длины, например, для строк, списков и т. д. -
__getitem__(self, key): Позволяет объекту поддерживать доступ к элементам по индексу, как это делают списки и словари. -
__setitem__(self, key, value): Позволяет объекту устанавливать значение элемента по ключу. -
__delitem__(self, key): Позволяет объекту удалять элемент по ключу. -
__iter__(self): Возвращает итератор для объекта, позволяя его использовать в циклахfor. -
__next__(self): Вызывается итератором для получения следующего элемента в последовательности. -
__enter__(self),__exit__(self, exc_type, exc_value, traceback): Позволяют объекту быть контекстным менеджером, что позволяет определять действия, которые должны выполняться при входе и выходе из контекста.
Это лишь несколько примеров магических методов, которые можно определить. Они предоставляют мощные средства для определения поведения объектов и делают Python гибким и выразительным.
Статья на хабре Руководство по магическим методам в Питоне
Отличие init() от new()
Основное различие между этими двумя методами состоит в том, что __new__ обрабатывает создание объекта, а __init__ обрабатывает его инициализацию.
__new__ вызывается автоматически при вызове имени класса (при создании экземпляра), тогда как __init__ вызывается каждый раз, когда экземпляр класса возвращается __new__, передавая возвращаемый экземпляр в __init__ в качестве параметра self, поэтому даже если вы сохранили экземпляр где-нибудь глобально/статически и возвращали его каждый раз из __new__, для него все-равно будет каждый раз вызываться __init__.
Из вышесказанного вытекает что сначала вызывается __new__, а потом __init__
__new__() — это конструктор класса, который создает и возвращает новый экземпляр. Он статический метод, вызывается первым при создании объекта и отвечает за его создание в памяти.
__init__() — это инициализатор, который получает уже созданный экземпляр от __new__() и инициализирует его атрибуты. Он вызывается после __new__() и не возвращает значения.
🛠 Пример реализации
class Example:
def __new__(cls, *args, **kwargs):
print("__new__ выполняется первым - создает экземпляр")
instance = super().__new__(cls)
return instance # Возвращает созданный объект
def __init__(self, value):
print("__init__ выполняется вторым - инициализирует объект")
self.value = value # Настраивает переданный объект
🚀 Практическое применение
__new__() используется редко — в основном для наследования от неизменяемых типов (tuple, str) или создания синглтонов:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
__init__() применяется постоянно — для начальной настройки созданных объектов и установки их начального состояния.
Отличие __getattr__ от __getattribute__
Это два магических метода, которые перехватывают доступ к атрибутам объекта. Главное отличие — когда они вызываются.
__getattribute__ — перехватывает ВСЁ
Вызывается при любом обращении к любому атрибуту — существующему или нет.
class Test:
def __init__(self):
self.name = "Анна"
def __getattribute__(self, name):
print(f"Перехвачен доступ к: {name}")
# Обязательно вызываем родительский метод, иначе всё сломается
return super().__getattribute__(name)
obj = Test()
print(obj.name) # Перехвачен доступ к: name → Анна
print(obj.age) # Перехвачен доступ к: age → AttributeError
Особенности:
- Срабатывает всегда при обращении к атрибуту
- Должен вызывать
super().__getattribute__()для реального доступа - Легко создать бесконечную рекурсию
__getattr__ — перехватывает только отсутствующее
Вызывается только если атрибут не найден обычным способом.
class Test:
def __init__(self):
self.name = "Анна"
def __getattr__(self, name):
print(f"Атрибут {name} не найден, создаём...")
return f"Значение для {name}"
obj = Test()
print(obj.name) # Анна (__getattr__ НЕ вызван)
print(obj.age) # Атрибут age не найден → "Значение для age"
Особенности:
- Срабатывает только для отсутствующих атрибутов
- Не нужно вызывать
super() - Безопаснее, нет риска рекурсии
Сравнение в таблице
__getattribute__ |
__getattr__ |
|
|---|---|---|
| Когда вызывается | При любом доступе к атрибуту | Только если атрибут не найден |
| Для существующих атрибутов | ✅ Да | ❌ Нет |
| Для несуществующих | ✅ Да | ✅ Да |
| Риск рекурсии | Высокий | Низкий |
| Нужен super() | Обязательно | Нет |
| Типичное использование | Логирование, контроль доступа | Значения по умолчанию, динамические атрибуты |
Главное правило
__getattribute__— всегда первый- Если атрибут есть — он возвращает значение и всё
- Если атрибута нет — после
__getattribute__вызывается__getattr__
Отличие __repr__ от __str__
Оба метода возвращают строковое представление объекта, но для разных целей.
__repr__ |
__str__ |
|
|---|---|---|
| Для кого | Для разработчиков - Форматное представление | Для пользователей - Неформатное представление |
| Цель | Однозначная идентификация объекта | Читаемое представление |
| Где используется | Консоль, отладка, логи | print(), str() |
| Должен ли быть eval | Желательно (eval(repr(obj)) == obj) |
Не обязательно |
__repr__ — для разработчиков
Должен быть максимально информативным, часто показывает тип и важные атрибуты.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point(x={self.x}, y={self.y})"
p = Point(3, 5)
print(repr(p)) # Point(x=3, y=5)
print(p) # По умолчанию тоже repr
В консоли:
>>> p = Point(3, 5)
>>> p # автоматически вызывается __repr__
Point(x=3, y=5)
__str__ — для пользователей
Дает "красивое" человекочитаемое представление.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return f"Person('{self.name}', {self.age})"
def __str__(self):
return f"{self.name} ({self.age} лет)"
p = Person("Анна", 25)
print(repr(p)) # Person('Анна', 25)
print(str(p)) # Анна (25 лет)
print(p) # Анна (25 лет) — print() вызывает str
Иерархия вызовов
print(obj)→ ищет__str__, если нет →__repr__str(obj)→ ищет__str__, если нет →__repr__repr(obj)→ всегда__repr__- В консоли (интерактивной) →
__repr__
class Demo:
def __repr__(self):
return "Это repr"
# __str__ не определен
d = Demo()
print(d) # Это repr (str не нашелся)
print(str(d)) # Это repr (str не нашелся)
print(repr(d)) # Это repr
Главное
-
Всегда определяйте
__repr__— он нужен для отладки.__str__определяйте, если нужно красивое представление для пользователя. -
__repr__— должен быть точным, желательно чтобы eval(repr(obj)) воссоздавал объект -
__str__— должен быть читаемым для обычного пользователя -
Если определен только
__repr__, он будет использоваться везде.
Что такое MRO?
Method resolution order - порядок разрешения методов. Алгоритм, по которому следует искать метод в случае, если у класса два и более родителей.
В классических классах поиск при наследовании по ссылкам на имена осуществляется в следующем порядке:
- Сначала экземпляр
- Затем его класс
- Далее все суперклассы его класса с обходом сначала с глубину, а затем слева направо
📚 Эволюция MRO
Python 2.x: Классические классы
В классических классах использовался алгоритм DFS (Depth-First Search) с обходом в глубину слева направо: text
A → B → D → C → E
Python 3.x: Современные классы
В Python 3 все классы наследуются от object и используют алгоритм C3 Linearization, который обеспечивает более предсказуемый порядок.
🔄 Алгоритм C3 Linearization
Основные правила:
- Порядок наследования сохраняется
- Дочерний класс имеет приоритет над родительским
- Порядок в списке родителей сохраняется
Формула C3:
L[C] = C + merge(L[B1], L[B2], ..., L[Bn], B1B2...Bn)
🔍 Примеры MRO
Простое множественное наследование
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(A):
def method(self):
print("C")
class D(B, C):
pass
print(D.__mro__)
# Output: (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
"Ромбовидное" наследование
class A:
def method(self):
print("A")
class B(A):
def method(self):
print("B")
class C(A):
def method(self):
print("C")
class D(B, C):
pass
# MRO: D → B → C → A → object
Миксины
В Python миксины (mixins) - это классы, предназначенные для добавления функциональности в другие классы через множественное наследование, не являясь при этом самостоятельными классами для создания экземпляров. Они предоставляют набор методов, которые можно "подмешать" к другим классам для расширения их возможностей.
-
Назначение: Миксины служат для повторного использования кода и предотвращения дублирования, добавляя определенную функциональность к нескольким классам без создания сложных иерархий наследования.
-
Особенности: Миксины обычно не содержат данных (атрибутов), а только методы, определяющие поведение. Они предназначены для использования в качестве "строительных блоков", которые можно комбинировать для создания более сложных классов.
-
Использование: Миксины включаются в другие классы через множественное наследование. Например, если у вас есть класс User и вы хотите добавить функциональность логирования, вы можете создать миксин LoggingMixin с методами логирования и унаследовать от него класс User.
-
Преимущества: Повторное использование кода: Миксины позволяют избежать дублирования кода, предоставляя общую функциональность для нескольких классов.
-
Гибкость: Они обеспечивают гибкость при проектировании классов, позволяя легко добавлять или изменять функциональность.
-
Поддержка чистого кода: Миксины способствуют созданию более читаемого и поддерживаемого кода, разделяя функциональность на отдельные компоненты.
Пример:
class LoggingMixin:
def log(self, message):
print(f"Logging: {message}")
class User(LoggingMixin):
def __init__(self, name):
self.name = name
self.log(f"User {self.name} created")
def say_hello(self):
self.log(f"Hello, my name is {self.name}")
user = User("Alice")
user.say_hello()
В этом примере, LoggingMixin предоставляет метод log, который используется классом User. Класс User при этом несет ответственность только за свою логику, а логирование делегируется LoggingMixin. Миксины - это полезный инструмент в Python для создания гибкого, повторно используемого и поддерживаемого кода, позволяющий избегать сложных иерархий наследования и дублирования.
Что такое slots?
__slots__ — это атрибут класса, который явно определяет фиксированный набор атрибутов для его экземпляров. Это механизм оптимизации, который заменяет стандартный словарь __dict__ на статическое размещение атрибутов, что приводит к уменьшению потребления памяти и увеличению скорости доступа к атрибутам.
Ключевые аспекты:
-
Ограничение атрибутов
- Экземпляры класса могут содержать только те атрибуты, которые перечислены в
__slots__ - Попытка добавить атрибут, не указанный в
__slots__, вызывает AttributeError
- Экземпляры класса могут содержать только те атрибуты, которые перечислены в
-
Оптимизация памяти
- Основная экономия памяти достигается за счет отсутствия
__dict__у каждого экземпляра - Вместо динамического словаря Python использует компактный массив с фиксированным размером
- Эффект особенно заметен при создании тысяч экземпляров
- Основная экономия памяти достигается за счет отсутствия
-
Ускорение доступа к атрибутам
- Скорость увеличивается благодаря использованию дескрипторов
- Атрибуты из
__slots__становятся дескрипторами данных в классе - При поиске атрибута алгоритм MRO находит их сразу на уровне класса, минуя поиск в
__dict__экземпляра
-
Ограничение добавления новых атрибутов
- Атрибуты, объявленные в
__slots__, доступны сразу после создания экземпляра - Место для них резервируется при создании экземпляра, подобно атрибутам в
__init__ - Это отличается от динамического добавления атрибутов в обычных классах, где атрибуты могут добавляться в любой момент
- Атрибуты, объявленные в
Пример:
class PointSlots:
__slots__ = ('x', 'y') # Фиксированный набор атрибутов
def __init__(self, x, y):
self.x = x # ✓ Разрешено
self.y = y # ✓ Разрешено
p = PointSlots(1, 2)
p.z = 3 # ❌ AttributeError: 'PointSlots' object has no attribute 'z'
__slots__ — это компромисс между гибкостью (возможность добавлять любые атрибуты) и производительностью (память + скорость).
Объяснение работы __slots__ при наследовании в Python 3.x:
-
Если родитель имеет
__slots__:
Потомок наследует эти слоты автоматически, даже если определяет свой собственный__slots__. -
Если потомок определяет свой
__slots__:
Он не теряет слоты родителя. Python объединяет их автоматически при одиночном наследовании. -
Если потомок НЕ определяет свой
__slots__:
Он наследует слоты родителя И получает__dict__, что позволяет динамически добавлять новые атрибуты. -
Множественное наследование:
Python автоматически объединяет__slots__разных родителей, если их структуры совместимы. При конфликте возникает ошибка.
Полная таблица наследования __slots__
| Ситуация | У экземпляра есть __dict__? |
Можем создавать новые атрибуты? | Что наследуется? |
|---|---|---|---|
1. Родитель БЕЗ __slots__Потомок БЕЗ __slots__ |
✅ Да | ✅ Да | Только __dict__ |
2. Родитель БЕЗ __slots__Потомок СО __slots__ |
❌ Нет | ❌ Только слоты потомка | Слоты потомка (__dict__ родителя не наследуется) |
3. Родитель СО __slots__Потомок БЕЗ __slots__ |
✅ Да | ✅ Да | Слоты родителя + __dict__ |
4. Родитель СО __slots__Потомок СО __slots__ |
❌ Нет | ❌ Только все слоты | Слоты родителя и потомка объединяются |
5. Класс с '__dict__' в __slots__ |
✅ Да | ✅ Да | Явное разрешение __dict__ наряду со слотами |
Примеры:
# Случай 3 из таблицы: родитель со slots, потомок без slots
class Parent:
__slots__ = ['x']
def __init__(self):
self.x = 10
class Child(Parent):
pass # Не определяет свой __slots__
c = Child()
c.x = 10 # ✅ Работает — унаследованный слот
print(hasattr(c, '__dict__')) # ✅ True — словарь есть!
c.y = 20 # ✅ Работает — через __dict__
c.z = 30 # ✅ Работает — через __dict__
print(c.__dict__) # {'y': 20, 'z': 30}
# Случай 4 из таблицы: оба имеют slots
class Parent:
__slots__ = ['x']
class Child(Parent):
__slots__ = ['y'] # ✅ Слот 'x' наследуется автоматически
c = Child()
c.x = 10 # ✅ Работает
c.y = 20 # ✅ Работает
# c.z = 30 # ❌ Ошибка: нет слота 'z'
print(hasattr(c, '__dict__')) # ❌ False
# Множественное наследование
class A:
__slots__ = ['a']
class B:
__slots__ = ['b']
class C(A, B):
__slots__ = ['c'] # ✅ Автоматически получаем слоты 'a' и 'b'
obj = C()
obj.a = 1 # ✅
obj.b = 2 # ✅
obj.c = 3 # ✅
# obj.d = 4 # ❌ AttributeError
# Явное объединение слотов (рекомендуется для ясности)
class Parent:
__slots__ = ['x']
class ExplicitChild(Parent):
__slots__ = Parent.__slots__ + ['y'] # ✅ Понятно, что слоты объединены
# Случай с __dict__ в слотах
class DynamicSlots:
__slots__ = ['x', '__dict__'] # Явно разрешаем __dict__
obj = DynamicSlots()
obj.x = 10 # ✅ Через слот
obj.y = 20 # ✅ Через __dict__
obj.z = 30 # ✅ Через __dict__
print(obj.__dict__) # {'y': 20, 'z': 30}
print(obj.x) # 10
Ключевые выводы:
- Слоты родителей автоматически наследуются в Python 3.x
- Если потомок не определяет
__slots__, он получает и слоты родителя, и__dict__(в отличие от распространенного заблуждения) - Если потомок определяет
__slots__,__dict__отсутствует (если не добавлен явно) - Для восстановления динамических атрибутов в классе со слотами добавьте
'__dict__'в__slots__ - При множественном наследовании все слоты родителей объединяются, если нет конфликтов структуры
Что изменилось в __slots__ в новых версиях Python
-
Снижение эффективности экономии памяти
- Раньше (версии 3.9-3.10): экономия 80-216 байт на объект
- Сейчас (версии 3.11-3.13): экономия 32-64 байта на объект
- Причина: стандартные словари (
__dict__) стали компактнее
-
Почти исчезло ускорение доступа к атрибутам
- Раньше (версия 3.9): доступ через
__slots__быстрее на 62% - Сейчас (версия 3.13): быстрее всего на 3%
- В оптимизированных сборках разница стремится к 0%
- Раньше (версия 3.9): доступ через
-
Исправление в Python 3.12
- Исправлена ошибка с наследованием классов, использующих
propertyи__slots__одновременно
- Исправлена ошибка с наследованием классов, использующих
Практический вывод
- Python ≤ 3.10:
__slots__даёт огромный выигрыш (используйте активно) - Python ≥ 3.11: выигрыш минимальный, используйте только если нужно запретить динамические атрибуты, а не ради производительности
Дескрипторы
Дескрипторы в Python — это объекты, которые реализуют один или несколько специальных методов: __get__(), __set__() и __delete__(). Они позволяют контролировать доступ к атрибутам класса и определять дополнительное поведение при их получении, изменении или удалении.
Используются для кэширования, валидации.
class PositiveValue:
def __get__(self, instance, owner):
return instance.__dict__.get(self, None)
def __set__(self, instance, value):
if value < 0:
raise ValueError("Значение должно быть положительным!")
instance.__dict__[self] = value
class MyClass:
number = PositiveValue()
obj = MyClass()
obj.number = 10 # Корректная установка значения
print(obj.number) # Выведет: 10
obj.number = -5 # Вызывает ValueError: Значение должно быть положительным!
Дескрипторы — это объекты Python, которые определяют, как другие объекты должны вести себя при доступе к атрибуту. Дескрипторы могут использоваться для реализации протоколов, таких как протокол доступа к атрибутам, протокол дескрипторов и протокол методов.
Декораторы — это функции Python, которые принимают другую функцию в качестве аргумента и возвращают новую функцию. Декораторы обычно используются для изменения поведения функции без изменения ее исходного кода.
Разница между дескриптором и декоратором заключается в том, что дескрипторы используются для определения поведения атрибутов объекта, в то время как декораторы используются для изменения поведения функций. Однако, декораторы могут использоваться для реализации протоколов дескрипторов.
Например, декоратор @property можно использовать для создания дескриптора доступа к атрибутам. Он преобразует метод класса в дескриптор, который позволяет получать, устанавливать и удалять значение атрибута как обычный атрибут объекта.
Data descriptors — это дескрипторы, у которых есть метод __get__ и как минимум один из __set__ или __delete__. Они имеют наивысший приоритет при поиске атрибута. Интерпретатор ищет их даже раньше, чем атрибуты в __dict__ экземпляра.
Non-data descriptors — это дескрипторы, у которых есть только метод __get__. Их приоритет ниже, чем у атрибутов экземпляра.
Это различие определяет порядок поиска атрибута obj.attr:
- Data descriptor в классе.
- Атрибуты в
obj.__dict__. - Non-data descriptor в классе.
- Обычные атрибуты класса.
- Поиск в родительских классах.
Data descriptors используются для полного контроля над атрибутом. Классический пример — это встроенный декоратор @property. Когда мы создаём свойство, мы по сути создаём data descriptor. Он перехватывает и чтение, и запись, позволяя нам добавить валидацию, логирование или вычисления при любом обращении к атрибуту. Механизм __slots__ также реализован через data descriptors для фиксации набора атрибутов и управления памятью.
Non-data descriptors используются для поведения "по умолчанию". Самый распространённый пример — это обычные методы класса. Функция, объявленная внутри класса, является non-data descriptor. Именно поэтому мы можем переопределить метод на уровне экземпляра (присвоив obj.method = lambda ...), и при вызове будет использована новая функция, а не исходный метод класса. Исходный метод остаётся в классе и действует как поведение по умолчанию.
Data descriptor — это "строгий" контроллер атрибута, а non-data descriptor — это "советчик", которого можно переопределить на уровне экземпляра.
Обычные функции в Python являются non-data дескрипторами, так как имеют метод __get__. При обращении к методу через экземпляр obj.method этот __get__ автоматически создает bound-метод, привязывая функцию к конкретному объекту (self). Этот механизм и обеспечивает магию связывания методов с экземплярами, позволяя им работать с данными объекта.
Метод __set_name__ — это полезное дополнение для дескрипторов, появившееся в Python 3.6.
Он автоматически вызывается при создании класса и сообщает дескриптору имя атрибута, которому он присваивается. Это позволяет дескриптору "узнать" своё имя в классе без необходимости указывать его вручную.
Например, дескриптор валидатора может использовать это имя для хранения данных в экземпляре под префиксом или для формирования понятных сообщений об ошибках. Раньше эту информацию приходилось передавать явно, а теперь дескриптор становится более самодостаточным и удобным в использовании.
Classmethod и staticmethod
classmethod и staticmethod - это специальные декораторы, которые позволяют определять методы в классах с особым поведением. Однако они имеют различия в том, как они обрабатывают аргументы и как они взаимодействуют с экземплярами класса.
classmethod:
- Декоратор преобразует обычный метод класса в тот, который принимает первым аргументом ссылку на класс (обычно называемый
cls). - Это означает, что метод
classmethodможет обращаться к атрибутам и вызывать другие методы класса через ссылку на сам класс, а не через экземпляр класса. - Может использоваться, например, для создания альтернативных конструкторов класса или для работы с классовыми переменными.
class MyClass:
class_attribute = 123
@classmethod
def class_method(cls):
return cls.class_attribute
print(MyClass.class_method()) # Выведет: 123
staticmethod:
- Декоратор создает метод класса, который не принимает ссылку на сам класс (неявно или явно), и не принимает ссылку на экземпляр класса (обычно называемый
self). - Это означает, что
staticmethodявляется статическим методом и может быть вызван как из класса, так и из экземпляра класса, но не имеет доступа к атрибутам и методам класса или экземпляра. - Статические методы могут быть полезны для группировки связанных функций внутри класса или для создания методов, которые не требуют доступа к состоянию класса или экземпляра.
class MyClass:
@staticmethod
def static_method():
return "This is a static method"
print(MyClass.static_method()) # Выведет: This is a static method
Основное отличие между classmethod и staticmethod заключается в том, что classmethod принимает ссылку на класс, а staticmethod - нет.
Чем отличается первый аргумент classmethod от метода экземпляра?
| Метод экземпляра | Classmethod | |
|---|---|---|
| Первый аргумент | self |
cls |
| Что получает | Сам объект (экземпляр) | Сам класс (не экземпляр!) |
| Доступ | К атрибутам конкретного объекта | К атрибутам класса |
| Когда использовать | Когда работаем с данными объекта | Когда работаем с классом в целом |
class Student:
school = "Школа №1" # Атрибут класса
def __init__(self, name):
self.name = name # Атрибут экземпляра
# Метод экземпляра — получает self (конкретного студента)
def show_name(self):
print(f"Студент: {self.name}") # Данные конкретного объекта
# Classmethod — получает cls (класс Student)
@classmethod
def show_school(cls):
print(f"Учебное заведение: {cls.school}") # Данные класса
@classmethod
def create_anonymous(cls):
return cls("Аноним") # Создает НОВЫЙ экземпляр класса
# Использование
s = Student("Маша")
s.show_name() # self = s (объект Маша)
Student.show_school() # cls = Student (сам класс)
anon = Student.create_anonymous() # cls = Student, создает нового студента
Что получают методы?
class Demo:
class_attr = "общее"
def __init__(self, value):
self.instance_attr = value
def instance_method(self):
print(f"self = {self}") # Конкретный объект
print(f"Через self: {self.instance_attr}") # Данные объекта
print(f"Через self: {self.class_attr}") # Тоже работает
@classmethod
def class_method(cls):
print(f"cls = {cls}") # Сам класс
print(f"Через cls: {cls.class_attr}") # Данные класса
# cls.instance_attr — ОШИБКА! у класса нет атрибутов экземпляра
obj = Demo("индивидуальное")
obj.instance_method() # self = <__main__.Demo object at 0x...>
Demo.class_method() # cls = <class '__main__.Demo'>
Таблица сравнения
| Аспект | self (метод экземпляра) |
cls (classmethod) |
|---|---|---|
| Что это | Экземпляр класса | Сам класс |
| Когда вызывается | obj.method() |
Class.method() |
| Может менять | Атрибуты объекта | Атрибуты класса |
| Может создавать | Новые атрибуты объекта | Новые экземпляры класса |
| Доступ к атрибутам класса | Да (через self.__class__) |
Да (прямо через cls) |
| Доступ к атрибутам объекта | Да | Нет |
self — это "конкретный объект", а cls — это "тип объекта" (сам класс).
Метаклассы
Метаклассы — это классы, которые определяют поведение других классов. Они используются для изменения способа, которым Python создает и обрабатывает классы.
Метаклассы могут быть полезны в следующих случаях:
- при необходимости динамического изменения поведения класса, например, если вы хотите добавить или удалить атрибут или метод класса во время выполнения программы;
- при создании классов из данных, которые не заранее известны. Например, вы можете создавать классы на основе определенных условий во время выполнения программы;
- для создания фреймворков и библиотек, которые нужно настраивать под конкретные требования и при этом сохранить простоту интерфейса.
- Они также могут использоваться для создания классов с определенными свойствами, например, классов, которые автоматически регистрируются в библиотеке или классов, которые автоматически сериализуются и десериализуются для совместимости с другими системами.
Пример использования метакласса для добавления атрибута к классу:
class MyMeta(type):
def __new__(cls, name, bases, dct):
dct['my_attribute'] = 42
return super(MyMeta, cls).__new__(cls, name, bases, dct)
class MyClass(metaclass=MyMeta):
pass
print(MyClass.my_attribute)
В этом примере создается метакласс MyMeta, который добавляет атрибут my_attribute к любому классу, который использует данный метакласс для своего создания. Затем создается класс MyClass, который использует метакласс MyMeta. При вызове print(MyClass.my_attribute) выводится значение 42, так как этот атрибут был добавлен в момент создания класса. Метаклассы - это концепция, которая позволяет контролировать создание классов. Классы являются объектами, и они создаются с помощью других классов, которые называются метаклассами. Вот некоторые ключевые моменты о них:
-
Классы как объекты: Классы являются объектами первого класса, что означает, что они могут быть созданы, изменены и переданы как аргументы функций.
-
Типы и метаклассы: Каждый объект имеет тип, который определяется его классом. Этот класс, определяющий тип объекта, называется метаклассом. По умолчанию для всех классов метаклассом является
type. -
Использование метаклассов: Метаклассы можно использовать для изменения поведения создания классов. Это может быть полезно для автоматического добавления методов, проверки атрибутов или изменения наследования классов.
Пример создания метакласса:
class MyMeta(type):
def __new__(cls, name, bases, dct):
# Изменяем или расширяем класс
dct['custom_attribute'] = 'This is a custom attribute'
return super().__new__(cls, name, bases, dct)
class MyClass(metaclass=MyMeta):
pass
obj = MyClass()
print(obj.custom_attribute) # Вывод: This is a custom attribute
В этом примере MyMeta - это метакласс, который изменяет класс MyClass, добавляя к нему новый атрибут custom_attribute.
Мы создаём класс для того, чтобы создавать объекты, так? А классы являются объектами. Метакласс это то, что создаёт эти самые объекты. Они являются классами классов, можно представить это себе следующим образом:
MyClass = MetaClass()
MyObject = MyClass()
Мы уже видели, что type позволяет делать что-то в таком духе:
MyClass = type('MyClass', (), {})
Это потому что функция type на самом деле является метаклассом. type это метакласс, который Python внутренне использует для создания всех классов.
Иерархия инструментов метапрограммирования:
-
Метаклассы — самый мощный, но и самый сложный инструмент. Полностью контролируют создание класса. Подходят для экзотических задач, где нужен тотальный контроль над жизненным циклом класса.
-
__init_subclass__— элегантная альтернатива метаклассам для многих сценариев. Позволяет родительскому классу "знать" о создании своих потомков и модифицировать их.Идеален для:
- Автоматической регистрации подклассов
- Проверки обязательных атрибутов
- Добавления общих методов ко всем наследникам
-
Декораторы классов — простейший способ модифицировать готовый класс. Работают по принципу "обёртки": принимают класс, изменяют его и возвращают. Отлично подходят для:
- Добавления методов и свойств
- Изменения поведения существующих методов
- Регистрации классов в системах плагинов
-
types.new_class()— инструмент для динамического создания классов во время выполнения программы. Используется в сложных случаях, когда структура класса неизвестна при компиляции.
Современный подход: начинать с самого простого подходящего инструмента. Сначала пробуйте декораторы, если нужно влиять на наследование — переходите к __init_subclass__, и только для действительно сложных задач используйте метаклассы. Это делает код понятнее и поддерживаемее.
Что такое абстрактный класс?
Абстрактный класс — это специальный класс-шаблон, который не предназначен для создания непосредственных экземпляров. Он служит основой для иерархии классов, определяя общий интерфейс и поведение, которые должны быть реализованы в дочерних классах.
Создание абстрактного класса
Для работы с абстрактными классами используется модуль abc (Abstract Base Classes). Основные компоненты:
- Наследование от ABC - делает класс абстрактным
- Декоратор @abstractmethod - помечает методы, которые должны быть реализованы в дочерних классах
- Декоратор @abstractproperty - для абстрактных свойств
Ключевое правило: если класс содержит хотя бы один абстрактный метод, он становится абстрактным и не может быть инстанциирован напрямую.
Бизнес-логика в абстрактных классах
Абстрактный класс может содержать готовую бизнес-логику. Это одно из его основных преимуществ:
- Реализованные методы - полностью рабочая бизнес-логика
- Конструкторы - общая инициализация объектов
- Свойства - логика getter/setter методов
- Вспомогательные методы - валидация, проверки, утилиты
Абстрактный класс сочетает в себе готовые реализации и "контракт" в виде абстрактных методов, которые должны быть реализованы наследниками.
Вызов методов абстрактного класса - можно, но с ограничениями:
-
Доступные способы:
- Через дочерние классы - наследники используют готовые методы абстрактного класса
- Через super() - явный вызов реализации родительского класса
- Наследование готовой логики - обычные методы абстрактного класса доступны как есть
-
Ограничения:
- Нельзя создавать экземпляры абстрактного класса напрямую
- Нельзя вызывать абстрактные методы без их предварительной реализации в дочернем классе
Абстрактные классы особенно полезны для:
- Принципа DRY - избежание дублирования кода
- Стандартизации архитектуры - единый интерфейс для семейства классов
- Шаблона проектирования Template Method - общий алгоритм с изменяемыми шагами
- Контроля качества - гарантия реализации критически важных методов
Таким образом, абстрактные классы представляют собой мощный инструмент для создания продуманных, поддерживаемых и расширяемых иерархий классов в Python.
Интерфейс и его отличие от Абстрактного класса
Интерфейс — это полностью абстрактный класс, который содержит только объявления методов без какой-либо реализации. Он определяет контракт, который должны выполнять реализующие его классы, но не предоставляет никакой готовой функциональности.
В Python нет специального ключевого слова interface, как в Java или C#. Однако интерфейсы реализуются через:
1. Абстрактные классы с одними абстрактными методами
from abc import ABC, abstractmethod
class PaymentInterface(ABC):
@abstractmethod
def process_payment(self, amount):
pass
@abstractmethod
def refund_payment(self, amount):
pass
@abstractmethod
def get_status(self):
pass
2. Протоколы (с Python 3.8+)
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
def scale(self, factor: float) -> None: ...
# Любой класс с методами draw() и scale() считается Drawable
class Circle:
def draw(self) -> None:
print("Рисуем круг")
def scale(self, factor: float) -> None:
print(f"Масштабируем круг в {factor} раз")
Протоколы в Python — это механизм структурной типизации (утиной типизации), введенный в Python 3.8, который позволяет определять интерфейсы на основе наличия определенных методов и атрибутов, а не через явное наследование. В отличие от абстрактных классов, протоколы не требуют прямой декларации реализации — любой класс, который имеет требуемые методы, автоматически считается совместимым с протоколом. Это обеспечивает гибкость и поддерживает идею "утиной типизации" в Python, позволяя определять контракты на основе структуры класса, а не его явной иерархии наследования. Протоколы особенно полезны для аннотаций типов и статической проверки кода, сохраняя при этом динамическую природу Python.
Интерфейс vs Абстрактный класс
Интерфейс:
- ✅ Только объявления методов (что делать)
- ❌ Никакой реализации
- ✅ Множественное наследование (в Python)
- ✅ Определяет только контракт
- ✅ Все методы абстрактные
Абстрактный класс:
- ✅ Объявления методов + реализация
- ✅ Готовая бизнес-логика (как делать)
- ❌ Одиночное наследование (в Python)
- ✅ Сочетает контракт + готовую функциональность
- ✅ Смесь абстрактных и обычных методов
Интерфейс (только контракт):
class DatabaseInterface(ABC):
@abstractmethod
def connect(self): pass
@abstractmethod
def execute_query(self, query): pass
@abstractmethod
def disconnect(self): pass
Абстрактный класс (контракт + логика):
class DatabaseConnector(ABC):
def __init__(self, host, port):
self.host = host
self.port = port
self.connection = None
@abstractmethod
def connect(self): pass # Должны реализовать
def execute_query(self, query): # Готовая логика
if not self.connection:
raise ConnectionError("Нет подключения")
return f"Выполняем: {query}"
@abstractmethod
def disconnect(self): pass
Используйте Интерфейс когда:
- Нужно определить только контракт
- Классы из разных иерархий должны иметь общее API
- Нужно множественное наследование поведения
Используйте Абстрактный класс когда:
- Есть общая логика для наследования
- Классы тесно связаны по смыслу
- Нужно предоставить готовые реализации части методов
В Python интерфейсы эмулируются через абстрактные классы без реализации или через Протоколы. Выбор между интерфейсом и абстрактным классом зависит от того, нужно ли только определить контракт или также предоставить готовую функциональность для переиспользования.
Суперкласс и метод super()
Суперкласс (родительский класс) — это обычный класс, от которого наследуются другие классы. Он предоставляет свои методы и атрибуты дочерним классам через механизм наследования.
Создание Суперкласса
class Animal: # Это суперкласс
def __init__(self, name, age):
self.name = name
self.age = age
def speak(self):
return f"{self.name} издает звук"
def get_info(self):
return f"Имя: {self.name}, Возраст: {self.age}"
class Dog(Animal): # Animal - суперкласс для Dog
def __init__(self, name, age, breed):
# Вызов конструктора суперкласса
super().__init__(name, age)
self.breed = breed
def speak(self):
return f"{self.name} гавкает"
Метод super()
super() — это встроенная функция, которая предоставляет доступ к методам суперкласса из дочернего класса.
Основные случаи использования:
1. Вызов конструктора родителя
class Cat(Animal):
def __init__(self, name, age, color):
super().__init__(name, age) # Вызов __init__ суперкласса
self.color = color
2. Расширение методов родителя
class Dog(Animal):
def get_info(self):
# Сначала получаем информацию от родителя
base_info = super().get_info()
# Затем добавляем свою
return f"{base_info}, Порода: {self.breed}"
3. Множественное наследование
class A:
def method(self):
print("Метод класса A")
class B(A):
def method(self):
print("Метод класса B")
super().method() # Вызовет method() класса A
class C(B):
def method(self):
print("Метод класса C")
super().method() # Вызовет method() класса B
Важные особенности super()
- Автоматически определяет какой класс является родительским
- Поддерживает множественное наследование (MRO - Method Resolution Order)
- Позволяет избежать прямого указания имени родительского класса
- Обеспечивает правильный порядок вызовов в сложных иерархиях наследования
Является ли ключевое слово class объектом?
Ключевое слово class в Python не является объектом. Это синтаксическая конструкция (ключевое слово) языка программирования, которая используется для определения нового класса.
Когда вы пишете class, вы создаёте описание (шаблон, определение) класса — не сам объект. Этот класс в свою очередь является объектом типа type. То есть класс — это объект, но ключевое слово class — нет.
Ключевое слово class сообщает интерпретатору, что далее следует определение класса. В рантайме класс становится полноценным объектом, который можно присвоить переменной, использовать для создания экземпляров (объектов), добавлять атрибуты и методы.
Какой метод вызывается при удалении объекта?
Основной метод: __del__()
class MyClass:
def __del__(self):
print("Объект удаляется")
obj = MyClass()
del obj # Явное удаление → вызов __del__
Важные особенности
__del__— деструктор, но работает не так, как в других языкахdelне гарантирует немедленный вызов__del__- Вызывается сборщиком мусора, когда объект действительно удаляется
- При циклических ссылках может не вызваться вообще
- Исключения внутри
__del__подавляются
Главный недостаток
obj = MyClass()
del obj # __del__ может вызваться сейчас... а может через секунду
Нет гарантии, когда именно выполнится __del__
Альтернатива
Для гарантированной очистки используйте контекстные менеджеры:
class Resource:
def __enter__(self): ...
def __exit__(self, *args): ... # ← Гарантированно выполнится
with Resource() as r: # __exit__ вызовется сразу после блока
pass
Итоги
__del__существует, но ненадежен для критической очистки- Используйте
withи контекстные менеджеры для гарантированного освобождения ресурсов __del__лучше вообще не использовать в реальном коде
Какой метод должен быть реализован в классе, чтобы его экземпляр можно было использовать как декоратор функций?
Чтобы экземпляр класса можно было использовать как декоратор, класс должен реализовать метод __call__. Этот метод делает объект вызываемым (callable).
class Counter:
def __init__(self):
self.count = 0
def __call__(self, func):
def wrapper(*args, **kwargs):
self.count += 1
print(f"Вызов #{self.count}")
return func(*args, **kwargs)
return wrapper
counter = Counter()
@counter
def say(message):
print(message)
say("Привет") # Вызов #1 → Привет
say("Пока") # Вызов #2 → Пока
Что происходит под капотом
# Запись:
@counter
def say(message): ...
# Превращается в:
say = counter(say) # counter.__call__(say)
Случаи использования
- Когда нужно сохранять состояние между вызовами (счетчики, кэши)
- Когда нужна сложная инициализация с параметрами
- Когда логика декоратора требует нескольких методов класса
Класс с __call__ = вызываемый объект = может быть декоратором