lamc
lamc

Reputation: 367

What is the value of <object name>.<method name> in Python?

Consider the following code.

class Foo:
    def bar(self, x):
        pass
    
foo1 = Foo()
foo2 = Foo()
foobar1 = foo1.bar
foobar2 = foo2.bar          #What are foobar1 and foobar2?

print(foobar1 is foobar2)   #prints False. Why?

On the one hand, I can see why foobar1 is not foobar2–if I call foobar1(some_arg) , the call should in general be independent of foo2 and vice versa. But don't foobar1 and foobar2 both refer to function object defined inside the class Foo?

Upvotes: 0

Views: 185

Answers (2)

rici
rici

Reputation: 241931

If method_name is actually defined in the class object of object's class, then <object>.<method_name> is a "bound method" (or closure), which effectively wraps the function from the class method in order to provide <object> as the self argument. If method_name is actually an attribute of <object>, or if it has been marked with the @staticmethod decorator, then <object>.<method_name> just the function itself.

That's a bit of an oversimplification. There's lots more information in the Python reference manual (for example, in the "Callable objects" section of Section 3.2, The standard type hierarchy, and, iirc, in the Python tutorial. I hope this is a reasonable starting point, but for the nitty-gritty details and a full understanding of all the corner cases, you should probably read the authentic docs.


When you call foobar1 or foobar2, you supply one argument and the function gets called with two arguments. How do you suppose that happens?

More specifically, when you call foobar1, the method bar will be passed foo1 as the self argument, while when you call foobar2, the method bar will be passed foo2. That seems like magic, but it has a simple cause: when you extracted the bar attribute from foo1 (foo1.bar), Python noticed that it didn't come from foo1 itself; rather, it came from the class Foo. Since it came from the class object, Python created a new function closure on the fly in which self is bound to foo1. Thus, when you later call the closure, the self argument is filled in from the closure.

You don't even need foo2 to see that. Every time you ask for the bar attribute of an instance of class Foo, you get a new closure. But that doesn't happen when you ask for the bar attribute of the class itself. In that case, the attribute bar is directly available, and no closure is constructed:

>>> foo1.bar is foo1.bar
False
>>> Foo.bar is Foo.bar
True

You can see that these are different things:

>>> foo1.bar
<bound method Foo.bar of <__main__.Foo object at 0x7f9c8d8f27b8>>
>>> Foo.bar
<function Foo.bar at 0x7f9c8d8e5c80>

The first one is a "bound method" (where self has been bound to a value), and the second one is just a garden variety function.

If you try to call Foo.bar, you'll notice that you need to supply the self argument yourself, precisely because you're calling a function and not a closure:

>>> Foo.bar(42)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: bar() missing 1 required positional argument: 'x'
>>> Foo.bar(foo1, 42)
>>> 

Upvotes: 2

Daniel Walker
Daniel Walker

Reputation: 6772

First off, you shouldn't expect the methods to be the same (i.e., in the sense of is) when you have different Foo objects. Consider

l1 = [1, 2, 3]
l2 = [11, 12, 13]
append1 = l1.append
append2 = l2.append

Clearly, calling append1 would do something different than calling append2 so they can't be the same object.

However, this is a red herring. Even if you used the same Foo object, you'd still get different functions:

foo = Foo()
foobar1 = foo.bar
foobar2 = foo.bar
print(foobar1 is foobar2) # False

This is due to a minor optimization that the Python compiler performs. Consider these two functions:

def get_bar(foo):
    return foo.bar

def call_bar(foo):
    foo.bar()

Disassembling them with dis.dis produces

 0 LOAD_FAST                0 (foo)
 2 LOAD_ATTR                0 (bar)
 4 RETURN_VALUE

for get_bar and

 0 LOAD_FAST                0 (foo)
 2 LOAD_METHOD              0 (bar)
 4 LOAD_CONST               1 (5)
 6 CALL_METHOD              1
 8 POP_TOP
10 LOAD_CONST               0 (None)
12 RETURN_VALUE

for call_bar. You can see that the logic of how foo.bar is acquired is different.

The reason why is, at rest (so to speak), there is no foo.bar function. There is a Foo.bar function which takes a Foo object as its first argument. When you ask Python for foo.bar as in get_bar, you want it to return to you a function that doesn't already exist. So, it makes one up for you on the spot (akin to functools.partial). That's why you get different objects each time you ask for foo.bar.

However, when you call foo.bar as in call_bar, the Python compiler knows that you're not interested in the foo.bar function itself but only in calling it. So, it does just that without creating an intermediate function.

Upvotes: 2

Related Questions