QQ登录

只需一步,快速开始

开启左侧

Python学习第七十天—类和对象19-函数、方法、静态方法、类方法的底层实现原理

[复制链接]
15271953841 发表于 2024-4-9 17:45:47 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
函数、方法、静态方法、类方法的底层实现原理
一、        让我们从函数和方法说起……
函数和方法其实就是一个东西;实现了对象绑定的函数我们就把它叫做方法呗,没有和对象绑定的,那么它通常就是一个普通的函数,既然是一个东西,那Python在底层是如何辨别函数和方法的呢?
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license()" for more information.
class C:
    def func(self,x):
        return x

   
c = C()
C.func
<function C.func at 0x000001F5D807CEA0>  (打印的是一个函数)
c.func
<bound method C.func of <__main__.C object at 0x000001F5D805BA10>>     (打印的是一个绑定的方法)
Python在底层是怎么知道大写的C.func就是函数,小写的c.func就是方法呢?参考官方文档《Functions and methods》,答案指向——>描述符。
Python中“万物皆对象”。
当我们定义函数的时候,其实是去实例化了一个叫做Function的类(看下面这段代码),它的底层是用C语言来实现的,这里文档写成Python的语法形式,主要为了方便读者理解,然后其它的细节也没有多说,也用……省略,这里的重点是文档想通过代码解释清楚Python内部是如何识别出函数和方法的,看一下Function类的代码,赫然出现了一个__get__()方法,给我们看的也只有一个__get__()方法,函数其实就是一个描述符,而且是一个非数据描述符,这里写的很清楚了。
class Function:
    ...
    def __get__(self,obj,objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return MethodType(self,obj)
那么当这个函数定义的过程被放到类里面的时候,也就是说,我去定义一个类,然后在类里面define一个函数的时候,那么这个__get__()方法,就要发光发热了,描述符就要发光发热了,我们来看一下它的执行逻辑,它先判断第2个参数obj是否为None,如果是None就返回自己,否则就返回MethodType,那么这里要注意:之前我们在演示的时候,说这个__get__()方法的第2个参数instance,第3个参数owner,是因为它的文档就这么叫的。但这里也是官方文档,又叫做obj(第2个参数),objtype(第3个参数),那么这个你就要知道了,以后你在阅读文档时可能就会遇到这种情况,后面可能还有叫cls、klass的,这个到无所谓,即便是官方文档,也是有很多人来协同编写的,在命名上会出现不同的想法也是很正常的事情,大家只要记住,不同位置的参数,代表各自的含义就行了,比如__get__()方法,第2个参数叫obj也好,叫instance也好,它表示的就是被描述符拦截的属性所在的类的实例对象;第3个参数对应的是owner,也就是被描述符拦截的属性所在的类。(可以参考69课时最后的代码),上面的代码所做的大概就是这个事情(看下面这段代码):
class D:
    def __get__(self,instance,owner):
        if instance is None:
            print("函数~")
        else:
            print("方法~")

            
class C:
    x = D()

   
c= C()
c.x
方法~
C.x
函数~
我们再来看一看MethodType(self,obj)又是一个什么东西呢?其实也是一个类,看下面它实现的细节:
class MethodType:
    "Emulate PyMethod_Type in objects/classobject.c"
    def __init__(self,func,obj):
        self.__func__ = func
        self.__self__ = obj
    def __call__(self,*args,**kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj,*args,**kwargs)
当它的实例化对象被当作一个函数调用的时候,那么这个__call__魔法方法(参考第65课时)就会被执行,__call__()这个魔法方法我们学过,它所做的事情是将传递进去的func和obj 两个参数进去整合,并且返回,简单地说,就是传递进来的第一个实参(func)变成函数名(func),然后第二个实参(obj)作为该函数func的第一个参数(obj),最后整合之后就返回,那么这个return MethodType(self,obj)相当于return self(obj),可以看到,只要掌握描述符的用法,将函数绑定为方法,其实就是一件非常简单不过的事情了。

二、        再看静态方法(static methods)
前面我们学习过静态方法:放到类里面的函数。
class C:
    @staticmethod
    def func():
        print("I love FishC")

        
c = C()
c.func()
I love FishC
C.func()
I love FishC
前面我们学习过静态方法,它与普通函数区别就是它可以放到类里面去,它不需要去绑定类和对象,它就是自由的放在类里面的函数,它不需要绑定。静态方法又是怎么实现的。
class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"
    def __init__(self,f):
        self.f = f
    def __get__(self,obj,objtype = None):
        return self.f
    def __call__(self,*args,**kwds):
        return self.f(*args,**kwds)
这个更简单,只要被StaticMethod装饰器碰过的函数就会变成StaticMethod类的对象,那么当这个对象被做为函数去调用的时候,它不管三七二十一,直接调用传递进来的函数自身,有没有类和对象什么事?没有,直接给忽略了吗。

三、        再看类方法的实现原理
class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"
    def __init__(self,f):
        self.f = f
    def __get__(self,obj,cls = None):
        if cls is None:
            cls = type(obj)
        if hasattr(type(self.f),"__get__"):
            return self.f.__get__(cls,cls)
        return MethodType(self.f,cls)
这个是ClassMethod的实现逻辑,到底是个什么逻辑呢?
首先,这里有个type函数,是什么意思呢?以前我们用type判断一个对象的类型。type如果传入一个对象的话,它还是返回该对象所属的类。
class C:
    pass

c = C()
type(c) is C       (如果传入一个对象作为参数,type(c)得到的就是这个对象所属的类)
True
其中if hasattr(type(self.f),”__get__”):
                return self.f.__get__(cls,cls)难理解!
这Python3.9才被加入的,为的是能让类方法和其它装饰器可以串连起来使用,向下面这个样子:
class G:
    @classmethod
    @property
    def __doc__(cls):
        return f"A doc for {cls.__name__!r}"            (在Python中,`!r`是格式化字符串的一种占位符,字符串会被转换成带有引号的字符串。)

   
G.__doc__
"A doc for 'G'"
它先@classmethod,然后下面再@property,然后才是对应的函数,为了自动使用,它才会有hasattr这么一个分支操作,它要追根朔源,找到以前的版本,以前版本的Python,它是没有这个hasattr,它长成这个样子,好理解:
class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"
    def __init__(self,f):
        self.f = f
    def __get__(self,obj,klass = None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass,*args)
        return newfunc
将传递进来的函数,通过__get__()魔法方法的klass参数,也就是被描述符拦截的属性所在的类,将它调整为新函数的第一个参数,这不就是类函数的定义,因为类函数要绑定的是类,而非对象。
我们再把镜头切回来:再讲讲Python 3.9最新的这个实现方案:
刚刚我们说了,它文档这里说了,加hasattr(参考地62课时)这个条件分支的原因,其实时为了能够让类方法和其它装饰器可以串连起来一起使用,像下面这个样子:
class G:
    @classmethod
    @property
    def __doc__(cls):
        return f"A doc for {cls.__name__!r}"

   
G.__doc__                      (这里省略了一个括号,__doc__是属性形式访问)
"A doc for 'G'"
那么在什么情况下,才需要这样链式串连两个装饰器呢?
class C:
    @classmethod
    def __doc__(cls):     (这是一个类方法,所以传入的是一个类)
        return f"I love FishC.--from class {cls.__name__}"

   
c = C()
c.__doc__()                   (从类的角度去访问,因为这是一个classmethod的装饰器,一个类方法)
'I love FishC.--from class C'
C.__doc__()
'I love FishC.--from class C'
那么现在有进一步的需求,希望添加属性访问的方式,也就是,我们这会儿希望不使用访问函数的形式,就是说这个__doc__是一个属性的形式,就可以得到相同的结果,那么做法其实就是像文档中那样。
class D:
    @classmethod
    @property
    def __doc__(cls):
        return f"I love FishC.--from class {cls.__name__}"

   
d = D()
d.__doc__            (少了一个(),我们使用属性的方式,不是用调用方法的方式,这就是property的功能之一)
'I love FishC.--from class D'
D.__doc__
'I love FishC.--from class D'
为什么我们要使用这个hasattr条件分支?是什么样的一个原理使得MethodType可以对这个@property装饰器进行一个修改和干扰的呢?
class MethodType:
    def __init__(self,func,obj):
        self.__func__ = func
        self.__self__ = obj
    def __call__(self,*args,**kwargs):
        func = self.__func__
        obj = self.__self__
        print("小白")                     (当这个MethodType被调用的时候,它就会打印一个“小白”)
        return func(obj,*args,**kwargs)

   
class ClassMethod:
    def __init__(self,f):
        self.f = f
    def __get__(self,obj,cls = None):
        if cls is None:              (当这个cls=None的时候,它就会打印旺财)
            print("旺财")
            cls = type(obj)
        if hasattr(type(self.f),"__get__"):
            print(f"来福,type(self.f) -> {type(self.f)}")     (当这个hasattr这个分支条件分支成立的时候,它就会打印“来福”,并且把这个type(self.f)这个内容给打印出来。)
            return self.f.__get__(cls,cls)
        return MethodType(self.f,cls)

   
class D:
    @ClassMethod       (注意这里用上自己的ClassMethod,它的C和M大写的,官方的全小写)
    @property            (property用官方的)(自下往上执行)
    def __doc__(cls):          (同样定义一个__doc__这个文档)
        return f"I love FishC.--from class {cls.__name__}"

   
d = D()
d.__doc__
来福,type(self.f) -> <class 'property'>
'I love FishC.--from class D'
如果是多个装饰器串连的话,那么这个type(self.f),也就是传入参数的类其实就是<class 'property’>,当多个装饰器装饰同一个对象的时候,它们的执行顺序是自下往上的,我们在学习装饰器那一个课时也说过,所以,@property先对__doc__函数进行了一波改造,那么它就相当于这个样子:
__doc__ = property(__doc__)
那么这个时候把它变成了一个property对象,这个__doc__其实就是property的实例对象。然后到@ClassMethod装饰器,因为它有hasattr这个分支,这个分支就要发挥作用了。
有了上面的分析,我们知道type(self.f)的结果就是property类,那么这个property类有没有这个__get__方法?肯定有哇,因为property就靠这个来拦截属性的吗,这就好办了,这里它就对__get__方法进行改造。注意self.f现在是一个property对象,那么这里self.f.__get__(cls,cls)调用的就是property类里面的__get__(0方法,并且还对它进行狸猫换太子,本来第一个参数应该是instance的位置,也就是指向对象的那个参数值被替换成了指向类cls,这个就是hasattr的实现原理。
如果现在我们把代码改成这个样子,就是不加@property了,@ClassMethod直接去装饰这个__doc__()函数,那么我们猜一下,我如果实例化一个对象,这会儿是哪个兄弟(小白?来福?旺财?)踩雷?
class D:
    @ClassMethod
    def __doc__(cls):
        return f"I love FishC.--from class {cls.__name__}"

   
d = D()
d.__doc__()
来福,type(self.f) -> <class 'function'>
'I love FishC.--from class D'
还是来福踩雷,为什么呀?课程刚开头我们学过Python万物皆对象,函数也是对象,所以这里type(self.f)拿到的是一个<class ‘function>的类,它不是去找这个类D,而是这个__doc__()它是一个函数吗,是一个函数它就是一个类的对象,所以它找到的是这个函数对应的function类,所以还是来福踩雷了。
课后一定要多去思考这些原理性的东西,因为我觉得只有讲这些东西,才会发现,Python背后的描述符竟然是如此强大的,这么多的变节机制都是由描述符实现的。

客服热线
400-1234-888 周一至周日:09:00 - 21:00
公司地址:襄阳市樊城区长虹路现代城5号楼188

创客帮MAKER.BAND青少年创客创意社区是一个融教育、科技、体育资讯为一体的综合服务平台,专注于教育创新、专注于科技体育、专注于教育资讯。

Powered by Discuz! X3.4 © 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表