Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add attribute to arbitrary object python?

For a project I'm working on, I want to be able to associate a name with an object. The way I would like to do it is to set the .name attribute of the object to the name I want. What I really need is a function that takes an instance of an object, and returns something that is identical in every way but with a .name attribute. The problem is that I don't know what type of data the object will be ahead of time, so I can't use subclassing for example

Every method I've tried has hit a problem. Trying to give it a .name attribute directly doesnt work, for example:

>>> cats = ['tabby', 'siamese']
>>> cats.name = 'cats'
Traceback (most recent call last):
  File "<pyshell#197>", line 1, in <module>
    cats.name = 'cats'
AttributeError: 'list' object has no attribute 'name'

Using setattr has the same problem.

I've tried creating a new class that on init copies all attributes from the instance and also has a .name attribute, but this doesn't work either. If I try:

class NamedThing:
  def __init__(self, name, thing):
    thing_dict = {#not all types have a .__dict__ method
      name: getattr(thing, name) for name in dir(thing)
    }
    self.__dict__ = thing_dict
    self.name = name

It copies over the dict without a problem, but for some reason unless I directly call the new methods, python fails to find them, so the object loses all of its functionality. For example:

>>> cats = ['tabby', 'siamese']
>>> named_thing_cats = NamedThing('cats', cats)

>>> named_thing_cats.__repr__()#directly calling .__repr__()
"['tabby', 'siamese']"
>>> repr(named_thing_cats)#for some reason python does not call the new repr method
'<__main__.NamedThing object at 0x0000022814C1A670>'

>>> hasattr(named_thing_cats, '__iter__')
True
>>> for cat in named_thing_cats:
    print(cat)
Traceback (most recent call last):
  File "<pyshell#215>", line 1, in <module>
    for cat in named_thing_cats:
TypeError: 'NamedThing' object is not iterable

I've also tried setting the type and attributes by setting class directly:

class NamedThing:
  def __init__(self, name, thing):
    thing_dict = {#not all types have a .__dict__ method
      name: getattr(thing, name) for name in dir(thing)
    }
    self.__class__ = type('NamedThing', (type(thing),), thing_dict)
    self.name = name

But this runs into a problem depending on what type thing is:

>>> cats = ['tabby', 'siamese']
>>> named_thing_cats = NamedThing('cats', cats)
Traceback (most recent call last):
  File "<pyshell#217>", line 1, in <module>
    named_thing_cats = NamedThing('cats', cats)
  File "C:/Users/61490/Documents/Python/HeirachicalDict/moduleanalyser.py", line 12, in __init__
    self.__class__ = type('NamedThing', (type(thing),), thing_dict)
TypeError: __class__ assignment: 'NamedThing' object layout differs from 'NamedThing'

I'm really stuck, help would be great

like image 869
Matthew B Avatar asked Aug 12 '26 14:08

Matthew B


1 Answers

What you want is called an object proxy. This is some pretty sophisticated stuff, as you're getting into the data model of python and manipulating some pretty fundamental dunder (double underscore) methods in interesting ways

class Proxy:

    def __init__(self, proxied):
        object.__setattr__(self, '_proxied', proxied)

    def __getattribute__(self, name):
        try:
            return object.__getattribute__(self, name)
        except AttributeError:
            p = object.__getattribute__(self, '_proxied')
            return getattr(p, name)
        
    def __setattr__(self, name, value):
        p = object.__getattribute__(self, '_proxied')
        if hasattr(p, name):
            setattr(p, name, value)
        else:
            setattr(self, name, value)
        
    def __getitem__(self, key):
        p = object.__getattribute__(self, '_proxied')
        return p[key]
        
    def __setitem__(self, key, value):
        p = object.__getattribute__(self, '_proxied')
        p[key] = value
        
    def __delitem__(self, key):
        p = object.__getattribute__(self, '_proxied')
        del p[key]

The most obvious thing that's going on here is that internally this class has to use the object implementation of the dunders to avoid recursing infinitely. What this does is holds a reference to a proxied object, then if you try to get or set an attribute it will check the proxied object, if the proxied object has that attribute it uses it, otherwise it sets the attribute on itself. For indexing, like with a list, it just directly acts on the proxied object, since the Proxy itself doesn't allow indexing.

If you need to use this in production, there's a package called wrapt you should probably look at instead.

like image 60
dcbaker Avatar answered Aug 14 '26 04:08

dcbaker