在Python中修改已定义类的属性(然后重新运行其定义)。

huangapple go评论60阅读模式
英文:

Modify an attribute of an already defined class in Python (and run its definition again)

问题

不要翻译代码部分,只翻译问题的内容:

"Is there a way to achieve this WITHOUT recompile the original class source code?"

"是否有一种方法可以在不重新编译原始类源代码的情况下实现这一点?"

英文:

I am trying to modify an already defined class by changing an attribute's value. Importantly, I want this change to propagate internally.

For example, consider this class:

class Base:
    x = 1
    y = 2 * x

    # Other attributes and methods might follow

assert Base.x == 1
assert Base.y == 2

I would like to change x to 2, making it equivalent to this.

class Base:
    x = 2
    y = 2 * x

assert Base.x == 2
assert Base.y == 4

But I would like to make it in the following way:

Base = injector(Base, x=2)

Is there a way to achieve this WITHOUT recompile the original class source code?

答案1

得分: 1

要实现你想要的效果,属于“响应式编程”的范畴 - 一种编程范式(现在广泛使用的Javascript库从中得到灵感并以此命名)。

虽然Python有很多机制可以实现这一点,但需要编写代码来实际利用这些机制。

默认情况下,普通的Python代码(就像你在示例中使用的那样)使用了急切的命令式范式:每当遇到一个表达式时,它就会被执行,并且该表达式的结果会被使用(在这种情况下,结果存储在类属性中)。

Python的优势也使得它可以让你编写一个代码库,使得某些响应式代码可以发生,代码库的“用户”不必知道这一点,一切似乎都“神奇”地运作。

但是,正如上面所述,这并不是免费的。对于能够在

class Base:
    x = 1
    y = 2 * x

中在x改变时重新定义y的情况,可以采取几种路径 - 最重要的一点是,在执行“*”运算符时(这是在Python解析类体时发生的),至少有一个操作数不再是一个普通的数字,而是一个实现了自定义__mul__方法(或__rmul__)的特殊对象。在这种情况下,不会在y中存储一个结果数字,而是在某个地方存储一个表达式,当作为类属性检索y时,其他机制会强制该表达式解析。

如果你想在实例级别而不是类级别上实现这个效果,那么实现起来会更容易。但请记住,你必须为你的特殊“源”类上的每个运算符定义_each_。

此外,这种方法和更简单的使用property的实例描述符方法都是“惰性求值”的:这意味着,当需要使用y的值时,才会计算它(如果将来会使用多次,可以进行缓存)。如果你想要在x被赋值时(而不是在y被使用时)计算它,那就需要其他机制。尽管缓存这种惰性方法可以缓解急切求值的需要,甚至可能使其不再需要。

1 - 在深入研究之前

Python处理这类代码的最简单方法就是将表达式编写为函数 - 然后使用内置的property作为描述符来检索这些值。缺点很小:
你只需将你的表达式包装在一个函数中(然后,将该函数
放入一个会将描述符属性添加到它的东西中,比如property)。而收获是巨大的:你可以在你的表达式中使用__任何__ Python代码,包括函数调用、对象实例化、I/O等(请注意,另一种方法要求连接起_每个_所需的运算符,只是为了开始)。

为了使你想要的功能在Base的_instances_上工作,可以采取的纯粹“入门”方法是:

class Base:
   x = 1
   @property
   def y(self):
       return self.x * 2

b = Base()
b.y
-> 2
Base.x = 3
b.y
-> 6

property的工作原理可以被重新编写,以便从类中检索y,而不是从实例中检索(这仍然比另一种方法简单)。

如果这种方法对你来说可以解决问题,我建议使用它。如果你需要缓存y的值,直到x实际更改,可以用正常的编码来实现。

2 - 正好是你要求的,用一个元类

正如上面所述,Python在计算其表达式2 * x时,需要知道y属性的特殊状态。在赋值时,为时已晚。
幸运的是,Python 3允许类体在一个_custom_命名空间中运行,用于通过在元类中实现__prepare__方法,并记录所有发生的事情,然后将感兴趣的原始属性替换为实现了__mul__等特殊方法的特殊制作对象。

这样做甚至可以允许值急切地被计算,以便它们可以作为普通的Python对象工作,但是注册信息,以便一个特殊的injector函数可以重新创建类,重做所有依赖于表达式的属性。它还可以实现懒惰求值,有点类似上面描述的那样。

