1

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?

4
  • don't know why you call it "recompile", but the class needs to be re-declared basing on original state and new values Commented Feb 5, 2023 at 21:57
  • @RomanPerekhrest thanks for your answer. I meant using exec on the source code again. Commented Feb 5, 2023 at 22:02
  • Trying to figure out the practical use of the type of code you are looking for. I would strongly suggest avoiding using exec in most cases where you think it might be an option. Maybe you want instance behavior to override the attribute information at the class level? Commented Feb 5, 2023 at 22:11
  • Take a look at stackoverflow.com/q/5189699/1126841, but I'd question the need for something like this without seeing the context in which you need a Base that behaves like this. Commented Feb 6, 2023 at 15:09

2 Answers 2

1

The effect you want to achieve belongs to the realm of "reactive programing" - 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'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't have to be aware of that, and things work more or less "magically".

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 "*" 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'd have to define each operator on your special "source" class for primitive values.

Also, both this and the easier, instance descriptor approach using property are "lazily evaluated": 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'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 "101" 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'd recommend doing it. If you need to cache y'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'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("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 # dynamically populate magic methods for operation overloading: for name in "mul add sub truediv pow contains".split(): op = getattr(operator, name) locals()[f"__{name}__"] = (lambda operator: (lambda self, other: ReactiveExpr(self, other, operator)))(op) locals()[f"__r{name}__"] = (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("_"): 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 
Sign up to request clarification or add additional context in comments.

2 Comments

Please check out my answer to a similar question: stackoverflow.com/a/7844038/165216
Yes - that is nice - and the way to generate the operator-methods is sure cleaner. I've done similar things a couple of times - I thiink the novelty here is using the __prepare__ mechanism so that expressions can be typed with no wrapping at all inside class bodies, and also, the eager calculation and explicit recalculation call - allowing attributes to be just plain Python objects for other uses.
0

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).
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 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.