Перейти к основному содержимому

Углубленное ООП

📺 Слайды к лекции

Материалы в разработке

Эта страница находится в процессе подготовки и не является финальной версией. Содержание будет дорабатываться и обновляться в течение текущего семестра.

Создание объектов: __new__ и __init__

В Python создание объекта происходит в два этапа: __new__ создаёт экземпляр, __init__ инициализирует его.

new_init.py
class MyClass:
def __new__(cls):
print("__new__ вызван")
instance = super().__new__(cls)
return instance

def __init__(self):
print("__init__ вызван")

obj = MyClass()
# __new__ вызван
# __init__ вызван
Важно

Если __new__ возвращает объект другого типа, __init__ не вызывается:

class A:
def __new__(cls):
return 1 # не объект A!
def __init__(self):
print("Никогда не выполнится")

A() # вернёт 1, __init__ не вызван

__slots__ — оптимизация памяти

По умолчанию атрибуты хранятся в __dict__. Использование __slots__ заменяет словарь на фиксированный массив, экономя память:

slots.py
class Point:
__slots__ = ('x', 'y')

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

p = Point(1, 2)
# p.z = 3 # AttributeError — нельзя добавить новый атрибут
Практический совет

Используйте __slots__ для классов с большим количеством экземпляров (например, ORM-модели, точки данных). Экономия памяти может составлять 30-50%.

Дескрипторы

Дескриптор — объект, определяющий один из методов __get__, __set__ или __delete__. Дескрипторы управляют доступом к атрибутам.

descriptor.py
class Validated:
def __init__(self, min_value, max_value):
self.min_value = min_value
self.max_value = max_value

def __set_name__(self, owner, name):
self.name = name

def __set__(self, instance, value):
if not self.min_value <= value <= self.max_value:
raise ValueError(f"{self.name}: {value} не в диапазоне")
instance.__dict__[self.name] = value

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)

class Temperature:
celsius = Validated(-273.15, 1000)

t = Temperature()
t.celsius = 36.6 # OK
# t.celsius = -300 # ValueError
Определение

Data descriptor определяет __set__ или __delete__ и имеет приоритет над __dict__ экземпляра. Non-data descriptor определяет только __get__ — атрибут экземпляра может его перекрыть.

Properties (@property)

@property — встроенный дескриптор для создания управляемых атрибутов:

property.py
class Circle:
def __init__(self, radius):
self._radius = radius

@property
def radius(self):
return self._radius

@radius.setter
def radius(self, value):
if value < 0:
raise ValueError("Радиус не может быть отрицательным")
self._radius = value

@property
def area(self):
import math
return math.pi * self._radius ** 2

Статические методы и методы класса

methods.py
class MyClass:
class_var = 0

@staticmethod
def static_method():
"""Не принимает self/cls, вызывается без объекта"""
return "static"

@classmethod
def class_method(cls):
"""Принимает cls — ссылку на класс"""
cls.class_var += 1
return cls.class_var

MRO и алгоритм C3-линеаризации

При множественном наследовании Python определяет порядок поиска методов с помощью C3-линеаризации.

mro.py
class A: x = 'a'
class B(A): pass
class C(A): x = 'c'
class D(B, C): pass

print(D.x) # 'c' — C перекрывает A
print(D.__mro__) # D -> B -> C -> A -> object
Определение

MRO (Method Resolution Order) — порядок, в котором Python ищет метод при вызове. В Python 3 используется алгоритм C3-линеаризации, который обеспечивает предсказуемый порядок обхода.

Частая ошибка

C3-линеаризация может отказать, если порядок наследования противоречив:

class X: pass
class Y: pass
class A(X, Y): pass
class B(Y, X): pass # обратный порядок!
# class C(A, B): pass # TypeError: Cannot create a consistent MRO

Абстрактные классы (ABC)

abc_example.py
from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def area(self) -> float:
...

@abstractmethod
def perimeter(self) -> float:
...

class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius

def area(self) -> float:
import math
return math.pi * self.radius ** 2

def perimeter(self) -> float:
import math
return 2 * math.pi * self.radius

# Shape() # TypeError: Can't instantiate abstract class

Dataclasses

dataclass.py
from dataclasses import dataclass, field

@dataclass
class Point:
x: float
y: float
label: str = "unnamed"
tags: list[str] = field(default_factory=list)

@property
def distance_from_origin(self) -> float:
return (self.x ** 2 + self.y ** 2) ** 0.5

p = Point(3.0, 4.0)
print(p) # Point(x=3.0, y=4.0, label='unnamed', tags=[])
print(p.distance_from_origin) # 5.0
Практический совет

Используйте @dataclass(frozen=True) для неизменяемых объектов — они автоматически получают __hash__ и могут использоваться как ключи словаря.

Контекстные менеджеры

context_manager.py
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode

def __enter__(self):
self.file = open(self.filename, self.mode)
return self.file

def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
return False # не подавлять исключения

with FileManager("test.txt", "w") as f:
f.write("Hello")

contextlib — упрощённый синтаксис

contextlib.py
from contextlib import contextmanager

@contextmanager
def timer(label):
import time
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")

with timer("operation"):
sum(range(1000000))