6.面向对象高级编程

面向对象高级编程

数据封装、继承和多态只是面向对象程序设计中最基础的三个概念。在Python中,面向对象还有很多高级特性,诸如多重继承、定制类、元类。

使用slots

正常情况下,当我们定义一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。

1
2
3
4
5
6
7
>>> class Student(object):
... pass
...
>>> s=Student()
>>> s.name='mick'
>>> print(s.name)
mick

这是给实例绑定了一个属性,还可以给实例绑定一个方法:

1
2
3
4
5
6
7
8
>>> def set_age(self, age):#定义一个函数作为实例方法
... self.age=age
...
>>> from types import MethodType
>>> s.set_age=MethodType(set_age, s)#给实例绑定一个方法
>>> s.set_age(25)#调用实例方法
>>> s.age#测试结果
25

但是,给一个实例绑定的方法,对另一个实例是不起作用的:

1
2
3
4
5
>>> s2=Student()
>>> s2.set_age(24)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'

为了给所有实例绑定方法,可以给class绑定方法:

1
2
3
4
5
6
7
8
9
10
>>> def set_score(self,score):
... self.score=score
...
>>> Student.set_score=set_score
>>> s.set_score(100)
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99

通常情况下,上面的set_score方法可以直接定义在class中,但是动态绑定允许我们在程序运行过程中给class加上功能,这在动态语言中很难实现。

使用slots

但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加nameage属性。为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

1
2
3
4
5
6
7
8
9
10
>>> class Student(object):
... __slots__=('name','age')
...
>>> s=Student()
>>> s.name='mic'
>>> s.age=25
>>> s.score=23
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

由于score没有被放到__slots__中,所以不能对其绑定属性。
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:

1
2
3
4
5
>>> class GraduateStudent(Student):
... pass
...
>>> g=GraduateStudent()
>>> g.score=100

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

使用@propetry

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来简单,但是,没有办法检查参数,导致可以把成绩随意修改:

1
2
>>> s=Student()
>>> s.score=10000

这显然不符合逻辑,为了限制score的范围,可以通过一个set_score()的方法来设置成绩,再通过一个get_score()的方法来取得成绩,这样,在set_score()方法里,就可以检查参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> s=Student()
>>> s.age=23
>>> s.age=23
>>> class Student(object):
... def get_score(self):
... return self._score
... def set_score(self,value):
... if not isinstance(value,int):
... raise ValueError('score must be an integer!')
... if value <0 or value>100:
... raise ValueError('score must between 0~100!')
... self._score=value
...

现在,对任意的Student实例进行操作,就不能随心所欲的设置score了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> s=Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(121)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 8, in set_score
ValueError: score must between 0~100!
>>> s.set_score('abc')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in set_score
ValueError: score must be an integer!

但是上面的调用方法略显复杂,没有直接用属性那么直接简单。
Python内置的@property装饰器负责把一个方法变成属性调用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Student(object):
...
... @property
... def score(self):
... return self._score
...
... @score.setter
... def score(self, value):
... if not isinstance(value, int):
... raise ValueError('score must be an integer!')
... if value<0 or value>100:
... raise ValueError('score must between 0~100')
... self._score=value
...

@property的实现比较复杂,我们先观察如何使用。把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@property,负责把一个setter方法变成属性赋值,于是,我们拥有一个可控的属性操作:

1
2
3
4
5
6
7
8
9
>>> s=Student()
>>> s.score=90
>>> s.score
90
>>> s.score=9900
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 12, in score
ValueError: score must between 0~100

注意到@property,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法实现的。还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

1
2
3
4
5
6
7
8
9
10
11
12
>>> class Student(object):
...
... @property
... def birth(self):
... return self._birth
... @birth.setter
... def birth(self, value):
... self._birth=value
... @property
... def age(self):
... return 2015-self._birth
...

上面的birth是可读写属性,而age是一个只读属性,因为age可以根据birth和当前时间计算出来。


多重继承

继承是面向对象编程的一个重要方式,因为通过继承,子类就可以扩展父类的功能。

前面讲到Animal类层次的设计,假设我们要实现下列四种动物,Dog、Bat、Parrot、Ostrich。

如果按照哺乳类动物和鸟类归类:
Alt text

如果按照能飞能跑来归类:
Alt text

把上面两种都包含进来,我们就得设计更多的层次:
哺乳类:能跑的和能飞的
鸟 类: 能跑的和能飞的
Alt text

继续这样弄下去,类的数量会呈指数增长,正确的做法是采用多重继承。首先主要的类仍然按照哺乳类和鸟类设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> class Animal(object):
... pass
...
>>> class Mammal(Animal):
... pass
...
>>> class Bird(Animal):
... pass
...
>>> class Dog(Mammal):
... pass
...
>>> class Bat(Mammal):
... pass
...
>>> class Parrot(Bird):
... pass
...
>>> class Ostrich(Bird):
... pass
...

现在,我们给动物再加上RunnableFlyable的功能,只需要定义好这两个类:

1
2
3
4
5
6
7
8
>>> class Runnable(object):
... def run(self):
... print('Running......')
...
>>> class Flyable(object):
... def fly(self):
... print('Flying......')
...

对于需要Runnable功能的动物,就多继承一个Runnable,如:

1
2
3
4
5
6
>>> class Dog(Mammal, Runnable):
... pass
...
>>> class Bat(Mammal,Flyable):
... pass
...

通过多重继承,一个子类可以同时获得多个父类的所有功能。

Mixln

在设计类的继承关系的时候,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要混入额外的功能,通过多重继承就可以实现,比如,让Ostrich额外再继承Runnable。这种设计通常称为Mixln.

为了更好的看出继承关系,我们把Runnable``Flyable改为RunnableMixlnFlyableMixln。类似的,还可以定义肉食动物和植食动物,让一个动物拥有好几个Mixln。Mixln的目的是为了给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixln的功能,而不是设计多层次的复杂的继承关系。

Python自带的很多库也使用了Mixln。比如,Python自带了TCPServerUDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixInThreadingMixIn提供,通过组合,我们可以创建出合适的服务来。
首先分别编写一个多进程的TCP服务和多线程的UDP服务,定义如下:

1
2
3
4
5
6
>>> class MyTCPServer(TCPServer,ForkingMixIn):
... pass
...
>>> class MyUDPServer(UDPServer,ThreadingMixIn):
... pass
...


定制类

看到类似__slots__这种形式的变量或者函数名要注意,这些在python中是有特殊的用途的,可以帮我们定制类。

__str__

我们先定义一个Student的类,打印一个实例:

1
2
3
4
5
6
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
>>> print(Student('BOB'))
<__main__.Student object at 0x00000153E7856CC0>

打印出来的<__main__.Student object at 0x00000153E7856CC0>并不好看,此时需要定义__str__方法,返回一个好看的字符串就好了:

1
2
3
4
5
6
7
8
>>> class Student(object):
... def __init__(self,name):
... self.name=name
... def __str__(self):
... return 'Student object (name: %s)' %self.name
...
>>> print(Student('BOB'))
Student object (name: BOB)

但是如果直接敲变量不用print,打印出来的实例和前面的结果是一样的,这是因为直接显示变量调用的不是__str__()而是__repr__,两者的区别在于前者返回用户看到的字符串,后者返回程序开发者看到的字符串,也就是说后者视为调试服务的。解决的办法是再定义一个__rper__()但是通常两者代码是一样的:

1
2
3
4
5
6
7
8
>>> class Student(object):
... def __init__(self,name):
... self.name=name
... def __str__(self):
... return 'Student object (name: %s)' %self.name
... __repr__=__str__
>>> print(Student('BOB'))
Student object (name: BOB)

__iter__

如果一个类想被用于for...in循环,类似list或tuple那样,就必须实现一个__iter__()方法拿到循环的下一个值,直到遇到StopIteration错误的时退出循环。我们以斐波那契数列为例,写一个Fib类:

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
>>> class Fib(object):
... def __init__(self):
... self.a , self.b =0,1
... def __iter__(self):
... return self
... def __next__(self):
... self.a , self.b = self.b, self.a+self.b
... if self.a>100000:
... raise StopIteration()
... return self.a
...
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
987
1597
2584
4181
6765
10946
17711
28657
46368
75025

__getitem__

Fib实例虽然能作用于for循环,看起来和list有点像,但是不可以把它当做list来使用。要表现的像list那样可以按照下标取出元素,需要实现__getitem__方法:

1
2
3
4
5
6
7
>>> class Fib(object):
... def __getitem__(self,n):
... a,b=1,1
... for x in range(n):
... a,b=b,a+b
... return a
...

此时,就可以按照下标来访问数列的任一项了:

1
2
3
4
5
6
7
>>> f=Fib()
>>> f[13]
377
>>> f[2]
2
>>> f[22]
28657

但是list有个神奇的切片方法:

1
2
>>> list(range(100))[5:10]
[5, 6, 7, 8, 9]

对于Fib却报错,原因是__getitem__传入的参数可能是一个int,也可能是一个切片对象slice,所以要做出判断:

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
>>> class Fib(object):
... def __getitem__(self,n):
... if isinstance(n,int):
... a,b=1,1
... for x in range(n):
... a,b=b,a+b
... return a
... if isinstance(n,slice):
... start=n.start
... stop=n.stop
... if start is None:
... start=0
... a,b=1,1
... L=[]
... for x in range(stop):
... if x >=start:
... L.append(a)
... a,b =b,a+b
... return L
...
>>> f=Fib()
>>> f[0:6]
[1, 1, 2, 3, 5, 8]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
>>> f[3:9]
[3, 5, 8, 13, 21, 34]

