5、面向对象进阶一
魔术方法
在Python的面向对象中有一些方法前后都有两个下划线,这类方法统称为魔术方法。这些方法有特殊的用途,有的不需要我们自己定义,有的则通过一些简单的定义可以实现比较神奇的功能,魔术方法支撑了整个Python的面向对象。
同时,魔法方法是Python内置方法,不需要主动调用,存在的目的是为了给Python的解释器进行调用,几乎每个魔法方法都有一个对应的内置函数,或者运算符,当我们对这个对象使用这些函数或者运算符 时就会调用类中的对应魔法方法,可以理解为重写这些python的内置函数。
创建实例
Python面向对象的实例化有两个步骤,第一步就是构造实例,__new__方法就是构造出一个新实例的方法,但是它只是构造出一个空实例,所以它还需要将这个空实例交给__init__方法去做出场配置。
当我们重写了new方法的时候,需要注意一点,就是要去手动的调用一些父类的__new__方法,这样才能完成整个实例化的过程,因为父类的__new__方法不仅做了一些实例的创建还给实例做了出厂配置等一些系列工作。
__new__:创建实例的时候会触发的一个方法,也称之为构造方法或实例化方法,__init__也是构造方法,__new__构造实例,__init__构造处长配置;
__init__:初始化或者实例化,实例化之前应该先把实例给构造出来,这个就要用到__new__方法,实例构造出来之后,还要对实例做出厂配置,这个时候就是__init__方法干的活了;
__del__:实例销毁,它主要是在引用计数为0时,解释器会自动调用__del__方法对实例进行垃圾清理工作,比如做一些资源的回收工作,所以它是和__init__配套的,__init__打开资源,__new__关闭资源;
需要注意的是,一般__new__方法不是用来注入类属性的,因为每次实例化都会调用__new__方法,这样带来的开销太大,一般是对实例进行在进行出场配置之前做一些操作的。
# 测试
class cce:
def __new__(cls, *args, **kwargs):
return 1
def __init__(self,name):
self.name=name
print(cce()) # 1
# 可以看到,当我们重写了__new__方法时,如果返回值是个自定义值,可以发现实例化时,得到的结果并非一个对象,这就说明一个问题,说明__new__方法的事情没有做完,说明我们重写的__new__方法不完整,那么此时,我们就需要调用一些父类的__new__方法,如下;
class cce:
def __new__(cls, *args, **kwargs):
return super().__new__(cls)
def __init__(self,name):
self.name=name
print(cce()) # missing 1 required positional argument: 'name
# 可以看到此处,直接抛出异常,说缺少一个name参数,说明直至此时,我们的实例化才算真正的完成了,这说明,__new__方法,不仅给我们创建出了一个空实例,还给我们调用了__init__方法,完成出厂配置;
同时,__new__方法实际上是一个staticmethod方法,而非classmethod,因为如果是classmethod,在下面调用super的时候,我们就无需手动加入cls了,它会直接将super也就是object传入,但是在此阶段,我们不需要将object传入到__new__方法,而是cls传入到__new__方法,所以从此处看来,staticmethod更合适,PyCharm给我们加上去了,那实际上是PyCharm的语法,同时__new__方法还接受args和kwargs两个参数,但是在调用父类的__new__方法时,并没有传这两个参数,这一步将移步到元编程;
实例构造流程
实例化的流程主要分为两步,第一步使用__new__方法构造实例,实例构造出来之后,将实例交给__init__方法进行初始化配置,然后才会得到一个实例;
可视化
可视化,在面向对象中,主要是用来在返回数据的时候,能够以一种较为友好的形式去返回数据,让程序员能够一看即懂,在目前Python可视化对象主要的有三种,分别为__str__、__repr__、__bytes__;
__str__:对应repr(object)这个函数,返回一个可以用来表示对象的可打印字符串,利用str、print和format情况时,将间接调用对象的__str__方法;
__repr__:对应str(object)这个函数,返回一个字符串对象,适合用于print输出;
__bytes__:对应bytes(object)这个函数,bytes方法会调用这个object对象的__bytes__方法,所以__bytes__的返回值应该是一个bytes对象;
示例
可以看到如下示例,这些方法主要是用于给人能友好的去查看这些对象属性的,当没有定义这些魔术方法时,默认会通过继承的原则,向上寻找,也就是找到object中定义的这个方法;
class cce:
def __init__(self, name):
self.name = name
def __str__(self):
return "str: {}".format(self.name)
def __repr__(self):
return "repr: {}".format(self.name)
def __bytes__(self):
return "bytes: {}".format(self.name).encode()
c1 = cce('cce')
print(c1) # str: cce
print(str(c1)) # str: cce
print(repr(c1)) # repr: cce
print(bytes(c1)) # b'bytes: cce'
哈希
哈希是一种算法,它将任意大小的数据映射成固定的长度的值,这个值就叫哈希值。哈希提高了执行效率,可以直接访问那些存储大量数据的数据结构,而且访问速度很快。哈希值可以通过哈希函数计算出来,这个hash函数,我们可以在编写类的时候自定义,如果不自定义将采用基类Object里面的__hash__方法,如果一个对象在其生命周期内(没有回收对象情况下)存在一个从来不会改变的哈希值,那么这个对象是可哈希的。一个可哈希的对象肯定有一个 __hash__()方法,。
class obj:
def __init__(self,name):
self.name=name
def __hash__(self):
return 1
o1=obj('cce')
o2=obj('cce')
print(hash(o1),hash(o2))
# 1 1
可以看到,当我们重写了hash方法时,调用hash函数,就会得到一个相同的值,但是此时会遇到一个问题,上面也说过,集合是通过hash来确定数据是否重复的,那么此时,我们如果将上述两个hash值相同的对象加入到一个集合中,会发现,没有进行去重;
class obj:
def __init__(self,name):
self.name=name
def __hash__(self):
return 1
o1=obj('cce')
o2=obj('cce')
print(hash(o1),hash(o2)) # 1 1
print({o1,o2}) # {<__main__.obj object at 0x102e83dd8>, <__main__.obj object at 0x102e83748>}
# 集合里面依旧是两个对象,虽然hash值一样,但是没有进行去重
可以看到上述案例,虽然hash值一样,但是还是没有去重,这是因为__hash__方法,仅仅是能够确保一个对象是可hash的,不能确保这个对象在集合里面是否是唯一的,如果为了便于比较对象哈希值是否相同,可哈希的对象还需要有一个__eq__() 方法。一个相同的哈希对象,他们一定有相同的哈希值,所以像Python中的集合和字典的KEY都是需要结合__hash__和__eq__方法来决定是否需要去重的。
__hash__方法仅仅能够保证是一个可hash对象而已,但是它不能判定是否相同,因为,对于hash算法来说,每次针对一个值进行hash的结果是不一样的(除非在同一个线程中),所以,__hash__仅能确定数据是否支持hash,如果要确定是否相同还需要借助__eq__方法来判断两个元素是否相同,那么对于字典就是这么做的,通过__hash__来判断数据是否可hash,然后通过__eq__来判断数据是否相同,从而达到去重的效果;
从上述的案例中也可以看到,虽然内容也一样,但是还是没有去重,这是因为,自定义类中并没有给定如何判断对象是否重复的方法,因此就采用了基类object的__eq__方法来进行比较,object里面的__eq__方法,它主要是比较的内存地址,如果内存地址不一样,就是不一样的对象,因为object不知道它的子孙类里面,如何比较内容,所以上述案例中就没有进行去重。
那么如果我们需要对上述案例中的两个相似的对象放在集合里面,能达到去重的效果,我们可以重写__eq__方法,如下案例,自定义一个__eq__方法,判断指定的属性是否相同,如果相同,说明两个对象是重复的。
class obj:
def __init__(self,name):
self.name=name
def __hash__(self):
return 1
def __eq__(self, other):
return self.name == other.name # 判断两个对象的name属性是否相同
o1=obj('cce')
o2=obj('cce')
print(hash(o1),hash(o2)) # 1 1
print({o1,o2}) # {<__main__.obj object at 0x102b7af98>}
# 当我们重写了__eq__方法,得到的结果就进行了去重的效果,集合里面只有一条数据
像字典的KEY和集合的元素,他们都需要hash值和内容不同两个条件同时具备才不会去重,如果hash值相同,内容不同,不会去重,如果是内容相同,hash值不相同也不会去重;
import random
class obj:
def __init__(self,name):
self.name=name
def __hash__(self):
return random.randint(1,10000)
def __eq__(self, other):
return self.name == other.name
o1=obj('cce')
o2=obj('cce')
print(hash(o1),hash(o2)) # 1 1
print({o1,o2}) # {<__main__.obj object at 0x105805ef0>, <__main__.obj object at 0x102683dd8>}
在Python里,list是不可hash的,究其原因是因为list类并未实现__hash__方法,通过源码可以看到,其__hash__为None,在今后的编码过程中,如果想要阻止hash的话,也可以沿用这么一个方法,将__hash__方法设定为一个None就行了。
另外,如果一个类,只实现了__eq__方法,没有实现__hash__方法,默认情况下这个类是不可__hash__的。
class cce:
def __init__(self, name):
self.name = name
def __eq__(self, other):
return self.name == other.name
c1 = cce("cfj")
c2 = cce("cfj")
print(c1 == c2) # True
print(hash(c1),hash(c2)) # TypeError: unhashable type: 'cce'
布尔
布尔,即bool,在Python中,所有等效True/False的时候,都需要用到它,即bool(object), 其实它背后是调用的object.__bool__方法的结果,当该对象不存在__bool__方法时,Python还会调用__len__方法。如果如果长度大于0返回True,等于0返回False。
默认情况下我们自定义的类的实例会被认为是真的,因为虽然自身没有实现__bool__或者__len__方法,但是基类object是全部都实现了的,除非自定义的类的__bool__和__len__方法有进行重写;
class A:
def __bool__(self):
return True
print(bool(A())) # True
class B:
def __len__(self):
return 0
print(bool(B())) # False
运算符重载
运算符重载,但凡我们在前面看到的所有运算符,在运算符重载里面全部都有,也就是说,每一种运算符都可以重载,重载即覆盖,就是在子类中重新实现这个方法,运算符重载的作用是让用户定义的对象使用运算符(如 + 和 |)或一元运算符(如 - 和 ~),在Python中有很多运算符重载的方法,大概如下,最全的实现可以看int类。
运算符
|
魔术方法
|
描述
|
<
|
__lt__
|
小于
|
<=
|
__le__
|
小于等于
|
==
|
__eq__
|
等于
|
>
|
__gt__
|
大于
|
>=
|
__ge__
|
大于等于
|
!=
|
__ne__
|
不等于
|
+
|
__add__
|
加
|
–
|
__sub__
|
减
|
*
|
__mul__
|
乘
|
/
|
__truediv__
|
除
|
%
|
__mod__
|
取模
|
//
|
__floordiv__
|
取整除法
|
divmod
|
__divmod__
|
求商和余
|
+=
|
__iadd__
|
加等
|
-=
|
__isub__
|
减等
|
*=
|
__imul__
|
乘等
|
/=
|
__itruediv__
|
除等
|
%=
|
__imod__
|
模等
|
//=
|
__ifloordiv__
|
整除等
|
**=
|
__imul__
|
次方等
|
加减法
如下,通过运算符重载的方式,实现两个实例相减,其实我们在日常的各种运算符操作,其实都是调用的内部运算符重载实现的。
class A:
def __init__(self,age):
self.age=age
def __sub__(self, other):
return self.age - other.age
def __isub__(self, other):
return self.__class__(self - other) # 此处减法会直接引用__sub__方法,因为上述__sub__有一个问题,就是一个对象减另一个对象得到的却是一个数值,而不是一个对象。
a1=A(20)
a2=A(16)
print(a1-a2) # 4 等价于 a1.__sub__(a2)
a1-=a2
print(a1,a1.age) # <__main__.A object at 0x102f05f28> 4
# 或者
class A:
def __init__(self,age):
self.age=age
def __sub__(self, other):
return self.age - other.age
def __isub__(self, other):
self.age-=other.age
return self
a1=A(20)
a2=A(16)
print(a1-a2) # 4 等价于 a1.__sub__(a2)
a1-=a2
print(a1.age) # <__main__.A object at 0x102e7af98>
应用场景
运算符重载,往往在使用面向对象实现的类需要进行大量的数学运算时,这些运算符是这种数学运算最常见的表达方式,为了符合运算本身的习惯,就需要用到加法,这个加法就需要使用到运算符重载的方法,而int类型,几乎实现了所有的算数运算符,可以作为参考;
容器
容器,就是一个容纳元素的空间,比如在Python中最常见的,列表、元组、集合、字典这都是容器,在Python中,也提供了很多容器实现的方法,可以供我们自定义一个容器出来,所以Python的魔术方法很好用,不像其他语言里面,想要构造一个容器出来难度之大,而在Python里面,内建函数配合所谓的魔术方法就可以完成各种各样的功能,这是Python编程的一种设计哲学,Python的容器方法如下;
__len__:内建函数len(),它返回对象的长度,即>=0的整数,如果把对象当作容器类型看,就如同list或者dict;
__iter__:迭代容器时调用,它返回一个新的迭代器对象;
__contains__:它是在程序员使用in成员运算符时所调用的,如果没有实现就会调用__iter__方法遍历;
__getitem__:它主要是实现self[key]这种访问方式的,key接受整数为索引,或切片,对于set和dict来讲,key为hashable,当key不存在时,会引发KeyError异常;
__setitem__:和value__getitem__类似,只不过__getitem__是访问数据,而__setitem__是设置只,如self[key]=value;
__missing__:__missing__只和字典相关,字典和字典的子类,当使用__getitem__找不到key,会执行该方法,在某些情况下,我们需要构造一个字典,如果这个字典找不到值,就可以使用__missing__。
购物车容器
将购物车,利用上面的方法,改造成方便操作的容器类;
class shop:
def __init__(self):
self.Cart=[]
def add(self,element):
return self.Cart.append(element)
def show(self):
return self.Cart
def __len__(self):
return len(self.Cart)
def __getitem__(self, index):
return self.Cart[index]
def __setitem__(self, index, value):
self.Cart[index]=value
def __iter__(self):
return iter(self.Cart) # 或者 for x in self.Cart: yield x 或者 yield from self.Cart
s1=shop()
s1.add(1)
s1.add(2)
print(s1.show()) # [1, 2]
print(len(s1)) # 2
print(s1[0]) # 1
s1[1]=1
print(s1.show()) # [1, 1]
print(100 in s1) # False
可调用对象
可调用对象指的是任意一个类的实例,也就是说一个类的实例,可以当作可调用对象来做,类似一个函数,可以使用在函数标识符加上一个括号,就是调用,或者调用其__call__方法,也是调用,在正常情况下,一个类的实例说不可调用的,但是只要我们在这个实例的类里面,加上一个__call__方法,它的实例就能像函数一样,变成一个可调用对象。
class A:
def __call__(self, *args, **kwargs):
return 1000
a=A()
print(a()) # 1000 等价于 a.__call__()
上下文管理
初学者可能对with语句比较熟悉,但是对于上下文管理器这样的概念不太清楚,但是作为一个程序员或者准程序员,那么你一定听说过内存泄露,内存泄露的根本原因在于创建了某个对象,却没有及时的释放掉,直到程序结束前,这个未被释放的对象一直占着内存。那这样有什么问题吗?其实量少的话还好,如果量大那么就会直接把内存占满,导致程序被kill掉,这就是内存泄露。那内存泄露和上下文管理器有什么关系呢。
首先,现在我们使用的很多高级编程语言已经不需要让我们过多的去关注内存的问题了,但是在某些情况下还是需要我们编写程序来关闭或释放某些对象。而最常见的就是文件操作。
在任何一门编程语言中,文件的输入输出、数据库的连接断开等,都是很常见的资源管理操作。但资源都是有限的,在写程序时,我们必须保证这些资源在使用过后得到释放,不然就容易造成资源泄露,轻者使得系统处理缓慢,重则会使系统崩溃。
比如,我们open打开了一千个文件,但是没有及时的close掉,这就是一个很典型的内存泄漏的问题,因为程序中如果文件打开了太多,占据了太多的资源,就极有可能造成系统崩溃,为了解决这个问题,不同的编程语言就提供了不同的解决方案。
而在Python中,这个解决方案就是上下文管理器,对于上下文管理这个东西,其实在之前就使用过,即with...as语法,可以使用上下文管理对文件对象进行IO操作,上下文管理对于文件IO来说,如果在使用文件对象的过程中,出现了任何异常,上下文管理的这种方式都可以保证在程序退出的时候,会关闭文件对象。
open模块支持上下文管理的主要原因是,open类加入了两个上下文魔术方法,即__enter__和__exit__,任何一个类想要实现上下文管理,都可以加入这两个方法,并且这两个方法两者必须共存,__enter__表示进入,__exit__表示退出。
class context:
def __init__(self):
print("__init__")
def __enter__(self): # 进入
print("__enter__")
def __exit__(self, exc_type, exc_val, exc_tb): # 退出
print("__exit__")
with context() as c1: # 等价于 c1 = context()
print("with")
# __init__
# __enter__
# with
# __exit__
# 可以看到上述的执行结果,使用with语法,第一步进入的是初始化方法__init__,然后接着就是__enter__,然后才是进入了with代码块,最后退出时,才会执行__exit__;
有了上下文管理,我们就可以利用__enter__来管理进入做什么操作,有了__exit__之后我们就可以管理当类退出做什么操作了,而对于上下文管理的安全性问题,不管with语句里面抛出什么异常,上下文管理都能保证这个with语法在进入时会执行__enter__和在退出时会执行__exit__。
需要知道的是,对于as后面的标识符,其实接收的是__enter__的返回值,所以在一个支持上下文管理的类中,其__enter__方法的返回值,所以,一般情况下应该返回self,否则就会抛出异常。
class context:
def __init__(self,name):
self.name = name
def show(self):
return self.name
def __enter__(self): # 进入
pass
def __exit__(self, exc_type, exc_val, exc_tb): # 退出
pass
with context('cce') as c1:
print(c1.show()) # 'NoneType' object has no attribute 'show'
class context:
def __init__(self,name):
self.name = name
def show(self):
return self.name
def __enter__(self): # 进入
return self
def __exit__(self, exc_type, exc_val, exc_tb): # 退出
pass
with context('cce') as c1:
print(c1.show()) # cce
可以发现,__exit__方法,默认会附带很多的参数,这几个参数代表的是异常信息,一个是异常类型,一个异常值,一个是异常traceback对象,但是需要注意的是,在__exit__里面写try是没有什么用的,依旧会抛出异常,但是,我们可以拿到这个异常值做一些特殊处理,因为__exit__语法,不论是否抛出异常,都会执行,比如如果出现异常,我们就通知一个程序,此次调用发生异常,调用失败,同时,如果在__exit__里面加入return语法,就可以实现压制异常,如果__exit__方法的return值为True则压制异常,如果为False则抛出异常。
# 当__exit__方法返回值为False,则继续抛出异常
class context:
def __init__(self):
pass
def __enter__(self): # 进入
return self
def __exit__(self, exc_type, exc_val, exc_tb): # 退出
print("exc_type",exc_type)
print("exc_val",exc_val)
print("exc_tb",exc_tb)
return False
with context():
raise TypeError("cce")
# Traceback (most recent call last):
# File "/usr/local/Project/cce/Py/edu.py", line 18, in <module>
# raise TypeError("cce")
# TypeError: cce
# exc_type <class 'TypeError'>
# exc_val cce
# exc_tb <traceback object at 0x105019848>
# 当__exit__方法返回值为True,则压制异常
class context:
def __init__(self):
pass
def __enter__(self): # 进入
return self
def __exit__(self, exc_type, exc_val, exc_tb): # 退出
print("exc_type",exc_type)
print("exc_val",exc_val)
print("exc_tb",exc_tb)
return True
with context():
raise TypeError("cce")
# exc_type <class 'TypeError'>
# exc_val cce
# exc_tb <traceback object at 0x106019848>
练习
我们可以通过上下文管理,对一个函数的执行时长进行统计,之前都会用装饰器来实现,上下文管理就很符合这种特性,因为执行时长,就是统计函数开始时,到结束时的一个总时长。
import time
class Timeit():
def __init__(self):pass
def __enter__(self):
self.start_time=time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
delta = time.time()-self.start_time
print(delta)
def add(x,y):
return x+y
with Timeit() as t:
print(add(1,2))
# 3
# 5.698204040527344e-05
# 上下文,只是对with语句下面的整个语句块进行计时,所以,需要计时的函数并不需要加入到上下文类中;
# 当然,我们也可以将函数传入进去,然后在__enter__阶段返回
import time
class Timeit():
def __init__(self,func):
self.func=func
def __enter__(self):
self.start_time=time.time()
return self.func
def __exit__(self, exc_type, exc_val, exc_tb):
delta = time.time()-self.start_time
print(delta)
def add(x,y):
return x+y
with Timeit(add) as t:
print(t(1,2))
# 3
# 2.7894973754882812e-05
# 同样的,我们也可以借助__call__可调用对象,来对这个进行优化,__enter__直接返回实例本身;
class Timeit():
def __init__(self,func):
self.func=func
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
def __enter__(self):
self.start_time=time.time()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
delta = time.time()-self.start_time
print(delta)
def add(x,y):
return x+y
with Timeit(add) as t:
print(t(1,2))
# 3
# 3.0994415283203125e-05
应用场景
上下文管理不仅可以进行资源管理,在进入主程序之前打开文件,在退出主程序之前关闭文件对象,还可以做代码增强,替代装饰器的功能,以增强指定方法的功能,同时上下文管理还可以应用在权限验证阶段,在执行特定操作时可以做权限的一个校验。
上下文管理器类
对于上下文管理器,Python给我们提供一个更简便的使用方法,需要用到一个装饰器,也就是实现上下文管理器的装饰器,这个装饰器还必须借助一个函数来完成,这个函数还必须是一个生成器函数,但是,需要注意的是,使用上下文管理器类来实现上下文管理器,缺少了一个功能,就是如果在yield之前抛出异常,那么yield之后将不再执行,对比自定义实现就是__enter__之间如果出现异常,那么将不会执行__exit__。
from contextlib import contextmanager
@contextmanager
def cce():
print("enter") # 之前
yield 1
print("exit") # 之后
return 1
with cce() as c:
print(c) # 1 # 等价于cce().__enter__()的返回值
cce()
# 在yiled之前的代码是进入代码,yiled之后的代码,是退出的代码
# yiled 的结果,会赋值给c