目录
面向对象高级编程
__slots__
- 在上一篇文章讲到过,我们可以通过简单的操作就能够给实例或类绑定属性。这里我们来讨论如何给实例和类绑定方法,先来看个例子:
from types import MethodType class Dog(): def __init__(self, name): self.Name = name def show(self): print('my name\'s %s' % self.Name) d = Dog('Dolly')
需要注意的是:若是通过下面这种方式给实例绑定方法则在调用方法时,解释器就不会自动把本实例作为一个参数传入self中了,也就是不会将实例和方法进行绑定:
In [2]: d.show = show In [3]: d.show() Traceback (most recent call last): File "<ipython-input-3-73a424ac9a82>", line 1, in <module> d.show() TypeError: show() missing 1 required positional argument: 'self'
正确的绑定操作应该是这样:
In [5]: d.show = MethodType(show, d) #通过MethodType函数给实例d绑定方法 In [6]: d.show() #调用刚刚绑定的方法 my name's Dolly
从例子中可以看到,我们通过MethodType函数来给实例绑定方法。这里我们仅仅是给实例d绑定了show方法,也就是说其它的实例并没有show方法:
In [8]: d2 = Dog('Mike') In [9]: d2.show() Traceback (most recent call last): File "<ipython-input-9-714779789114>", line 1, in <module> d2.show() AttributeError: 'Dog' object has no attribute 'show'
为了解决这个问题,我们可以尝试给类绑定方法,因为类的方法是所有实例所共有的,给类绑定方法非常简单:
In [10]: def bark(self): ...: print('wang wang wang !') ...: In [11]: Dog.bark = bark
现在我们来试试:
In [12]: d.bark() wang wang wang ! In [13]: d2.bark() wang wang wang !
- 有时我们想给实例绑定的属性添加一个限定,使得属性的绑定不能是任意的,而只能绑定我们规定的几种属性。这就要使用一个特殊的变量——__slots__:
class Dog(): __slots__ = ('name', 'gender') #限定Dog的实例只能够绑定name和gender这两个属性
现在我们来试试给实例绑定属性:
In [2]: d = Dog() In [3]: d.name = 'Dolly' #给d绑定name属性 In [4]: d.name Out[4]: 'Dolly' In [5]: d.gender = 'femal' #给d绑定gender属性 In [6]: d.gender Out[6]: 'femal' In [7]: d.color = 'yello' #给d绑定color属性 Traceback (most recent call last): File "<ipython-input-7-dd28abd9292c>", line 1, in <module> d.color = 'yello' AttributeError: 'Dog' object has no attribute 'color'
我们看到在给实例d绑定name和gender这两个属性时没问题,在绑定属性color时就抛出了错误,因为color这个属性是不被允许的!如果再定义一个Dog的子类Husky,并且Husky类中没有对__slots__进行声明,那么Husky的实例对属性的绑定是不受其父类Dog影响的,可以任意绑定;如果Husky中对__slots__也进行了声明,那么Husky实例所允许绑定的属性是两个声明的__slots__属性之和。
@property
- 通过对前面知识的学习,我们在给实例绑定属性时一般进行类似以下操作:
In [13]: class Dog(): ...: def __init__(self, name): ...: self._name = name #绑定属性_name ...: In [14]: d = Dog('Dolly') In [16]: d.age = 999999 #绑定属性age
但是这样会产生的一个问题是:如果没有任何措施加以限制的话,属性值是可以改成任意值的,就比如上述例子中将age设为999999,这明显不符合实际!也许我们可以定义一个set_age()方法用于修改属性值,在方法中对属性的取值加以限制:
class Dog(): def get_age(self): return self.age def set_age(self, val): if not isinstance(val, int): #检查val的类型 raise ValueError('年龄只能为整数!') elif val < 0 or val > 30: #检查val的取值 raise ValueError('年龄只能在0~30之间!') self.age = val
现在我们来试试:
In [18]: d = Dog() In [19]: d.set_age('六岁') Traceback (most recent call last): File "<ipython-input-19-8626661de8b1>", line 1, in <module> d.set_age('六岁') File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in set_age raise ValueError('年龄只能为整数!') ValueError: 年龄只能为整数! In [20]: d.set_age(35) Traceback (most recent call last): File "<ipython-input-20-b611f54c4999>", line 1, in <module> d.set_age(35) File "C:/Users/Whisky/.spyder-py3/temp.py", line 12, in set_age raise ValueError('年龄只能在0~30之间!') ValueError: 年龄只能在0~30之间! In [21]: d.set_age(6) In [22]: d.get_age() Out[22]: 6
其实心细的同学会发现这没从根本上解决问题,我们还是可以随意更改age的值,只要不通过调用set_age()方法就行了:
In [24]: d.age = 100 In [25]: d.age Out[25]: 100
那么到底怎么解决这个问题呢?这就要使用Pyhton提供的@property装饰器了!先来看下面的例子:
class Dog(): @property def age(self): return self._age ##这里不要写成self.age @age.setter def age(self, val): if not isinstance(val, int): raise ValueError('年龄必须为整数值!') elif val < 0 or val > 30: raise ValueError('年龄只能在0~30间') self._age = val ##这里不要写成self.age = val
Python内置的@property装饰器能够把方法转成属性的调用,而另一个装饰器@age.setter能够把方法转成属性的赋值,来看下怎么使用:
In [21]: d = Dog() In [22]: d.age = 6 #实际上是转化为d.set_age(6) In [23]: d.age #实际上是转化为d.get_age() Out[23]: 6 In [27]: d.age = 999 #实际上是转化为d.set_age(999) Traceback (most recent call last): File "<ipython-input-27-8e0eec63a531>", line 1, in <module> d.age = 999 File "C:/Users/Whisky/.spyder-py3/temp.py", line 14, in age raise ValueError('年龄只能在0~30间') ValueError: 年龄只能在0~30间
我们看到,在使用d.age = 999对age的值进行修改时,实际上是转化为执行d.set_age(999),而999是age不能接受的取值故抛出错误。_在两个方法中我们使用的属性是_age而非age,其实将_age改成其他的名字也行,但就是不能写成和age一样!!!自己试试看会出现什么情况!这实际上是给属性age取了个别名_age,试试看直接访问_age会是什么结果:
In [24]: d._age Out[24]: 6 In [25]: d._age = 999 In [26]: d.age Out[26]: 999
可以看到,我们其实还是可以直接修改age,你非要给age胡乱的设值也没办法,所以只能要求你遵守编程规范,按规矩办事😅。从上面这些例子我们可以到,@property修饰的方法是用于“读”,而@属性.setter修饰的方法是用于“写”。所以如果要创建一个只读属性那么就只需要添加@property装饰器:
class Dog(): @property def age(self): return self._age @age.setter def age(self, val): if not isinstance(val, int): raise ValueError('年龄必须为整数值!') elif val < 0 or val > 30: raise ValueError('年龄只能在0~30间') self._age = val @property def birth(self): #birth为只读属性 return 2020 - self.age
In [58]: print(Dog.birth) <property object at 0x000002976BDCE868> In [59]: d.birth Out[59]: 2014 In [60]: d.birth = 1996 #尝试修改只读属性的值 Traceback (most recent call last): File "<ipython-input-60-d4aa4bab9960>", line 1, in <module> d.birth = 1996 AttributeError: can't set attribute
我们可以用一句话来总结这两个装饰器的功能:@property能够使我们在访问属性时按照我们自己规定的方式进行,@属性.setter能够使我们在设定属性的值时按照我们自己规定的方式进行。
定制类
- __str__()。当我们在类中实现了__str__()方法后,在使用print()函数打印某个对象时,会自动调用__str__()函数:
class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name
In [70]: d = Dog('Dolly') In [71]: print(d) hello, my name's Dolly
那么直接在控制台输入d会打印出什么结果呢?试试看:
In [72]: d Out[72]: <__main__.Dog at 0x2976bdb2f28>
我们希望输出的和print(d)结果一样,实际上这两者理应是一样的。这时就需要实现__repr__()方法了,也许你会这样实现:
class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name def __repr__(self): return 'hello, my name\'s %s' % self._name
但其实上只需要这样就行了:
class Dog(): def __init__(self, name): self._name = name def __str__(self): return 'hello, my name\'s %s' % self._name __repr__ = __str__
实现__repr__()是针对开发者,也就是说,
__repr__()
是为调试服务的;而__str__()是针对用户的,返回用户能够靠的字符串。 -
__iter__()。在第一篇文章中讲到过:凡是能用于for循环的就是Iterable类型,凡是能够调用__next__()方法的就是Iterator类型,Iterator也属于是Iterable类型。如果想要自己写的类也能用于for...in,那就要在类中实现__iter__()方法,这样,在使用for循环时就会不断调用__next__()方法不断生成下一个元素,直到抛出
StopIteration为止。
class Fib(): def __init__(self): self.a, self.b = 0, 1 def __iter__(self): return self def __next__(self): if self.b >= 500: raise StopIteration() self.a, self.b = self.b, self.a + self.b return self.b
现在来试试:
In [117]: f = Fib() In [118]: for x in f: ...: print(x) ...: 1 2 3 5 8 13 21 34 55 89 144 233 377 610
那么f是否是Iterable和Iterator类型的呢:
In [119]: from collections import Iterable In [123]: from collections import Iterator In [124]: isinstance(f, Iterable) Out[124]: True In [125]: isinstance(f, Iterator) Out[125]: True
-
__getitem__()。如果要让我们的类能像list,str那样能用下标对元素进行访问,那么就要实现__getitem__()方法:
class Fib(): def __getitem__(self, idx): a, b = 1, 1 for i in range(idx): a, b = b, a + b return a
In [139]: f = Fib() In [140]: f[10] Out[140]: 89 In [141]: f[100] Out[141]: 573147844013817084101
如果要能够像list,str那样能使用切片呢?那么就要对__getitem__()再进行修改:
class Fib(): def __getitem__(self, oj): if isinstance(oj, int): #判断传入的对象是否是int类型 a, b = 1, 1 for i in range(oj): a, b = b, a + b return a elif isinstance(oj, slice): #判断传入的类型是否是slice类型 a, b = 1, 1 L = [] #定义一个list用于保存结果 start = oj.start stop = oj.stop if start == None: start = 0 if stop == None: #默认设置stop最大为1000 stop = 1000 for i in range(start): a, b = b, a + b for i in range(start, stop): L.append(a) a, b = b, a + b return L
试试看:
In [147]: f[0] Out[147]: 1 In [148]: f[2] Out[148]: 2 In [149]: f[:20] Out[149]: [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
这里只是实现了简单的切片操作,没有对步长进行处理,也没有考虑一些异常情况。
-
__getattr__()。我们先定义一个类,并创建个实例:
class Dog(): def __init__(self, name): self._name = name d = Dog('Dolly')
当访问该实例已绑定的属性时自然没有问题:
In [186]: d._name Out[186]: 'Dolly'
而访问不存在的属性时就会抛出错误:
In [187]: d._gender Traceback (most recent call last): File "<ipython-input-187-984b0f00d8a6>", line 1, in <module> d._gender AttributeError: 'Dog' object has no attribute '_gender'
为此,python中提供了__getattr__()应对此问题。我们可以在类中实现该方法,这样,在访问到不存在的属性时就会自动调用该方法生成属性,比如:
class Dog(): def __init__(self, name): self._name = name def __getattr__(self, attr): #调用该方法,自动生成属性,注意!这里的attr是字符串(经传入的属性名转成了字符串形式) if attr == '_gender': return 'male' #会将_gender赋值为'male'
In [226]: Dog('Dolly')._gender Out[226]: 'male'
该方法默认响应所有的属性,如果属性在__getattr__()中没经过处理,默认返回的是None,试试看:
In [239]: hasattr(d, 'age') #测试是否有age属性 Out[239]: True In [240]: hasattr(d, 'color') #测试是否有color属性 Out[240]: True In [241]: d.age #默认返回None,在控制台None是不显示的 In [242]: d.color #默认返回None,在控制台None是不显示的 In [243]: d._gender #经过处理,返回给定的值 Out[243]: 'male'
若要规定只响应规定的几个属性,那么就要这样做:
class Dog(): def __init__(self, name): self._name = name def __getattr__(self, attr): if attr == '_gender': return 'male' raise AttributeError("'Dog'的实例没有绑定该属性!")
In [246]: d = Dog('Mike') In [247]: d._gender Out[247]: 'male' In [248]: hasattr(d, 'color') #测试是否有color属性 Out[248]: False In [249]: d.color #访问未绑定的color属性 Traceback (most recent call last): File "<ipython-input-249-6063e1808d7c>", line 1, in <module> d.color File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的实例没有绑定该属性!") AttributeError: 'Dog'的实例没有绑定该属性! In [250]: d.age #访问未绑定的age属性 Traceback (most recent call last): File "<ipython-input-250-ec62f7b57bf8>", line 1, in <module> d.age File "C:/Users/Whisky/.spyder-py3/temp.py", line 10, in __getattr__ raise AttributeError("'Dog'的实例没有绑定该属性!") AttributeError: 'Dog'的实例没有绑定该属性!
-
__call__()。python中还提供了一个很有趣的方法__call__(),我们在类中实现这个方法就能够像调用函数一般来调用对象了:
class Dog(): def __call__(self, breed): print('I\'m %s!' % breed)
In [268]: d = Dog() In [269]: d('Husky') I'm Husky!
这样看起来好像对象和函数就没什么区别了,所以我们就可以将对象看成是函数,将函数看成是对象,实际上这两者本来就没啥区别!既然如此,我们怎么判断一个对象是否能够像函数那样被调用呢?可以通过判断该对象是否是Callable类型,能被调用就是Callable类型,比如函数和实现了__call__()方法的类实例:
In [270]: callable(d) Out[270]: True In [271]: callable(abs) Out[271]: True In [272]: callable(123) Out[272]: False In [273]: callable('hello') Out[273]: False
-
在本节的最后来看个链式调用的例子:
class Chain(): def __init__(self, info): self._info = info def __str__(self): return self._info def __call__(self): return Chain(self._info) def __getattr__(self, attr): return Chain(self._info) def test(self): return Chain(self._info)
In [283]: print(Chain('链式调用测试!').test().user1.user2()) 链式调用测试!
相信大家已经看出来了,就这么一句代码将类中定义的方法全部都用上了!!现在我们来对这句代码进行剖析。首先“Chain('链式调用测试!')”是创建个Chain实例(为了方面叙述,将这里的实例记为INST1),“.test()”是调用该实例的test的方法,而该方法返回的是Chain实例(将该实例记为INS2),这里相信大家都能够看得懂。接下来“.user1”是访问INS2的属性user1,但是INS2并没有预先绑定该属性,因此这里就调用了INS2的__getattr__()方法,该方法得到的也是一个Chain实例(将该实例记为INS3),接着,同样也先是访问INS3的user2属性,但INS3没有预先绑定该属性,则调用__getattr__()返回一个实例(记为INS4),大家发现没,这里对该实例是以函数的方式调用的,因此调用INS4的__call__()方法,同样返回一个实例(记为INS5),最后回到最外层的print()函数对INS5进行打印调用INS5的__str__()方法,该方法返回INS5的info属性。至此代码运行完毕!!简单的一行代码藏了很深的知识!!!理解这个链式调用栗子,那么这一节的知识就算是弄通了!!!!
使用枚举类
- 枚举类型可以看成是一些标签的集合,比如:月份包含一到十二个月,星期包含周一到周日,颜色包含红橙黄绿青蓝紫等等。python中提供了Enum类来实现枚举类型,而我们可以通过继承Enum来定制我们自己的枚举类型,比如:
from enum import Enum, unique class Month(Enum): Jan = 1 Feb = 2 Mar = 3 Apr = 4 May = 5 Jun = 6 Jul = 7 Aug = 8 Sep = 9 Oct = 10 Nov = 11 Dec = 12
这里要注意,①我们无法直接实例化枚举类,枚举类中的成员称为单例,都是枚举类型的;②每个标签都被赋予一个固定的值,标签的值也是不能够更改的:
In [336]: m = Month() #尝试实例化枚举类 Traceback (most recent call last): File "<ipython-input-336-1c51a47c3f35>", line 1, in <module> m = Month() TypeError: __call__() missing 1 required positional argument: 'value' In [337]: isinstance(Month.Jan, Month) Out[337]: True In [338]: Month.Jan = 13 Traceback (most recent call last): File "<ipython-input-337-27c71d0b74c9>", line 1, in <module> Month.Jan = 13 File "E:\Anaconda3\lib\enum.py", line 386, in __setattr__ raise AttributeError('Cannot reassign members.') AttributeError: Cannot reassign members.
标签中的值可以重复,值重复的多个标签会被认为是具有多个名字的同一个标签,若要限定不同标签值不能重复则要使用@unique装饰器:
from enum import Enum, unique @unique class Month(Enum): Jan = 1 Feb = 2 Mar = 3 Apr = 4 May = 5 Jun = 6 #值重复 Jul = 6 #值重复 Aug = 6 #值重复 Sep = 9 Oct = 10 Nov = 11 Dec = 12
由于标签Jun、Jul、Aug三个标签的值重复,抛出以下错误:
ValueError: duplicate values found in <enum 'Month'>: Jul -> Jun, Aug -> Jun
枚举类型访问方式有多种:
In [354]: Month.Feb #由标签访问值 Out[354]: <Month.Feb: 2> In [355]: Month['Nov'] #由标签访问值 Out[355]: <Month.Nov: 11> In [356]: Month(12) #由值访问标签 Out[356]: <Month.Dec: 12> In [357]: for m in Month: #这种方式对于值相同的标签只打印一次 ...: print(m) ...: Month.Jan Month.Feb Month.Mar Month.Apr Month.May Month.Jun Month.Jul Month.Aug Month.Sep Month.Oct Month.Nov Month.Dec In [358]: for m in Month.__members__: #这种方式会打印出所有标签,包括值相同的标签 ...: print(m) ...: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec In [359]: for name, member in Month.__members__.items(): ...: print(name, '=>', member) ...: Jan => Month.Jan Feb => Month.Feb Mar => Month.Mar Apr => Month.Apr May => Month.May Jun => Month.Jun Jul => Month.Jul Aug => Month.Aug Sep => Month.Sep Oct => Month.Oct Nov => Month.Nov Dec => Month.Dec
这里的特殊属性__members__是一个将名称映射到成员的有序字典,也可以通过它来完成遍历,大家可以自己试试遍历__members__.keys()、__members__.values()和__members__.items()看看会得到什么结果。
使用元类
- 前面我们用到过type()这个函数,可以用来判断一个对象的类型:
In [43]: type(1) Out[43]: int In [44]: type('hello') Out[44]: str In [45]: type((1,)) Out[45]: tuple In [46]: type({}) Out[46]: dict
它还有另一个功能——能够创建类。之前我们创建类都是用下面这种形式:
class Dog(): def __init__(self, name): self._name = name
其实在运行过程中,解释器只是扫描了下语法,最终还是要通过调用type()函数来创建,下面来看下如何用type来创建类:
In [63]: def fun(self): #先定义函数 ...: print('hello world!') ...: In [64]: x = 1 In [66]: A = type('A', (object,), dict(show = fun, _x = x)) #调用type函数创建类A In [67]: A._x #访问A的类属型 Out[67]: 1 In [68]: A().show() #调用方法 hello world!
type()需要传入三个参数:①类名;②所继承的父类; ③绑定的类属型和方法。
-
除了用type函数创建类,我们还可用元类(metaclass)定制我们自己的类,metaclass就是类的模板。我们知道,对象是类的实例,其实我们也可以将类理解为metaclass的“实例”。下面看看元类怎么使用:
class MyMetaclass(type): #定义元类 def __new__(cls, name, bases, attr): attr['show'] = lambda self: print('hello world!') #给类添加show方法 attr['x'] = 7 #给类添加属性x bases = (A,) #让类继承A return type.__new__(cls, name, bases, attr) class A(): #类A def __print__(self): print('I\'m \'A\'')
定义元类和定义一般的类是一样的方式,但是记得要继承type(其实所有的类都是type类型的),接着我们要在元类中实现一个__new__方法,__new__方法中的参数含义分别是:①当前要创建的类,相当于我们在定义一般类时的self; ②类名; ③类需要继承的父类集合;④dict类型,类方法和集属性集合。接下来创建类B,同时传入关键字参数metaclass,表明使用我们定义的MyMetaclass来定制类:
class B(metaclass = MyMetaclass): pass
接下来进行测试:
In [78]: b = B() In [79]: B.x Out[79]: 7 In [80]: b.show() hello world! In [81]: b.__print__() I'm 'A'