但是没有对step参数的处理,也没有对负数做处理,所以,要正确实现一个__getitem__还是有很多工作要做的。此外,如果把对象看成dict,__getitem__()参数也可能是一个座位key的object,例如str。与之对应的是__setitem__()方法,把对象是做list或者是dictionary来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。总之,通过上面的方法,我们自己定义的类表现的和python自带的list。tuple,dict没什么区别。

__getattr__

正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self):
... self.name='BOB'
...
>>> s=Student()
>>> s.name
'BOB'
>>> s.score
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

错误信息告诉我们没有找到score这个attribute。要避免这个错误,除了可以加上一个score属性之外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> class Student(object):
... def __init__(self):
... self.name='BOB'
... def __getattr__(self,attr):
... if attr=='score':
... return 99
... if attr=='age':
... return lambda:25
...
>>> s=Student()
>>> s.name
'BOB'
>>> s.score
99
>>> s.age()
25

当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self,'score')来尝试获取属性,这样,我们就有机会返回score的值。也可以返回函数,注意调用方法。

只有在没有找到属性的情况下,才会调用__getattr__(),已有的属性不会再__getattr__()里面查找。此外我们注意到任意调用如s.abc都会返回None,这是因为我们定义的__getattr__()默认返回的就是None。要让class只相应特定的几个属性,我们就要按照约定抛出AttributeError的错误。

1
2
3
4
5
6
7
...
def __getattr__(self,attr):
... if attr=='score':
... return 99
... if attr=='age':
... return lambda:25
... raise AttributeError('\'Student\' object has no attribution \'%s\'' %attr)

这个实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要特殊手段,这种完全动态调用的特性有什么实际作用呢??作用就是可以针对完全动态的情况作调用。

举个例子:
现在很多网站都搞REST API,调用API的URL类似:

http://api.server/user/friends
http://api.server/user/timeline/list

如果要写SDK,给每个URL对应的API都写一个方法,工作量很大,而且API改动的话,SDK也要改动。

利用完全动态的__getattr__(),我们可以写出一个链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class Chain(object):
... def __init__(self,path=''):
... self._path=path
...
... def __getattr__(self,path):
... return Chain('%s/%s ' %(self._path,path))
...
... def __str__(self):
... return self._path
...
... __repr__=__str__
...
>>> Chain().status.user.timeline.list
/status /user /timeline /list

这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用。

__call__

一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用,也可以直接在实例本身调用。

任何类,只要定义一个__call__()方法,就可以直接对实例进行调用。请看示例:

1
2
3
4
5
6
7
8
9
10
11
>>> class Student(object):
... def __init__(self,name):
... self.name=name
...
... def __call__(self):
... print('My name is %s ' %self.name)
...
>>> s=Student('Bob')
>>>
>>> s()
My name is Bob

__call__()还可以定义参数,对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,两者之间本身就没啥根本区别。

如果你把对象看成函数,那么函数本身其实也可以在运行期间动态创建出来,因为类的实例都是在运行期间创建出来的。这样就模糊了函数和对象的区别。

那么如何判断一个变量是函数还是对象呢?更多的时候,我们需要判断一个函数能否被调用,能被调用的对象就是一个Callable函数,比如函数和我们上面定义的带有__call__()的类实例:

1
2
3
4
5
6
>>> callable(Student('ddd'))
True
>>> callable(max)
True
>>> callable([1,2,3])
False

通过callable()函数,可以判断一个对象是否可以被调用。

使用枚举类

当我们需要定义常量时,一个办法就是用大写变量通过整数来定义,例如月份:

1
2
3
4
JAN=1
FEB=2
...
DEC=12

好处是简单,缺点是类型是int,并且仍然是变量。

