开发者

How does assignment of a function as a class attribute become a method in Python?

开发者 https://www.devze.com 2022-12-20 17:00 出处:网络
>>> class A(object): pass >>> def func(cls): pass >>> A.func = func >>> A.func
>>> class A(object): pass
>>> def func(cls): pass
>>> A.func = func
>>> A.func
<unbound method A.func>

How does this assignment create a method? It seems unintuitive that assignment does the following for classes:

  • Turn functions into unbound instance methods
  • Turn functions wrapped in classmethod() into class methods (actually, this is pretty intuitive)
  • Turn functions wrapped in staticmethod() into functions

It seems that for the first, there should be an instancemethod(), and for the last one, there shouldn't be a wrapper function at all. I understand that these are for uses within a class block, but why should they apply outside of it?

But more importantly, how exactly does assignment of the function into a class work? What magic happens that resolves those 3 things?

Even more confusing开发者_运维百科 with this:

>>> A.func
<unbound method A.func>
>>> A.__dict__['func']
<function func at 0x...>

But I think this is something to do with descriptors, when retrieving attributes. I don't think it has much to do with the setting of attributes here.


You're right that this has something to do with descriptor protocol. Descriptors are how passing the receiver object as the first parameter of a method is implemented in Python. You can read more detail about Python attribute lookup from here. The following shows on a bit lower level, what is happening when you do A.func = func; A.func:

# A.func = func
A.__dict__['func'] = func # This just sets the attribute
# A.func
#   The __getattribute__ method of a type object calls the __get__ method with
#   None as the first parameter and the type as the second.
A.__dict__['func'].__get__(None, A) # The __get__ method of a function object
                                    # returns an unbound method object if the
                                    # first parameter is None.
a = A()
# a.func()
#   The __getattribute__ method of object finds an attribute on the type object
#   and calls the __get__ method of it with the instance as its first parameter.
a.__class__.__dict__['func'].__get__(a, a.__class__)
#   This returns a bound method object that is actually just a proxy for
#   inserting the object as the first parameter to the function call.

So it's the looking up of the function on a class or an instance that turns it into a method, not assigning it to a class attribute.

classmethod and staticmethod are just slightly different descriptors, classmethod returning a bound method object bound to a type object and staticmethod just returns the original function.


Descriptors are the magic1 that turns an ordinary function into a bound or unbound method when you retrieve it from an instance or class, since they’re all just functions that need different binding strategies. The classmethod and staticmethod decorators implement other binding strategies, and staticmethod actually just returns the raw function, which is the same behavior you get from a non-function callable object.

See “User-defined methods” for some gory details, but note this:

Also notice that this transformation only happens for user-defined functions; other callable objects (and all non-callable objects) are retrieved without transformation.

So if you wanted this transformation for your own callable object, you could just wrap it in a function, but you could also write a descriptor to implement your own binding strategy.

Here’s the staticmethod decorator in action, returning the underlying function when it’s accessed.

>>> @staticmethod
... def f(): pass
>>> class A(object): pass
>>> A.f = f
>>> A.f
<function f at 0x100479398>
>>> f
<staticmethod object at 0x100492750>

Whereas a normal object with a __call__ method doesn’t get transformed:

>>> class C(object):
...         def __call__(self): pass
>>> c = C() 
>>> A.c = c 
>>> A.c
<__main__.C object at 0x10048b890>
>>> c
<__main__.C object at 0x10048b890>

1 The specific function is func_descr_get in Objects/funcobject.c.


What you have to consider is that in Python everything is an object. By establishing that it is easier to understand what is happening. If you have a function def foo(bar): print bar, you can do spam = foo and call spam(1), getting of course, 1.

Objects in Python keep their instance attributes in a dictionary called __dict__ with a "pointer" to other objects. As functions in Python are objects as well, they can be assigned and manipulated as simple variables, passed around to other functions, etc. Python's implementation of object orientation takes advantage of this, and treats methods as attributes, as functions that are in the __dict__ of the object.

Instance methods' first parameter is always the instance object itself, generally called self (but this could be called this or banana). When a method is called directly on the class, it is unbound to any instance, so you have to give it an instance object as the first parameter (A.func(A())). When you call a bound function (A().func()), the first parameter of the method, self, is implicit, but behind the curtains Python does exactly the same as calling directly on the unbound function and passing the instance object as the first parameter.

If this is understood, the fact that assigning A.func = func (which behind the curtains is doing A.__dict__["func"] = func) leaves you with an unbound method, is unsurprising.

In your example the cls in def func(cls): pass actually what will be passed on is the instance (self) of type A. When you apply the classmethod or staticmethod decorators do nothing more than take the first argument obtained during the call of the function/method, and transform it into something else, before calling the function.

classmethod takes the first argument, gets the class object of the instance, and passes that as the first argument to the function call, while staticmethod simply discards the first parameter and calls the function without it.


Point 1: The function func you defined exists as a First-Class Object in Python.

Point 2: Classes in Python store their attributes in their __dict__.

So what happens when you pass a function as the value of a class attribute in Python? That function is stored in the class' __dict__, making it a method of that class accessed by calling the attribute name you assigned it to.


Relating to MTsoul's comment to Gabriel Hurley's answer:

What is different is that func has a __call__() method, making it "callable", i.e. you can apply the () operator to it. Check out the Python docs (search for __call__ on that page).

0

精彩评论

暂无评论...
验证码 换一张
取 消