from collections import UserDict
import operator


class Reactive:
    def __init__(self, value):
        self._initial_value = value
        self.values = {}

    def __set_name__(self, owner, name):
        self.name = name
        self.values[owner] = self._initial_value

    def __get__(self, instance, owner):
        return self.values[owner]

    def __set__(self, instance, value):
        raise AttributeError("value can't be set directly - call 'injector' to change this value")

    def value(self, cls=None):
        return self.values.get(cls, self._initial_value)

    op1 = value

    @property
    def result(self):
        return self.value

    # 动态生成用于操作重载的魔术方法:
    for name in "mul add sub truediv pow contains".split():
        op = getattr(operator, name)
        locals()[f"__{name}__"] = (lambda operator: (lambda self, other: Reactive

<details>
<summary>英文:</summary>

The effect you want to achieve belongs to the realm of &quot;reactive programing&quot; - a programing paradigm (from were the now ubiquitous Javascript library got its name as an inspiration).

While Python has a lot of mechanisms to allow that, one needs to write his code to actually _make_ _use_ of these mechanisms. 

By default, plain Python code as the one you put in your example, uses the Imperative paradigm, which is eager: whenever an expression is encoutered, it is executed, and the result of that expression is used (in this case, the result is stored in the class attribute).

Python&#39;s advantages also can make it so that once you write a codebase that will allow some reactive code to take place, _users_ of your codebase don&#39;t have to be aware of that, and things work more or less &quot;magically&quot;.

But, as stated above, that is not free. For the case of being able to redefine `y` when `x` changes in 

class Base:
x = 1
y = 2 * x


There are a couple paths that can be followed - the most important is that, at the time the &quot;*&quot; operator is executed (and that happens when Python is _parsing_ the class body), at least one side of the operation is not a plain number anymore, but a special object which implements a custom `__mul__` method (or `__rmul__`) in this case. Then, instead of storing a resulting number in `y`, the expression is stored somewhere, and when `y` is retrieved either as a class attribute, other mechanisms force the expression to resolve.

If you want this at __instance__ level, rather than at class level, it would be easier to implement. But keep in mind that you&#39;d have to define _each_ operator on your special &quot;source&quot; class for primitive values.

Also, both this and the easier, instance descriptor approach using `property` are &quot;lazily evaluated&quot;: that means, the value for `y` is calcualted when it is to be used (it can be cached if it will be used more than once). If you want to evaluate it whenever `x`  is assigned (and not when  `y` is consumed), that will require other mechanisms. Although caching the lazy approach can mitigate the need for eager evaluation to the point it should not be needed.

# 1 - Before digging there

Python&#39;s easiest way to do code like this is simply to write the expressions to be calculated as functions - and use the `property` built-in as a descriptor to retrieve these values. The drawback is small: 
you just have to wrap your expressions in a function (and then, that function
in something that will add the descriptor properties to it, such as `property`). The gain is huge: you are free to use __any__ Python code inside your expression, including function calls, object instantiation, I/O, and the like. (Note that the other approach requires wiring up _each_ desired operator, just to get started).

The plain &quot;101&quot; approach to have what you want working for _instances_ of Base is:

class Base:
x = 1
@property
def y(self):
return self.x * 2

b = Base()
b.y
-> 2
Base.x = 3
b.y
-> 6


The work of `property` can be rewritten so that retrieving `y` from the class, instead of an instance, achieves the effect as well (this is still easier than the other approach).

If this will work for you somehow, I&#39;d recommend doing it. If you need to cache `y`&#39;s value until `x` actually changes, that can be done with normal coding

# 2 - Exactly what you asked for, with a metaclass

as stated above, Python&#39;d need to know about the special status of your `y` attribute when calculcating its expression `2 * x`. At assignment time, it would be already too late. 
Fortunately Python 3 allow class bodies to run in a _custom_ namespace for the attribute assignment by implementing the `__prepare__` method in a metaclass, and then recording all that takes place, and replacing primitive attributes of interest by special crafted objects implementing `__mul__` and other special methods.

Going this way could even allow values to be eagerly calculated, so they can work as plain Python objects, but register information so that a special `injector` function could recreate the class redoing all the attributes that depend on expressions. It could also implement lazy evaluation, somewhat as described above.

from collections import UserDict
import operator

class Reactive:
def init(self, value):
self._initial_value = value
self.values = {}

def __set_name__(self, owner, name):
    self.name = name
    self.values[owner] = self._initial_value

def __get__(self, instance, owner):
    return self.values[owner]

def __set__(self, instance, value):
    raise AttributeError(&quot;value can&#39;t be set directly - call &#39;injector&#39; to change this value&quot;)

def value(self, cls=None):
    return self.values.get(cls, self._initial_value)

op1 = value

@property
def result(self):
    return self.value

# dynamically populate magic methods for operation overloading:
for name in &quot;mul add sub truediv pow contains&quot;.split():
    op = getattr(operator, name)
    locals()[f&quot;__{name}__&quot;] = (lambda operator: (lambda self, other: ReactiveExpr(self, other, operator)))(op)
    locals()[f&quot;__r{name}__&quot;] = (lambda operator: (lambda self, other: ReactiveExpr(other, self, operator)))(op)

class ReactiveExpr(Reactive):
def init(self, value, op2, operator):
self.op2 = op2
self.operator = operator
super().init(value)

def result(self, cls):
    op1, op2 = self.op1(cls), self.op2
    if isinstance(op1, Reactive):
        op1 = op1.result(cls)
    if isinstance(op2, Reactive):
        op2 = op2.result(cls)
    return self.operator(op1, op2)

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

class AuxDict(UserDict):
def init(self, *args, _parent, **kwargs):
self.parent = _parent
super().init(*args, **kwargs)

def __setitem__(self, item, value):
    if isinstance(value, self.parent.reacttypes) and not item.startswith(&quot;_&quot;):
        value = Reactive(value)
    super().__setitem__(item, value)

class MetaReact(type):
reacttypes = (int, float, str, bytes, list, tuple, dict)

def __prepare__(*args, **kwargs):
    return AuxDict(_parent=__class__)

def __new__(mcls, name, bases, ns, **kwargs):
    pre_registry = {}
    cls = super().__new__(mcls, name, bases, ns.data, **kwargs)
    #for name, obj in ns.items():
        #if isinstance(obj, ReactiveExpr):
            #pre_registry[name] = obj
            #setattr(cls, name, obj.result()
    for name, reactive in pre_registry.items():
        _registry[cls, name] = reactive
    return cls

def injector(cls, inplace=False, **kwargs):
original = cls
if not inplace:
cls = type(cls.name, (cls.bases), dict(cls.dict))
for name, attr in cls.dict.items():
if isinstance(attr, Reactive):
if isinstance(attr, ReactiveExpr) and name in kwargs:
raise AttributeError("Expression attributes can't be modified by injector")
attr.values[cls] = kwargs.get(name, attr.values[original])
return cls

class Base(metaclass=MetaReact):
x = 1
y = 2 * x

And, after pasting the snippet above in a REPL, here is the
result of using `injector`:

In [97]: Base2 = injector(Base, x=5)

In [98]: Base2.y
Out[98]: 10


</details>



# 答案2
**得分**: 0

以下是翻译好的部分:

这个想法变得复杂的一方面是,`Base` 类声明了**依赖**动态评估属性。虽然我们可以检查类的静态属性,但我认为除了解析类的`sourcecode`,找到并替换“注入”的属性名称为其值,然后再次执行`exec/eval`定义之外,没有其他获取动态表达式的方法。但这不是你想要的方式(而且如果你希望`injector`对所有类都是统一的,那就更不行了)。

如果你想继续依赖动态评估的属性,将依赖属性定义为`lambda`函数。

```python
class Base:
    x = 1
    y = lambda: 2 * Base.x

Base.x = 2
print(Base.y())   # 4
英文:

The idea is complicated with that aspect that Base class is declared with dependent dynamically evaluated attributes. While we can inspect class's static attributes, I think there's no other way of getting dynamic expression except for parsing the class's sourcecode, find and replace the "injected" attribute name with its value and exec/eval the definition again. But that's the way you wanted to avoid. (moreover: if you expected injector to be unified for all classes).
<br>If you want to proceed to rely on dynamically evaluated attributes define the dependent attribute as a lambda function.

class Base:
    x = 1
    y = lambda: 2 * Base.x


Base.x = 2
print(Base.y())   # 4

huangapple
  • 本文由 发表于 2023年2月6日 05:40:08
  • 转载请务必保留本文链接:https://go.coder-hub.com/75355711.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定