Is calling a method on a class and passing an instance manually always equivalent to calling the method on an instance?

Viewed 56

I.e. given a class and an instance like this:

class Foo:
    def bar(self):
        ...

foo = Foo()

is Foo.bar(foo) always equivalent to foo.bar(), or are there corner cases where the former call can lead to unexpected results?

3 Answers

Not necessarily. The descriptor protocol allows you to define what it means for an object to be accessed as an attribute.

  • Foo.bar(foo) is equivalent to Foo.__dict__['bar'].__get__(None, Foo)
  • foo.bar() is equivalent to type(foo).__dict__['bar'].__get__(foo, type(foo))

There are two points of divergence. First, type(foo) may or may not be Foo, and second, depending on how __get__ is defined, the results when __get__ receives foo vs. None as its first argument may differ.

The corner case comes into play when Foo.bar() is decorated, e.g. in one of the two following ways:

class Foo:
    @classmethod
    def bar(cls, inst):
        ...

    @staticmethod
    def bar(inst):
        ...

In the former case, calling Foo.bar() automatically passes the class Foo as an argument - not an instance of the class, but the class itself. If you were to make a subclass Bar, then Bar.bar() would pass the class Bar as an argument. This is useful for polymorphism.

In the latter case, there are no implicit parameters being passed. You still have to call the method through its class (Foo.bar()) but neither the instance on which it is called (if you do that for some reason) nor the class on which it is called are passed as parameters.


In general, though, Foo.bar(foo) is equivalent to foo.bar() - the default behavior of methods in a class is to accept the instance on which the method is called as the first parameter of that method.

When you do foo.bar, it first tries to lookup bar on foo itself. As bar is defined in the class Foo and not the instance foo, it will fail. Only then does it move on and attempts to lookup bar on foo.__class__ (i.e. Foo), and succeeds. We can exploit this:

class Foo:
    def bar(self):
        ...

foo = Foo()
foo.bar = lambda: print('hello')

Now, foo.bar() and Foo.bar(foo) results in distinct actions, as different functions will be resolved in the two cases.

Related