Магические методы Python: превращение кода в элегантное решение

Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Опытные программисты, желающие углубить свои знания Python
  • Разработчики, изучающие объектно-ориентированное программирование и магические методы
  • Люди, интересующиеся созданием элегантного и профессионального кода на Python

    Магические методы — это то, что превращает обычный Python-код в элегантное, читаемое и мощное решение. Сталкивались ли вы когда-нибудь с кодом, где оператор + применяется к вашему собственному классу? Или где экземпляр класса можно вызывать как функцию? За всей этой "магией" стоят специальные методы, обрамлённые двойными подчёркиваниями, которые открывают целый новый уровень взаимодействия с языком Python. В этом руководстве мы разберём всю магию под капотом Python, превращая вас из обычного кодера в архитектора элегантных объектно-ориентированных решений. 🐍✨

Стремитесь освоить магические методы и другие продвинутые концепции Python на реальных проектах? Обучение Python-разработке от Skypro — это именно то, что вам нужно. Наш курс построен на практических задачах, где вы не только теоретически изучите dunder-методы, но и научитесь применять их в реальных проектах, создавая элегантный и профессиональный код. Присоединяйтесь к сообществу разработчиков, которые поднимают свои навыки на новый уровень!

Сущность магических методов в Python и их назначение

Магические методы в Python (также известные как dunder-методы или специальные методы) — это предопределённые методы, обрамлённые двойным подчёркиванием, которые позволяют классам взаимодействовать с встроенными функциями и операциями языка. По сути, это механизм, через который Python реализует перегрузку операторов и другие продвинутые возможности объектно-ориентированного программирования.

Термин "dunder" происходит от сокращения "double underscore" (двойное подчёркивание), поскольку все эти методы начинаются и заканчиваются двойным подчёркиванием: __method__.

Александр Петров, Lead Python Developer

Когда я только начинал глубже изучать Python, магические методы казались мне странной и непонятной особенностью языка. Однажды я столкнулся с проектом, где команда использовала собственный класс Vector для работы с векторными вычислениями. В коде я увидел конструкции вида v1 + v2 и v1 * 5, которые работали с объектами класса. Это выглядело настолько элегантно, что я решил разобраться, как это реализовано.

Оказалось, что разработчики класса просто перегрузили операторы с помощью магических методов __add__ и __mul__. Когда я осознал всю мощь этого подхода, для меня открылись новые горизонты в дизайне API. С тех пор я активно использую магические методы, чтобы делать свой код более интуитивным и самодокументируемым.

Чтобы понимать значимость магических методов, рассмотрим несколько примеров их влияния на код:

  • Без магических методов: result = instance.add(5) — явный вызов метода
  • С магическими методами: result = instance + 5 — интуитивно понятный код
  • Без магических методов: string_representation = instance.to_string() — специальный метод для строкового представления
  • С магическими методами: string_representation = str(instance) — использование стандартной функции Python

Основное назначение магических методов — интеграция пользовательских классов с языковыми конструкциями Python. Они позволяют создавать объекты, которые ведут себя как встроенные типы данных, но с кастомной логикой.

Категория Примеры методов Функциональность
Инициализация/деструкция __init__, __del__ Создание и уничтожение объектов
Строковое представление __str__, __repr__ Представление объекта в виде строки
Математические операции __add__, __sub__, __mul__ Арифметические действия с объектами
Сравнение __eq__, __lt__, __gt__ Операции сравнения объектов
Функциональное поведение __call__ Вызов объекта как функции
Управление атрибутами __getattr__, __setattr__ Доступ и изменение атрибутов
Контейнеры __len__, __getitem__, __iter__ Поведение, подобное контейнерам (списки, словари)

Вот простой пример использования магического метода __str__ для кастомизации строкового представления объекта:

Python
Скопировать код
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages

def __str__(self):
return f"{self.title} by {self.author}, {self.pages} pages"

book = Book("Python Cookbook", "David Beazley", 706)
print(book) # Output: Python Cookbook by David Beazley, 706 pages

Без магического метода __str__ вызов print(book) вывел бы нечто вроде <__main__.Book object at 0x7f8a5b7b1d90>, что не несёт полезной информации о содержимом объекта.

Пошаговый план для смены профессии

Основные магические методы для инициализации объектов

Первое знакомство большинства Python-разработчиков с магическими методами происходит через __init__ — метод инициализации, который вызывается при создании нового экземпляра класса. Однако это только верхушка айсберга в процессе жизненного цикла объекта. 🧙‍♂️

Жизненный цикл объекта в Python контролируется целой группой магических методов:

  • __new__(cls, *args, **kwargs) — создаёт новый экземпляр класса перед инициализацией
  • __init__(self, *args, **kwargs) — инициализирует новосозданный экземпляр
  • __del__(self) — вызывается перед уничтожением объекта сборщиком мусора

Рассмотрим более подробно метод __new__. В отличие от __init__, который только инициализирует объект, __new__ фактически создаёт его. Это единственный магический метод, который вызывается как классовый метод (первый аргумент — класс, а не экземпляр).

Типичный пример использования __new__ — создание синглтонов или изменение типа создаваемого объекта:

Python
Скопировать код
class Singleton:
_instance = None

def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance

def __init__(self, data=None):
self.data = data

# Проверка работы синглтона
s1 = Singleton("первый")
s2 = Singleton("второй")
print(s1.data) # Output: второй
print(s2.data) # Output: второй
print(s1 is s2) # Output: True

В этом примере, несмотря на два вызова конструктора, создаётся только один экземпляр класса. Метод __new__ проверяет, существует ли уже экземпляр, и возвращает его, если это так. Метод __init__ вызывается каждый раз, поэтому данные обновляются.

Рассмотрим теперь __init__ и __del__ более детально:

Python
Скопировать код
class Resource:
def __init__(self, name):
print(f"Инициализация ресурса '{name}'")
self.name = name
self.open = True

def __del__(self):
if hasattr(self, 'open') and self.open:
print(f"Автоматическое закрытие ресурса '{self.name}'")
self.open = False

def close(self):
if self.open:
print(f"Закрытие ресурса '{self.name}'")
self.open = False

# Пример использования
r = Resource("database")
# Вывод: Инициализация ресурса 'database'

# Если не закрыть ресурс вручную:
# r.close()

# Когда r выходит из области видимости или программа завершается,
# вызывается __del__, и ресурс закрывается автоматически
# Вывод: Автоматическое закрытие ресурса 'database'

Здесь __del__ выступает как деструктор, обеспечивающий корректное освобождение ресурсов при уничтожении объекта. Однако следует помнить, что Python не гарантирует немедленный вызов __del__ после завершения работы с объектом из-за наличия сборщика мусора, поэтому не стоит полагаться на него для критически важных операций освобождения ресурсов.

Мария Соколова, Python-архитектор

В одном из наших высоконагруженных проектов мы столкнулись с утечкой памяти. После долгих часов отладки выяснилось, что проблема крылась в неправильном использовании магического метода __del__.

В нашем классе DatabaseConnection мы использовали __del__ для закрытия соединений с базой данных. Однако из-за циклических ссылок (наш объект хранил ссылку на себя через замыкание в callback-функции) сборщик мусора не мог определить, что объект больше не используется. В результате соединения оставались открытыми и утекали.

Мы переработали дизайн, заменив __del__ на явный метод закрытия соединения и использование менеджера контекста через магические методы __enter__ и __exit__. Теперь код выглядел так:

Python
Скопировать код
with DatabaseConnection() as db:
db.execute_query()

Утечка была устранена, и мы получили более чистый, предсказуемый код. Этот случай напомнил мне о важности понимания жизненного цикла объектов и правильного применения магических методов.

Ещё один важный аспект инициализации объектов — копирование. Python предоставляет два магических метода для этого:

  • __copy__(self) — создаёт поверхностную копию объекта
  • __deepcopy__(self, memo) — создаёт глубокую копию объекта

Эти методы вызываются функциями из модуля copy:

Python
Скопировать код
import copy

