简体   繁体   English

Python - Class Property.Setter 不验证字典键上的直接集

[英]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()具有包含例如strattribute时 -> @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'

Original Answer原始答案

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 in frozen=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'

Using 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM