【问题标题】:how to add methods to descriptors or properties in python如何在python中向描述符或属性添加方法
【发布时间】:2020-07-15 11:26:32
【问题描述】:

我正在尝试编写一个可以轻松扩展的模拟类。为此,我想使用类似于属性的东西,但它也提供了一个update 方法,可以针对不同的用例以不同的方式实现:

class Quantity(object):
    
    def __init__(self, initval=None):
        self.value = initval

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        self.value = value
    
    def update(self, parent):
        """here the quantity should be updated using also values from
        MySimulation, e.g. adding `MySimulation.increment`, but I don't
        know how to link to the parent simulation."""

        
class MySimulation(object):
    "this default simulation has only density"
    density = Quantity()
    increment = 1
    
    def __init__(self, value):
        self.density = value
    
    def update(self):
        """this one does not work because self.density returns value
        which is a numpy array in the example and thus we cannot access
        the update method"""
        self.density.update(self)

默认模拟可以这样使用:

sim = MySimulation(np.arange(5))

# we can get the values like this
print(sim.density)
> [0, 1, 2, 3, 4]

# we can call update and all quantities should update
sim.update()  # <- this one is not possible

我想以这样一种方式编写它,以便模拟可以以任何用户定义的方式扩展,例如添加另一个以不同方式更新的数量:

class Temperature(Quantity):
    def update(self, parent):
        "here we define how to update a temperature"


class MySimulation2(MySimulation):
    "an improved simulation that also evolves temperature"
    temperature = Temperature()
    
    def __init__(self, density_value, temperature_value):
        super().__init__(density_value)
        self.temperature = temperature_value
    
    def update(self):
        self.density.update(self)
        self.temperature.update(self)

这有可能以某种方式实现,还是有其他方法可以实现类似的行为?我见过this question,这可能会有所帮助,但答案似乎很不优雅——我的案例有没有好的面向对象的方法?

【问题讨论】:

  • 如果您基本上只想更新所有类型为 Quantity 或任何派生类的实例变量,请使用 issubclass() 检查 self.__dict__ 中的所有项目,并在 issubclass 返回 true 时对其调用 update
  • @Pablo:John 标记了他的问题 [oop],使用 issubclass() 并访问 __dict__ 对我来说听起来恰恰相反。参见例如这些原则:en.wikipedia.org/wiki/SOLID.
  • @John:密度和温度是静态成员有什么特殊原因吗?
  • 我对 OOP 不太熟悉,甚至不确定静态成员是什么。感谢任何建设性的批评。
  • 参见例如en.wikipedia.org/wiki/Class_variable。关键是,一个类的变量对于所有对象(运行时类实例)是否只存在一次(“静态”),或者每个对象都有自己的,允许不同的值(“正常”情况)。在 Python 中,访问权限是 MyClass.a_valueself.a_value

标签: python oop python-descriptors


【解决方案1】:

这有可能以某种方式实现,还是有其他方法可以实现类似的行为?

有一种方法可以实现类似的行为。

第 1 步:在 instance/MySimulation 上设置标志。

第 2 步:检查标志,如果标志已设置,则在 Quantity.__get__ 中返回 self

简单的实现

4 行变化。

class Quantity(object):

    def __init__(self, initval=None):
        self.value = initval

    def __get__(self, instance, owner):
        if hasattr(instance, '_update_context'):  # 1
            return self                           # 2
        return self.value

    def __set__(self, instance, value):
        self.value = value

    def update(self, parent):
        self.value += parent.increment  # Example update using value from parent


class MySimulation(object):
    "this default simulation has only density"
    density = Quantity()
    increment = 1

    def __init__(self, value):
        self.density = value

    def update(self):
        setattr(self, '_update_context', None)  # 3
        self.density.update(self)
        delattr(self, '_update_context')        # 4

请注意,这对MySimulation 及其子类非常具有侵入性。
缓解这种情况的一种方法是为子类定义一个 _update 方法来覆盖:

def update(self):
    setattr(self, '_update_context', None)  # 3
    self._update()
    delattr(self, '_update_context')        # 4

def _update(self):
    self.density.update(self)

更强大的实现

使用元类,我们可以对原始代码进行 3 行更改。

