zoukankan      html  css  js  c++  java
  • Python科普系列——类与方法(下篇)

    书接上回,继续来讲讲关于类及其方法的一些冷知识和烫知识。本篇将重点讲讲类中的另一个重要元素——方法,也和上篇一样用各种神奇的例子,从原理和机制的角度为你还原一个不一样的Python。在阅读本篇之前,推荐阅读一下上篇的内容:Python科普系列——类与方法(上篇)

    对象方法的本质

    说到面向对象编程,大家应该对方法这一概念并不陌生。其实在上篇中已经提到,在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>
    # <function plus at 0x7fa58af95620>
    # <function plus at 0x7fa58af95620>
    # <bound method plus of <__main__.T object at 0x7fa58afa7630>>
    

    在这个程序中, plus 函数被单独定义,且在类 T 中被引入为字段。而观察一下上面的输出,会发现一个事实—— plusT.plus完全就是同一个对象,但t.plus就并不是同一个。根据上篇中的分析,前者是显而易见的,但是 t.plus 却成了一个叫做 method 的东西,这又是怎么回事呢?我们继续来实验,接着上一个程序

    from types import MethodType
    
    print(type(t.plus), MethodType)  # <class 'method'> <class 'method'>
    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 的部分只有一行概述性文本而没有实质性内容,所以只好去翻源代码了,如果有有读者找到的正经的文档或说明欢迎贴在评论区。不过这么一看,依然有很关键的发现——这个__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来更加科学也更加符合实际代码行为地再次搭建一回,程序如下

    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)
        # <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 对象在被执行的时候,其内部原理可能是什么样的?

    欢迎评论区讨论!

    类方法与静态方法

    说完了对象方法,咱们再来看看另外两种常见方法——类方法和静态方法。首先是一个最简单的例子

    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)  # <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 的实际内容

    print(T.method_stt)  # <function method_stt at 0x7fd64fa70620>
    
    t = T(2, 3)
    print(t.method_stt)  # <function method_stt at 0x7fd64fa70620>
    

    这个结果很出乎意料,但仔细想想也完全合乎逻辑——静态方法的本质就是一个附着在类和对象上的原生函数。换言之,无论是 T.method_stt 还是 t.method_stt ,实际获取到的都是原本的那个 method_stt 函数。

    延伸思考3:为什么类方法中的主体被命名为 cls 而不是 self ,有何含义?

    延伸思考4:如果将类方法中的 cls 参数重新更名为 self ,是否会影响程序的正常运行?为什么?

    延伸思考5:类方法一种最为常见的应用是搭建工厂函数,例如 T.new_instance ,可用于快速创建不同特点的实例。而在Python中类本身就具备构造函数,因此类工厂方法与构造函数的异同与分工应该是怎样的呢?请通过对其他语言的类比与实际搭建来谈谈你的看法。

    欢迎评论区讨论!

    魔术方法的妙用

    对于学过C++的读者们,应该知道有一类特殊的函数是以 operator 开头的,它们的效果是运算符重载。实际上,在Python中也有类似的特性,比如,让我们通过一个例子来看看加法运算是如何被重载的

    class T:
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
        def __add__(self, other):
            print('Operating self + other ...')
            if isinstance(other, T):
                return T(self.x + other.x, self.y + other.y)
            else:
                return T(self.x + other, self.y + other)
    
        def __radd__(self, other):
            print('Operating other + self ...')
            return T(other + self.x, other + self.y)
    
        def __iadd__(self, other):
            print('Operating self += other ...')
            if isinstance(other, T):
                self.x += other.x
                self.y += other.y
            else:
                self.x += other
                self.y += other
    
            return self
    
    
    t1 = T(2, 3)
    t2 = T(8, -4)
    
    t3 = t1 + t2
    print(t3.x, t3.y)
    
    t4 = t1 + 10
    print(t4.x, t4.y)
    
    t5 = -1 + t2
    print(t5.x, t5.y)
    
    t1 += 20
    print(t1.x, t1.y)
    
    

    输出结果如下

    Operating self + other ...
    10 -1
    Operating self + other ...
    12 13
    Operating other + self ...
    7 -5
    Operating self += other ...
    22 23
    

    对上述例子,可以作一组简单的解释:

    • __add__为常规的加法运算,即当执行 t = a + b 时会进入 __add__ 方法,其中 selfaotherb ,返回值为 t
    • __radd__为被加运算,即当执行 t = b + a 时会进入 __radd__ 方法,其中 selfaotherb ,返回值为 t
    • __iadd__为自加法运算,即当执行 a += b 时会进入 __iadd__ 方法,其中 self 为运算前的 aotherb ,返回值为运算后的 a

    其中,常规的加法运算不难理解,加法自运算也不难理解,但是这个被加运算可能略微难懂。实际上可以结合上述代码中的例子 t5 = -1 + t2 来看, -1作为int类型对象,并不支持对T类型对象的常规加法运算,并且Python中也没有提供类似Ruby那样重载原生类型的机制,此时如果需要能支持-1 + t2这样的加法运算,则需要使用右侧主体的__radd__方法

    在上述例子中提到的三个方法,实际上还有很多的例子,并且这类方法均是以两个下划线作为开头和结尾的,它们有一个共同的名字——魔术方法。魔术方法一个最为直接的应用当然是支持各类算术运算符,我们来看下都支持了哪些算术运算

    魔术方法 结构示意 解释
    add self + other 加法 常规加法运算
    radd other + self 被加运算
    iadd self += other 自加运算
    sub self - other 减法 常规减法运算
    rsub other - self 被减运算
    isub self -= other 自减运算
    mul self * other 乘法 常规乘法运算
    rmul other * self 被乘运算
    imul self *= other 自乘运算
    matmul self @ other 矩阵乘法 常规矩阵乘法运算
    rmatmul other @ self 矩阵被乘运算
    imatmul self @= other 矩阵自乘运算
    truediv self / other 普通除法 常规普通除法运算
    rtruediv other / self 普通被除运算
    itruediv self /= other 普通自除运算
    floordiv self // other 整除 常规整除运算
    rfloordiv other // self 被整除运算
    ifloordiv self //= other 自整除运算
    mod self % other 取余 常规取余运算
    rmod other % self 被取余运算
    imod self %= other 自取余运算
    pow self ** other 乘方 常规乘方运算
    rpow other ** self 被乘方运算
    ipow self **= other 自乘方运算
    and self & other 算术与 常规算术于运算
    rand other & self 被算术于运算
    iand self &= other 自算术于运算
    or self | other 算术或 常规算术或运算
    ror other | self 被算术或运算
    ior self |= other 自算术或运算
    xor self ^ other 算术异或 常规算术异或运算
    rxor other ^ self 被算术异或运算
    ixor self ^= other 自算术异或运算
    lshift self << other 算术左移 常规算术左移运算
    rlshift other << self 被算术左移运算
    ilshift self <<= other 自算术左移运算
    rshift self >> other 算术右移 常规算术右移运算
    rrshift other >> self 被算术右移运算
    irshift self >>= other 自算术右移运算
    pos +self 取正 取正运算
    neg -self 取反 取反运算
    invert ~self 算术取反 算术取反运算
    eq self == other 大小比较 等于比较运算
    ne self != other 不等于比较运算
    lt self < other 小于比较运算
    le self <= other 小于或等于比较运算
    gt self > other 大于比较运算
    ge self >= other 大于或等于比较运算

    可以看到,常见的算术运算可谓一应俱全。不过依然有一些东西是没法通过魔术方法进行重载的,包括但不限于(截止发稿时,Python最新版本为 3.10.0 ):

    • 三目运算,即 xxx if xxx else xxx
    • 逻辑与、逻辑或、逻辑非运算,即 xxx and yyyxxx or yyynot xxx

    除此之外,还有一些比较常见的功能性魔术方法:

    魔术方法 结构示意 解释
    getitem self[other] 索引操作 索引查询
    setitem self[other] = value 索引赋值
    delitem del self[other] 索引删除
    getattr self.other 属性操作 属性获取
    setattr self.other = value 属性赋值
    delattr del self.other 属性删除
    len len(self) 长度 获取长度
    iter for x in self: pass 枚举 枚举对象
    bool if self: pass 真伪 判定真伪
    call self(*args, **kwargs) 运行 运行对象
    hash hash(self) 哈希 获取哈希值

    当然,也有一些功能性的东西是无法被魔术方法所修改的,例如:

    • 对象标识符,即 id(xxx)

    如此看来,魔术方法不可谓不神奇,功能还很齐全,只要搭配合理可以起到非常惊艳的效果。那这种方法的本质是什么呢,其实也很简单——就是一种包含特殊语义的方法。例如在上述加法运算的例子中,还可以这样去运行

    t1 = T(2, 3)
    t2 = T(8, -4)
    
    t3 = t1.__add__(t2)
    print(t3.x, t3.y)
    
    # Operating self + other ...
    # 10 -1
    

    上面的 t1.__add__(t2) 其实就是 t1 + t2 的真正形态,而Python的对象系统中将这些魔术方法进行了包装,使之与特殊的语法和用途绑定,便形成了丰富的对象操作模式。

    延伸思考6:在算术运算中,常规魔术方法、被动运算魔术方法和自运算魔术方法之间是什么样的关系,当存在不止一组可匹配模式时,实际上会执行哪个?请通过实验尝试一下。

    延伸思考7:为什么三目运算、逻辑运算无法被魔术方法重载?可能存在什么样的技术障碍?以及如果开放重载可能带来什么样的问题?

    延伸思考8:为什么对象标识符运算无法被魔术方法重载?对象标识符本质是什么?如果开放重载可能带来什么样的问题?

    延伸思考9:在你用过的Python库中,有哪些用到了魔术方法对运算符和其他功能进行的重载?具体说说其应用范围与方式。

    延伸思考10:考虑一下numpy和torch等库中的各类诸如加减乘除的算术运算,其中有矩阵(张量)与矩阵的运算,有矩阵对数值的运算,也有数值对矩阵的运算,它们是如何在Python的语言环境下做到简单易用的呢?请通过翻阅文档或阅读源代码给出你的分析。

    延伸思考11__matmul__ 运算在哪些类型对象上可以使其支持 @ 运算?在numpy和torch库中,使用 @ 作为运算符对矩阵(张量)进行运算,其运算结果和哪个运算函数是等价的

    欢迎评论区讨论!

    对象属性的本质

    在Python的类中,还有一种与方法类似但又不同的存在——对象属性。比如这样的例子

    class T:
        def __init__(self, x):
            self.__x = x
    
        @property
        def x(self):
            print('Access x ...')
            return self.__x
    
        @x.setter
        def x(self, value):
            print(f'Set x from {self.__x} to {value} ...')
            self.__x = value
    
        @x.deleter
        def x(self):
            print('Delete x\'s value ...')
            self.__x = None
    
    
    t = T(2)
    print(t.x)
    
    t.x = 233
    del t.x
    
    # Access x ...
    # 2
    # Set x from 2 to 233 ...
    # Delete x's value ...
    

    通过访问t.x会进入第一个getter函数,为t.x进行赋值会进入第二个setter函数,而如果尝试删除t.x则会进入第三个deleter函数,对于对象 t 来说,这是显而易见的。不过为了研究一下原理,我们还是看看位于类 T 上的 T.x 的实际内容是什么(代码接上文)

    print(T.x)  # <property object at 0x7faf16853db8>
    

    可以看到 T.x 是一个属性(property)对象,紧接着咱们再来看看这里面所包含的结构

    print(set(dir(T.x)) - set(dir(lambda: None)))
    print(T.x.fget)
    print(T.x.fset)
    print(T.x.fdel)
    
    # {'fget', '__delete__', 'deleter', 'fdel', '__set__', '__isabstractmethod__', 'getter', 'setter', 'fset'}
    # <function T.x at 0x7f39d32f41e0>
    # <function T.x at 0x7f39d32f4268>
    # <function T.x at 0x7f39d32f42f0>
    

    可以看到 T.x 比一般的函数对象要多出来的部分,基本上分为get、set和del相关的部分,而其中的T.x.fgetT.x.fsetT.x.fdel则分别指向三个不同的函数。基于目前的这些信息,尤其是这几个命名来分析,距离正确答案已经很近了。为了进行证实,我们来尝试手动制造一个属性,并将其添加到类上,如下所示

    def xget(self):
        print('Access x ...')
        return self.xvalue
    
    
    def xset(self, value):
        print(f'Set x from {self.xvalue} to {value} ...')
        self.xvalue = value
    
    
    def xdel(self):
        print('Delete x\'s value ...')
        self.xvalue = None
    
    
    class T:
        def __init__(self, x):
            self.xvalue = x
    
        x = property(xget, xset, xdel)
    
    
    t = T(2)
    print(t.x)
    
    t.x = 233
    del t.x
    
    # Access x ...
    # 2
    # Set x from 2 to 233 ...
    # Delete x's value ...
    

    由此可见,上述的例子运行完全正常。因此实际上,property对象是一个支持 __get____set____delete__ 三个魔术方法的特殊对象,关于这三个魔术方法由于涉及到的内容较多,后续可能专门做一期来讲讲。简单来说,可以理解为通过在类上进行这样的一个赋值,使得被实例化的对象的该属性可以被访问、赋值和删除,Python中对象属性的本质也就是这样的。

    延伸思考12:如何利用 property 类来构造一个只能读写不能删除的属性?以及如何构造只读的属性呢?

    延伸思考13property 对象中的 gettersetterdeleter 方法的用途分别是什么?

    欢迎评论区讨论!

    后续预告

    本文重点针对方法的各种机制与特性,从原理角度进行了分析。经过这两篇关于Python类与方法的科普,基本的概念和机制已经基本讲述完毕。在此基础上,treevalue第三弹也将不久后推出,包含以下主要内容:

    • 树化方法与类方法,将基于treevalue第二弹中的函数树化,结合本篇中对方法本质的论述进行讲解。
    • 树化运算,基于算术类型魔术方法的函数树化,会配合例子进行讲解与展示。
    • 基于树化运算的应用,基于功能性魔术方法的函数树化,讲解之余集中展示其高度易用性。

    此外,欢迎欢迎了解OpenDILab的开源项目:

    以及我本人的几个开源项目(部分仍在开发或完善中):

  • 相关阅读:
    常用Linux命令
    KDevolop使用小技巧
    微软在5/10/2006发布新版的LINQ Preview (May 2006).msi 无为而为
    盼望已久的Office Live Beta 已经发布,大家可以去尝尝鲜 无为而为
    需求工程:TFS MSF模版中UI Flow model的应用 无为而为
    Visual Studio 2005 Team Foundation Server 试用版及中文说明文件下载,中文版可能在2006年5月15日发布 无为而为
    让我们努力从“不可救药的乐观主义者”华尔街知名投资人约翰。多尔那里学点东西(永远放弃尝试改变这个世界) 无为而为
    Visual Studio 2005 开发Office(Word/Excel)项目的若干资源和示例 无为而为
    IT人看《国富论》系列:第一篇之第十章:论工资与利润随劳动与资本用途的不同而不同,分析分析IT界薪水起伏的原因 无为而为
    非正常状态,彻底删除Exchange服务器 无为而为
  • 原文地址:https://www.cnblogs.com/HansBug/p/15584738.html
Copyright © 2011-2022 走看看