Reputation: 367
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
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
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