7 面向对象高级编程
数据封装、继承和多态只是面向对象程序设计中最基础的3个概念,还有其他的高级特性,比如:多重继承、定制类、元类等概念
7.1 使用slots
正常情况下,当我们定义了一个class,创建了一个class实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性,比如:
class Student(object):
pass
然后,尝试给实例绑定一个属性:
>>>s = Student()
>>>s.name = 'Mical'
>>>print(s.name)
Mical
还可以尝试给实例绑定一个方法:
>>>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
但是,给一个实例绑定方法,对另外一个实例并不起作用。为了解决,可以给class类直接绑定方法:
>>>def set_score(self,score):
... self.score = score
...
>>>Student.set_score = set_score #给类绑定一个方法
这样,该类Student 就可以使用该方法。通常情况下,上面的set_score()
方法可以定义在类中。
但是,如果我们想要限制实例的属性,比如只能对Student
类添加name和age属性。我们可以使用在定义class时,定义一个特殊的__slots__
变量,来限制class实例能添加的属性:
class Student(object):
__solts__ = ('name','age') #用tuple定义允许绑定的属性名称
注意,__slots__
定义的属性只针对当前类起作用,对继承该类的子类不起作用。
7.2使用@property
@property
是一个装饰器,负责把一个方法变成属性调用:
class Student(object):
@property #把一个getter变成属性,只需要加上@property
def score(self):
return self._score
@score.setter #@property又创建了一个@score.setter 把setter方法编程属性
def score(self,value):
if not isinstance(value,int):
raise ValueError('score must be an integer')
if value < 0 and value > 100:
raise ValueError('score must be between 0~100')
self._score = value
@property的实现比较复杂,我们在对实例操作的时候,就知道该属性很肯能不是直接暴露的,而是通过getter和setter方法来实现的。
小结:
@property广泛应用在类的定义中,可以让调用者写出间断的代码,同时保证对参数进行必要的检查。
7.3 多重继承
对于子类C,既可以继承父类A,又可以继承父类B:
class C(A,B):
pass
通过多重继承关系,一个子类可以获得多个父类的多个方法。
- MixIn
多重继承关系,比如上述子类C,既可以继承父类A,又可以继承父类B,这种行为成为MixIn
无需类似于java如果要继承,有强大的继承链。Python只需要选择组合不同的类功能,就可以快速构造出所需的子类。
小结:
Python允许使用多重继承,因此,MixIn是一种常见的涉及。
只允许单一继承的语言-Java,不能使用MixIn涉及
7.4 定制类
看到类似__slots__
这种形如__XXX__
的变量或者函数时,这在Python中是有特殊用途的。
- str
class Student(object):
def __init__(self,name):
self.name = name
我们打印一个实例:
>>>print(Student("Mical"))
<__main__.Student object at 0x109afb190>
我们看到,打印的是一个对象的地址信息,如何能打印我们能看懂的字符串呢?
只需要定义一个__str__()
方法,返回一个好看的字符串就行了:
class Student(object):
def __init__(self,name):
self.name = name
def __str__(self):
return 'Student object (name :%s) % self.name'
下面看两种调用方式:
1.打印:
>>>print(Student('Hello'))
Student object (name :Hello)
>>>s = Student('Hello')
>>>s
<__main__.Student object at 0x109afb190>
注意到两者显示的效果不一样,这是因为直接显示变量s,并不是调用的__str()__
而是调用的__repr()__
,两者的区别是__str()__
返回的是用户看到的字符串,__repr()__
返回的是程序开发者看到的,用于程序调试
__iter__
如果一个类想被用于for…in循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个对象,然后Python的for循环就会不断的调用该迭代对象的__next__()
方法,拿到下一个元素的值,直到遇到StopIteration
异常才会停止。
我们编写一个类,使其可以for循环,比如斐波那契数列函数:
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 #返回下一个值
现在,试图遍历Fib()类的实例
>>>for n in Fib():
... print(n)
...
1
1
2
3
5
- getitem
Fib实例虽然能作用于for循环,但是,把它当list来使用,还是不行,比如获取第index个元素:
>>>Fib()[5]
会提示Fib对象并不支持indexing
对Fib类进行改造:
class Fib(object):
def __getitem__(self, n):
a, b = 1, 0
for x in range(n):
a, b = a,a+ b
return a
现在就可以使用下标来访问Fib()实例对象了
7.5 使用枚举类
Python提供了Enum
类来实现每个变量都是class
的一个实例:
>>>from enum import Enum
>>>Month = Enum('Month',('Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'))
这样,我们获取一个Month类的枚举类型。可以使用Month.Jan来引用一个常量,或者枚举他的所有成员。
for name,member in Month.__members__.items():
print(name,'=>',member,',',member.value)
value属性则是自动赋值给成员int常量,默认从1开始。
如果需要更精确的控制枚举类型,可以从Enum派生出自定义的类:
from enum import Enum,unique
@unique
Class Weekday(Enum):
Sun=0 #Sun的value被设定为0
Mon=1
Tue=2
Wed=3
Thu=4
Fri=5
Sat=6
@unique装饰器可以检查枚举类型中有无重复元素
访问这些枚举类型可以有一下几种方式:
>>>Weekday.Sun
>>>Weekday['Sun']
>>>Weekday(1)
>>>Weekday.Sun.value
7.6使用元类
- type()
动态语言和静态语言不同,就是函数和类的定义,不是编译时定义的,而是在运行时动态创建的。
比如定义一个Class Hello()类,当Python载入Hello模块时,就会依次执行该模块中的所有语句,执行结果就是动态创建出一个Hello的class对象。
>>>hello = Hello()
>>>print(type(Hello))
<class 'type'>
>>>print(type(hello))
<class hello.Hello>
type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而hello是一个实例,它的类型就是class Hello
type()函数不仅可以查看类型,也可以创建一个类:
>>>#先定义函数
>>>def fn(self,name='world'):
print('hello, %s.' % name )
>>>Hello = type('Hello',(object,),dict(hello=fn))#创建Hello class
要创建一个class对象,type()
函数一次传入三个参数:
1. class的名称
2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple单元素的写法:用”,”号
3. class方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上
通过type()函数创建的类和直接写class是完全一样的。
- metaclass
除了使用type()
动态创建类外,要控制类的创建行为,还可以使用metaclass
metaclass
,称为元类。
当我们定义了类以后,就可以根据这个类创建出示例,所以顺序为:先定义类,再创建实例。如果我们要创建类,必须先定义metaclass
。
metaclass
允许创建类和修改类,也可以认为,类是metaclass
创建出的“实例”
例如:给我们自定义的类MyList增加一个add方法:
定义ListMetaclass
,按照默认习惯,metaclass 的类名总是以Metaclass结尾,以便清楚地表示这是一个metaclass:
#metaclass 是类的模板,所以必须从'type'类型派生:
class ListMetaclass(type):
def __new__(cls,name,bases,attrs):
attrs['add'] = lambda self,value: self.append(value)
retrun type.__new__(cls,name,bases,attrs)
有了MetaClass类这个局部模板,我们可以定义类时,指示使用ListMetaclass
元类来定制这个类:
class MyList(list,metaclass=ListMetaclass):
pass
当我们传入参数metaclass=ListMetaclass
时,它指示Python解释器在创建 Mylist
时,要通过ListMetaclass._new__()
来创建,在此,我们可以修改类的定义,比如加上新方法,然后返回修改后的定义。
__new__()
方法接收的参数依次是:
1. 当前准备创建的类的对象 #cls
2. 类的名字 #name
3. 类继承的父类集合 #bases
4. 类的方法集合 #attrs
通常情况下,如果要定义某一个方法,在类定义时,就已经确定,但是也有另外情况——ORM.
ORM称为”Object Relation Mapping”,对象——关系映射,要编写一个ORM框架,所有的类都只能动态定义,因为只有使用者才能根据表的结构定义出对应的类来。
举例写一个ORM框架:
1. 编写底层模块的第一步,就是先把调用接口写出来,比如使用者使用这个ORM框架,想定义一个User类来操作对应的数据库表user.我们期待他写出的代码是:
class User(Model):
#定义类的属性到列的映射
id = IntegerField('id')
name = StringField('username')
email = StringField('email')
password = StringField('password')
创建一个实例
u = User(id=12345,name='Weishzhu',email='12133@Weishizhu.com',password='weishzhu')
#保存到数据库
u.save()
其中,父类Model
和域类型IntegerField
|StringField
都是由ORM提供的。剩下的方法,比如save()
全部由metaclass自动完成。虽然metaclass的编写会比较复杂,但orm的使用者用起来会非常简单
现在我们按照上面的接口来实现该ORM
首先定义Filed类,它负责保存数据库表字段名和字段类型:
class Field(object):
def __init__(self,name,column_type):
self.name = name
self.column_type
def __str__(self):
return '<%s,%s>' % self.__class__.name,self.name
在Field的基础上,进一步定义各种类型的Field,比如IntegerField
|StringField
:
class IntegerField(Filed):
def __init__(self,name):
super(IntegerField,self).__init__(name,'bigint')
class StringField(Field):
def __init__(self,name):
super(StringField,self).__init__(name,'varchar(100)')
下面就是编写复杂的ModelMetaclass类了:
class ModelMetaclass(type):
def __new__(cls,name,bases,attrs):
if name='Model'
return type.__new__(cls,name,bases,attrs)
print('Found model:%s' % name)
mappings = dict()
for k,v in attrs.items():
if isinstance(v,Field):
print('Found mapping :%s ==> %s' % (k,v)))
mapping[k] = v
for k in mappings.keys():
attrs.pop(k)
attrs['__mappings__'] = mappings #保存属性和列的映射关系
attrs['__table__'] = name #假设表名和类名一致
return type.__new__(cls,name,bases,attrs)
#基类 Model
class Model(dict,metaclass=ModelMetaclass):
def __init__(self,**kw):
super(Model,self).__init__(**Kw)
def __getattr__(self,key):
try:
return self[key]
except keyError:
raise AttributeError(r"'Model' object has no attribute '%s' " % key)
def __setattr__(self, key, value):
self[key] = value
def save(self):
fields = []
params = []
args = []
for k,v in self.__mappings__.items():
fields.append(v.name)
params.append('?')
args.append(getattr(self,k,None))
sql = 'insert into %s (%s) values (%s) ' % (self.__table__, ',' .join(fields),','.join(params))
print('SQL: %s' % sql)
print('ARGS:%s' % attr(args))
当用户定义一个class User(Model)时,Python解释器首先在当前类User的定义中查找metaclass ,如果没有找到,则在父类Model中找metaclass。找到了就用Model中定义的metaclass的ModelMetaclass来创建一个User
类,也就是说metaclass可以隐式地继承到子类。但子类自己感觉不到。
在ModelMetaclass中,要做以下几步:
1. 排除掉队Model类的修改;
2. 在当前类中,比如User中查找定义的类的所有属性,如果找到一个Filed属性,就把它保存到一个mappings
的dict中,同时从类属性中删除该Field属性,否则,容易造成运行时错误。
3. 把表名保存到__table__
中,这里简化为表名默认为类名。
在Model类中,就可以定义各种操作数据库的方法,save()
、update()
小结:
metaclss是Python中非常具有魔术性的对象,它可以改变类创建时的行为,这种强大的功能使用起来务必小心。