上期为大家准备的Python攻略,从原理上讲了Python类的本质,接下来的两篇Python专栏里,继续来讲讲关于类及其方法的一些冷知识和烫知识。
我们也会和前面两篇专栏一样,用各种神奇的例子,从原理和机制的角度为你还原一个不一样的Python。
没有读过前文的朋友,看下面链接:
玩转Python|类与方法的隐藏秘密(1)
玩转Python|类与方法的隐藏秘密(2)
下面是本篇正文
对象方法的本质
说到面向对象编程,大家应该对方法这一概念并不陌生。其实在上篇中已经提到,在Python中方法的本质就是一个字段,将一个可执行的对象赋值给当前对象,就可以形成一个方法,并且也尝试了手动制造一个对象。
但是,如果你对Python有更进一步的了解,或者有更加仔细的观察的话,会发现实际上方法还可以被如下的方式调用起来:
class T:
def __init__(self, x, y):
self.x = x
self.y = y
def plus(self, z):
return self.x + self.y + z
t = T(2, 5)
t.plus(10) # 17
T.plus(t, 10) # 17, the same as t.plus(10)
没错,就是 T.plus(t, 10) 这样的用法,这在其他一些面向对象语言中似乎并没见到过,看起来有些费解。先别急,咱们再来做另外一个实验。
def plus(self, z):
return self.x + self.y + z
class T:
def __init__(self, x, y):
self.x = x
self.y = y
plus = plus
t = T(2, 5)
print(t)
print(plus)
print(T.plus)
print(t.plus)
# <__main__.T object at 0x7fa58afa7630>
#
#
# >
在这个程序中, plus 函数被单独定义,且在类 T 中被引入为字段。而观察一下上面的输出,会发现一个事实—— plus和T.plus完全就是同一个对象,但t.plus就并不是同一个。根据以往我们的分析,前者是显而易见的,但是 t.plus 却成了一个叫做 method 的东西,这又是怎么回事呢?我们继续来实验,接着上一个程序。
from types import MethodType print(type(t.plus), MethodType) #assert isinstance(t.plus, MethodType)
会发现传说中的method 原来是 types.MethodType 这个对象。
既然已经有了这个线索,那么我们继续翻阅一下这个 types.MethodType 的源代码,源代码有部分内容不可见,只找到了这些(此处Python版本为 3.9.6 )
class MethodType:
__func__: _StaticFunctionType
__self__: object
__name__: str
__qualname__: str
def __init__(self, func: Callable[..., Any], obj: object) -> None: ...
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
此处很抱歉没有找到官方文档,types库的文档在MethodType 的部分只有一行概述性文本而没有实质性内容,所以只好去翻源代码了。
types库文档链接:https://docs.python.org/3/library/types.html#types.MethodType
如果有有读者找到的正经的文档或说明欢迎后台同步我们,我们第一时间分享给大家。
不过这么一看,依然有很关键的发现——这个__init__方法有点东西,从名字和类型来看,func应该是一个函数,obj应该是一个任意对象。咱们再来想想,从逻辑要素的角度想想, t.plus 这个东西要想能运行起来,必要因素有那些,答案显而易见:
-
运行逻辑,通俗来说就是实际运行的函数plus
-
运行主体,通俗来说在方法前被用点隔开的那个对象t
到这一步为止答案已经呼之欲出了,不过本着严谨的科学精神接下来还是需要进行更进一步的验证,我们需要尝试拆解这个 t.plus ,看看里面到底都有些什么东西(接上面的程序)。
print(set(dir(t.plus)) - set(dir(plus))) # {'__self__', '__func__'}
print(t.plus.__func__) #
print(t.plus.__self__) # <__main__.T object at 0x7fa58afa7630>
首先第一行,将 dir 结果转为集合,看看那些字段是t.plus拥有而T.plus没有的。果不其然,刚好就俩字段: __self__ 和 __func__ 。
然后分别将这两个字段的值进行输出,发现—— t.plus.__func__就是之前定义的那个plus,而t.plus.__self__就是实例化出来的t。
到这一步,与我们的猜想基本吻合,只差一个终极验证。还记得上篇中那个手动制造出来的对象不,没错,让我们来用MethodType来更加科学也更加符合实际代码行为地再次搭建一回,程序如下:
from types import MethodType
class MyObject(object):
pass
if __name__ == '__main__':
t = MyObject() # the same as __new__
t.x = 2 # the same as __init__
t.y = 5
def plus(self, z):
return self.x + self.y + z
t.plus = MethodType(plus, t) # a better implement
print(t.x, t.y) # 2 5
print(t.plus(233)) # 240
print(t.plus)
# >
运行结果和之前一致,也和常规方式实现的对象完全一致,并且这个 t.plus 也正是之前实验中所看到的那种 method 。至此,Python中对象方法的本质已经十分清楚——对象方法一个基于原有函数,和当前对象,通过types.MethodType类进行组合后实现的可执行对象。
延伸思考1:基于上述的分析,为什么 T.plus(t, 10) 会有和 t.plus(10) 等价的运行效果?
延伸思考2:为什么对象方法开头第一个参数是 self ,而从第二个参数开始才是实际传入的?MethodType 对象在被执行的时候,其内部原理可能是什么样的?
类方法与静态方法
说完了对象方法,咱们再来看看另外两种常见方法——类方法和静态方法。首先是一个最简单的例子:
class T:
def __init__(self, x, y):
self.x = x
self.y = y
def plus(self, z):
return self.x + self.y + z
@classmethod
def method_cls(cls, suffix):
return str(cls.__name__) + suffix
@staticmethod
def method_stt(content):
return ''.join(content[::-1])
其中 method_cls 是一个类方法, method_stt 是一个静态方法,这一点大家应该并不陌生。那废话不多说,先看看这个 method_cls 到底是什么(程序接上文)
print(T.method_cls) #> t = T(2, 3) print(t.method_cls) # >
很眼熟对吧,没错——无论是位于类T上的T.method_cls,还是位于对象t上的t.method_cls,都是在上一章节中所探讨过的types.MethodType类型对象,而且还是同一个对象。接下来再看看其内部的结构(程序接上文)
print(T.method_cls.__func__) #print(T.method_cls.__self__) # print(T) # assert T.method_cls.__self__ is T
其中 __func__ 就是这个原版的 method_cls 函数,而 __self__ 则是类对象 T 。由此不难发现一个事实——类方法的本质是一个将当前类对象作为主体对象的方法对象。换言之,类方法在本质上和对象方法是同源的,唯一的区别在于这个 self 改叫了 cls ,并且其值换成了当前的类对象。看完了类方法,接下来是静态方法。首先和之前一样,看下 method_stt 的实际内容:
print(T.method_stt) #t = T(2, 3) print(t.method_stt) #
这个结果很出乎意料,但仔细想想也完全合乎逻辑——静态方法的本质就是一个附着在类和对象上的原生函数。换言之,无论是 T.method_stt 还是 t.method_stt ,实际获取到的都是原本的那个 method_stt 函数。