class DataContainer:
def __init__(self, values):
self.values = values

def __copy__(self):
print("Метод __copy__ вызван")
return DataContainer(self.values)

def __deepcopy__(self, memo):
print("Метод __deepcopy__ вызван")
return DataContainer(copy.deepcopy(self.values, memo))

# Пример использования
data = DataContainer([1, [2, 3], 4])
shallow_copy = copy.copy(data) # Вызывает __copy__
deep_copy = copy.deepcopy(data) # Вызывает __deepcopy__

# Проверка различий между копиями
data.values[1][0] = 200
print(shallow_copy.values) # [1, [200, 3], 4] – вложенный список изменился
print(deep_copy.values) # [1, [2, 3], 4] – вложенный список остался прежним

Метод Когда вызывается Типичное использование Особенности
__new__ При создании объекта Синглтоны, метаклассы, изменение типа создаваемого объекта Должен возвращать экземпляр, обычно не переопределяется
__init__ После создания объекта Инициализация атрибутов, валидация начальных данных Не должен возвращать значение (кроме None)
__del__ Перед уничтожением объекта Освобождение ресурсов Не гарантируется немедленный вызов, лучше использовать менеджеры контекста
__copy__ При вызове copy.copy() Кастомизация поверхностного копирования Должен возвращать новый экземпляр
__deepcopy__ При вызове copy.deepcopy() Кастомизация глубокого копирования Получает memo-словарь для отслеживания уже скопированных объектов

Операторные магические методы для математических действий

Одна из наиболее мощных возможностей, которую предоставляют магические методы в Python — это перегрузка операторов. Она позволяет использовать стандартные математические операторы (+, -, *, /) с пользовательскими объектами, создавая интуитивно понятный и элегантный код. 🔢