class UpdateHostMeta(type):
    UPDATE_CONTEXT_KEY = '_update_context'

    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)
        __class__.patch_update(cls)

    @staticmethod
    def patch_update(update_host_class):
        _update = update_host_class.update

        def update(self, *args, **kwargs):
            try:
                setattr(self, __class__.UPDATE_CONTEXT_KEY, None)
                _update(self, *args, **kwargs)
            finally:
                delattr(self, __class__.UPDATE_CONTEXT_KEY)

        update_host_class.update = update

    @staticmethod
    def is_in_update_context(update_host):
        return hasattr(update_host, __class__.UPDATE_CONTEXT_KEY)
class Quantity(object):

    def __init__(self, initval=None):
        self.value = initval

    def __get__(self, instance, owner):
        if UpdateHostMeta.is_in_update_context(instance):  # 1
            return self                                    # 2
        return self.value

    def __set__(self, instance, value):
        self.value = value

    def update(self, parent):
        self.value += parent.increment  # Example update using value from parent


class MySimulation(object, metaclass=UpdateHostMeta):  # 3
    "this default simulation has only density"
    density = Quantity()
    increment = 1

    def __init__(self, value):
        self.density = value

    def update(self):
        self.density.update(self)

【讨论】:

  • 感谢您的回答,我想这应该可行,但比我希望的要麻烦一些。如果这是唯一的方法,我想我宁愿直接使用它,但将数量存储在带下划线的变量中。如果没有更好的答案,我会接受这个。
  • 我认为对我来说更好的方法是从 numpy ndarray 继承并另外赋予它我想要的属性。
  • 我真的很喜欢这个答案。我整天都在尝试为类似的应用程序解决相同的问题。非常好的解决方案。
【解决方案2】:

鉴于不同的用例描述符允许(可能的调用绑定https://docs.python.org/3/reference/datamodel.html?highlight=descriptor%20protocol#invoking-descriptors),因此更难理解和维护, 如果真的不需要描述符协议,我建议使用property 方法。

您也可以考虑使用dataclasses 模块,如果重点是保持价值而不是提供功能。

我希望以下内容或多或少正确地解释了您的意图。

import numpy as np

LEN = 5

AS_PROPERTY = True  # TODO remove this line and unwanted ``Quantity`` implementation
if AS_PROPERTY:
    class Quantity:
        def __init__(self, value=None):
            self._val = value

        def getx(self):
            return self._val

        def setx(self, value):
            self._val = value

        def __repr__(self):
            return f"{self._val}"

        value = property(getx, setx)
else:
    class Quantity:  # descriptor, questionable here
        def __init__(self, value=None):
            self._val = value

        def __get__(self, instance, owner):
            return self._val

        def __set__(self, instance, value):
            self._val = value

        def __repr__(self):
            return f"{self._val}"


class Density(Quantity):
    def update(self, owner):
        idx = owner.time % len(self._val)  # simulation time determines index for change
        self._val[idx] += 0.01


class Temperature(Quantity):
    def update(self, owner):
        idx = owner.time % len(self._val)
        self._val[idx] += 1.0


class MySimulation:  # of density
    time_increment = 1

    def __init__(self, value):
        self.time = 0
        self.density = Density(value)

    def __repr__(self):
        return f"{self.density}"

    def time_step(self):
        self.time += MySimulation.time_increment

    def update(self):
        self.density.update(self)


class MySimulation2(MySimulation):  # of density and temperature
    def __init__(self, density_value, temperature_value):
        super().__init__(density_value)
        self.temperature = Temperature(temperature_value)

    def update(self):
        super().update()
        self.temperature.update(self)


if __name__ == '__main__':
    sim = MySimulation(np.arange(5.))
    sim.update()  # => [0.01, 1., 2., 3., 4.]
    print(f"sim: {sim}")

    sim2 = MySimulation2(np.linspace(.1, .5, LEN), np.linspace(10., 50., LEN))
    print(f"sim2:")
    for _ in range(2 * LEN + 1):
        print(f"{sim2.time:2}| D={sim2}, T={sim2.temperature}")
        sim2.update()
        sim2.time_step()

【讨论】:

  • 这不是我的意思。如果我调用sim.density,这确实会写出结果,但我不能做sim.density + 5,我必须做sim.density.value + 5,这对于很多计算来说有点麻烦。
  • sim.densitynp.array,您希望每个数组元素增加5
  • 不,我的意思是我希望 getter 返回那个值,这样我就可以用它进行计算,而不必在每个数量之后写 .value。
猜你喜欢
  • 1970-01-01
  • 2013-03-09
  • 2011-03-14
  • 1970-01-01
  • 1970-01-01
  • 2016-04-14
  • 1970-01-01
  • 1970-01-01
  • 2012-10-02
相关资源
最近更新 更多