pythongetattr

Is the problem with __getattribute__ or __getattr__? What else can be fixed in this code?


I am writing a tutorial project to work with __getattribute__ and __getattr__, what I want is: I have an ATM that only works with one currency and I want it to be impossible to add and use other currencies. But with the uncommented __getattribute__ method I get an error

... line 87, in <module>
atm.add_rub(50)
TypeError: 'NoneType' object is not callable

I understand that the problem is that in _rub_ I get None, but I don't understand why

code:

from decimal import Decimal
from typing import Any


class Atm:
    def __init__(self, rub: int | float = 0) -> None:
        """ Метод инициализации экземпляра """
        self._rub = Decimal(rub)
        self._count = 0

    def add_rub(self, money: Decimal) -> None:
        """ Метод для работы с начислением рублей экземпляра """
        if money > 0 and self.check50(money):
            self += money
        else:
            raise ValueError('Некорректное значение')

    def take_rub(self, money: Decimal) -> None:
        """ Метод для работы со списанием рублей экземпляра """
        if money > self._rub:
            raise ValueError('Сумма больше, чем денег на счете')
        percent = money * Decimal(1.015)
        if not 30 < percent < 600:
            if percent < 30:
                percent = 30
            elif percent > 600:
                percent = 600
        self -= percent  # доделать in place вычитание
    
    def count(self) -> None:
        """ Метод для проверки """

    def check50(self, money: Decimal) -> None:
        """ Метод для проверки кратности 50 """
        if money % 50:
            raise ValueError('Значение должно быть кратным 50')
        return not money % 50

    def check_wealth(self):
        """ Метод для списания налога на богатство """
        if self._rub > 5_000_000:
            self._rub *= Decimal(0.9)

    def __str__(self) -> str:
        """ Метод для строчного представления экземпляра """
        return f'rub = {self._rub:0.2f}'

    def __repr__(self) -> str:
        """ Метод для представления экземпляра для программиста"""
        return f'Atm({self._rub})'

    def __iadd__(self, money: Decimal) -> Any:
        """ Метод для сложения экземпляра и числа \n(вспомогательный метод для начисления рублей) """
        self._rub = self._rub + money
        return Atm(self._rub)

    def __getattribute__(self, currency: str) -> Any:
        """ Метод для работы с попыткой обращения к атрибутам экземпляра """
        if not currency in ('_rub', '_count'):
            raise AttributeError(
                'Error 1: Ведутся технические работы, пока что возможна только работа в рублях')
        return object.__getattribute__(self, currency)

    def __setattr__(self, name, value) -> None:
        """ Метод для попытки присвоения значения атрибуту экземпляра """
        if not name in ('_rub', '_count'):
            raise AttributeError(
                'Error 2: Ведутся технические работы, пока что возможна только работа в рублях')
        return object.__setattr__(self, name, value)

    def __getattr__(self, item) -> None:
        """ Метод для попытки присвоения значения несуществующему атрибуту экземпляра """
        return None

    def __delattr__(self, item):
        if item in ('_rub', '_count'):
            setattr(self, item, 0)
        else:
            object.__delattr__(self, item)


if __name__ == '__main__':
    # help(Atm)
    atm = Atm()
    print(atm)

    atm.add_rub(50)
    print(atm)

    # atm._usd = 100  # AttributeError: Error 2: Ведутся технические работы, пока что возможна только работа в рублях
    print(atm._usd)  # None

    atm._rub = 100
    print(atm)

    print(atm.check50(100))

    del atm._rub
    print(atm)

when commenting __gettatribute__ the error disappears, but why?


Solution

  • __getattribute is called unconditionally, so atm.add_rub(5) first calls atm.__getattribute__('add_rub'). Upon returning an AttributeError, then atm.__getattr__('add_rub') is called an returns None.

    Use __slots__ to prevent additional instance attributes from being created:

    class ATM:
        __slots__ = ('_rub', '_count')
    

    and don't mess around with __getattribute__ or __getattr__ at all. (For your stated purpose, you would be more concerned about additional class attributes being added to ATM as well, but there's nothing you can do about that without delving into metaclasses, and that's absolutely overkill for any reasonable concern.)