python类内置隐式方法全解

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

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

1. init

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

class People:
    def __init__(self, name, age):
        self.name = name
        self.age = age

2. del

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

class DBConnect:
    def __init__(self):
        self.connect = ...

    def __del__(self):
        # 如果数据库连接没有被关闭,则自动关闭
        if self.connect.ping():
            self.connect.close()

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)

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))

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))

上面程序的输出结果都是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)

你会发现在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)

可以发现,程序正常执行了。现在我们再来总结一下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])

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']

※注意:如果同时定义了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)

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())

通过上例可以发现,实现__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)

__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)

上述程序即实现了一个单例模式程序,但是需要注意如果类是含参构造,则新的实例参数会自动更新旧的实例属性(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

上述程序的缺点是无法控制运算结果,如 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--

__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
==============

在类中对属性进行赋值操作时,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__ (^)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值