上期为大家准备的Python攻略,从原理上讲了Python类的本质,接下来的两篇Python专栏里,继续来讲讲关于类及其方法的一些冷知识和烫知识。
我们也会和前面两篇专栏一样,用各种神奇的例子,从原理和机制的角度为你还原一个不一样的Python。
没有读过前文的朋友,看下面链接:
玩转Python|类与方法的隐藏秘密(1)
玩转Python|类与方法的隐藏秘密(2)
对象方法的本质
说到面向对象编程,大家应该对方法这一概念并不陌生。其实在上篇中已经提到,在Python中方法的本质就是一个字段,将一个可执行的对象赋值给当前对象,就可以形成一个方法,并且也尝试了手动制造一个对象。
但是,如果你对Python有更进一步的了解,或者有更加仔细的观察的话,会发现实际上方法还可以被如下的方式调用起来:
没错,就是 T.plus(t, 10) 这样的用法,这在其他一些面向对象语言中似乎并没见到过,看起来有些费解。先别急,咱们再来做另外一个实验。
在这个程序中, plus 函数被单独定义,且在类 T 中被引入为字段。而观察一下上面的输出,会发现一个事实—— plus和T.plus完全就是同一个对象,但t.plus就并不是同一个。根据以往我们的分析,前者是显而易见的,但是 t.plus 却成了一个叫做 method 的东西,这又是怎么回事呢?我们继续来实验,接着上一个程序。
会发现传说中的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__) # <function plus at 0x7fa58af95620>
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来更加科学也更加符合实际代码行为地再次搭建一回,程序如下:
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)
# <bound method plus of <__main__.MyObject object at 0x7fbbb9170748>>`
运行结果和之前一致,也和常规方式实现的对象完全一致,并且这个 t.plus 也正是之前实验中所看到的那种 method 。至此,Python中对象方法的本质已经十分清楚——对象方法一个基于原有函数,和当前对象,通过types.MethodType类进行组合后实现的可执行对象。
延伸思考1:基于上述的分析,为什么 T.plus(t, 10) 会有和 t.plus(10) 等价的运行效果?
延伸思考2:为什么对象方法开头第一个参数是 self ,而从第二个参数开始才是实际传入的?MethodType 对象在被执行的时候,其内部原理可能是什么样的?
类方法与静态方法
说完了对象方法,咱们再来看看另外两种常见方法——类方法和静态方法。首先是一个最简单的例子:
其中 method_cls 是一个类方法, method_stt 是一个静态方法,这一点大家应该并不陌生。那废话不多说,先看看这个 method_cls 到底是什么(程序接上文)
`
print(T.method_cls) # <bound method T.method_cls of <class 'main.T'>>
t = T(2, 3)
print(t.method_cls) # <bound method T.method_cls of <class 'main.T'>>
`
很眼熟对吧,没错——无论是位于类T上的T.method_cls,还是位于对象t上的t.method_cls,都是在上一章节中所探讨过的types.MethodType类型对象,而且还是同一个对象。接下来再看看其内部的结构(程序接上文)
print(T.method_cls.__func__) # <function T.method_cls at 0x7f78d86fe2f0>
print(T.method_cls.__self__) # <class '__main__.T'>
print(T) # <class '__main__.T'>
assert T.method_cls.__self__ is T
其中 func 就是这个原版的 method_cls 函数,而 self 则是类对象 T 。由此不难发现一个事实——类方法的本质是一个将当前类对象作为主体对象的方法对象。换言之,类方法在本质上和对象方法是同源的,唯一的区别在于这个 self 改叫了 cls ,并且其值换成了当前的类对象。看完了类方法,接下来是静态方法。首先和之前一样,看下 method_stt 的实际内容:
这个结果很出乎意料,但仔细想想也完全合乎逻辑——静态方法的本质就是一个附着在类和对象上的原生函数。换言之,无论是 T.method_stt 还是 t.method_stt ,实际获取到的都是原本的那个 method_stt 函数。
延伸思考3:为什么类方法中的主体被命名为 cls 而不是 self ,有何含义?
延伸思考4:如果将类方法中的 cls 参数重新更名为 self ,是否会影响程序的正常运行?为什么?
延伸思考5:类方法一种最为常见的应用是搭建工厂函数,例如 T.new_instance ,可用于快速创建不同特点的实例。而在Python中类本身就具备构造函数,因此类工厂方法与构造函数的异同与分工应该是怎样的呢?请通过对其他语言的类比与实际搭建来谈谈你的看法。
👏:欢迎大家体验OpenDILab开源的项目
https://github.com/opendilab
🪐:作者小哥的开源项目(部分仍在开发中)
命令行工具:https://github.com/HansBug/plantumlcli
对象转可执行代码:https://github.com/HansBug/potc
好用的工具库:https://github.com/HansBug/hbutils