I stumbled across this behaviour, which suggests that you can use getattr
to call methods on a class instance, as an alternative to the intuitively named operator.methodcaller
:
from operator import methodcaller
class Foo():
def __init__(self, lst):
self.lst = lst
def summer(self):
return sum(self.lst)
my_obj = Foo(range(11))
res1 = methodcaller('summer')(my_obj) # 55
res2 = getattr(my_obj, 'summer')() # 55
assert res1 == res2
I'd like to understand, internally, why this works. Is it because all methods are also attributes? This seems to be the case because dir(Foo)
or dir(my_obj)
includes 'summer'
. But I have never heard methods referred to as attributes of a class or class instance, e.g. this isn't mentioned in What is a “method” in Python?
There is an explanation in the docs which mentions differentiation between "data attributes" and "non-data attributes" which I failed to understand.
Update: Comments by @Amadan have clarified most of the above. The only remaining bit I do not understand is this excerpt from the docs:
If you still don’t understand how methods work, a look at the implementation can perhaps clarify matters. When a non-data attribute of an instance is referenced, the instance’s class is searched. If the name denotes a valid class attribute that is a function object, a method object is created by packing (pointers to) the instance object and the function object just found together in an abstract object: this is the method object.
So is a non-data attribute determined by checking whether it is callable, or is there some other way that's used to determine it's a function object? What does "packing pointers" to the instance object mean? What's an abstract object?
Yes, methods are just attributes containing functions of appropriate form (they must accept at least one parameter, the receiver, usually called self
).
Here's an example that explains the quoted paragraph:
class Foo():
def a(self):
print("FOO")
foo = Foo()
foo.a()
# => FOO
So, def
there actually attached an attribute a
to Foo
as an "unbound method" (as you can see when we inspect it - meaning, it doesn't know who is receiving it yet) in Python 2, or just a plain function value (in Python 3):
Foo.a
# => <unbound method Foo.a> (Python 2)
# => <function Foo.a at 0x10919fea0> (Python 3)
You can invoke it just like any function (... with one exception, in Python 2: the first argument must be of type Foo
):
Foo.a(foo)
# => FOO
Foo.a(42)
# => TypeError: unbound method a() must be called with Foo instance as first argument (got int instance instead) (Python 2)
# => 42 (Python 3)
However, if you try to find it on an instance ("instance attribute reference"), it is now reporting as a "bound method":
foo.a
# => <bound method Foo.a of <__main__.Foo instance at 0x10ba11320>>
This can be said to be "packing (pointers to) the instance object and the function object together": there is a reference to the instance object, <__main__.Foo instance at 0x10ba11320>
(a.k.a. foo
), and a reference to the function object, Foo.a
, in one package we call "bound method".
Unlike JavaScript, it's not purely a syntactic thing. In JavaScript, the difference between a method invocation and a function invocation is in the call itself: if it has a dot, it's a method invocation:
// JavaScript
let foo = new Foo()
foo.a(); // method invocation: `a` is told that the receiver is `foo`
let z = foo.a; z() // function invocation, `a` doesn't know about `foo`
In Python, the bound function carries its receiver inside it:
// Back to Python
foo.a() // method invocation: `a` is told that the receiver is `foo`
z = foo.a; z() // STILL method invocation; `z` knows both `foo` and `Foo.a`
How does this calling even work? The rest of the paragraph explains:
When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list.
So, when we say
foo.a()
it will unpack foo.a
into Foo.a
and foo
; prepend foo
, the receiver, to the list of the arguments (I have none here, so the argument list is [foo] + []
for the final argument list of [foo]
), and what ends up being called is Foo.a(foo)
. Incidentally, that's exactly what you can do manually:
Foo.a(foo)
# => FOO
Even with built-in objects:
"-".join(["foo", "bar", "baz"])
# => 'foo-bar-baz'
str.join("-", ["foo", "bar", "baz"])
# => 'foo-bar-baz'
Here, "-".join
is a bound method that packs together the receiver "-"
and the function str.join
; when we invoke the first line, the receiver "-"
is prepended to the rest of the arguments [["foo", "bar", "baz"]]
for the final argument list of ["-", ["foo", "bar", "baz"]]
, and that's sent to the function that is sitting in the join
attribute of str
(i.e. str.join
). This gives us a clear translation between the first line and the second line.