• python类内置隐式方法全解


    python类中,有很多已经定义好、具有特殊功能的隐式方法(魔法函数),如常用的__init____call__等,这些方法可以帮助我们实现一些特殊的功能。

    python类中的隐式方法名都以__(双下划线)开头,__(双下划线)结尾,并且都是内置定义好的,注意和自定义的私有方法区分。

    1. init

    __init__是python中的构造方法,可在__init__中定义类的成员变量,初始化行为等。

    class People:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
    • 1
    • 2
    • 3
    • 4

    2. del

    __del__是python中的析构方法,可在__del__中定义对象被回收的行为动作。例如,为了防止开发人员忘记释放数据库连接,或者因为异常释放连接的行为没有被执行,则可以在__del__中定义关闭连接的操作。

    class DBConnect:
        def __init__(self):
            self.connect = ...
    
        def __del__(self):
            # 如果数据库连接没有被关闭,则自动关闭
            if self.connect.ping():
                self.connect.close()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3. str/repr

    __str__是python中的字符串序列化函数,调用str函数时的输出内容,类似于java中的toString。__repr____str__功能很像,但是__repr__更多是面向开发人员使用,当__repr____str__都没有定义时,print§是对象p的内存地址,如果同时定义__repr____str__,不管是print§还是print(str§),结果都是__str__返回的结果(实际上print会隐式调用str函数),如果只定义了__repr__,则才会打印__repr__的结果。但是当进入debug模式时,即使同时定义了__repr____str__,在终端中直接p回车,会发现结果就是__repr__的结果。大多数情况下,都是使用__str__

    class People:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __str__(self):
            return 'name: ' + self.name + ', age: ' + str(self.age)
    
    
    p = People('Tom', 18)
    print(p)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4. len/abs/int/float/hash

    • __len__是len函数的内置支持函数;
    • __abs__是abs函数的内置支持函数;
    • __int__是int函数的内置支持函数;
    • __float__是float函数的内置支持函数;
    • __hash__是hash函数的内置支持函数;
    class People:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __len__(self):
            return len(self.name)
    
    
    p = People('Tom', 18)
    print(len(p))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5. iter/next

    一个类实例如果想通过 for…in… 遍历,则实例本身必须是一个迭代器(iterator)。python中类成为迭代器需要实现两个方法:
    __iter__:返回一个迭代器对象,这个对象必须包含__next__方法
    __next__:该方法用于返回下一个迭代元素,且当迭代结束时要抛出StopIteration异常。

    其实,当实现了__next__方法时,类实例就已经可以通过next方法迭代了:

    class A:
        def __next__(self):
            return 3
    
    a = A()
    print(next(a))
    print(next(a))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面程序的输出结果都是3,且可以一直调用next,因为我们实现的__next__方法返回值就是3,且没有定义StopIteration异常逻辑(终止条件),所以可以一直调用。

    但是我们发现只有__next__方法只能通过next函数调用,使用for…in…语法会出现:TypeError: ‘A’ object is not iterable。什么是iterable(可迭代对象)呢?这就涉及到__iter__方法,如果一个类实现了__iter__方法,那么这个类就是可迭代对象。例如下面的程序:

    class A:
        def __iter__(self):
            return self
    
    a = A()
    for i in a:
        print(i)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    你会发现在ide中上述程序是没有任何警告的,但是一执行,就会出现:TypeError: iter() returneed non-iterator of type ‘A’,意思就是__iter__返回的不是iterator类型。前面我们已经说过python中类成为iterator需要实现两个方法__iter____next__。我们把上述程序整合一下:

    class A:
        def __init__(self, n):
            self.i = 0
            self.n = n
            
        def __iter__(self):
            return self
            
        def __next__(self):
            if self.a < self.n:
                self.a += 1
                return self.a
            else:
                raise StopIteration()
    
    
    a = A(10)
    for k in a:
        print(k)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以发现,程序正常执行了。现在我们再来总结一下for i in a循环遍历的原理和两个概念:

    • 首先调用__iter__方法返回一个iterator(iterator必须有__next__方法)
    • 对这个iterator循环调用__next__方法(相当于手动通过next函数调用)
    • 一直到触发__next__方法的StopIteration逻辑,循环结束

    iterable和iterator的不同:

    • iterable:实现了__iter__方法
    • iterator:同时实现了__iter____next__方法

    至此,我们理解了for i in a的原理,但是我们发现如果再次执行for i in a循环遍历,什么都没有输出。因为此时的迭代器游标已经指向结尾了,所以无法再次遍历(参考多次执行next函数后的异常现象)。

    6. getitem/setitem/delitem

    __getitem__也可使类实例成为可迭代对象,并通过for循环遍历,且支持多次遍历。

    class People:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __getitem__(self, item):
            return self.name[item]
    
    
    p = People('Tom', 18)
    for i in p:
        print(i)
    print(p[1])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    for…in…在遍历p时,实际是调用了__getitem__方法,item隐式传参0、1、2、…。

    相对于for…in…遍历,__getitem__方法更多是用于通过显式key索引查询:

    class A:
        def __init__(self):
            self.data = {'a': 1, 'b': 2, 'c': 3}
    
        def __getitem__(self, item):
            return self.data[item]
    
        def __setitem__(self, key, value):
            self.data[key] = value
    
        def __delitem__(self, key):
            del self.data[key]
    
    
    a = A()
    print(a['b'])
    a['f'] = 9
    print(a['f'])
    del a['c']
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ※注意:如果同时定义了iter/next和getitem,则for…in…会执行iter/next的逻辑,而通过key显式索引会执行getitem对应逻辑。

    根据名称可见__setitem__是和__getitem__逻辑相反的方法,即可通过[]对类实例直接赋值,__delitem__同样表示del操作,案例见6. getitem。

    ※附:使用__getitem__方法和__dict__内置属性可以实现通过[]索引访问类成员变量。

    7. contains

    实现__contains__隐式方法,可支持in语句。

    class A:
        def __init__(self):
            self.data = [1, 2, 3]
    
        def __contains__(self, item):
            if item in self.data:
                return True
            else:
                return False
    
    
    class B:
        def __init__(self):
            self.lower = 1
            self.upper = 10
    
        def __contains__(self, item):
            if self.lower < item < self.upper:
                return True
            else:
                return False
    
    
    a = A()
    b = B()
    print(7 in a)
    print(7 in b)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    8. call

    __call__方法相当于重载了()运算符,可通过 实例名() 的形式直接执行__call__方法,即把实例变成了可调用对象。可调用对象的思想在众多python编程框架中应用非常广泛,如fastapi等。

    ※附录:可调用对象,如函数,都有__call__属性,可通过hasattr(函数名/实例名, ‘__call__’)或者callable(函数名/实例名)判断。注意是实例名不是类名,因为类可定义实例,一定是可调用的。

    class A:
        def __init__(self):
            self.name = 'haha'
    
        def __call__(self, *args, **kwargs):
            return self.name
    
    
    a = A()
    print(a())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    通过上例可以发现,实现__call__方法后,可直接使用a()调用__call__方法。一般在开发中不建议直接使用__call__方法定义业务逻辑,因为根据见名知意、逻辑清晰的原则,我们会尽量把对应的逻辑定义在一个合适的方法中。但是在需要传递一个可调用对象参数的场景中,__call__方法就很好用了,这样这个参数就可以同时支持函数、类实例等不同类型参数。

    9. new

    __new__是python类实例初始化过程中非常重要的隐式方法,而且在初始化类实例时,__new__是在__init__之前被调用的。

    在面向对象编程语言中,初始化类实例主要包含两步:(1)分配内存空间,在内存中创建对象;(2)初始化实例,如给成员变量赋初值等。在python中,(1)由__new__完成,(2)由__init__完成。

    class People:
        def __init__(self, name, age):
            self.name = name
            self.age = age
    
        def __new__(cls, *args, **kwargs):
            return super().__new__(cls)
    
    
    p = Person('haha', 16)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    __new__必须返回一个自身类实例(返回分配的内存空间地址引用),这里的cls说明__new__是一个类方法,cls表示People,return的实例会作为参数传给__init__,即__init__的self参数。如果__new__不返回类本身实例,或者返回的是其他类实例(cls换成其他类,如B),__init__方法都不会被调用。另外,super().__new__(cls)的参数只有cls,*args和**kwargs会被自动传递给__init__。类成员变量及业务逻辑初始化多放在__init__中,所以大部分情况下我们都不会用到__new__方法,那么什么情况下会用到呢?

    针对__new__返回内存引用的特性,可用来开发单例模式:

    class A:
        instance = None
        # value_flag = False
        # 
        # def __init(self, name):
        #     if not self.value_flag:
        #         self.name = name
        #         self.value_flag = True
    
        def __init(self, name):
            self.name = name
        
        def __new__(cls, *args, **kwargs):
            if cls.instance is None:
                cls.instance = super().__new__(cls)
                return cls.instance
            else:
                return cls.instance
    
    a = A('aaa')
    b = A('bbb')
    print(id(a), id(b))
    print(a.name, b.name)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    上述程序即实现了一个单例模式程序,但是需要注意如果类是含参构造,则新的实例参数会自动更新旧的实例属性(b.name和a.name都会变成’bbb’),如果不想新的引用实例修改旧值,则可以定义一个静态属性标识,具体实现参考注释部分程序。

    除了用于单例模式以外,还可以利用__new__对一些内置类型进行封装,从而实现一些特殊逻辑,如定义一个非负整数类型:

    class NonNeg(int):
        def __init__(self, value):
            super().__init__()
    
        def __new__(cls, value):
            return super().__new__(cls, value) if value >= 0 else None
    
    
    a = NonNeg(10)
    b = NonNeg(7)
    print(a-b)  # 3
    c = NonNeg(-3)  # None
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上述程序的缺点是无法控制运算结果,如 b-a 仍然是一个负数。当然也可以做一次运算类型封装解决这个问题:NonNeg(b-a),但是如果计算过程比较复杂,运算表达式就会显得臃肿。

    此外,在某些场景下,利用__new__方法还可以实现拷贝构造:b = A(a)。

    10. enter/exit

    __exit____enter__方法是用于上下文语义管理的,实现这两个方法,可用于with语句。

    在管理文件句柄(读写文件)或者管理数据库事务(写数据库)时,经常会用到with语句,因为不需要手动去关闭文件句柄,而且即使异常也可保证安全性。那么这个机制是怎么实现的呢?

    python类提供了__exit____enter__隐式方法用于支持上下文语义管理,with语句在执行时会首先执行__enter__方法内容,在运行结束后会执行__exit__方法内容。

    class File:
        def __init__(self, path, mode='r'):
            self.f = open(path, mode)
    
        def read(self, n):
            return self.f.read(n)
    
        def write(self, s):
            self.f.write(s)
    
        def __enter__(self):
            print('--enter--')
            return self
    
        def __exit__(self, exec_type, exc_val, exc_tb):
            print('--exit--')
            self.f.close()
    
    
    with File('./test.txt', 'a') as f:
        print('--test--')
        f.write('hello')
        
    # 运行结果
    --enter--
    --test--
    --exit--
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    __enter__的返回结果就是with as 的结果,通常会返回类实例自身,__exit__会在with语句内容全部执行结束后自动运行,__exit__的几个参数表示异常信息,如果with语句内容出现异常,则exec_type表示异常类型,exc_val表示具体的异常信息,exc_tb表示异常跟踪信息(地址信息),没有异常的情况下,这三个参数都是None。如果__exit__的返回值是True,即使with语句内容出现异常,程序也不会异常终止,但是with语句中异常后面的程序不会再执行,with代码块后面的程序依然会执行(类似try/except),而且__exit__中的参数依然可以拿到异常信息。

    ※附录:enter/exit机制只适用于类的上下文语义管理,如果是函数可使用contextlib库,并且也可以使用contextlib对类进行上下文语义封装。

    11. setattr/getattr/getattribute

    在介绍setattr/getattr/getattribute之前先说明一下python类实例是怎么保存类成员变量的。在python类实例中,有一个字典类型的__dict__属性值用于保存所有的成员变量和成员方法,key是成员变量/方法名,value就是对应的值。我们设置的成员变量实际上都保存在了__dict__中,当通过 类名.属性 名访问数据的时候实际上也是从__dict__中读取数据。那这个过程是怎么实现的呢?

    python类内置了__setattr____getattr__两个隐式方法来实现上述过程。

    class A:
        def __init__(self, name, age):
            print('--name--')
            self.name = name
            print('--age--')
            self.age = age
    
        def __setattr__(self, key, value):
            print('---', key)
            self.__dict__[key] = value
            # super().__setattr__(key, value)
    
        def __getattr__(self, item):
            print('not found')
            return f'没有属性值{item}'
    
        def __getattribute__(self, item):
            print('!!!')
            return super().__getattribute__(item)
            # 下面的写法是错误的
            # return self.__dict__[item]
    
    d = D('haha', 18)
    print('==============')
    print(d.name)
    print('==============')
    print(d.address)
    print('==============')
    
    # 输出结果
    --name--
    --- name
    !!!
    --age--
    --- age
    !!!
    ==============
    !!!
    haha
    ==============
    !!!
    not found
    没有属性值address
    ==============
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    在类中对属性进行赋值操作时,python会自动调用__setattr__方法来实现对属性的赋值。如果需要重写__setattr__方法则有两种更新属性值方法:(1)直接更新实例自身的__dict__属性;(2)调用父类的__setattr__方法。从上述程序结果也可以看出,在__init__方法中每初始化一个成员变量都会调用一次__setattr__方法。如果没有在__setattr__方法中把属性值保存到__dict__中,__init__的初始化也是无效的。

    __getattribute__是获取属性值的方法(属性访问拦截器),当访问某个属性(成员变量/成员方法)时,会自动调用__getattribute__方法。这里有一个极易遇到的坑,为什么上面程序 __getattribute__方法中return self.__dict__[item]的写法不对呢?因为程序在执行到self.__dict__时,发现是在访问__dict__这个属性,所以又去调用__getattribute__方法,导致无限递归循环,所以在__getattribute__方法中注意尽量不要直接访问类属性。

    由上面程序的执行结果可知,__getattr__方法是在访问不存在的属性时才会被触发的,python类会先执行__getattribute__方法,如果找不到这个属性,则会调用__getattr__方法。

    大部分情况下,我们都不会去重写这三个隐式方法,尤其是重写__getattribute__方法有很大的风险。但是合理利用这几个方法可以实现一些特殊的功能。例如:

    (1)在__getattribute__方法中通过判断item的值可以实现真正的属性私有化(禁止外部通过 类名.属性名 直接访问成员变量)。
    (2)通过重写__setattr__方法把某些属性变成const常量,只要key已经在__dict__中存在,就不允许更新。这也是常见的python const常量解决方案。

    ※注意:尽量不要重写这三个方法。

    12. dir

    __dir__方法可支持dir函数,通过dir函数可查看某个对象的所有属性名和方法名(包括从父类继承)。所以除了使用dir函数外,我们也可以直接调用实例的__dir__方法。

    13. 数学运算

    • 比较运算:__lt__ (<)、__le__ (<=)、__eq__ (==)、__ne__ (!=)、__gt__ (>)、__ge__ (>=) 。
    • 单目运算:__neg__ (-,负数操作,单目运算符)、__pos__ (+,单目运算符) 、__invert__(~,取反,单目运算符)。
    • 算术运算:__add__ (+,加法,双目运算符)、__sub__ (-,减法,双目运算符)、__mul__ (*)、__truediv__ (/)、__floordiv__ (//)、__mod__ (%)、__divmod__ 或divmod()、__pow__ 或pow()、__round__ 或round()。
    • 反向运算:__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod____rpow__
    • 增量赋值运算:__iadd____isub____imul____ifloordiv____ipow__
    • 位运算:__lshift__ (<<)、__rshift__ (>>)
    • 逻辑运算:__and__ (&)、__or__ (|)、__xor__ (^)
  • 相关阅读:
    Docker容器的应用部署
    使用基于swagger的knife4j自动生成接口文档
    100天精通Python(数据分析篇)——第56天:Pandas读写txt和csv文件(read_csv、to_csv)
    从零开始Blazor Server(9)--修改Layout
    Linux基础开发工具之yum与vim
    Java/Kotlin 使用Redis模拟发送验证码
    【imessage苹果群发苹果推】软件安装Apple证书,服务或其他方式建立任何涵盖的产品或其他代码或程序
    【可观测性系列】 OpenTelemetry Collector的部署模式分析
    mybatisplus代码生成覆盖
    Origin绘制彩色光谱图
  • 原文地址:https://blog.csdn.net/haveanybody/article/details/133696582