Рассмотрим основные магические методы для математических операций:

  • __add__(self, other) — операция сложения (a + b)
  • __sub__(self, other) — операция вычитания (a – b)
  • __mul__(self, other) — операция умножения (a * b)
  • __truediv__(self, other) — операция деления (a / b)
  • __floordiv__(self, other) — целочисленное деление (a // b)
  • __mod__(self, other) — остаток от деления (a % b)
  • __pow__(self, other[, modulo]) — возведение в степень (a ** b)

Важно понимать, что для каждой бинарной операции существует правая версия метода, которая вызывается, когда ваш объект находится справа от оператора. Например, __radd__ вызывается для выражения 5 + obj, если у 5 нет собственного метода __add__ для работы с типом obj.

Также существуют операции присваивания (+=, -=, *=), для которых используются методы с префиксом i (in-place): __iadd__, __isub__, __imul__ и т.д.

Давайте рассмотрим практический пример — создадим класс для работы с комплексными числами:

Python
Скопировать код
class ComplexNumber:
def __init__(self, real, imag):
self.real = real
self.imag = imag

def __str__(self):
if self.imag >= 0:
return f"{self.real} + {self.imag}i"
return f"{self.real} – {abs(self.imag)}i"

def __add__(self, other):
if isinstance(other, ComplexNumber):
return ComplexNumber(self.real + other.real, self.imag + other.imag)
if isinstance(other, (int, float)):
return ComplexNumber(self.real + other, self.imag)
raise TypeError("Unsupported operand type")

def __radd__(self, other):
# Коммутативная операция, можно просто вызвать __add__
return self.__add__(other)

def __sub__(self, other):
if isinstance(other, ComplexNumber):
return ComplexNumber(self.real – other.real, self.imag – other.imag)
if isinstance(other, (int, float)):
return ComplexNumber(self.real – other, self.imag)
raise TypeError("Unsupported operand type")

def __mul__(self, other):
if isinstance(other, ComplexNumber):
# (a+bi) * (c+di) = (ac-bd) + (ad+bc)i
real = self.real * other.real – self.imag * other.imag
imag = self.real * other.imag + self.imag * other.real
return ComplexNumber(real, imag)
if isinstance(other, (int, float)):
return ComplexNumber(self.real * other, self.imag * other)
raise TypeError("Unsupported operand type")

def __iadd__(self, other):
# Модифицируем текущий объект и возвращаем его (для +=)
result = self.__add__(other)
self.real, self.imag = result.real, result.imag
return self

# Пример использования
c1 = ComplexNumber(2, 3) # 2 + 3i
c2 = ComplexNumber(1, -2) # 1 – 2i

print(c1 + c2) # 3 + 1i
print(c1 – c2) # 1 + 5i
print(c1 * c2) # 8 – 1i
print(c1 + 5) # 7 + 3i
print(5 + c1) # 7 + 3i (благодаря __radd__)

c1 += c2
print(c1) # 3 + 1i (объект c1 модифицирован)

В этом примере мы реализовали несколько математических операций для комплексных чисел. Обратите внимание, как мы обрабатываем различные типы операндов и реализуем правые версии операторов для поддержки выражений вида 5 + complex_obj.

Еще один интересный пример — унарные операторы, такие как отрицание и абсолютное значение:

Python
Скопировать код
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y

def __str__(self):
return f"Vector({self.x}, {self.y})"

def __neg__(self):
"""Унарный минус: -obj"""
return Vector(-self.x, -self.y)

def __abs__(self):
"""Абсолютное значение: abs(obj)"""
return (self.x**2 + self.y**2)**0.5

def __pos__(self):
"""Унарный плюс: +obj"""
return self # Для вектора это обычно не меняет ничего

def __round__(self, ndigits=0):
"""Округление: round(obj, ndigits)"""
return Vector(round(self.x, ndigits), round(self.y, ndigits))

# Пример использования
v = Vector(3.14159, -2.71828)
print(v) # Vector(3.14159, -2.71828)
print(-v) # Vector(-3.14159, 2.71828)
print(abs(v)) # 4.14...
print(round(v)) # Vector(3, -3)

Таблица ниже суммирует основные операторные магические методы и соответствующие им операторы:

Категория Метод Оператор/Функция Пример использования
Арифметика __add__, __radd__ + a + b, b + a
Арифметика __sub__, __rsub__ - a – b, b – a
Арифметика __mul__, __rmul__ * a * b, b * a
Арифметика __truediv__, __rtruediv__ / a / b, b / a
Арифметика __floordiv__, __rfloordiv__ // a // b, b // a
Арифметика __mod__, __rmod__ % a % b, b % a
Инкременты __iadd__ += a += b
Инкременты __isub__ -= a -= b
Инкременты __imul__ *= a *= b
Унарные __neg__ - -a
Унарные __pos__ + +a
Унарные __abs__ abs() abs(a)
Возведение в степень __pow__, __rpow__, __ipow__ **, **= a ** b, b ** a, a **= b

Методы сравнения и преобразования типов в Python

Магические методы сравнения позволяют объектам участвовать в логических операциях сравнения (==, !=, <, >, <=, >=), а методы преобразования типов — в операциях приведения типов и форматирования. Эти функциональные возможности делают пользовательские классы совместимыми со всей экосистемой Python. 🔄

Начнём с методов сравнения:

  • __eq__(self, other) — равенство (==)
  • __ne__(self, other) — неравенство (!=)
  • __lt__(self, other) — меньше чем (<)
  • __gt__(self, other) — больше чем (>)
  • __le__(self, other) — меньше или равно (<=)
  • __ge__(self, other) — больше или равно (>=)

Важно отметить, что в Python 3 не обязательно определять все методы сравнения. Если определён метод __eq__, но не определён __ne__, то != будет выполнять not (a == b). Аналогично, если определены __lt__ и __eq__, то __le__ автоматически выводится из них.

Вместо реализации всех методов сравнения по отдельности, можно воспользоваться декоратором @functools.total_ordering, который автоматически создаст отсутствующие методы, исходя из минимального набора.

Python
Скопировать код
import functools

@functools.total_ordering
class Version:
def __init__(self, major, minor, patch):
self.major = major
self.minor = minor
self.patch = patch

def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"

def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)

def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)

# Пример использования
v1 = Version(1, 2, 3)
v2 = Version(1, 3, 0)
v3 = Version(1, 2, 3)

