【问题标题】:Recreate class object changing one constructor argument更改一个构造函数参数重新创建类对象
【发布时间】:2017-05-24 14:11:00
【问题描述】:

我想在我的类中实现一个update 方法,该方法重新创建类但只更改一个构造函数参数。

我的尝试:

class Updateable:
    def update(self, var, var_str, **kwargs):
        kwargs.update(self._vars)
        kwargs[var_str] = var
        self.__init__(**kwargs)

class Rectangle(Updateable):
    def __init__(self, length, perimeter):
        self._vars = locals()
        self.length = length
        self.width = 0.5*(perimeter - 2.*length)      

r = Rectangle(10, 20)
r.update('perimeter', 16)

问题是,我认为整个locals() 非常狡猾,这意味着任何Updateable 的类都需要分配self._vars

实现此功能的正确方法是什么?装饰器,元类?更简单的?

【问题讨论】:

  • 在对象已经存在之后调用__init__ 似乎有点奇怪。为什么不只是有一个recalculate_width 方法之类的?
  • 因为我希望它适用于我拥有的其他类,它们都有不同的参数名称。有时是width,有时是angle,有时是wavelength,可以是任何东西。

标签: python python-3.x class object


【解决方案1】:

如果我误解了您的问题,或者您不想要高级建议并且您的问题已解决,请纠正我。

您的__init__ 当前所做的是在更改(可能相关的)变量时重新计算几何形状的属性。第 1 步是将其从 __init__ 中取出,并放入由 init 调用的单独的 def 中。这里的主要内容是您不要将变量传递给此函数,而是使用已在__init__ 或超类更新方法之一中设置的类变量。

第 2 步是更改更新功能。 Python 有一种称为 properties 的 getter 和 setter 形式,允许您挂钩任务以更新变量。另一方面,更通用的方式更类似于您自己的更新,并在下面列为选项 2

替代示例

class Updateable:
    # Option 1
    @property
    def perimeter(self):
        return self.__perimeter

    @perimeter.setter
    def perimeter(self, perimeter):
        self.__perimeter = perimeter
        self.recalculate_everything() # or self.calculate_width() or something

    # Option 2
    def update(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)
        self.recalculate_everything

class Rectable(Updateable):
    def __init__(self, length, perimeter):
        self.__length = length
        self.__perimeter = perimeter
        recalculate_everything()

    def recalculate_everything():
        self.calculate_width()
        ...     

    def calculate_width():
        self.__width = 0.5*(self.__perimeter - 2.*self.__length)     

【讨论】:

  • 1/ 不要对实现属性使用双下划线 - 这会触发名称修改功能,从而破坏继承。只使用一个前导下划线。 2/如果你使用继承,那么调用超类初始化器。 3/ 在 Python 2.x 中,属性不能与旧式类一起正常工作。 4/ 您的“可更新”类应该使用默认实现定义“recalculate_everything()”,或者是一个抽象基类(参见 stlib 的 ABC 模块)。 5/ 你在Rectangle.__init__ 中有一个 NameError (...)
  • 还有 6. 在初始化程序中手动设置实现属性,而不是使用属性设置器,这会破坏拥有属性的全部意义。
  • 哦,也是的:Updatable 的重点是通用的——它应该对特定的实现属性一无所知。
  • 感谢您的建设性批评
【解决方案2】:

Laurens Koppenol 建议使用properties(Python 对计算属性的通用支持),这是一个好主意,但他的示例代码在很多方面都被破坏了,而且比它必须的更复杂,所以这里有一个更简单、工作和 pythonic示例(不需要Updatable 类,也不需要任何其他无关的东西):

class Rectangle(object):
    def __init__(self, length, perimeter):
        self.length = length
        self.perimeter = perimeter

    @property
    def width(self):
        return 0.5*(self.perimeter - 2.*self.length)     

如果您想缓存 width 值(以避免无用计算)但仍确保在 lengthperimeter 更改时更新,您需要将它们设为所有属性:

