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?
__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.)