Узнайте, как создавать собственные методы dunder/magic в Python, которые можно вызывать из пользовательской встроенной функции.
Содержание
· Встроенные модули Python
∘ Что такое встроенные модули Python?
∘ Создание встроенных модулей в Python
· Пользовательский метод Dunder
· Плохо ли использовать Волшебные методы?
· Сводка
Python — это объектно-ориентированный язык программирования, позволяющий расширять классы с помощью специальных методов dunder. Типичными примерами этого являются __init__
и __str__
, которые позволяют вам установить инициализатор класса и строковое представление соответственно. Это означает, что при первом вызове класса он запускает некоторый код инициализации, и вы можете вызвать str
для экземпляра класса.
Python позволяет реализовать определенные методы dunder, которые позволяют нам использовать встроенные операторы, такие как сложение (+
) и вычитание (-
). Мы достигаем этого, добавляя определенные методы dunder в наши классы. Ниже приведен пример этого:
class Point: def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): return Point(self.x + other.x, self.y + other.y) def __sub__(self, other): return Point(self.x - other.x, self.y - other.y)
В этом примере у нас есть класс Point
с определенными методами __add__
и __sub__
. Это позволяет нам добавлять или вычитать Point
экземпляров. Когда мы добавляем два экземпляра Point
, Python вызывает метод __add__
. Точно так же, когда мы вычитаем между двумя экземплярами Point
, Python вызывает метод __sub__
. Давайте посмотрим на несколько примеров:
Добавить:
>>> point_1 = Point(1, 2) >>> point_2 = Point(3, 4) >>> point_3 = point_1 + point_2 >>> point_3.x 4 >>> point_3.y 6
Когда мы используем оператор +
, он вызывает метод __add__
, устанавливающий от other
до point_2
.
Вычесть
>>> point_1 = Point(1, 2) >>> point_2 = Point(3, 4) >>> point_3 = point_1 - point_2 >>> point_3.x -2 >>> point_3.y -2
Когда мы используем оператор -
, он вызывает метод __sub__
, устанавливающий other
в point_2
.
Встроенные Python
Что такое встроенные функции Python?
Вы когда-нибудь задумывались, почему мы можем вызывать определенные функции, такие как abs
, sum
и min
, без необходимости напрямую импортировать какую-либо из этих функций? Когда вы начинаете изучать Python, вы, вероятно, уже знаете, что эти функции просто доступны.
Попробуйте запустить в оболочке Python следующее:
import builtins dir(builtins)
Вы заметите набор функций и классов, которые можно использовать в Python, не импортируя их. Вы можете убедиться в этом, сравнив функцию/класс из модуля builtins
с функцией/классом, который вы обычно используете. Давайте проверим это с помощью функции sum
:
>>> import builtins >>> builtins.sum == sum True
Создание встроенных модулей в Python
Мы рассмотрим, как мы можем расширить встроенные функции, добавив свои собственные функции, которые будут доступны из любого места. Я не рекомендую использовать этот метод, поскольку создание глобальных функций может затруднить отслеживание ошибок. Вместо этого мы рассмотрим лучшие методы позже.
Мы реализуем встроенную функцию для преобразования килограммов в фунты. Чтобы перевести килограммы в фунты, умножаем массу на 2,205. Чтобы представить это в функции, мы можем создать следующее:
def kg_to_lbs(mass): return mass * 2.025
В нынешнем виде функцию kg_to_lbs
нужно будет импортировать, прежде чем ее можно будет использовать. Давайте представим, что kg_to_lbs
было добавлено к custom_builtins.py
.
У нас есть два других файла, file_1.py
и file_2.py
. Оба файла должны иметь строку, которая читает from custom_builtins import kg_to_lbs
, чтобы использовать функцию. Но это не то поведение, которое нам нужно.
Функцию можно добавить как встроенную, чтобы она была доступна глобально. Единственное предостережение заключается в том, что модуль, который устанавливает функцию как встроенную, должен быть в какой-то момент импортирован (подробнее об этом позже).
Чтобы добавить kg_to_lbs
в качестве встроенного, мы делаем следующее:
import builtins def kg_to_lbs(mass): return mass * 2.025 builtins.kg_to_lbs = kg_to_lbs
При условии, что внутри нашего проекта в какой-то момент импортируется custom_builtins.py
, kg_to_lbs
его можно использовать как встроенный.
Давайте вернемся к нашему сценарию file_1.py
и file_2.py
. Предположим, что в file_1.py
мы пытаемся сделать следующее:
>>> kg_to_lbs(50) Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'kg_to_lbs' is not defined
Это вызывает NameError
, так как модуль, содержащий kg_to_lbs
, не был импортирован. Давайте исправим это:
>>> import custom_builtins >>> kg_to_lbs(50) 110.231
Несмотря на то, что явно не импортировался kg_to_lbs
и не вызывался custom_builtins.kg_to_lbs
, функция была доступна. Что, если file_2.py
импортировал file_1.py
, но не импортировал custom_builtins
? Это будет работать?
>>> import file_1 >>> kg_to_lbs(50) 110.231
Да, это работает!
Модуль, который обновляет встроенные модули, необходимо импортировать один раз. Если Python знает об импорте, функция будет доступна.
Аккуратный трюк, верно? Но хорошая ли это идея?
Предположим, у нас есть большой проект, и где-то в вашем проекте есть пользовательская встроенная функция. Функция называется calculate_total
, и все, что она делает, — это умножает сумму на 1000. Предположим, что в другом модуле вы создали функцию с таким же именем. Все, что делает эта функция, — умножает сумму на 100 вместо 1000.
Вы собираетесь использовать функцию, которая умножает число на 100, но забыли импортировать эту функцию. Обычно это вызовет NameError
, и вам будет предложено импортировать функцию. Однако если эта функция доступна вам как часть встроенной функции, вы не получите сообщение об ошибке, а вместо этого код продолжит выполнение. Риск здесь заключается в том, что вы можете не заметить ошибку, поскольку она не вызвала исключение. Худший тип ошибок — это те, которые не вызывают никаких ошибок. По этой причине я бы не рекомендовал создавать собственные встроенные методы и вместо этого явно импортировал функцию.
Выбор явного импорта функции безопаснее, и это означает, что мы все еще можем использовать наш собственный метод dunder. Мы добавим поддержку для этого в следующем разделе.
Пользовательский метод Дандера
Предположим, у нас есть класс, который содержит некоторую базовую информацию о человеке:
class Person: def __init__(self, name, age, weight_kg): self.name = name self.age = age self.weight_kg = weight_kg
Чтобы использовать метод kg_to_lbs
, мы должны сделать следующее:
>>> person = Person("Tom", 99, 50) >>> kg_to_lbs(person.weight_kg) 110.231
Давайте настроим нашу функцию для поддержки метода __kg_to_lbs__
dunder, который будет пользовательским методом, который мы добавим в наш класс.
def kg_to_lbs(mass): if hasattr(mass, "__kg_to_lbs__"): return mass.__kg_to_lbs__() else: return mass * 2.205
Теперь функция проверит, есть ли у объекта mass
метод __kg_to_lbs__
, и запустит его, если он существует. В противном случае он рассчитает массу по исходной формуле. Вы могли заметить, что мы используем функцию hasattr
, и вам может быть интересно, можете ли вы использовать hasattr
с числами. Ответ - да! Мы можем, потому что все является классом, а функцию hasattr
можно запускать для всех экземпляров класса.
Все, что осталось сделать, это добавить метод dunder в класс Person
:
class Person: def __init__(self, name, age, weight_kg): self.name = name self.age = age self.weight_kg = weight_kg def __kg_to_lbs__(self): return kg_to_lbs(self.weight_kg)
Давайте посмотрим на это в действии:
>>> person = Person("Tom", 99, 50) >>> kg_to_lbs(person) 110.231
Когда kg_to_lbs
вызывается впервые, он замечает, что person
имеет метод __kg_to_lbs__
. Поэтому он вызывает person.__kg_to_lbs__
. Этот метод вызывает саму функцию kg_to_lbs
. На этот раз он предоставляет weight_in_kg
в качестве аргумента. kg_to_lbs
this видит, что weight_in_kg
не имеет аргумента __kg_to_lbs__
, и вычисляет его вес в фунтах.
Плохо ли использовать пользовательские магические методы?
Методы Dunder обычно предоставляются Python. Существует риск того, что в будущем Python может представить новый метод dunder, который окажется таким же, как тот, который вы создали. Итак, каковы ваши варианты?
- Вы можете сделать свои методы dunder достаточно уникальными, чтобы Python не создал метод с таким же именем. Как насчет
__my_precious__
? - Вместо метода dunder создайте абстрактный класс, который ожидает определенный метод.
Я рекомендую избегать создания методов dunder. Вместо этого создайте абстрактный базовый класс, который ожидает реализации определенного метода.
Давайте преобразуем наш класс Person
для использования абстрактного базового класса и обновим kg_to_lbs
, чтобы он указывал на новый метод.
from abc import ABC, abstractmethod class KgToLbs(ABC): @abstractmethod def kg_to_lbs(self): """Coverts kg to lbs""" pass class Person(KgToLbs): def __init__(self, name, age, weight_kg): self.name = name self.age = age self.weight_kg = weight_kg def kg_to_lbs(self): return kg_to_lbs(self.weight_kg) def kg_to_lbs(mass): if hasattr(mass, "kg_to_lbs"): return mass.kg_to_lbs() else: return mass * 2.205
KgToLbs
определяет один абстрактный метод ( kg_to_lbs
). Это означает, что любой класс, наследующий KgToLbs
, должен переопределить метод kg_to_lbs
. В противном случае будет поднято NotImplementedError
.
В классе Person
мы заменили __kg_to_lbs__
на kg_to_lbs
.
Метод kg_to_lbs
теперь вызывает kg_to_lbs
, если у объекта есть атрибут с таким же именем.
Краткое содержание
В этой статье мы обсудили, как работают методы dunder и как вы можете создавать свои собственные магические методы. Мы также посетили библиотеку Python builtins
и увидели, как такие функции, как sum
, становятся глобально доступными. Обладая этими знаниями, мы смогли создать собственные пользовательские методы dunder, которые были бы доступны по всему миру.
Однако, несмотря на возможность сделать это, мы обсудили, почему может быть опасно создавать глобальные встроенные функции. Мы также обсудили, почему рекомендуется избегать создания пользовательских методов dunder.
Мы увидели, как вместо этого мы можем настроить наши методы dunder и глобальные функции, чтобы сделать их более безопасными. Для этого мы решили явно импортировать наши функции, а не устанавливать их как глобальные встроенные. Мы также решили создать абстрактный базовый класс, который определял бы любые методы, которые потребуются нашим функциям для работы. Метод, реализованный в классе, больше не будет методом дандера, чтобы избежать возможных конфликтов или путаницы со стандартными методами дандера Python.
Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Посетите наш Community Discord и присоединитесь к нашему Коллективу талантов.