Search code examples
pythonmethodsfirst-class-functions

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

Solution

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