print(v1 == v3) # True
print(v1 != v2) # True
print(v1 < v2) # True
print(v1 <= v3) # True (автоматически из __eq__ и __lt__)
print(v2 > v1) # True (автоматически из __lt__)
print(v2 >= v3) # True (автоматически из __eq__ и __lt__)

Теперь перейдём к методам преобразования типов. Эти методы вызываются, когда объект используется в контексте, требующем другого типа:

  • __str__(self) — строковое представление для конечных пользователей: str(obj), print(obj)
  • __repr__(self) — строковое представление для разработчиков: repr(obj), в интерактивной консоли
  • __bytes__(self) — байтовое представление: bytes(obj)
  • __format__(self, format_spec) — форматированное строковое представление: format(obj, format_spec), f-строки
  • __bool__(self) — логическое значение: bool(obj), в условиях
  • __int__(self) — целочисленное представление: int(obj)
  • __float__(self) — представление с плавающей точкой: float(obj)
  • __complex__(self) — комплексное представление: complex(obj)
  • __hash__(self) — хэш-значение: hash(obj), использование в качестве ключа словаря

Рассмотрим пример класса, реализующего различные методы преобразования:

Python
Скопировать код
class Temperature:
def __init__(self, celsius):
self.celsius = celsius

def __str__(self):
"""Строковое представление для пользователей"""
return f"{self.celsius}°C"

def __repr__(self):
"""Строковое представление для разработчиков"""
return f"Temperature({self.celsius})"

def __format__(self, format_spec):
"""Поддержка форматированного вывода"""
if format_spec == 'F':
# По Фаренгейту
fahrenheit = self.celsius * 9/5 + 32
return f"{fahrenheit:.1f}°F"
elif format_spec == 'K':
# По Кельвину
kelvin = self.celsius + 273.15
return f"{kelvin:.1f}K"
else:
# По умолчанию в Цельсиях
return f"{self.celsius:.1f}°C"

def __float__(self):
"""Преобразование в число с плавающей точкой"""
return float(self.celsius)

def __bool__(self):
"""Температура считается истинной, если выше нуля"""
return self.celsius > 0

def __eq__(self, other):
"""Сравнение температур"""
if isinstance(other, Temperature):
return self.celsius == other.celsius
if isinstance(other, (int, float)):
return self.celsius == other
return NotImplemented

# Пример использования
t = Temperature(25)
print(str(t)) # 25°C
print(repr(t)) # Temperature(25)
print(f"{t}") # 25°C
print(f"{t:F}") # 77.0°F
print(f"{t:K}") # 298.1K
print(float(t)) # 25.0
print(bool(t)) # True
print(bool(Temperature(-5))) # False
print(t == 25) # True
print(t == Temperature(25)) # True

Особое внимание стоит уделить магическому методу __hash__, который связан с __eq__. Если вы определяете __eq__, то автоматическая реализация __hash__ отключается, и объекты класса становятся нехешируемыми. Если вам нужно, чтобы экземпляры вашего класса можно было использовать в качестве ключей в словарях или элементов множеств, вам нужно явно определить __hash__.

Python
Скопировать код
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __eq__(self, other):
if not isinstance(other, Person):
return NotImplemented
return self.name == other.name and self.age == other.age

def __hash__(self):
# Хеш должен быть неизменным, если объект не меняется
# и соответствовать условию: если a == b, то hash(a) == hash(b)
return hash((self.name, self.age))

# Пример использования
p1 = Person("Alice", 30)
p2 = Person("Alice", 30)
p3 = Person("Bob", 25)

print(p1 == p2) # True
print(hash(p1) == hash(p2)) # True
print(p1 == p3) # False

# Теперь мы можем использовать Person в качестве ключа словаря
person_data = {p1: "Данные о Алисе", p3: "Данные о Бобе"}
print(person_data[p2]) # "Данные о Алисе" (p2 == p1, поэтому работает)

