Магические методы Python и __getattr__

На второй неделе нашего курса Наука о данных с Python мы говорим о важности хороших, чистых и интуитивно понятных интерфейсов и о том, насколько прекрасна объектно-ориентированная модель Python (тема для другого поста). Пока мы объясняем это, мы вводим несколько полезных магических методов, таких как __len__, __str__, __repr__ и т. Д. Среди них мы также показываем и __getattr__, и __getattribute__, и кажется, что это всегда интересно нашим ученикам, поэтому мы подумали, что должны поделиться им. с миром.

Учебник по магическим методам

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

  • __init__, который служит инициализатором объекта (иногда его неправильно называют конструктором)
  • __str__, который обеспечивает «строковое представление» вашего объекта.
  • __add__, что позволяет «перегружать» оператор +.

Что общего у всех этих методов? Ну, очевидно, все они начинаются и заканчиваются двойным подчеркиванием (__). Но помимо этого, их делает «волшебными методами» то, что они вызываются каким-то «особым образом». Мы не вызываем эти методы вручную; Это делает Python. Например, мы не делаем obj.__str__(), мы делаем str(obj).

Существует МНОГО волшебных методов, но, как я сказал в начале, я хочу сосредоточиться на __getattr__ и __getattribute__.

__getattr__

Начнем с __getattr__. Этот метод позволит вам «перехватывать» ссылки на атрибуты, которых нет в вашем объекте. Давайте посмотрим на простой пример, чтобы понять, как это работает:

class Dummy(object):
    pass
d = Dummy()
d.does_not_exist  # Fails with AttributeError

В этом примере не удается получить доступ к атрибуту (с AttributeError), потому что атрибут does_not_exist не существует.

Но с помощью __getattr__ волшебного метода мы можем перехватить этот поиск несуществующего атрибута и сделать что-нибудь, чтобы он не потерпел неудачу:

class Dummy(object):
    def __getattr__(self, attr):
        return attr.upper()
d = Dummy()
d.does_not_exist # 'DOES_NOT_EXIST'
d.what_about_this_one  # 'WHAT_ABOUT_THIS_ONE'

Но если атрибут существует, __getattr__ не будет вызываться:

class Dummy(object):
    def __getattr__(self, attr):
        return attr.upper()
d = Dummy()
d.value = "Python"
print(d.value)  # "Python"

__getattribute__

__getattribute__ похож на __getattr__ с той важной разницей, что __getattribute__ будет перехватывать КАЖДЫЙ поиск атрибутов, не имеет значения, существует атрибут или нет. Позвольте мне показать вам простой пример:

class Dummy(object):
    def __getattribute__(self, attr):
        return 'YOU SEE ME?'
d = Dummy()
d.value = "Python"
print(d.value)  # "YOU SEE ME?"

В этом примере объект d уже имеет атрибут value. Но когда мы пытаемся получить к нему доступ, мы не получаем исходное ожидаемое значение («Python»); мы просто получаем то, что __getattribute__ вернули. Это означает, что мы практически потеряли атрибут value; он стал «недоступным».

Если вам когда-нибудь понадобится __getattribute__ для моделирования чего-то похожего на __getattr__, вам придется проделать более сложную обработку Python:

class Dummy(object):
    def __getattribute__(self, attr):
        __dict__ = super(Dummy, self).__getattribute__('__dict__')
        if attr in __dict__:
            return super(Dummy, self).__getattribute__(attr)
        return attr.upper()
d = Dummy()
d.value = "Python"
print(d.value)  # "Python"
print(d.does_not_exist)  # "DOES_NOT_EXIST"

Более реалистичный пример

Нередко случайный выбор каждого атрибута не является обычным явлением. Обычно мы используем __getattr__ с четкой целью: всякий раз, когда мы хотим предоставить некоторый динамический доступ к нашим объектам. Хорошим примером может быть расширение базового кортежа Python, чтобы добавить в него немного Scala (мне очень нравится Scala 🙌). В Scala кортежи создаются очень похоже на Python:

val a_tuple = ("z", 3, “Python”, -1)

Но доступ к ним осуществляется по-другому:

println(a_tuple._1) // “z”
println(a_tuple._3) // “Python”

К каждому элементу в кортеже обращаются как к атрибуту, причем первый элемент является атрибутом _1, вторым _2 и так далее.

Мы можем легко расширить наш общий кортеж Python, чтобы он соответствовал этому поведению, код действительно прост:

Вы не знали, что можете расширять встроенные коллекции Python 😱? Ознакомьтесь с нашим Курсом Data Science with Python!

В этом примере вы можете увидеть, как мы отлавливаем отсутствующие атрибуты с помощью __getattr__, но если этот атрибут не в форме _n (где n - целое число), мы просто поднимаем AttributeError вручную.

Заключение

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

Особая благодарность Fabrício Calado (@fabricalado) за вычитку и предложения 🙇🏻.