Перевод статьи 5 Pairs of Magic Methods in Python That You Should Know.
В этой статье мы изучим пять ключевых концепций Python, связанных с «магическими» методами.
Содержание
Введение
Часто программируя на Python мы сталкиваемся с выбором имен для функций и обычно используем для этого буквы (конечно же алфавита английского языка) и цифры, а также символ подчеркивания. При этом использование в имени функций и методов символа подчеркивания между отдельными словами, лишь улучшает их читаемость (и соответственно понимаемость), имитируя пробелы между отдельными словами. Этот стиль именования известен как snake naming style или «змеиный» стиль. Например, следующее имя для переменной или функции: calculate_mean_score
читается значительно легче и понятнее, чем calculatemeanscore
. Помимо этого наиболее распространенного способа применения символа подчеркивания, он может использоваться в качестве префикса для имени функции (метода), например, _func
, __func
, для обозначения того, что эта функция (метод) предназначены для внутреннего private использования в классе или модуле. Имена без префикса подчеркивания считаются общедоступными public API.
В этой статье будет рассмотрен подробнее другой способ использования символов подчеркивания при именовании функций (методов) — обозначение «магических» или специальных методов. В этом случае, мы помещаем два символа подчеркивания перед и после имени функции (метода) и в результате получим что-то вроде этого: __func__
. В виду подобного использования двойного подчеркивания некоторые разработчики называют специальные методы «dunder-методами» или просто «dunders» (double under). В настоящей статье рассмотрены пять тесно связанных друг с другом пар наиболее известных «магических» методов, каждый из которых иллюстрирует основные концепции реализации Python как языка программирования.
1. Создание: __new__
и __init__
Изучая основы реализации и применения на практике различных структур данных в Python (например, словарей, списков и т.д.), вы уже наверняка успели познакомиться с примерами определения пользовательских классов, и тогда, наверняка, впервые обратили внимание на следующий «магический»: __init__
. Он используется для описания поведения класса при инициализации его экземпляра. В частности, в методе __init__
вы можете инициализировать начальные значения для атрибутов класса при создании его экземпляра. Рассмотрим простой пример использования этого метода:
class Product: def __init__(self, name, price): self.name = name self.price = price
Когда мы вызываем метод __init__
, мы не делаем это непосредственно. Метод __init__
используется в роли «строительной» основы метода конструктора класса, который имеет ту же функциональную сигнатуру, что и метод __init__
. Например, чтобы создать новый экземпляр класса Product
, используется код следующего вида:
product = Product("Vacuum", 150.0)
С методом __init__
тесно связан другой «магический» метод __new__
, который обычно не реализуется в пользовательских классах. По сути, метод __new__
создает пустой объект экземпляра класса, который затем передается методу __init__
для завершения процесса инициализации класса: непосредственной передачи начальных значений его атрибутам.
Другими словами, создание нового экземпляра класса или процесс, более известный как реализация экземпляра класса, влечет последовательный вызов двух «магических» методов __new__
и __init__
.
Следующий код демонстрирует цепочку вызовов этих методов:
>>> class Product: def __new__(cls, *args): new_product = object.__new__(cls) print("Product __new__ gets called") return new_product def __init__(self, name, price): self.name = name self.price = price print("Product __init__ gets called") >>> product = Product("Vacuum", 150.0) Product __new__ gets called Product __init__ gets called
2. Строковое представление: __repr__
и __str__
Оба этих метода важны для определения корректного строкового представления содержимого пользовательского класса. И прежде мы рассмотрим их поподробнее, обратите внимание на код ниже:
class Product: def __init__(self, name, price): self.name = name self.price = price def __repr__(self): return f"Product({self.name!r}, {self.price!r})" def __str__(self): return f"Product: {self.name}, ${self.price:.2f}"
Метод __repr__
фактически возвращает строку кода, с помощью которой был создан экземпляра класса. Проще говоря, эту строку можно передать в функцию eval()
для воспроизведения нового экземпляра класса. Следующий фрагмент кода иллюстрирует использование этой возможности на практике:
>>> product = Product("Vacuum", 150.0) >>> repr(product) "Product('Vacuum', 150.0)" >>> evaluated = eval(repr(product)) >>> type(evaluated)
Метод __str__
может вернуть нечто более описательное об объекте экземпляра класса. Следует отметить, что метод __str__
используется «под капотом» функцией print()
для отображения информации, связанной с экземпляром класса, как это показано в примере кода ниже.
>>> print(product) Product: Vacuum, $150.00
Хотя эти оба метода возвращают строку, метод __repr__
обычно используется разработчиками, для отображения информации о реализации конкретного экземпляра класса. В то время как метод __str__
предназначен в большей степени для использования обычными пользователями, когда мы хотим узнать общую информацию об экземпляре класса.
3. Итерирование: __iter__
и __next__
Рассмотрим реализацию еще одной ключевой возможности, которую мы можем автоматизировать с помощью нашего кода — это повторение однотипных действий. В частности ее реализация предполагает использование цикла for
для организации управляемого, определенной логикой, потока выполнения. Пользуясь принятой терминологией говорят, что некоторые объекты являются iterable
, то есть могут быть использованы для обработки в цикле. Самый простой пример использования цикла for
для этого случая представлен ниже:
for item in iterable: # действия с item
«Под капотом» этой инструкции некоторый iterable
объект, который преобразуется в итератор, что позволяет в каждой итерации цикла получать его элементы по одному для последующей обработки. Вообще говоря, итераторы — это специальные объекты Python, которые можно использовать для извлечения отдельных элементов, подлежащих перебору (итерации). Процесс этого преобразования осуществляется с помощью специального «магических» метода __iter__
. Кроме того, получение следующего элемента итератора включает реализацию другого «магических» метода __next__
. Давайте допишем предыдущий пример кода для использования класса Product
в качестве итератора в цикле for
:
>>> class Product: ... def __init__(self, name, price): ... self.name = name ... self.price = price ... ... def __str__(self): ... return f"Product: {self.name}, ${self.price:.2f}" ... ... def __iter__(self): ... self._free_samples = [Product(self.name, 0) for _ in range(3)] ... print("Iterator of the product is created.") ... return self ... ... def __next__(self): ... if self._free_samples: ... return self._free_samples.pop() ... else: ... raise StopIteration("All free samples have been dispensed.") ... >>> product = Product("Perfume", 5.0) >>> for i, sample in enumerate(product, 1): ... print(f"Dispense the next sample #{i}: {sample}") ... Iterator of the product is created. Dispense the next sample #1: Product: Perfume, $0.00 Dispense the next sample #2: Product: Perfume, $0.00 Dispense the next sample #3: Product: Perfume, $0.00
Как мы уже говорили выше, сначала с помощью метода __iter__
мы создаем список объектов, содержащий несколько элементов типа _free_samples
, и который реализует итератор для экземпляров нашего класса. Затем для того, чтобы реализовать «итерационное» поведение, мы реализуем метод __next__
, который возвращает очередной объект из списка. Итерирование заканчивается, когда заканчиваются все элементы из списка.
4. Менеджер контекста: __enter__
и __exit__
Когда мы имеем дело с файловыми объектами Python, то нам часто встречается код, имеющий следующий синтаксис:
with open('filename.txt') as file: # Ваши действия с содержимым файла
Эта техника использования оператора with
более известна как менеджер контекста. В частности, в примере выше для обработки содержимого файла оператор with
создает менеджер контекста для работы с файловым объектом. Затем после всех действий с содержимым файла менеджер контекста обеспечивает своевременное закрытие файлового объекта и файл снова станет доступным для других процессов, т.е. корректно «освободить» некоторый общий ресурс.
Итак, в общем случае менеджеры контекста — это специальные объекты Python, которые управляют доступом к некоторым общим ресурсам, то есть корректно открывают и закрывают их. Без них нам бы пришлось управлять этими процессами вручную, что в конечном счете приводит к возникновению различного рода ошибок.
Для того, чтобы реализовать такое поведения в нашем пользовательском классе, необходимо реализовать соответствующие «магические» методы: __enter__
и __exit__
. Метод __enter__
настраивает новый менеджер контекста, который подготавливает необходимый для дальнейшей работы ресурс, а метод __exit__
соответственно освобождает используемые ресурсы для того, чтобы снова сделать их доступными. Рассмотрим следующий фрагмент кода, который содержит простой пример с уже знакомым нам классом Product
:
>>> class Product: ... def __init__(self, name, price): ... self.price = price ... ... def __str__(self): ... return f"Product: {self.name}, ${self.price:.2f}" ... ... def _move_to_center(self): ... print(f"The product ({self}) occupies the center exhibit spot.") ... ... def _move_to_side(self): ... print(f"Move {self} back.") ... ... def __enter__(self): ... print("__enter__ is called") ... self._move_to_center() ... ... def __exit__(self, exc_type, exc_val, exc_tb): ... print("__exit__ is called") ... self._move_to_side() ... >>> product = Product("BMW Car", 50000) >>> with product: ... print("It's a very good car.") ... __enter__ is called The product (Product: BMW Car, $50000.00) occupies the center exhibit spot. It's a very good car. __exit__ is called Move Product: BMW Car, $50000.00 back.
Как видите, когда объект экземпляра нашего класса встраивается в инструкцию оператора with
, будет вызван метод __enter__
. Если выполнение блока кода ниже инструкции with
завершено, будет вызван метод __exit__
.
Тем не менее, следует отметить, что мы можем реализовать методы __enter__
и __exit__
намного проще с помощью декораторов функций.
5. Полный контроль доступа к атрибутам: __getattr__
и __setattr__
Если у вас имеется опыт программирования на других языках, то вы, возможно, уже использовали технику реализации явных методов для установки и получения значений атрибутов объекта или экземпляра класса (так называемых геттеров и сеттеров). В Python нам не нужно использовать эту технику методов управления доступом для каждого отдельного атрибута объекта. Тем не менее, вполне возможно, что мы можем захотим иметь некоторый контроль над ними путем реализации «магических» методов __getattr__
и __setattr__
. В частности, метод __getattr__
вызывается при обращении к атрибутам объекта или экземпляра класса, а метод __setattr__
— когда мы устанавливаем значения для их атрибутов.
>>> class Product: ... def __init__(self, name): ... self.name = name ... ... def __getattr__(self, item): ... if item == "formatted_name": ... print(f"__getattr__ is called for {item}") ... formatted = self.name.capitalize() ... setattr(self, "formatted_name", formatted) ... return formatted ... else: ... raise AttributeError(f"no attribute of {item}") ... ... def __setattr__(self, key, value): ... print(f"__setattr__ is called for {key!r}: {value!r}") ... super().__setattr__(key, value) ... >>> product = Product("taBLe") __setattr__ is called for 'name': 'taBLe' >>> product.name 'taBLe' >>> product.formatted_name __getattr__ is called for formatted_name __setattr__ is called for 'formatted_name': 'Table' 'Table' >>> product.formatted_name 'Table'
Метод __setattr__
вызывается неявно каждый раз, когда мы пытаемся установить значение для атрибута объекта (экземпляра класса). И чтобы использовать его правильно, мы должны использовать метод суперкласса super()
. В противном случае это может привести к бесконечной рекурсии, а затем к зависанию нашего скрипта.
После установки значения атрибута formatted_name
, он станет частью объекта __dict__
, и поэтому __getattr__
не будет бесконечно вызываться.
Атрибуты объекта можно условно разделить две группы: определённые в Python (такие как
__class__
,) и определённые пользователем.__dict__
согласно этой классификации, относится к “системным” (определённым Python) атрибутам. Его задача — хранить пользовательские атрибуты. Он представляет собой словарь dictionary, в котором ключом является имя_атрибута, а значением, соответственно, значение_атрибута.
В качестве примечания, есть еще один «магический» метод, тесно связанный с управлением доступом к атрибутам __getattribute__
, который похож на __getattr__
, но вызывается каждый раз при доступе к атрибуту. В этом отношении он по аналогии похож на метод __setattr__
, и вы так же должны использовать super()
для для собственной реализации метода __getattribute__
, чтобы избежать ошибки (бесконечной рекурсии).
Вывод
В этой статье мы достаточно подробно рассмотрели пять пар «магических» методов, с помощью которых изучили на практических примерах пять основных концепций языка Python, непосредственно связанных с каждой из них. Я надеюсь, что теперь вы станете лучше понимать смысл этих концепций, а так же способы использования «магических» методов в ваших проектах Python.