Сообщить об ошибке.

Настройка доступа к атрибутам класса Python

Создание вычисляемых атрибутов класса

В разделе рассматриваются специальные (магические) методы, которые могут быть определены для настройки поведения при доступе к атрибутам экземпляров класса (использование, присвоение или удаление x.name).

Содержание:


object.__getattr__(self, name):

Метод object.__getattr__() вызывается, когда доступ к атрибуту по умолчанию завершается с ошибкой AttributeError (либо __getattribute__() вызывает исключение AttributeError, потому что name не является атрибутом экземпляра, либо атрибутом в дереве классов для self; либо метод __get__() свойства name вызывает AttributeError). Этот метод должен либо вернуть (вычисленное) значение атрибута, либо вызвать исключение AttributeError.

Обратите внимание, что если атрибут найден с помощью обычного механизма, то метод __getattr__() не вызывается. (Это преднамеренная асимметрия между __getattr__() и __setattr__().) Это делается как из соображений эффективности, так и потому, что в противном случае __getattr__() не сможет получить доступ к другим атрибутам экземпляра. Обратите внимание, что, по крайней мере, для переменных экземпляра можно подделать полный контроль, не вставляя никаких значений в словарь атрибутов экземпляра, а вместо этого вставляя их в другой объект.

Смотрите метод __getattribute__(), чтобы получить полный контроль над доступом к атрибутам.

class Order:
    
    bascket = 0
    
    def __init__(self, price, num):
        self.price = price
        self.num = num
    
    def __getattr__(self, attrname):
        # магический метод `__getattr__` будет вызываться при 
        # попытке доступа к любому несуществующему атрибуту, 
        # имя атрибута сохраняется в переменной `attrname`
        if attrname == "total":
            # условие выполнится при доступе к атрибуту `total`,  
            # т.к. он фактически не определен и является вычисляемым
            return self.price * self.num
        elif attrname == "bascket":
            # условие выполнятся не будет, т.к. 
            # атрибут `bascket` существует
            return self.bascket + 100
        else:
            # условие выполнится при доступе к 
            # любому, несуществующему атрибуту
            print(f'Атрибута {attrname} не существует!')
            raise AttributeError

>>> order = Order(10, 15)
# вычисляемый атрибут
>>> order.total
# 150

# атрибут `bascket` задан, для него
# магический метод `__getattr__` не вызывается
>>> order.bascket
# 0

# атрибут `basket_total` не существует
>>> order.basket_total
# Атрибута basket_total не существует!
# Traceback (most recent call last):
# ...
# AttributeError

# присвоим значение `на лету`
>>> order.basket_total = 0
# теперь все нормально
>>> order.basket_total
# 0

Из примера отчетливо видно, что специальный (магический) метод __getattr__() вызывается автоматически только у тех атрибутов, которые не определены (когда доступ к атрибуту по умолчанию завершается с ошибкой AttributeError).

object.__getattribute__(self, name):

Метод object.__getattribute__() вызывается безоговорочно (всегда) для реализации доступа к атрибутам для экземпляров класса. Если класс также определяет специальный метод __getattr__(), то он не будет вызываться, если только метод __getattribute__() либо вызовет его явно, либо вызовет исключение AttributeError.

Этот метод должен возвращать (вычисленное) значение атрибута или вызывать исключение AttributeError. Чтобы избежать бесконечной рекурсии в этом методе, его реализация всегда должна вызывать метод базового класса с тем же name для доступа к любым нужным атрибутам, например, object.__getattribute__(self, name).

Примечание. Специальный метод object.__getattribute__() может быть пропущен при поиске специальных методов в результате неявного вызова через синтаксис языка или встроенных функций.

