[英]Python - Class Property.Setter does not validate a direct set on dictionary key
Please refer to the function main():
the results are mentioned as comments.请参考 function
main():
结果作为注释提到。 *General note: When Class()
has an attribute
containing eg str
-> the @propery.setter
will validate once set. *一般注意事项:当
Class()
具有包含例如str
的attribute
时 -> @propery.setter
将在设置后生效。
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.attribute
中存储dictionary
时,如果我直接设置一个key: value
,我的@property.setter
将不起作用
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()
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.当需要设置属性本身的值时,将调用属性的属性设置器。 Ie when you assign a new dictionary to the attribute for a
dict
attribute.即当您将新字典分配给
dict
属性的属性时。
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:也就是说,如果您的词典包含更多词典,它们不会自动变成
MyDict
,所以会发生这种情况:
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:您可以通过让
MyDict
将任何dict
值转换为MyDict
来继续添加功能,但是还有其他问题需要考虑——根据需要进行调整:
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)
Sorry for any initial confusion due to me not understanding the ask initially.抱歉,由于我最初不理解问题而造成的任何最初的困惑。 Now, if I understand the problem statement correctly as below:
现在,如果我正确理解问题陈述如下:
[...] however, if I directly set a
key: value
, the@property.setter
is not being called.[...]但是,如果我直接设置一个
key: value
,则不会调用@property.setter
。
To enable any updates to the object self.proxy_dict
- such as key assignment, d['key'] =...
- to trigger the property setter or @price_dict.setter
method:要启用对 object
self.proxy_dict
的任何更新 - 例如键分配, d['key'] =...
- 触发属性设置器或@price_dict.setter
方法:
One option can be to define and use a ProxyDict
, a custom dict
implementation which can internally call the desired setter method when __setitem__()
is called, as indicated above.一种选择是定义和使用
ProxyDict
,这是一个自定义的dict
实现,它可以在调用__setitem__()
时在内部调用所需的设置方法,如上所示。
class ProxyDict(dict):
# noinspection PyMissingConstructor, PyArgumentList
def __init__(self, obj, prop: property, d: dict, _init=dict.__init__):
# calls super().__init__()
_init(self, d)
# `prop` is a property object, and `prop.fset` is the setter function
self._setter = lambda value, _set=prop.fset: _set(obj, value)
def __setitem__(self, key, value):
print(f'__setitem__() is called with key={key!r}, value={value!r}')
self._setter({key: value})
def update(self, *args, **kwargs) -> None:
if args:
kwargs.update(*args)
print(f'update() is called with {kwargs!r}')
self._setter(kwargs)
Usage would then be as follows:用法如下:
class CoinJar:
def __init__(self):
self._price_dict = ProxyDict(
self,
CoinJar.price_dict, # or: self.__class__.price_dict
{'current': 0.0, 'high': 0.0, 'low': 0.0},
)
@property
def price_dict(self):
return self._price_dict
@price_dict.setter
def price_dict(self, pd: dict):
print('setting price')
try:
new_price = float(pd.get('current'))
price_is_set = True
except ValueError as e:
price_is_set = False
print(f'Input cannot be converted to float {e}')
# Exceptions
if (not pd # equivalent to: len(priceDict.keys()) == 0
or not price_is_set):
raise ValueError('setting illegal value')
# setting high, low, current
# noinspection PyUnboundLocalVariable
changes = {'current': new_price}
if new_price > self._price_dict['high']:
changes['high'] = new_price
curr_low = self._price_dict['low']
if new_price < curr_low or curr_low == 0.0:
changes['low'] = new_price
# call `dict.update`, so we don't call this setter method again
dict.update(self._price_dict, changes)
def main():
btc = CoinJar()
btc.price_dict = {'current': 500} # This is calling priceDict.setter => accepts and performs the .setter expressions
print(btc.price_dict)
print()
btc.price_dict['current'] = 250 # This is calling priceDict.setter
print(btc.price_dict)
print()
btc.price_dict['current'] = 1000 # This is calling priceDict.setter
print(btc.price_dict)
print()
btc.price_dict['flamingo'] = '500' # This is calling priceDict.setter => raises Exception (as expected)
print(btc.price_dict)
if __name__ == '__main__':
main()
Output: Output:
setting price
{'current': 500.0, 'high': 500.0, 'low': 500.0}
__setitem__() is called with key='current', value=250
setting price
{'current': 250.0, 'high': 500.0, 'low': 250.0}
__setitem__() is called with key='current', value=1000
setting price
{'current': 1000.0, 'high': 1000.0, 'low': 250.0}
__setitem__() is called with key='flamingo', value='500'
setting price
Traceback (most recent call last):
File "C:\Users\<user>\path\to\script.py", line 85, in <module>
main()
File "C:\Users\<user>\path\to\script.py", line 80, in main
btc.price_dict['flamingo'] = '500' # This is calling priceDict.setter => raises Exception (as expected)
File "C:\Users\<user>\path\to\script.py", line 12, in __setitem__
self._setter({key: value})
File "C:\Users\<user>\path\to\script.py", line 8, in <lambda>
self._setter = lambda value, _set=prop.fset: _set(obj, value)
File "C:\Users\<user>\path\to\script.py", line 40, in price_dict
new_price = float(pd.get('current'))
TypeError: float() argument must be a string or a real number, not 'NoneType'
This is my original answer, which was a direct result of me not understanding the problem or ask in its entirety.这是我的原始答案,这是我不理解问题或不完整提问的直接结果。
Here, I understand the problem statement being defined as:在这里,我理解问题陈述被定义为:
I am changing the dictionary by accessing the class
priceDict.__setitem__
.我正在通过访问 class
priceDict.__setitem__
来更改字典。 In this way, using a dictionary in a@setter
way has not much use if someone can screw the Obj<values>
.这样,如果有人可以搞砸 Obj
<values>
,以@setter
方式使用字典就没有多大用处。
So to clarify, the ask as I understood it was an approach to raise an error when priceDict.__setitem__
is called, as we only want the priceDict
object to be mutable when the @setter
method is called.所以澄清一下,据我所知,ask 是一种在调用
priceDict.__setitem__
时引发错误的方法,因为我们只希望priceDict
object 在调用@setter
方法时可变。
One option could be to use a custom dict
implementation such as a FrozenDict
, as shown below.一种选择是使用自定义
dict
实现,例如FrozenDict
,如下所示。
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).请注意,这类似于数据类的操作方式,当您传入
frozen=True
以创建冻结数据类时(属性值在dataclasses
实例化后无法更新)。
# 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: 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'
dataclass(frozen=True)
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:为了更健壮的实现,您可以考虑重构以使用冻结的数据类,以及
replace()
来更新冻结的实例:
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: 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'
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.