5 ключевых понятий Python и их магические методы

Введение

Когда дело касается именования функций в Python, мы вольны использовать нижние подчеркивания, наряду с буквами и числами. Символы подчеркивания между словами особой роли не играют —создавая пробелы, они просто способствуют лучшей читаемости кода. Такой стиль написания называется змеиным регистром. Например, calculate_mean_score читается легче, чем calculatemeanscore. Как вам известно, помимо этого традиционного способа мы также можем поставить одно или два нижних подчеркивания перед именами функций, например _func, __func, тем самым давая понять, что они предназначены для использования внутри класса или модуля. Имена же без такого префикса считаются публичными API.

При именовании функций эти символы также используются в магических методах, которые подразумевают наличие двух нижних подчеркиваний до и после имени, например __func__. Иначе их еще называют dunder-методами. В текущей статье представлен обзор 5-ти тесно связанных пар распространенных магических методов, воплощающих определенные понятия Python. 

1. Инстанциирование: __new__ and __init__ 

После изучения основ структур данных Python, например словарей, списков и т. д., вам, скорее всего, встречались примеры определения пользовательских классов, где и происходила ваша первая встреча с магическим методом __init__. Этот метод используется для определенияинициализации экземпляра. Точнее говоря, в __init__ вы устанавливаете начальные атрибуты создаваемого экземпляра. Приведем простой пример:  

class Product:
    def __init__(self, name, price):
        self.name = name
        self.price = price

При использовании метода __init__ мы не вызываем его напрямую. Вместо этого он становится основойметода конструктора класса, обладающего такой же сигнатурой функции, что и __init__. Например, для создания нового экземпляра Product пишете следующий код: 

product = Product("Vacuum", 150.0)

С методом __init__ тесно связан метод __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__ and __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)
<class '__main__.Product'>

Метод __str__ может вернуть более описательные данные экземпляра. Следует отметить, что этот метод используется функцией print() для отображения информации экземпляра, как показано ниже: 

>>> print(product)
Product: Vacuum, $150.00

Хотя оба метода должны возвращать строки, метод __repr__, как правило, предназначен для разработчиков, поэтому его цель  —  показать информацию об инстанциировании. А вот метод __str__ рассчитан на обычных пользователей, в связи с чем в нем реализуется намерение отобразить что-то более информативное. 

3. Итерация: __iter__ and __next__  

С помощью кода мы можем автоматизировать одну из ключевых операций, а именно повторение некоторого действия, реализация которого подразумевает использование цикла for, выступающего в роли логического процесса. Речь идет об итерируемом объекте, который можно использовать в этом цикле. Ниже представлена самая простая форма цикла for

for item in 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

Как показано выше, мы создаем список из объекта, содержащего free samples (бесплатные образцы) в методе __iter__, который образует итератор для экземпляра пользовательского класса. Чтобы произвести итерацию, мы реализуем метод __next__, предоставляя объект из списка free samples. Перебор элементов завершается в тот момент, когда заканчиваются free samples.

4. Контекстный менеджер: __enter__ and __exit__ 

Работая с файловыми объектами Python, вы, возможно, не раз наталкивались на такой распространенный синтаксис:

with open('filename.txt') as file:
    # Далее следуют ваши операции с файлом 

Инструкция with является методом контекстного менеджера. В вышеприведенном примере она создает контекстный менеджер для файлового объекта, который по завершении нужных операций будет закрыт этим же менеджером, что снова сделает его доступным для других процессов.

В целом, контекстные менеджеры  —  это объекты Python, которые управляют совместно используемыми ресурсами, например открывают и закрывают за нас файлы. Не будь их, нам бы пришлось управлять ими вручную, что, возможно, повлекло бы за собой ошибки. 

Чтобы реализовать такое поведение с помощью пользовательского класса, в этот класс нужно добавить методы __enter__ и __exit__. Первый из них послужит для создания контекстного менеджера, подготавливающего необходимый нам ресурс для работы. Второй же будет закрывать использованные ресурсы, снова делая их доступными для остального кода. Рассмотрим простой пример с классом Product:

>>> 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 _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__. По завершении в ней операции происходит вызов метода __exit__.

Однако следует отметить, что для создания контекстного менеджера мы можем реализовать методы __enter__ и __exit__. Это намного легче сделать с помощью функции декоратора contextmanager.

5. Улучшенный контроль доступа к атрибутам: __getattr__ and __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__ вызываться не будет.

Отмечу, что есть и другой магический метод, тесно связанный с контролем доступа  — это __getattribute__. Он похож на __getattr__, но вызывается при каждом обращении к атрибуту. Кроме того, он схож и с __setattr__, в связи с чем также подразумевает использование super() в своей реализации во избежание ошибки бесконечной рекурсии.

Заключение

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

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

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Yong Cui, Ph.D.: 5 Pairs of Magic Methods in Python That You Should Know

Предыдущая статьяНастоящие беспилотные такси выезжают на улицы города
Следующая статьяКонцепции разработки UI на примерах еды