class Order:
    
    basket = 0
    
    def __init__(self, price, num):
        self.price = price
        self.num = num
    
    def __getattribute__(self, attrname):
        # магический метод `__getattribute__` будет вызываться  
        # ВСЕГДА, при попытке доступа к любому атрибуту, 
        # имя атрибута сохраняется в переменной `attrname`
        if attrname == "total":
            # `total` - вычисляемый атрибут,  
            return self.price * self.num
        if attrname == "price":
            # `price` - существующий атрибут, что бы не было 
            # рекурсии обратимся к нему как сказано в документации
            return object.__getattribute__(self, attrname)
        elif attrname == "basket":
            # `basket` существующий атрибут, 
            # попробуем к нему обратиться через `self`
            return self.basket
        elif attrname == "total_basket":
            # `basket` атрибут не существует, при этом 
            # сразу возвращается значение
            return 0
        else:
            # определяем как вызывать остальные существующие
            # атрибуты - в частности `num`
            return object.__getattribute__(self, attrname)

>>> order = Order(10, 15)
>>> order.total
# 150
>>> order.price
# 10

# назначим другую стоимость
>>> order.price = 5
# вычисляемый атрибут работает
>>> order.total
# 75

# к этому атрибуту обратились через `self`
>>> order.basket  # в итоге рекурсия (как описано в документации)
# Traceback (most recent call last):
# ...
#   [Previous line repeated 995 more times]
#   File "<stdin>", line 13, in __getattribute__
# RecursionError: maximum recursion depth exceeded in comparison

# в определении метода `__getattribute__` 
# к атрибуту `total_basket` не обращаемся
# а сразу возвращаем значение 
>>> order.total_basket 
# 0

# попробуем что нибудь присвоить
>>> order.total_basket = 100
# значение осталось то же, как и ожидалось
>>> order.total_basket
# 0

# атрибут `other` - не определен
>>> order.other
# Traceback (most recent call last):
# ...
# AttributeError: 'Order' object has no attribute 'other'

# присвоим значение `на лету`
>>> order.other = 'ok'
# теперь работает
>>> order.other
'ok'

Из примера отчетливо видно, что специальный (магический) метод __getattribute__() вызывается автоматически для всех атрибутов, существуют они или нет. Но, для предотвращения рекурсии, к существующим атрибутам необходимо обращаться явно object.__getattribute__(self, attrname).

object.__setattr__(self, name, value):

Метод object.__setattr__() вызывается при попытке присвоения значения атрибуту. Метод вызывается вместо обычного механизма (т.е. сохранения значения в словаре экземпляра). Аргумент name - это имя атрибута, value - значение, которое будет ему присвоено.

Если специальный метод __setattr__() назначает атрибут экземпляра класса, то он должен вызвать метод базового класса с тем же именем name, например, object.__setattr__(self, name, value).

class Order:
    
    basket = 0 # кол-во товара в корзине
    
    def __init__(self, price, num):
        self.price = price
        self.num = num
        
    def __setattr__(self, attrname, value):
        if attrname == 'basket':
            # увеличиваем количество товаров в корзине на 
            # значение, которое передаем атрибуту `basket` 
            self.__dict__[attrname] = self.basket + value
        else:
            # для остальных атрибутов, просто 
            # присваиваем новое значение
            self.__dict__[attrname] = value
    
    def __getattr__(self, attrname):
        # магический метод `__getattr__` вызываться всегда  
        # если атрибут фактически не определен и не создан `на лету`
        if attrname == "total":
            # `total` - вычисляемый атрибут (общая стоимость товара)  
            return self.price * self.num
        elif attrname == "total_basket":
            # `basket` - вычисляемый атрибут (стоимость товара в корзине)
            return self.price * self.basket
        else:
            raise AttributeError

>>> order = Order(10, 15)
>>> order.total
# 150

# смотрим что в корзине
>>> order.basket
# 0

# положим 1 товар в корзину
>>> order.basket = 1
# смотрим
>>> order.basket
# 1

# положим еще 2 товара в корзину
>>> order.basket = 2
# смотрим
>>> order.basket
# 3

# стоимость товаров в корзине
>>> order.total_basket
# 30

object.__delattr__(self, name):

Метод object.__delattr__() работает так же как __setattr__(), но только для удаления атрибута вместо присвоения. Метод должен быть реализован, только если имеет смысл операции удаления del obj.name для этого атрибута объекта.

object.__dir__(self):

Метод object.__dir__() вызывается, когда для объекта вызывается функция dir(). Должна быть возвращена последовательность. Функция dir() преобразует возвращенную последовательность в список и сортирует его.