5

我正在尝试编写一个可以轻松扩展的模拟类。为此,我想使用类似于属性的东西,但这也提供了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)

这有可能以某种方式还是有其他方法可以实现类似的行为?我已经看到了这个问题,这可能会有所帮助,但答案似乎很不优雅——我的案例有没有好的面向对象的方法?

4

2 回答 2

2

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

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

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

第 2 步:检查标志selfQuantity.__get__如果设置了标志,则返回。

幼稚的实现

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)
于 2020-07-21T19:18:13.973 回答
0

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

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()
于 2020-07-25T06:53:57.023 回答