Методы преобразования типов и сравнения — это мощный инструмент для создания интуитивно понятных пользовательских типов данных, которые хорошо интегрируются с встроенными операциями Python. При правильном использовании они делают ваш код более читаемым и естественным.

Расширенные магические методы для продвинутых сценариев

Помимо уже рассмотренных категорий, Python предлагает множество расширенных магических методов для специфических сценариев использования, таких как создание объектов, подобных коллекциям, поддержка вызываемых объектов, управление доступом к атрибутам и реализация менеджеров контекста. 🔍

Давайте исследуем эти продвинутые возможности, начиная с магических методов для коллекций:

  • __len__(self) — возвращает количество элементов: len(obj)
  • __getitem__(self, key) — доступ по индексу/ключу: obj[key]
  • __setitem__(self, key, value) — установка значения по индексу/ключу: obj[key] = value
  • __delitem__(self, key) — удаление элемента по индексу/ключу: del obj[key]
  • __contains__(self, item) — проверка наличия элемента: item in obj
  • __iter__(self) — возвращает итератор: for x in obj
  • __next__(self) — возвращает следующий элемент итератора: next(obj) (если сам объект является итератором)

Вот пример реализации пользовательской коллекции — класса Range, который эмулирует функциональность встроенной функции range, но с дополнительными возможностями:

Python
Скопировать код
class Range:
def __init__(self, start, stop=None, step=1):
if stop is None:
stop = start
start = 0

self.start = start
self.stop = stop
self.step = step

