将一些对象属性重置为初始值

Reset some object attributes to initial values

要求:

我有一个 class,其中许多字段在 __init__ 方法中初始化。 这些字段中的一些 应该可以通过 reset() 方法重置为初始值。 我想为这些属性提供输入信息,并让 Flake、MyPy、PyCharm(和我)对解决方案感到满意。

可能的解决方案:

  1. 重复初始值

    在此解决方案中,所有工具(MyPy、Flake、PyCharm)都很满意,但我不满意。我在两个地方(__init__reset)有初始值,我需要让它们保持同步。有可能以后要修改一个初始值,那我两处都不改了。

    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.resetableCounter: int = 1  # the same value is in reset method. Keep them in sync!!!
        # seven more attributes of different types and initial values
    
      def reset(self) -> None:
        self.resetableCounter = 1  # the same value is in __init__ method. Keep them in sync!!!
        # reset remaining seven attributes to the same values as in __init__()
    
  2. 仅在一种方法中保留初始值

    修复似乎很简单:仅在 reset 方法中保留初始值并从 __init__.

    调用 reset
    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.reset()
    
      def reset(self) -> None:
        self.resetableCounter: int = 1
        # seven more attributes of different types and initial values
    

    我很高兴(Flake8 和 MyPy 也一样)有这样的解决方案,但 PyCharm 抱怨

    Instance attribute resetableCounter defined outside __init__

    可以关闭此警告,但有时它很有用 - 当我忘记在 __init__reset 方法中定义某些属性时。

  3. 定义属性为None

    所以我们可以改进第二种解决方案-在__init__中定义属性,但将其设置为None,然后调用reset方法。

    from typing import Optional
    
    
    class Test:
    
      def __init__(self) -> None:
        self.persistentCounter: int = 0
    
        self.resetableCounter: Optional[int] = None
        # seven more attributes of different types - all defined as Optional
    
        self.reset()
    
      def reset(self) -> None:
        self.resetableCounter = 1
        # seven more attributes with different initial values
    
      def foo(self) -> bool:
        return self.resetableCounter > 10
    

    这个解决方案的缺点是该属性被定义为 Optional[int],而不是 int,当我在 foo 方法中使用它时,mypy 会抱怨

    error: Unsupported operand types for < ("int" and "None")
    note: Left operand is of type "Optional[int]"
    

    当我在 foo 方法中添加一个额外的断言时,这个问题可以得到解决:

      def foo(self) -> bool:
        assert self.resetableCounter is not None
        return self.resetableCounter > 10
    

    它让所有工具都满意,但我不满意 - 我不想用许多“不必要的”断言填充源代码。

问题:

如何满足上述要求并减轻所提出解决方案的缺点?

Instance attribute attribute_name defined outside __init__ 描述了一个类似的问题,但没有答案符合我的要求(见上文)。

您可以编写一个自定义描述符来存储默认值并处理重置。

from typing import Type, Any, TypeVar, Generic, Optional

T = TypeVar('T')


class ResettableAttribute(Generic[T]):
    def __init__(self, default_value: T) -> None:
        # the attribute stores the default value
        self.default_value = default_value
        # and its name (will be filled later)
        self.name = ''

    def __set_name__(self, owner: Type[Any], name: str) -> None:
        # this method requires python 3.6+
        # it is called when we instantiate a descriptor inside a class

        # prepend an underscore to prevent name collisions
        self.name = f'_{name}'

    def __get__(self, instance: Optional[Any], owner: Type[Any]) -> T:
        # this is called when we access the descriptor from a class (or instance)
        assert instance is not None  # you could handle the case ClassName.attribute
        # return the stored value or the default if it was never set
        return getattr(instance, self.name, self.default_value)

    def __set__(self, instance: Any, value: T) -> None:
        # this is called when we assign to the descriptor from an instance
        setattr(instance, self.name, value)

    def __delete__(self, instance: Any) -> None:
        # this is called when you write del instance.attribute
        # we can use this to reset the value to the default
        setattr(instance, self.name, self.default_value)

注意:这仅适用于不可变的默认值,因为 default_values 对于所有实例都是相同的。

您可以像这样使用属性:

class Test:
    x = ResettableAttribute(1)
    y = ResettableAttribute("default")

    def __init__(self) -> None:
        self.persistent = 0

    def reset(self) -> None:
        del self.x
        del self.y


a = Test()
b = Test()

a.x = 123
print(a.x, b.x)  # 123 1
b.x = 234
print(a.x, b.x)  # 123 234
a.reset()
print(a.x, b.x)  # 1 234

现在默认值只提到一次。 Flake、Mypy 和 Pycharm 不要抱怨。

如果您仍然不想在 类 中重复所有可重置的属性,您可以这样做:

def reset_all_resettable_attributes(instance: Any) -> None:
    for attribute in vars(type(instance)).values():
        if isinstance(attribute, ResettableAttribute):
            attribute.__delete__(instance)

那么reset()方法就变成了这样:

def reset(self) -> None:
    reset_all_resettable_attributes(self)

如果您想支持可变默认值,请执行以下操作:让 ResettableAttribute 接受构造默认值的工厂函数并在构造函数中调用该工厂。

受到@Wombatz 回复的启发,我以这个解决方案结束:

class Test:

  resetableCounter: int = 1
  # seven more attributes of different types

  def __init__(self) -> None:
    self.persistentCounter: int = 0
    self.reset()

  def reset(self) -> None:
    if 'resetableCounter' in self.__dict__:
      del self.resetableCounter
    # del also rest seven attributes

所有工具都很满意,代码干净、简洁且可读。

非常感谢所有贡献。

更新

我必须将 if 'resetableCounter' in self.__dict__: 行添加到上面的代码中。否则,reset 方法在从 __init__ 调用时失败,或者当它在行中被调用两次而第一次和第二次调用之间没有变量修改时。

现在的代码不是很好(尤其是需要手动将变量名写成字符串)但我认为它仍然是最好的(我知道的)解决方案。