Alex Forbes
Alex Forbes

Reputation: 3559

Why is @staticmethod not preserved across classes, when @classmethod is?

Take the following example script:

class A(object):
    @classmethod
    def one(cls):
        print("I am class")

    @staticmethod
    def two():
        print("I am static")


class B(object):
    one = A.one
    two = A.two


B.one()
B.two()

When I run this script with Python 2.7.11 I get:

I am class
Traceback (most recent call last):
  File "test.py", line 17, in <module>
    B.two()
TypeError: unbound method two() must be called with B instance as first argument (got nothing instead)

It appears that the @classmethod decorator is preserved across the classes, but @staticmethod is not.

Python 3.4 behaves as expected:

I am class
I am static

Why does Python2 not preserve @staticmethod, and is there a workaround?

edit: taking two out of a class (and retaining @staticmethod) seems to work, but this still seems strange to me.

Upvotes: 25

Views: 2071

Answers (2)

ShadowRanger
ShadowRanger

Reputation: 155323

classmethod and staticmethod are descriptors, and neither of them are doing what you expect, not just staticmethod.

When you access A.one, it's creating a bound method on A, then making that an attribute of B, but because it's bound to A, the cls argument will always be A, even if you call B.one (this is the case on both Python 2 and Python 3; it's wrong everywhere).

When you access A.two, it's returning the raw function object (the staticmethod descriptor doesn't need to do anything special aside from preventing binding that would pass self or cls, so it just returns what it wrapped). But that raw function object then gets attached to B as an unbound instance method, because without the staticmethod wrapping, it's just like you'd defined it normally.

The reason the latter works in Python 3 is that Python 3 has no concept of unbound methods. It has functions (which if accessed via an instance of a class become bound methods) and bound methods, where Python 2 has functions, unbound methods and bound methods.

Unbound methods check that they're called with an object of the correct type, thus your error. Plain functions just want the correct number of arguments.

The staticmethod decorator in Python 3 is still returning the raw function object, but in Python 3, that's fine; since it's not a special unbound method object, if you call it on the class itself, it's just like a namespaced function, not a method of any sort. You'd see the problem if you tried to do:

B().two()

though, because that will make a bound method out of that instance of B and the two function, passing an extra argument (self) that two does not accept. Basically, on Python 3, staticmethod is a convenience to let you call the function on instances without causing binding, but if you only ever call the function by referencing the class itself, it's not needed, because it's just a plain function, not the Python 2 "unbound method".

If you had some reason to perform this copy (normally, I'd suggest inheriting from A, but whatever), and you want to make sure you get the descriptor wrapped version of the function, not whatever the descriptor gives you when accessed on A, you'd bypass the descriptor protocol by directly accessing A's __dict__:

class B(object):
    one = A.__dict__['one']
    two = A.__dict__['two']

By directly copying from the class dictionary, the descriptor protocol magic is never invoked, and you get the staticmethod and classmethod wrapped versions of one and two.

Upvotes: 17

Leon
Leon

Reputation: 32444

DISCLAIMER: This is not really an answer, but it doesn't fit into a comment format either.

Note that with Python2 @classmethod is NOT correctly preserved across classes either. In the code below, the call to B.one() works as though it was invoked through class A:

$ cat test.py 
class A(object):
    @classmethod
    def one(cls):
        print("I am class", cls.__name__)

class A2(A):
    pass

class B(object):
    one = A.one


A.one()
A2.one()
B.one()

$ python2 test.py 
('I am class', 'A')
('I am class', 'A2')
('I am class', 'A')

Upvotes: 5

Related Questions