# Вычисляем длину заранее
self.length = max(0, (self.stop – self.start + (self.step – 1 if self.step > 0 else self.step + 1)) // self.step)

def __len__(self):
return self.length

def __getitem__(self, index):
if isinstance(index, slice):
# Обработка срезов: r[1:5:2]
start = index.start or 0
stop = index.stop if index.stop is not None else self.length
step = index.step or 1

if start < 0:
start += self.length
if stop < 0:
stop += self.length

return [self[i] for i in range(start, min(stop, self.length), step)]

if index < 0:
index += self.length

if not 0 <= index < self.length:
raise IndexError("Range index out of range")

return self.start + index * self.step

def __iter__(self):
current = self.start
for _ in range(self.length):
yield current
current += self.step

def __contains__(self, item):
if self.step > 0:
return self.start <= item < self.stop and (item – self.start) % self.step == 0
else:
return self.stop < item <= self.start and (self.start – item) % abs(self.step) == 0

def __repr__(self):
if self.start == 0 and self.step == 1:
return f"Range({self.stop})"
elif self.step == 1:
return f"Range({self.start}, {self.stop})"
else:
return f"Range({self.start}, {self.stop}, {self.step})"

# Пример использования
r = Range(1, 10, 2) # 1, 3, 5, 7, 9
print(len(r)) # 5
print(r[2]) # 5
print(r[-1]) # 9
print(r[1:4]) # [3, 5, 7]
print(5 in r) # True
print(6 in r) # False

# Итерация
for num in r:
print(num, end=' ') # 1 3 5 7 9

# Преобразование в список
print(list(r)) # [1, 3, 5, 7, 9]

Другая важная категория — магические методы для вызываемых объектов и управления атрибутами:

  • __call__(self, *args, **kwargs) — позволяет экземплярам класса быть вызываемыми как функции: obj(arg1, arg2)
  • __getattr__(self, name) — вызывается, когда атрибут не найден обычным способом: obj.name
  • __getattribute__(self, name) — вызывается при любой попытке доступа к атрибуту
  • __setattr__(self, name, value) — вызывается при установке атрибута: obj.name = value
  • __delattr__(self, name) — вызывается при удалении атрибута: del obj.name
  • __dir__(self) — возвращает список доступных атрибутов: dir(obj)
Python
Скопировать код
class Validator:
def __init__(self, validation_func):
self.validation_func = validation_func

def __call__(self, value):
if not self.validation_func(value):
raise ValueError(f"Validation failed for value: {value}")
return value

# Создание валидатора как экземпляр класса
is_positive = Validator(lambda x: x > 0)

# Использование валидатора как функции
try:
print(is_positive(10)) # 10
print(is_positive(-5)) # ValueError: Validation failed for value: -5
except ValueError as e:
print(e)

class AttributeTracker:
def __init__(self):
self._attributes = {}
self._access_count = {}

def __getattr__(self, name):
"""Вызывается, когда атрибут не найден"""
if name in self._attributes:
# Увеличиваем счетчик доступа
self._access_count[name] = self._access_count.get(name, 0) + 1
return self._attributes[name]
raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'")

def __setattr__(self, name, value):
"""Вызывается при установке атрибута"""
if name.startswith('_'):
# Внутренние атрибуты устанавливаем обычным способом
super().__setattr__(name, value)
else:
# Пользовательские атрибуты отслеживаем
self._attributes[name] = value
if name not in self._access_count:
self._access_count[name] = 0

def __dir__(self):
"""Возвращает список доступных атрибутов"""
# Объединяем встроенные атрибуты и наши кастомные
return sorted(set(super().__dir__()) | set(self._attributes.keys()))

def get_access_stats(self):
"""Возвращает статистику доступа к атрибутам"""
return dict(self._access_count)

# Пример использования
tracker = AttributeTracker()
tracker.name = "Alice"
tracker.age = 30

print(tracker.name) # Alice
print(tracker.name) # Alice
print(tracker.age) # 30

print(tracker.get_access_stats()) # {'name': 2, 'age': 1}

try:
print(tracker.unknown) # AttributeError
except AttributeError as e:
print(e)

print(dir(tracker)) # список атрибутов, включая 'age' и 'name'

Наконец, рассмотрим магические методы для реализации менеджеров контекста (используемых в конструкции with):

  • __enter__(self) — вызывается в начале блока with
  • __exit__(self, exc_type, exc_value, traceback) — вызывается при выходе из блока with
Python
Скопировать код
class Timer:
def __init__(self, name="Operation"):
self.name = name

def __enter__(self):
"""Вызывается при входе в контекст with"""
import time
self.start_time = time.time()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
"""Вызывается при выходе из контекста with"""
import time
elapsed = time.time() – self.start_time
print(f"{self.name} took {elapsed:.6f} seconds")
# Если вернуть True, то исключение будет подавлено
return False # не подавляем исключения

class DatabaseConnection:
def __init__(self, connection_string):
self.connection_string = connection_string
self.connection = None

def __enter__(self):
"""Открываем соединение при входе в контекст"""
print(f"Connecting to database: {self.connection_string}")
# В реальном коде здесь было бы что-то вроде:
# self.connection = db.connect(self.connection_string)
self.connection = "Dummy connection"
return self.connection

def __exit__(self, exc_type, exc_val, exc_tb):
"""Закрываем соединение при выходе из контекста"""
print(f"Closing database connection: {self.connection_string}")
if exc_type:
print(f"An error occurred: {exc_val}")
# В реальном коде:
# if self.connection:
# self.connection.close()
self.connection = None

# Пример использования менеджеров контекста
with Timer("Calculation") as timer:
# Выполняем какую-то операцию
result = sum(i ** 2 for i in range(1000000))
print(f"Result: {result}")

# Пример с вложенными менеджерами контекста
with DatabaseConnection("postgres://example.com/db") as db1:
print(f"Working with {db1}")

try:
with DatabaseConnection("mysql://example.com/db") as db2:
print(f"Working with both {db1} and {db2}")
# Имитация ошибки
if True:
raise ValueError("Simulated error")
except ValueError as e:
print(f"Caught error: {e}")

Магические методы — ваш ключ к созданию элегантного и профессионального кода на Python. Используя их правильно и не злоупотребляя ими, вы сможете создавать API, которые интуитивно понятны и "естественны" для других разработчиков. Помните основной принцип Python: "Explicit is better than implicit" — всегда стремитесь к ясности и предсказуемости в своём коде, даже когда используете "магию". Овладение магическими методами — это не конечная цель, а инструмент, который поможет вам создавать более элегантные решения для реальных проблем программирования.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Что такое магические методы в Python?
1 / 5

Загрузка...