Узнайте, как создавать собственные методы 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 и присоединитесь к нашему Коллективу талантов.