面向对象高级编程
数据封装、继承和多态只是面向对象程序设计中最基础的三个概念。在Python中,面向对象还有很多高级特性,诸如多重继承、定制类、元类。
使用slots
正常情况下,当我们定义一个class,创建了一个class的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。
这是给实例绑定了一个属性,还可以给实例绑定一个方法:
但是,给一个实例绑定的方法,对另一个实例是不起作用的:
为了给所有实例绑定方法,可以给class绑定方法:
通常情况下,上面的set_score
方法可以直接定义在class中,但是动态绑定允许我们在程序运行过程中给class加上功能,这在动态语言中很难实现。
使用slots
但是,如果我们想要限制实例的属性怎么办?比如,只允许对Student实例添加name
和age
属性。为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__
变量,来限制该class实例能添加的属性:
由于score
没有被放到__slots__
中,所以不能对其绑定属性。
使用__slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
使用@propetry
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来简单,但是,没有办法检查参数,导致可以把成绩随意修改:
这显然不符合逻辑,为了限制score的范围,可以通过一个set_score()
的方法来设置成绩,再通过一个get_score()
的方法来取得成绩,这样,在set_score()
方法里,就可以检查参数:
现在,对任意的Student实例进行操作,就不能随心所欲的设置score了:
但是上面的调用方法略显复杂,没有直接用属性那么直接简单。
Python内置的@property
装饰器负责把一个方法变成属性调用的:
@property
的实现比较复杂,我们先观察如何使用。把一个getter方法变成属性,只需要加上@property
就可以了,此时,@property
本身又创建了另一个装饰器@property
,负责把一个setter方法变成属性赋值,于是,我们拥有一个可控的属性操作:
注意到@property
,我们在对实例属性操作的时候,就知道该属性很可能不是直接暴露的,而是通过getter和setter方法实现的。还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:
上面的birth是可读写属性,而age是一个只读属性,因为age可以根据birth和当前时间计算出来。
多重继承
继承是面向对象编程的一个重要方式,因为通过继承,子类就可以扩展父类的功能。
前面讲到Animal类层次的设计,假设我们要实现下列四种动物,Dog、Bat、Parrot、Ostrich。
如果按照哺乳类动物和鸟类归类:
如果按照能飞能跑来归类:
把上面两种都包含进来,我们就得设计更多的层次:
哺乳类:能跑的和能飞的
鸟 类: 能跑的和能飞的
继续这样弄下去,类的数量会呈指数增长,正确的做法是采用多重继承。首先主要的类仍然按照哺乳类和鸟类设计:
现在,我们给动物再加上Runnable
和Flyable
的功能,只需要定义好这两个类:
对于需要Runnable
功能的动物,就多继承一个Runnable
,如:
通过多重继承,一个子类可以同时获得多个父类的所有功能。
Mixln
在设计类的继承关系的时候,通常,主线都是单一继承下来的,例如,Ostrich
继承自Bird
。但是,如果需要混入额外的功能,通过多重继承就可以实现,比如,让Ostrich
额外再继承Runnable
。这种设计通常称为Mixln.
为了更好的看出继承关系,我们把Runnable``Flyable
改为RunnableMixln
和FlyableMixln
。类似的,还可以定义肉食动物和植食动物,让一个动物拥有好几个Mixln。Mixln的目的是为了给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个Mixln的功能,而不是设计多层次的复杂的继承关系。
Python自带的很多库也使用了Mixln。比如,Python自带了TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供,通过组合,我们可以创建出合适的服务来。
首先分别编写一个多进程的TCP服务和多线程的UDP服务,定义如下:
定制类
看到类似__slots__
这种形式的变量或者函数名要注意,这些在python中是有特殊的用途的,可以帮我们定制类。
__str__
我们先定义一个Student的类,打印一个实例:
打印出来的<__main__.Student object at 0x00000153E7856CC0>
并不好看,此时需要定义__str__
方法,返回一个好看的字符串就好了:
但是如果直接敲变量不用print,打印出来的实例和前面的结果是一样的,这是因为直接显示变量调用的不是__str__()
而是__repr__
,两者的区别在于前者返回用户看到的字符串,后者返回程序开发者看到的字符串,也就是说后者视为调试服务的。解决的办法是再定义一个__rper__()
但是通常两者代码是一样的:
__iter__
如果一个类想被用于for...in
循环,类似list或tuple那样,就必须实现一个__iter__()
方法拿到循环的下一个值,直到遇到StopIteration
错误的时退出循环。我们以斐波那契数列为例,写一个Fib类:
|
|
__getitem__
Fib实例虽然能作用于for循环,看起来和list有点像,但是不可以把它当做list来使用。要表现的像list那样可以按照下标取出元素,需要实现__getitem__
方法:
此时,就可以按照下标来访问数列的任一项了:
但是list有个神奇的切片方法:
对于Fib却报错,原因是__getitem__
传入的参数可能是一个int,也可能是一个切片对象slice,所以要做出判断:
但是没有对step参数的处理,也没有对负数做处理,所以,要正确实现一个__getitem__
还是有很多工作要做的。此外,如果把对象看成dict,__getitem__()
参数也可能是一个座位key的object,例如str。与之对应的是__setitem__()
方法,把对象是做list或者是dictionary来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。总之,通过上面的方法,我们自己定义的类表现的和python自带的list。tuple,dict没什么区别。
__getattr__
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错:
错误信息告诉我们没有找到score
这个attribute。要避免这个错误,除了可以加上一个score
属性之外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。修改如下:
当调用不存在的属性时,比如score,Python解释器会试图调用__getattr__(self,'score')
来尝试获取属性,这样,我们就有机会返回score的值。也可以返回函数,注意调用方法。
只有在没有找到属性的情况下,才会调用__getattr__()
,已有的属性不会再__getattr__()
里面查找。此外我们注意到任意调用如s.abc
都会返回None,这是因为我们定义的__getattr__()
默认返回的就是None
。要让class
只相应特定的几个属性,我们就要按照约定抛出AttributeError
的错误。
这个实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要特殊手段,这种完全动态调用的特性有什么实际作用呢??作用就是可以针对完全动态的情况作调用。
举个例子:
现在很多网站都搞REST API,调用API的URL类似:
http://api.server/user/friends
http://api.server/user/timeline/list
如果要写SDK,给每个URL对应的API都写一个方法,工作量很大,而且API改动的话,SDK也要改动。
利用完全动态的__getattr__()
,我们可以写出一个链式调用:
这样,无论API怎么变,SDK都可以根据URL实现完全动态的调用。
__call__
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()
来调用,也可以直接在实例本身调用。
任何类,只要定义一个__call__()
方法,就可以直接对实例进行调用。请看示例:
__call__()
还可以定义参数,对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,两者之间本身就没啥根本区别。
如果你把对象看成函数,那么函数本身其实也可以在运行期间动态创建出来,因为类的实例都是在运行期间创建出来的。这样就模糊了函数和对象的区别。
那么如何判断一个变量是函数还是对象呢?更多的时候,我们需要判断一个函数能否被调用,能被调用的对象就是一个Callable
函数,比如函数和我们上面定义的带有__call__()
的类实例:
|
|
通过callable()
函数,可以判断一个对象是否可以被调用。
使用枚举类
当我们需要定义常量时,一个办法就是用大写变量通过整数来定义,例如月份:
好处是简单,缺点是类型是int,并且仍然是变量。
更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum
类来实现这个功能:
这样我们就获得了Month
的枚举类,可以直接使用Month.Jan
来引用一个常量,或者枚举他的所有类型。value
属性是自动赋给成员的int常量,默认从1开始。如果需要更准确的控制枚举类型,可以从Enum派生出自定义类:
@unique
装饰器可以帮助我们检查保证没有重复值。
使用元类
type()
动态语言和静态语言最大的不同,就是函数和类的定义,不是编译时决定的,而是运行时动态创建的。
比如我们要写一个Hello
的class ,就写一个hello.py
模块:
当Python解释器载入hello
模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的对象:
|
|
type()
函数可以查看一个类型或变量的类型,Hello
是一个class,它的类型就是type
,而h是一个实例,他的类型就是classHello
。
我们说class的定义是运行时动态创建的,而创建出class的方法就是使用type()
函数。
type()
函数既可以返回一个函数的类型,也可以创建出新的类型,比如我们可以通过type()
函数创造出Hello
类,而无需通过class Hello(object)
的定义:
|
|
要创建一个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方法:
有了ListMetaclass,我们在定义类的时候还要指示使用ListMetaclass来定制类,传入关键字参数metaclass
:
当我们传入关键字参数metaclass
时,它指示Python解释器在创建MyList
时,要通过ListMetaclass.__new__()
来创建,在此,我们可以修改类的定义,比如,加上新的方法,然后返回修改后的定义。
__new__()
方法接收到的参数依次是
1.当前准备创建的类的对象
2.类的名字
3.类继承的父类集合
4,类的方法集合
测试一下MyList
:
而普通的list没有add()
方法:
动态修改的意义在哪里?正常情况下直接在MyList
定义中写上add()
,通过metaclass修改类的定义很麻烦,但是也有应用的地方,比如ORM。
ORM全程为’Object Relational Mapping’,也就是对象-关系映射,就是把关系数据库的一行映射为一个对象,也就是一个类对应一个表,这样写代码会更加简单,不用直接操作SQL语句。
要编写一个ORM框架,所有类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
我们来编写一个ORM框架。编写底层模块的第一步,就是先把调用的接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应的数据库表User,我们期待写出的代码: