Please refer to the function main():
the results are mentioned as comments. *General note: When Class()
has an attribute
containing e.g. str
-> the @propery.setter
will validate once set.
However, when I am storing a dictionary
in the Class.attribute
, My @property.setter
is not working if I directly set a key : value
class CoinJar():
def __init__(self):
self._priceDict = {'current' : 0.0, 'high' : 0.0, 'low' : 0.0}
self.klines = {}
self.volume = {}
def __str__(self):
return f'\n\U0001F36A'
@property
def priceDict(self):
return self._priceDict
@priceDict.setter
def priceDict(self, priceDict):
print('setting price')
try:
newPrice = float(priceDict.get('current'))
except ValueError as e:
print(f'Input cannot be converted to float {e}')
# Exceptions
if len(priceDict.keys()) == 0 or newPrice == None or type(newPrice) not in [int, float]:
raise ValueError('setting illegal value')
#setting high, low, current
self._priceDict['current'] = newPrice
if newPrice > self._priceDict['high']:
self._priceDict['high'] = newPrice
if newPrice < self._priceDict['low'] or self._priceDict['low'] == 0.0:
self._priceDict['low'] = newPrice
def main():
btc = CoinJar()
btc.priceDict = {'current' : 500} # This is calling priceDict.setter => accepts and performs the .setter expressions
btc.priceDict['flamingo'] = 20 # This is not calling priceDict.setter => can do what i want, with 'current, high, flamingo, *'
btc.priceDict = {'flamingo' : 500} # This is calling priceDict.setter => raises Exception (as expected)
print(btc)
def retrieveData():
...
def analyseData():
...
if __name__ == '__main__':
main()
CodePudding user response:
Have a look at this simplified example:
class AClass:
def __init__(self):
self._some_dict = None
@property
def some_dict(self):
print('getting')
return self._some_dict
@some_dict.setter
def some_dict(self, value):
print('setting')
self._some_dict = value
an_obj = AClass()
an_obj.some_dict = {} # setter gets called
an_obj.some_dict['a_key'] = 1 # getter gets called, as dict is being accessed
A property setter for an attribute gets called when the value of the attribute itself needs to be set. I.e. when you assign a new dictionary to the attribute for a dict
attribute.
A property getter gets called when the attribute is accessed ('read' / 'gotten').
The setter does not get called when you manipulate the attribute otherwise, like setting a key or value for the dictionary. You could trigger on that, but you'd have to override the dictionary.
Something like this:
class MyDict(dict):
def __setitem__(self, key, *args, **kwargs):
print('setting a dictionary value')
super().__setitem__(key, *args, **kwargs)
class AClass:
def __init__(self):
self._some_dict = None
@property
def some_dict(self):
print('getting')
return self._some_dict
@some_dict.setter
def some_dict(self, value):
print('setting')
self._some_dict = MyDict(value)
an_obj = AClass()
an_obj.some_dict = {} # setter gets called
an_obj.some_dict['a_key'] = 1 # getter gets called, as well as item setter
# Note: this just calls setter, as it just directly sets the attribute to the new dict:
an_obj.some_dict = {'a_key': 1}
Another thing to note is that the above doesn't automatically work recursively. That is, if your dictionary contains further dictionaries, they are not automatically turned into MyDict
, so this happens:
an_obj = AClass()
an_obj.some_dict = {} # setter
an_obj.some_dict['a_key'] = {} # getter and item setter
an_obj.some_dict['a_key']['another_key'] = 1 # only getter
You can keep adding functionality, by having the MyDict
turn any dict
value into a MyDict
, but there's further issues to consider - adjust as needed:
class MyDict(dict):
def __setitem__(self, key, value, *args, **kwargs):
print('setting a dictionary value')
if isinstance(value, dict) and not isinstance(value, MyDict):
value = MyDict(value)
super().__setitem__(key, value, *args, **kwargs)
CodePudding user response:
One option could be to use a custom dict
implementation such as a FrozenDict
, as shown below.
Note that this is similar to how
dataclasses
does it, when you pass infrozen=True
to create a frozen dataclass (attribute values can't be updated after object instantiation).
# Raised when an attempt is made to modify a frozen dict.
class FrozenDictError(KeyError): pass
class FrozenDict(dict):
def __setitem__(self, key, value):
raise FrozenDictError(f'cannot assign to key {key!r}')
def update(self, *args, **kwargs):
raise FrozenDictError(f'cannot assign to {self!r}')
Usage:
class CoinJar:
def __init__(self):
self._priceDict = FrozenDict({'current': 0.0, 'high' : 0.0, 'low' : 0.0})
@property
def priceDict(self):
return self._priceDict
@priceDict.setter
def priceDict(self, priceDict):
print('setting price')
try:
newPrice = float(priceDict.get('current'))
except ValueError as e:
print(f'Input cannot be converted to float {e}')
# Exceptions
if len(priceDict.keys()) == 0 or newPrice == None or type(newPrice) not in [int, float]:
raise ValueError('setting illegal value')
#setting high, low, current
_priceDict = self._priceDict.copy()
_priceDict['current'] = newPrice
if newPrice > _priceDict['high']:
_priceDict['high'] = newPrice
if newPrice < _priceDict['low'] or _priceDict['low'] == 0.0:
_priceDict['low'] = newPrice
self._priceDict = FrozenDict(_priceDict)
def main():
btc = CoinJar()
btc.priceDict = {'current' : 500} # This is calling priceDict.setter => accepts and performs the .setter expressions
btc.priceDict['flamingo'] = 20 # This is not calling priceDict.setter => can do what i want, with 'current, high, flamingo, *'
btc.priceDict = {'flamingo' : 500} # This is calling priceDict.setter => raises Exception (as expected)
print(btc)
if __name__ == '__main__':
main()
Output:
setting price
Traceback (most recent call last):
File "C:\Users\<user>\path\to\script.py", line 62, in <module>
main()
File "C:\Users\<user>\path\to\script.py", line 56, in main
btc.priceDict['flamingo'] = 20 # This is not calling priceDict.setter => can do what i want, with 'current, high, flamingo, *'
File "C:\Users\<user>\path\to\script.py", line 8, in __setitem__
raise FrozenDictError(f'cannot assign to key {key!r}')
__main__.FrozenDictError: cannot assign to key 'flamingo'
Using dataclass(frozen=True)
For a more robust implementation, you might consider refactoring to use a frozen dataclass instead, along with replace()
to update a frozen instance:
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class Price:
current: float = 0.0
high: float = 0.0
low: float = 0.0
class CoinJar:
def __init__(self):
self._priceDict = Price()
@property
def priceDict(self):
return self._priceDict
@priceDict.setter
def priceDict(self, priceDict):
print('setting price')
try:
newPrice = float(priceDict.get('current'))
except ValueError as e:
print(f'Input cannot be converted to float {e}')
# Exceptions
if len(priceDict.keys()) == 0 or newPrice == None or type(newPrice) not in [int, float]:
raise ValueError('setting illegal value')
#setting high, low, current
changes = {'current': newPrice}
if newPrice > self._priceDict.high:
changes['high'] = newPrice
if newPrice < self._priceDict.low or self._priceDict.low == 0.0:
changes['low'] = newPrice
self._priceDict = replace(self._priceDict, **changes)
def main():
btc = CoinJar()
print(btc.priceDict)
btc.priceDict = {'current': 500}
print(btc.priceDict)
btc.priceDict = {'current': 1000, 'high': 200}
print(btc.priceDict)
btc.priceDict.flamingo = 20 # This is not calling priceDict.setter => can do what i want, with 'current, high, flamingo, *'
btc.priceDict = {'flamingo': 500} # This is calling priceDict.setter => raises Exception (as expected)
print(btc)
if __name__ == '__main__':
main()
Output:
Price(current=0.0, high=0.0, low=0.0)
setting price
Price(current=500.0, high=500.0, low=500.0)
setting price
Price(current=1000.0, high=1000.0, low=500.0)
Traceback (most recent call last):
File "C:\Users\<usr>\path\to\script.py", line 62, in <module>
main()
File "C:\Users\<usr>\path\to\script.py", line 56, in main
btc.priceDict.flamingo = 20 # This is not calling priceDict.setter => can do what i want, with 'current, high, flamingo, *'
File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'flamingo'