Accessing methods via getattr

回眸只為那壹抹淺笑 提交于 2021-02-08 05:45:18

问题


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?


回答1:


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.




回答2:


Yes, methods are attributes.

From the python glossary:

attribute

A value associated with an object which is referenced by name using dotted expressions. For example, if an object o has an attribute a it would be referenced as o.a.

Clearly we can access methods like that, so consequently they must be attributes. They're just attributes that happen to be functions.

Further, there's this sentence in the getattr documentation:

getattr(x, 'foobar') is equivalent to x.foobar

As a direct consequence of that, x.foobar() is also equivalent to getattr(x, 'foobar')(). There is no reason to believe that methods are special in any way.


The reason why methods are rarely referred to as attributes in practice is, I think, two-fold:

  1. Methods serve a completely different purpose than other attributes. Methods are intended to be called, whereas other attributes are usually just data. It's the same reason why we don't refer to functions as "variables", even though they technically are.
  2. "Method" is much easier to say than "callable class attribute".

Lastly, regarding data attributes vs. non-data attributes: The documentation makes a distinction between methods (i.e. callable attributes; "non-data attributes") and data attributes (i.e. everything else).

There are two kinds of valid attribute names, data attributes and methods.

data attributes correspond to “instance variables” in Smalltalk, and to “data members” in C++.

The other kind of instance attribute reference is a method.

The excerpt you posted is rather confusing, but I think it's trying to provide a basic explanation of the descriptor protocol, which is responsible for turning functions into methods. Let's see that again:

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.

In other words: When you do some_object.some_method, python grabs the function(!) some_method from some_object's class and then turns it into a bound method where the self argument is implicitly included. Why they call it an "abstract object" is a mystery to me. Anyway, for more detail about this process, see How does assignment of a function as a class attribute become a method in Python? or the relevant section in the descriptor HowTo.

(Warning: Python distinguishes between data descriptors and non-data descriptors. Don't confuse these with data attributes and non-data attributes! These are two entirely unrelated concepts.)



来源:https://stackoverflow.com/questions/51858649/accessing-methods-via-getattr

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!