class Rectangle(object):
    def __init__(self, length, perimeter):
        self.length = length
        self.perimeter = perimeter

    @property
    def length(self):
        return self._length

    @length.setter
    def length(self, value):
        self._length = value
        self._width = None


    @property
    def perimeter(self):
        return self._perimeter

    @length.setter
    def perimiter(self, value):
        self._perimeter = value
        self._width = None

    @property
    def width(self):
        if self._width is None:
            self._width = 0.5*(self.perimeter - 2.*self.length)     
        return self._width

或者(如果你有很多这样的东西)使用一些“cached_property with invalidation”实现:Storing calculated values in an object

编辑:wrt/您的问题,对locals 的调用确实很难看(并且很容易中断-您可能有不应该是_vars 一部分的局部变量),以及需要明确在子类中设置self._vars。恕我直言,update() API 本身也很丑陋。现在你不需要任何花哨的东西来让整个事情变得更加 Pythonic - 这是一个解决方案,其唯一的样板是需要使用命名参数调用 Updateable.__init__(不适用于位置参数):

class Updateable(object):
    def __init__(self, **kwargs):
        self._vars = kwargs

    def update(self, **kwargs):
        vars = self._vars.copy()
        vars.update(**kwargs)
        self.__init__(**vars)


class Rectangle(Updateable):
    def __init__(self, length, perimeter):
        super(Rectangle, self).__init__(length=length, perimeter=perimeter)
        self.length = length
        self.width = 0.5*(perimeter - 2.*length)

r = Rectangle(10, 20)
r.update(perimeter=40)

作为旁注,我个人觉得您的 Rectangle 类采用 perimeter 参数但存储 width 而不是......也许您应该考虑使用 perimeter 属性非常令人不安? (即使是只读以避免重新计算等)

【讨论】:

  • 在您的第二个示例中,您的意思是在初始化方法中分配给self._length/_perimeter 吗?
  • 所以问题是,我想将更新应用到许多具有不同参数的类,并且我想避免将每个参数都写为propery。这就是为什么我试图创建一个可以做到这一点的通用类。不确定这是否可能。
  • @snakecharmerb 不,我的意思是我写的:分配给self.lengthself.perimeter。为什么我要绕过我的属性设置器并破坏width getter 代码?
  • @Jean-Luc 是的,抱歉-请参阅我编辑的答案,它确实解决了您的问题。但我不能保证它永远不会破坏任何东西......
【解决方案3】:

正如其他人所观察到的,width 之类的计算应该移至属性或方法;它们不属于初始化程序。

如果您真的想返回一个新实例,这适用于最简单的情况,其中实例的属性是字符串或整数等不可变对象:

import copy

class Copyable:

    """Mixin to create copies with a changed attribute."""

    def copy_and_modify(self, var, var_str, **kwargs):
        new = copy.copy(self)
        setattr(new, var, var_str)
        return new

class Rectangle(Copyable):

    def __init__(self, length, perimeter):
        self.perimeter = perimeter
        self.length = length

    @property
    def width(self):
        return 0.5 * (self.perimeter - 2.0 * self.length)

但是,如果您的对象包含嵌套的可变结构(例如字典或列表),您需要将 copy_and_modify 更改为使用 copy.deepcopy(但请注意,深度复制很慢)

class Copyable:

    def copy_and_modify(self, var, var_str, **kwargs):
        new = copy.deepcopy(self)
        setattr(new, var, var_str)
        return new

您可以在子类as described in the docs 中定义__copy____deepcopy__ 方法来微调对复制过程的控制。

【讨论】:

  • 如果它创建一个新实例,该方法不应命名为update()
  • @brunodesthuilliers 我遵守了 OP 的原始约定,但你是对的。 OP 正在重新初始化现有实例,而不是创建一个新实例。今天晚些时候我会更正这个名字。 create_modified_copy 也许?此外,我会将 mixin 重命名为更接近其用途的名称。
猜你喜欢
  • 2020-02-08
  • 2019-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多