frnhr
frnhr

Reputation: 12903

Setting attributes on a method after class creation raises "'instancemethod' object has no attribute" but the attribiute is clearly there

In Python (2 and 3) we can assign attributes to function:

>>> class A(object):
...     def foo(self):
...         """ This is obviously just an example """
...         return "FOO{}!!".format(self.foo.bar)
...     foo.bar = 123
...
>>> a = A()
>>> a.foo()
'FOO123!!'

And that's cool.

But why cannot we change foo.bar at a later time? For example, in the constructor, like so:

>>> class A(object):
...     def __init__(self, *args, **kwargs):
...         super(A, self).__init__(*args, **kwargs)
...         print(self.foo.bar)
...         self.foo.bar = 456  # KABOOM!
...     def foo(self):
...         """ This is obviously just an example """
...         return "FOO{}!!".format(self.foo.bar)
...     foo.bar = 123
...
>>> a = A()
123
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __init__
AttributeError: 'instancemethod' object has no attribute 'bar'

Python claims there is no bar even though it printed it fine on just the previous line.

Same error happens if we try to change it directly on the class:

>>> A.foo.bar
123
>>> A.foo.bar = 345  # KABOOM!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'instancemethod' object has no attribute 'bar'

What's happening here, i.e. why are we seeing this behaviour?

Is there a way to set attributes on a function after class creation?

(I'm aware of multiple alternatives, but I'm explicitly wondering about attributes on methods here, or possibly a broader issue.)


Motivation: Django makes use of the possibility to set attributes on methods, e.g:

class MyModelAdmin(ModelAdmin):
    ...

    def custom_admin_column(self, obj):
        return obj.something()
    custom_admin_column.admin_order_field ='relation__field__span'
    custom_admin_column.allow_tags = True

Upvotes: 4

Views: 368

Answers (1)

user2357112
user2357112

Reputation: 281663

Setting foo.bar inside the class body works because foo is the actual foo function. However, when you do

self.foo.bar = 456

self.foo isn't that function. self.foo is an instance method object, created on demand when you access it. You can't set attributes on it for several reasons:

  1. If those attributes are stored on the foo function, then assigning to a.foo.bar has an unexpected effect on b.foo.bar, contrary to all the usual expectations about attribute assignment.
  2. If those attributes are stored on the self.foo instance method object, they won't show up the next time you access self.foo, because you'll get a new instance method object next time.
  3. If those attributes are stored on the self.foo instance method object and you change the rules so self.foo is always the same object, then that massively bloats every object in Python to store a bunch of instance method objects you almost never need.
  4. If those attributes are stored in self.__dict__, what about objects that don't have a __dict__? Also, you'd need to come up with some sort of name mangling rule, or store non-string keys in self.__dict__, both of which have their own problems.

If you want to set attributes on the foo function after the class definition is done, you can do that with A.__dict__['foo'].bar = 456. (I've used A.__dict__ to bypass the issue of whether A.foo is the function or an unbound method object, which depends on your Python version. If A inherits foo, you'll have to either deal with that issue or access the dict of the class it inherits foo from.)

Upvotes: 1

Related Questions