Python - why can I call a class method with an instance?
Asked Answered
N

2

13

New to Python and having done some reading, I'm making some methods in my custom class class methods rather than instance methods.

So I tested my code but I hadn't changed some of the method calls to call the method in the class rather than the instance, but they still worked:

class myClass:
   @classmethod:
   def foo(cls):
      print 'Class method foo called with %s.'%(cls)

   def bar(self):
      print 'Instance method bar called with %s.'%(self)

myClass.foo()
thing = myClass()
thing.foo()
thing.bar()

This produces:

class method foo called with __main__.myClass.
class method foo called with __main__.myClass.
instance method bar called with <__main__.myClass instance at 0x389ba4>.

So what I'm wondering is why I can call a class method (foo) on an instance (thing.foo), (although it's the class that gets passed to the method)? It kind of makes sense, as 'thing' is a 'myClass', but I was expecting Python to give an error saying something along the lines of 'foo is a class method and can't be called on an instance'.

Is this just an expected consequence of inheritance with the 'thing' object inheriting the foo method from its superclass?

If I try to call the instance method via the class:

myClass.bar()

then I get:

TypeError: unbound method bar() must be called with myClass instance...

which makes perfect sense.

Negress answered 12/5, 2015 at 11:52 Comment(6)
In Python, methods are not called on something. thing.foo is not a method call, it is an attribute access, It is basically the same thing as getattr(thing, "foo").Cholula
@Cholula Surely 'foo` is a method and thing.foo() is calling the foo method of the object thing? Isn't an attribute Python's terminology for what other languages refer to as a variable or property?Negress
I think that "(thing.foo)() is calling the foo method of the object thing" (parentheses for clarity) is a metaphor for "(thing.foo)() is calling the callable object thing.foo", that is it. AFAIU, this object is created dynamically when getattr(thing, "foo") is called, and of course it has a reference to thing object.Cholula
Note the difference between object "attributes" that are defined by its class and the attributes that are "stored" in the object itself. Because if you have a function (or any object/value) bar, you can write thing.foo = bar, and then call it thing.foo() if bar was callable. In this case the object returned by thing.foo is actually stored in the dictionary thing.__dict__, and not generated dynamically, as, i suppose, is the case with "instance methods" defined as a part of class definition.Cholula
"Isn't an attribute Python's terminology for what other languages refer to as a variable or property?" -- AFAIU from my experiments with Python, Python plays its tricks by allowing arbitrary customisation of attribut getters and setters. The "magic" happens when you write or read an attribute. Attributes are not just record fields: using them can trigger arbitrary code.Cholula
@SteveIves See my answer. The class statement defines two functions: one is bound to the class attribute bar, the other is wrapped in an instance of classmethod and bound to the class attribute foo. thing.foo, then, is an attribute lookup whose result is a callable object, not the original function or the classmethod instance itself but an instance of the method class, produced by the descriptor protocol.Mycosis
V
14

You can call it on an instance because @classmethod is a decorator (it takes a function as an argument and returns a new function).

Here is some relavent information from the Python documentation

It can be called either on the class (such as C.f()) or on an instance (such as C().f()). The instance is ignored except for its class. If a class method is called for a derived class, the derived class object is passed as the implied first argument.

There's also quite a good SO discussion on @classmethod here.

Vincennes answered 12/5, 2015 at 12:6 Comment(5)
Thank you - I did various searches for inheritance, classmethods etc, but as you can imagine, the results were overwhelming. I would have been quite happy to simply accept that it works, but as an assembler programmer, I like to know how things work under the covers.Negress
"You can call it on an instance because @classmethod is a decorator" -- i do not follow this logic.Cholula
@Cholula do you know what a decorator is?Vincennes
Yes. But i believe i can make a decorator such that the decorated function will produce a class attribute that will raise errors if called as an instance method (in fact, which will not be callable at all). For example: def decor(f): return 42.Cholula
Technically, classmethod is a type; calling it returns an instance of classmethod, not a function.Mycosis
M
1

Let's start with a quick overview of the descriptor protocol. If a class defines a __get__ method, an instance of that class is a descriptor. Accessing a descriptor as the attribute of another object produces the return value of the __get__ method, not the descriptor itself.

A function is a descriptor; this is how instance methods are implemented. Given

class myClass:
    @classmethod:
    def foo(cls):
        print('Class method foo called with %s.' % (cls,))

    def bar(self):
        print 'Instance method bar called with %s.'%(self)

c = myClass()

the expression c.bar is equivalent to

myClass.__dict__['bar'].__get__(c, myClass)

while the expression myClass.bar is equivalent to the expression

myClass.__dict__['bar'].__get__(None, myClass)

Note the only difference is in the object passed as the first argument to __get__. The former returns a new method object, which when called passes c and its own arguments on to the function bar. This is why c.bar() and C.bar(c) are equivalent. The latter simply returns the function bar itself.

classmethod is a type that provides a different implementation of __get__. This means that c.foo() and myClass.foo() call __get__ as before:

# c.foo
myClass.__dict__['foo'].__get__(c, myClass)

# myClass.foo
myClass.__dict__['foo'].__get__(None, myClass)

Now, however, both calls return the same method object, and this method object, when called, passes myClass as the first argument to the original function object. That is, c.foo() is equivalent to myClass.foo(), which is equivalent to x(myClass) (where x is the original function defined before the decoration bound the name foo to an instance of classmethod).

Mycosis answered 4/5, 2020 at 18:47 Comment(2)
A good complement to this answer, where seeing __get__ implemented helps complete the picture. Though I'm unsure why cls is being passed to self.method instead of instance, and why instance is unusedSevilla
That's by design. A class method always receives a class as its first argument. If you invoke it from a class, you get the class. If you invoke it from an instance of the class, you get that instance's type.Mycosis

© 2022 - 2024 — McMap. All rights reserved.