跟踪Python中变量的变化

问题描述 投票:0回答:3

我想创建一个包含多个设置的数据结构,这些设置将用于计算硬件设备的寄存器值。为了避免重新配置硬件设备的所有设置,我希望让数据结构内的每个变量记住它是否已更改。然后我会调用所有变量来查看哪些变量被更改,然后只写入连接的寄存器。 我可以创建一个类来记住其内部存储值是否发生任何更改,但是我在返回和重置

has_changed
变量方面遇到了困难。这是由于
__get__
函数的重载禁止在类内使用其他函数。

在简化的示例中,我创建了一个名为

Table
的类(其中应包含变量,例如:
height
width
length
,...)当前的实现具有类
TrackedValidatedInteger
,它检查是否更改有效。 我希望变量属性
has_changed
可以从类内部获取和重置
Table

class TrackedValidatedInteger():
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.has_changed = False
        self.value = None
        
    def __get__(self, obj, objecttype=None):
        return self.value
    
    def __set__(self, obj, value):
        if self.validate_set(value):
            self.value = value
            self.has_changed = True
            return 1
        return 0

    def get_has_changed(self):
        return self.has_changed
    
    def reset_has_changed(self):
        self.has_changed = False
        
    def validate_set(self, value):
        if self.min_value:
            if self.min_value > value:
                print("Value should be between " + str(self.min_value) + " and " + str(self.max_value))
                return 0
        if self.max_value:
            if self.max_value < value:
                print("Value should be between " + str(self.min_value) + " and " + str(self.max_value))
                return 0
        return 1

class Table():
    length = TrackedValidatedInteger(min_value=0, max_value=3)
    height = TrackedValidatedInteger(min_value=0, max_value=6)
    width = TrackedValidatedInteger(min_value=0, max_value=7)
    
    def __init__(self, length=0, height=0, width=0):
        self.length = length
        self.height = height
        self.width = width 

    def reset_has_changed_1(self):
        self.length.has_changed = False
        self.height.has_changed = False
        self.width.has_changed = False
        
    def reset_has_changed_2(self):
        self.length.reset_has_changed()
        self.height.reset_has_changed()
        self.width.reset_has_changed()


p = Table()
p.length = 3 # will set the variable
p.length = 9 # will not set the variable

# p.length.get_has_changed() # This does not work as the p.length will call __get__ resulting in an integer which does not have get_has_changed()
# p.reset_has_changed_1() # This does not work for the same reason
# p.reset_has_changed_2() # This does not work for the same reason

我发现的问题是,每当我尝试访问

__get__
类的任何其他部分时,都会自动调用
TrackedValidatedInteger
函数。我可以通过其他方式访问其他变量和函数吗?如果有任何关于如何以另一种方式实现相同结果的建议,我将很高兴听到。我个人希望保留变量的简单设置(
p.length = 3
),如果不可能的话可以更改。

任何帮助将不胜感激。

python descriptor
3个回答
0
投票

我会将验证和更改跟踪逻辑提升到表中,如下所示:

class Table:
    _fields = {}

    def __init__(self, **kwargs):
        self._data = {}
        self._changed = set()
        for name, value in kwargs.items():
            setattr(self, name, value)
        self._changed.clear()  # do not consider the initial values as changed

    def __repr__(self):
        return f"<{self.__class__.__name__} {self._data}>"

    def __getattr__(self, item):
        return self._data[item]

    def __setattr__(self, key, value):
        rules = self._fields.get(key)
        if rules is not None:  # existing field
            if "min_value" in rules and value < rules["min_value"]:
                raise ValueError(f'{key} should be greater than {rules["min_value"]}')
            if "max_value" in rules and value > rules["max_value"]:
                raise ValueError(f'{key} should be less than {rules["max_value"]}')
            self._data[key] = value
            self._changed.add(key)
            return
        if not key.startswith("_"):
            raise AttributeError(f"Cannot set attribute {key}")
        super().__setattr__(key, value)

    def reset_has_changed(self):
        self._changed.clear()

    @property
    def changed(self):
        return tuple(self._changed)


class HardwareTable(Table):
    _fields = {
        "reg1": {"min_value": 0, "max_value": 100},
        "reg2": {"min_value": 0, "max_value": 100},
    }


p = HardwareTable(reg1=30)
print(f"{p=} / {p.changed=}")
p.reg2 = 13
print(f"{p=} / {p.changed=}")

打印出来

