3

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

3 Answers 3

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

wrapt.ObjectProxy(value) doesn't work with builtins. Writing your own, built on @dcbaker 's template, with every single operator overloaded might be the way
0

Why not just create a __iter__ magic method with yield from:

class NamedThing():
    def __init__(self, name, thing):
        self.thing = thing
        self.name = name
    def __iter__(self):
        yield from self.thing

cats = ['tabby', 'siamese']
named_thing_cats = NamedThing('cats', cats)
for cat in named_thing_cats:
    print(cat)

Output;

tabby
siamese

1 Comment

This works great for lists, the problem is that I don't know what type the object will be beforehand. It could be a class, it could be an entire module. I need something that copies the entire functionality from whatever obejct I give it, as well as adding a .name
0

Does this work?

class Thingy(list):
    def __init__(self, name, thing):
        list.__init__(self, thing)
        self.name = name

cats = Thingy('cats', ['tabby', 'siamese'])

print(cats.name) # shows 'cats'

for cat in cats:
    print(cat) # shows tabby, siamese

Or you could do:

class Thingy:
    def __init__(self, name, thing):
        self.thing = thing
        self.name = name

3 Comments

That works great for lists, the problem is that I don't know what type the data will be beforehand. It could be a class, it could be an entire module. Not all types work with super().__init__(self, thing) sadly. Thats why I was trying to just copy the dict across, but I don't know why that doesn't work
@MatthewB I updated my answer. You can just store whatever data type it is in self.thing in class Thingy.
I am using this as part of a larger project, and I want the new objects to directly retain all the functionality of the original. If I just needed to link an object with a name, I could use a tuple or dict with (name, object).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.