Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating views on an object by sharing its __dict__ attribute

I'm currently looking at a maintainable, and easy-to-use way of creating "views" on python objects. Specifically, i have a very large collection of classes that share a couple common methods, and i would like to wrap instances of these classes in order to modify the behaviour of these methods.

Of course, i could create for every class a new class, following the wrapper pattern, redefining the full interface and redirecting every method to the original object, except those that i wish to override. This isn't practical, due to the shear amount of code required and the maintenance required when any class would change.

Some experimentation have shown me i could generate the wrappers with heavy use of metaclasses and introspection to "recreate" the interface in a wrapper object, but this proved fairly horrendous to use and debug, especially if A's have properties (code not included)

A second attempt showed that it can be done with fairly minimal code by sharing the __dict__ attribute and overriding __class__. This leads to the following code (https://repl.it/repls/InfantileAshamedProjector):

##############################
# Existing code
##############################

class A1:
  def __init__(self, eggs):
    self.eggs = eggs

  # Lots of complicated functions and members

  def hello(self):
    print ("hello, you have %d eggs" % self.eggs)

  def meeting(self):
    self.hello()
    print ("goodbye")

  # Lots of complicated functions calling hello.

# Lots of A2, A3, A4 with the same pattern

##############################
# "Magic" code for view generation
##############################

class FutureView:
  pass

def create_view(obj, name):
  class View(obj.__class__):
    def hello(self):
      print ("hello %s, you have %d eggs" % (name, self.eggs))

  view = FutureView()
  view.__dict__ = obj.__dict__
  view.__class__ = View

  return view

##############################
# Sample of use
##############################

a = A1(3)
a.hello() # Prints hello, you have 3 eggs

v = create_view(a, "Bob")
v.hello() # Prints hello Bob, you have 3 eggs

a.eggs = 5
a.hello() # Prints hello, you have 5 eggs
v.hello() # Prints hello Bob, you have 5 eggs

a.meeting() # Prints hello, you have 5 eggs. Goodbye
v.meeting() # Prints hello Bob, you have 5 eggs. Goodbye

This makes for fairly short code, and modifying the A1, A2, etc... classes doesn't require any change to the patch, which is very nice. However, i'm obviously worried about the implications of sharing __dict__ between multiple classes. My questions are:

  • Do you see any other way to achieve my goal, either by slightly improving the above method, or by something totally different? (Note that allowing class modification / addition without requiring any change to patching machinery is a hard requirement)
  • What pitfalls should i be aware when i have multiple objects sharing the same __dict__?
  • What is the best (less bad?) way of creating an object by providing explictly the __dict__ and the __class__?
  • Bonus: As shown in the above example, i need to attach one extra piece of information to my wrapper. Because i cannot add it to the object __dict__, i'm forced to add it to the class itself, either as a class member, or as a "captured" variable. Is there any other position where i could put it? Ideally, i would like to avoid have to create a new class for every instance of name (i want to only dynamically create a new class for every original class)

Other considered solutions:

Proxy objects (see juanpa.arrivillaga answer) is an almost perfect solution, but it falls short when the patched function is called internally by another function. Specifically, in the code posted above, the final calls to the meeting function will use the original implementation instead of the patched implementation. See https://repl.it/repls/OrneryLongField for an example.

like image 977
Rémi Bonnet Avatar asked Aug 28 '19 20:08

Rémi Bonnet


People also ask

What is the function of the __ dict __ attribute?

What does built-in class attribute __dict__ do in Python? A special attribute of every module is __dict__. This is the dictionary containing the module's symbol table. A dictionary or other mapping object used to store an object's (writable) attributes.

What is the meaning of __ dict __ in Python?

The __dict__ in Python represents a dictionary or any mapping object that is used to store the attributes of the object. They are also known as mappingproxy objects. To put it simply, every object in Python has an attribute that is denoted by __dict__.


1 Answers

It sounds to me like you want a proxy object, which is what a view generally is. Briefly, the pattern can be as simple (for read-only proxies) as this:

class View:
    def __init__(self, obj):
        self._obj = obj
    def __getattr__(self, attr):
        return getattr(self._obj, attr)

The nice thing about __getattr__ is that it is only called when an attribute is not found. If you want write access, then you'll need to be a bit more careful, and implement __setattribute__ which is always called, and it becomes easy to inadvertently trigger infinite recursion.

Note, because we are using getattr on the object being proxied, we don't have to manage recreating the interface! Method resolution, the descriptor protocol (so property), inheritance, etc is all handled by the usual machinery:

In [1]: class View:
   ...:     def __init__(self, obj):
   ...:         self._obj = obj
   ...:     def __getattr__(self, attr):
   ...:         return getattr(self._obj, attr)
   ...:

In [2]: class UrFoo:
   ...:     def __init__(self, value):
   ...:         self.value = value
   ...:     def foo(self):
   ...:         return self.value
   ...:

In [3]: class Foo(UrFoo):
   ...:     def frognicate(self):
   ...:         return self.value * 42
   ...:     @property
   ...:     def baz(self):
   ...:         return 0
   ...:

In [4]: foo = Foo(8)

In [5]: view = View(foo)

In [6]: view.foo()
Out[6]: 8

In [7]: view.frognicate()
Out[7]: 336

In [8]: view.baz
Out[8]: 0
like image 103
juanpa.arrivillaga Avatar answered Nov 15 '22 00:11

juanpa.arrivillaga