更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> from enum import Enum
>>> Month=Enum('Month',('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
>>> for name,member in Month.__members__.items():
... print(name,'=>',member,',',member.value)
...
Jan => Month.Jan , 1
Feb => Month.Feb , 2
Mar => Month.Mar , 3
Apr => Month.Apr , 4
May => Month.May , 5
Jun => Month.Jun , 6
Jul => Month.Jul , 7
Aug => Month.Aug , 8
Sep => Month.Sep , 9
Oct => Month.Oct , 10
Nov => Month.Nov , 11
Dec => Month.Dec , 12

这样我们就获得了Month的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举他的所有类型。value属性是自动赋给成员的int常量,默认从1开始。如果需要更准确的控制枚举类型,可以从Enum派生出自定义类:

1
2
3
4
5
6
7
8
9
10
11
>>> from enum import Enum,unique
>>> @unique
... class Weekday(Enum):
... Sun=0
... Mon=1
... Tue=2
... Wed=3
... Thu=4
... Fri=5
... Sat=6
...

@unique装饰器可以帮助我们检查保证没有重复值。

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
>>> day1=Weekday.Mon
>>> print(day1)
Weekday.Mon
>>> print(Weekday.Tue)
Weekday.Tue
>>> print(Weekday['Tue'])
Weekday.Tue
>>> print(Weekday.Tue.value)
2
>>> print(day1==Weekday.Mon)
True
>>> print(day1==Weekday.Tue)
False
>>> print(Weekday(1))
Weekday.Mon
>>> Weekday(1)
<Weekday.Mon: 1>
>>> Weekday(7)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "C:\Users\cdxu0\AppData\Local\Programs\Python\Python35\lib\enum.py", line 235, in __call__
return cls.__new__(cls, value)
File "C:\Users\cdxu0\AppData\Local\Programs\Python\Python35\lib\enum.py", line 470, in __new__
raise ValueError("%r is not a valid %s" % (value, cls.__name__))
ValueError: 7 is not a valid Weekday
>>> for name,member in Weekday.__members__.items():
... print(name,'=>',member)
...
Sun => Weekday.Sun
Mon => Weekday.Mon
Tue => Weekday.Tue
Wed => Weekday.Wed
Thu => Weekday.Thu
Fri => Weekday.Fri
Sat => Weekday.Sat


使用元类

type()

动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时决定的,而是运行时动态创建的。

比如我们要写一个Hello的class ,就写一个hello.py模块:

1
2
3
4
>>> class Hello(object):
... def hello(self,name='world'):
... print('Hello,%s' %name)
...

当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的对象:

1
2
3
4
5
6
7
>>> h=Hello()
>>> h.hello()
Hello,world
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,他的类型就是classHello

我们说class的定义是运行时动态创建的,而创建出class的方法就是使用type()函数。

type()函数既可以返回一个函数的类型,也可以创建出新的类型,比如我们可以通过type()函数创造出Hello类,而无需通过class Hello(object)的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def fn(self,name='world'):
... print('Hello, %s' %name)
...
>>> Hello=type('Hello',(object,),dict(hello=fn))
>>> h=Hello()
>>> h.hello
<bound method fn of <__main__.Hello object at 0x000001C2A4956470>>
>>> h.hello()
Hello, world
>>> print(type(Hello))
<class 'type'>
>>> print(type(h))
<class '__main__.Hello'>

要创建一个class对象,type()函数要依次传入3个参数:
1.class的名称
2.继承的父类集合,注意Python支持多重继承,如果只有一个父类,记得tuple的单元素写法
3.class的方法和函数名称绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxxx..来定义类,但是type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期冬天创建类,这和静态语言有很大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译 ,很复杂。

metaclass

除了使用type()动态创建类外,要控制类的创建行为,还可以用metaclass。

metaclass,直译为元类,也就是当我们定义了类之后,就可以根据这个类创建出实例,所以先定义类,然后创建出实例。但是如果我们想创建出类,就必须根据metaclass来创建出类,也就是先定义metaclass,然后创建类。
也就是先定义metaclass,然后创建类,最后创建实例。

所以metaclass允许你创建类或者修改类,可以把类看成metaclass创建出的实例。

metaclass比较难理解,可以用metaclass给我们定义的MyList增加一个add方法:

1
2
3
4
5
>>> class ListMetaclass(type):
... def __new__(cls, name, bases, attrs):
... attrs['add']=lambda self, value:self.append(value)
... return type.__new__(cls,name,bases,attrs)
...

有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass:

1
2
3
>>> class MyList(list,metaclass=ListMetaclass):
... pass
...

当我们传入关键字参数metaclass时,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后返回修改后的定义。

__new__()方法接收到的参数依次是
1.当前准备创建的类的对象
2.类的名字
3.类继承的父类集合
4,类的方法集合

测试一下MyList

1
2
3
4
>>> L=MyList()
>>> L.add(1)
>>> L
[1]

而普通的list没有add()方法:

1
2
3
4
5
>>> L2=list()
>>> L2.add(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute 'add'

动态修改的意义在哪里?正常情况下直接在MyList定义中写上add(),通过metaclass修改类的定义很麻烦,但是也有应用的地方,比如ORM。

ORM全程为’Object Relational Mapping’,也就是对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样写代码会更加简单,不用直接操作SQL语句。

要编写一个ORM框架,所有类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。

我们来编写一个ORM框架。编写底层模块的第一步,就是先把调用的接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待写出的代码:

1
2

Donate comment here