p=<HardwareTable {'reg1': 30}> / p.changed=()
p=<HardwareTable {'reg1': 30, 'reg2': 13}> / p.changed=('reg2',)

0
投票

有很多问题需要解决:

  • 首先,
    TrackedValidatedInteger
    的实例耦合到
    Table
    ,而不是它的实例, 所以你不应该将
    value
    存储在
    TrackedValidatedInteger
    实例上。 (尝试创建两个
    Table
    实例, 在一个实例上设置这些属性之一,然后看到它反映在另一个实例上。)
  • 如果验证失败,您应该引发异常,而不是打印值并继续。
  • 如果
    min_value
    max_value
    0
    ,则不进行验证。请使用
    if self.min_value is not None
    来代替。
  • 如果其中之一是
    None
    ,您将打印类似
    Value should be between None and 7
  • 的消息
class TrackedValidatedInteger:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        
    def __get__(self, obj, objecttype=None):
        return getattr(obj, self.private_name)
    
    def __set__(self, obj, value):
        self.validate(value)
        obj.has_changed.add(self.public_name)
        setattr(obj, self.private_name, value)

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def validate(self, value):
        if (self.min_value is not None and value < self.min_value or
            self.max_value is not None and value > self.max_value):
            if self.min_value is None:
                comparison = f'at most {self.max_value}'
            elif self.max_value is None:
                comparison = f'at least {self.min_value}'
            else:
                comparison = f'between {self.min_value} and {self.max_value}'
            raise ValueError(f'{self.public_name} should be {comparison}')

class Table():
    length = TrackedValidatedInteger(min_value=0, max_value=3)
    height = TrackedValidatedInteger(min_value=0, max_value=6)
    width = TrackedValidatedInteger(min_value=0, max_value=7)
    
    def __init__(self, length=0, height=0, width=0):
        # this needs to happen first:
        self.reset_has_changed()
        self.length = length
        self.height = height
        self.width = width 

    def reset_has_changed(self):
        self.has_changed = set()

p = Table()
p.length = 3 # will set the variable
try:
    p.length = 9 # will not set the variable
except ValueError as e:
    print(e)

print('length' in p.has_changed)

0
投票

我喜欢从描述符中执行此操作的想法。您可以利用描述符可以通过

__set_name__
方法知道其绑定的属性名称这一事实,并使用它来维护目标对象上的属性:

class TrackedValidatedInteger:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.has_changed = False
        self.value = None

    def __set_name__(self, obj, name):
        self.name = name
        setattr(obj, f"{self.name}_changed", False)

    def __get__(self, obj, objecttype=None):
        return self.value

    def __set__(self, obj, value):
        if (self.min_value is not None and value < self.min_value) or (
            self.max_value is not None and value > self.max_value
        ):
            raise ValueError(
                f"{value} must be >= {self.min_value} and <= {self.max_value}"
            )
        self.value = value
        setattr(obj, f"{self.name}_changed", True)

鉴于上述实现,我们可以创建一个类

Example
,如下所示:

class Example:
    v1 = TrackedValidatedInteger()
    v2 = TrackedValidatedInteger()

然后观察以下行为:

>>> e = Example()
>>> e.v1_changed
False
>>> e.v1 = 42
>>> e.v1_changed
True
>>> e.v2_changed
False
>>> e.v2 = 0
>>> e.v2_changed
True

您可以维护已更改属性的

<name>_changed
,而不是维护每个属性
set
变量:

class TrackedValidatedInteger:
    def __init__(self, min_value=None, max_value=None):
        self.min_value = min_value
        self.max_value = max_value
        self.has_changed = False
        self.value = None

    def __set_name__(self, obj, name):
        self.name = name
        if not hasattr(obj, "_changed_attributes"):
            setattr(obj, "_changed_attributes", set())

    def __get__(self, obj, objecttype=None):
        return self.value

    def __set__(self, obj, value):
        if (self.min_value is not None and value < self.min_value) or (
            self.max_value is not None and value > self.max_value
        ):
            raise ValueError(
                f"{value} must be >= {self.min_value} and <= {self.max_value}"
            )
        self.value = value
        obj._changed_attributes.add(self.name)

在这种情况下,我们得到:

>>> e = Example()
>>> e._changed_attributes
set()
>>> e.v1 = 1
>>> e._changed_attributes
{'v1'}
>>> e.v2 = 1
>>> e._changed_attributes
{'v1', 'v2'}

这很好,因为如果您需要记录所有更改的值,您可以迭代

e._changed_attributes

© www.soinside.com 2019 - 2024. All rights reserved.