Python面向对象高级编程
参考原文
PS:数据封装、继承和多态只是OOP中最基础的3个概念。在Python中,面向对象还有很多高级的特性,我们会讨论多重继承、定制类、元类等概念。
动态语言的灵活性
正常情况下,当我们定义了一个class,创建了该类的实例后,我们可以给该实例绑定任何属性和方法,这就是动态语言的灵活性。先定义一个类:
class Student(object): pass
然后给一个实例绑定一个属性:
s = Student() s.name = 'Alice' print(s.name) #result Alice
还可以为实例绑定一个方法:
>>> 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
也可以为类动态添加方法使所有实例均可调用:
>>> 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
但我们不一定想要它“随心所欲”,如果我们想要限制实例的属性怎么办?--使用__slots__。
__slots__
为了达到限制目的,Python允许在定义class的时候,定义一个特殊的变量__slots__来限制该class的实例能添加的属性、方法。如:
class Student(object): __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性、方法名称
试试:
>>> s = Student() >>> s.name = 'Alice' >>> s.score = 12 Traceback (most recent call last): File "<pyshell#4>", line 1, in <module> s.score = 12 AttributeError: 'Student' object has no attribute 'score'
Tips:__slots__定义的属性仅对当前实例起作用,对继承的子类是无效的,除非在子类也定义__slots__
@property
我们之前说过,可以将属性设置成私有的,然后通过一个方法来进行操作该属性,这样就可以检查参数的有效性。如
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:
>>> s = Student() >>> s.set_score(60) # ok! >>> s.get_score() 60 >>> s.set_score(9999) Traceback (most recent call last): ... ValueError: score must between 0 ~ 100!
但这样做又难免复杂,不符合Python简洁的定义,倒和C#这种“严格”的语言差不多了,那有没有既能检查参数,又可以用类似属性这样简单的方式来访问累的变量呢?对于追求完美的Python程序员来说,这是必须的!回想下,已经学过的知识,似乎装饰器(decorator)可以给函数动态加上功能。对于类的方法,装饰器一样起作用。
Python内置的@property装饰器就是负责把一个方法变成属性调用,如:
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 and 100') self._score = value
要把一个方法变成属性,只需在get方法上加上@property,此时@property本身又自动创建了另一个装饰器@属性名.setter,用于对属性赋值。此时,我们就拥有了一个可控的属性操作:
>>> s = Student() >>> s.score = 60 #ok,实际调用形如s.set_score(60) >>> s.score #s实际调用形如s.get_score() 60 >>> s.score = 101 Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> s.score = 101 File "<pyshell#1>", line 12, in score raise ValueError('score must between 0 and 100') ValueError: score must between 0 and 100
Tips:当我们看到@property,就应该知道该属性不是直接进行操作的,而是通过getter、setter方法来实现的,也可以只定义只读属性(不定义setter方法 )。
多重继承
前面已经说过继承了,通过继承,子类可以获得父类的全部功能。但如果子类还想获得更多的功能怎么办呢?除了扩展自己的特色方法外,还可以通过多重继承来获得多个父类的功能。如:
class Runable(object): def run(self): print('Runnng...') class Eatable(object): def eat(self): print('Eating...') class Dog(Runable, Eatable): pass dog = Dog() dog.run() dog.eat() ''' Runnng... Eating... '''
MixIn
在设计类的继承关系时,通常主线都是单一继承下来的,如果要加入额外的功能可以通过多重继承来实现。让A继承A1,同时继承A2,这种设计通常称之为MixIn。
这样就可以把Runable和Eatable改成RunbleMixIn和EatableMixIn了,这样就更加了然了。在Python中自带的很多库也使用了MixIn。举个例子,Python中自带了TCPServer和UDPServer这两种网络服务,而要同时服务多个用户就必须使用多进程和多线程模型,这两种模型分别由ForkingMixIn和ThreadingMixIn提供。通过组合就可以创造出合适的服务出来了,如编写一个多进程的TCP服务:
class MyTCPServer(TCPServer, ForkingMixIn): pass
多线程的UDP服务:
class MyUDPServer(UDPServer, ThreadingMixIn): pass
Tips:由于Python允许多重继承,所以MixIn是一种常见的设计,而只允许单一继承的语言(如Java)不能使用MixIn设计。
定制类
定制类,就是通过一些特殊的方法来使我们的类具有特殊的功能来应对某些特定的场合。前面我们已经说过了一些特殊的变量或函数名(形如__xx__)如__slots__和__len__()。接下来我们就要说一些特殊的方法了。
__str__
为了说明__str__的作用,我们先定义一个Student类,并打印出一个实例:
>>> class Student(object): def __init__(self, name): self.name = name >>> print(Student('Alice')) <__main__.Student object at 0x000001DFB4FC1DA0>
这打印出来的字符串明显不好看,这时__str__()方法就可以派上用场了:返回一个好看的字符串:
>>> class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name: %s)' % self.name >>> print(Student('Alice')) Student object (name: Alice) >>>
这样打印出来的实例不但好看,而且容易看出实例内部的重要数据。但是在Python解释器下,直接敲变量打印出来的实例还是和原来一样不好看:
>>> s = Student('Alice') >>> s <__main__.Student object at 0x000001DFB4FC1D30>
为什么呢?这是因为直接敲变量调用的不是__str()__,而是__repr__(),前者是返回给用户看的字符串,后者是返回给程序开发者看到的字符串,也就是说__repr__()是为调试服务的。解决的办法是再定义一个__repr__(),但是通常下__str()__和__repr__()代码是一样的,所以可以偷个懒直接使--repr__ = __str__:
class Student(object): def __init__(self, name): self.name = name def __str__(self): return 'Student object (name=%s)' % self.name __repr__ = __str__
__iter__
我们已经知道list或tuple的数据类型可以被用于for ... in ... 循环,那么如果类也想被用于for .. in ... 循环,该怎么办呢?那就是实现__iter()__方法,该方法返回一个迭代对象,for循环不断调用该迭代对象的__next()__方法拿到循环的下一个值,知道遇到StopIteration错误时退出循环。
我们以斐波那契数为例,写一个Fib类,用作for循环:
class Fib(object): def __init__(self): self.a, self.b = 0, 1 #初始化两个计数器a,b def __iter__(self): return self #实例本身就是迭代对象,故返回自己 def __next__(self): self.a, self.b = self.b, self.a + self.b #计算下一个值 if self.a > 100: # 退出循环 raise StopIteration() return self.a #返回下一个值 for n in Fib(): print(n) ''' 1
1 2 3 5 8 13 21 34 55 89 '''
__getitem__
上面的Fib实例虽然能用作for循环了,但把它当成list来使用还是不行的,比如按索引取元素:
>>> Fib()[5] Traceback (most recent call last): File "<pyshell#29>", line 1, in <module> Fib()[5] TypeError: 'Fib' object does not support indexing
此时如果要表现得想lsit那样按索引取元素,就需要实现__getitem__()方法了:
>>> class Fib(object): def __getitem__(self, n): a, b = 1, 1 for x in range(n): a, b = b, a + b return a >>> Fib()[9] 55
试试list的切片:
>>> Fib()[1:3] Traceback (most recent call last): File "<pyshell#33>", line 1, in <module> Fib()[1:3] File "<pyshell#31>", line 4, in __getitem__ for x in range(n): TypeError: 'slice' object cannot be interpreted as an integer
报错是因为__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice所以要做判断:
class Fib(object): def __getitem__(self, n): if isinstance(n, int): # n是索引 a, b = 1, 1 for x in range(n): a, b = b, a + b return a if isinstance(n, slice): # n是切片 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
再试试切片:
>>> Fib()[0:5]
[1, 1, 2, 3, 5]
但还是却没对step参数作处理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
所以要实现一个完整的__getitem__()还是要很多工作要做的。
Tips:如果把对象看成一个dict,那么__getitem__()的参数也可能是一个可以作为key的对象如str,与之对应的是__setitem_()方法,把对象视作为list来赋值,还有一个__delitem__(),用于删除某个元素。所以我们可以通过定义特殊的方法来使自己定义的类表现得和Python自带的list、tuple、dict一样(Python动态语言的“鸭子类型”)。
__getattr__
我们知道正常情况下,当调用类不存在的属性或方法时,就会报错:
>>> class Student(object): pass >>> s = Student() >>> s.name Traceback (most recent call last): File "<pyshell#40>", line 1, in <module> s.name AttributeError: 'Student' object has no attribute 'name'
我们可以避免这个错误,除了加上这属性外,Python还有另一个机制,那就是实现一个__getattr__()方法,动态返回一个属性。修改上面的代码如下:
>>> class Student(object): def __getattr__(self, attr): if attr == 'name': return 'Alice' >>> s = Student() >>> s.name 'Alice'
Tips:也可以返回函数,只有在没有找到属性的情况下,才调用__getattr__,已有的属性不会在__getattr__中查找。
当我们实现了__getattr__方法后,调用任何实例没有的属性都会返回None,这是因为我们定义的__getattr__()方法默认返回None,要让类只响应几个特点的属性,对于其他的属性,我们就要按照约定,抛出AttributeError的错误:
>>> class Student(object): def __getattr__(self, attr): if attr == 'name': return 'Alice' raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) >>> s = Student() >>> s.age Traceback (most recent call last): File "<pyshell#49>", line 1, in <module> s.age File "<pyshell#47>", line 6, in __getattr__ raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr) AttributeError: 'Student' object has no attribute 'age
这实际上可以把一个类的所有属性和方法调用全部动态化处理,不需要其他的特殊手段。这种完全动态调用的特性有什么实际的作用呢?作用就是可以针对完全动态的情况来调用。如要写SDK,如果给每个URL对应的API都写一个方法,那太困难了,而且API一旦改变SDK也要改。所以此时可以利用完全动态的__getattr__来写一个链式调用:
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'
__call__
我们知道一个对象额实例可以有自己的属性和方法,我们可以用instance.method()来调用,那么能不能直接在实例本身上调用呢?OK,只需要实现__call__()方法。请看示例:
>>> class Student(object): def __init__(self, name): self.name = name def __call__(self): print('My name is %s' % self.name) >>> s = Student('Alice') >>> s() My name is Alice
__call__()还可以定义参数相当于对一个函数进行调用,所以你完全可以把对象看成函数,函数看成对象,这2者本来就没有根本的区别。如果你把对象看成函数,那么函数本身也可以在运行期动态地创建出来(类的实例都是运行期创建出来的)。
Tips:通过callable()函数,可以判断一个对象是否是“可调用”对象。
枚举类
我们应该或多或少都知道一点关于枚举的知识,枚举就是列举一个可数集合的元素。如,人的性别可以看成一个集合,通过枚举,可以拿到‘男’和‘女’。在Python中,提供了Enum类来实现枚举这个功能:
from enum import Enum Sex = Enum('Sex', ('Male', 'Female'))
我们可以直接使用Sex.Male来引用一个常量:
>>> Sex.Male
<Sex.Male: 1>
也可以枚举出它的所有成员:
>>> for name, member in Sex.__members__.items(): print(name, '=>', member, ',', member.value) Male => Sex.Male , 1 Female => Sex.Female , 2
注意:value属性是自动赋给成员的int常量,默认从1开始计数。
我们也可以从Enum派生出自定义类,用于更精确地控制:
from enum import Enum, unique @unique #@unique装饰器可以帮我们检查保证有没有重复值 class Sex(Enum): Male = 0 #Male的value被设定为0 FeMale = 1
for name, member in Sex.__members__.items(): print(name, '=>', member, ',', member.value) ''' Male => Sex.Male , 0 FeMale => Sex.FeMale , 1 '''
访问这些枚举类型可以有若干种方法:
>>> male = Sex.Male >>> print(male) Sex.Male >>> male = Sex['Male'] >>> print(male) Sex.Male >>> male = Sex(0) >>> print(male) Sex.Male >>>
Tips:枚举常量既可以使用成员名称引用,又可以直接根据value的值获取。
元类
type()
我们应该知道动态语言和静态语言最大的不同,就是函数和类的定义不是在编译时定义的,而是在运行是动态创建的。比如说我们要定义一个Hello的类,就写一个hello.py的模块:
class Hello(object): def hello(self, name='world'): print('Hello,%s' % name)
当Python解释器载入hello模块时,就会依次执行该模块的所有语句,执行结果就是动态创建出一个Hello的class对象,测试如下:
>>> from hello import Hello >>> h = Hello() >>> h.hello() Hello,world
type()函数可以查看一个类型或变量的类型,Hello是一个class,它的类型就是type,而h是一个实例,它的类型是class Hello:
>>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class 'hello.Hello'>
我们已经说过了class的定义是运行时动态创建的,而创建class的方法就是使用type()函数。那么type()函数就既可以返回一个对象的类型,又可以创建出新的类型。所以我们就应该可以通过type()函数创建出hello类,而不需通过class Hello(object)的定义。试试:
>>> def fn(self, name='world'): #先定义函数 print('Hello, %s.' % name) >>> Hello = type('Hello', (object,), dict(hello=fn)) #创建出Hello class >>> h = Hello() >>> h.hello() Hello, world. >>> print(type(Hello)) <class 'type'> >>> print(type(h)) <class '__main__.Hello'>
要创建一个class对象,type()函数依次传入3个参数:1.class的名称 2.继承的父类集合,如果只有一个父类,别忘了tuple的单元素写法 。 3.class的方法的名称与已定义的函数绑定(这里,我们把函数fn绑定到hello上)。
Tips:通过type函数创建出来的类和直接写class是一样的,本质上都是通过type()函数创建出class。动态语言本身支持运行期动态创建类,而如果要在静态语言运行期间创建类,必须构造源代码字符创再调用编译器,或者借助一下工具生成字节码实现,会非常复杂,但本质都是动态编译。
metaclass(元类)
前面已经说过了type()可以动态地创建类,但除此之外,还可以使用metaclass以控制类的创建行为。什么是metaclass呢?简单的解释就是:类是metaclass创建出来的“实例”,metaclass是类的“模板”。使用metaclass时,就先定义metaclass,再创建类,最后创建出实例。
那metaclass到底有什么用呢?不急,我们先来看一个简单的例子,先定义出一个简单的metaclass(用来干啥?不急,先定义出来。),定义ListMetaclass(元类默认以‘Metaclass’结尾):
#metaclass是类的模板,所以必须从‘type’类型派生 class ListMetaclass(type): def __new__(cls, name, bases, attrs): attrs['add'] = lambda self, value: self.append(value) return type.__new__(cls, name , bases, attrs)
有了这个元类,我们再定义一个普通的类,指示使用元类来定制类(传入关键字参数metaclass):
class MyList(list, metaclass=ListMetaclass): pass
这样,magic就生效了,它指示Python解释器在创建MyList时,要通过ListMetaclass.__new__()来创建。这样,就可以定制MyList类了,比如加上新的方法add。来说下__new__()方法,该方法一共接收4个参数,分别是:1.当前准备创建类的对象 2.类的名字 3.类继承的父类集合 4.类的方法集合。
此时,应该明白ListMetaclass代码的含义了:将需定制类的add方法(没有就创建)修改为“为实例对象添加值”,并返回给MyList类。来测试下:
>>> L = MyList() >>> L.add(1) >>> L [1]
好想是很magic,但为什么要这样呢?动态修改有什么意义呢?直接在类定义上add()不是更简单吗?正常情况下,确实如此,但是总会遇到需要通过metaclass修改类的定义的,如ORM。
那问题又来了,什么是ORM?学过数据库或者用过数据库的应该知道,ORM全称“Object Relation Mapping”,即对象-关系映射。简单的来说,就是把关系数据库的一行映射成为一个对象,一个类对应成一张表。
所以为了说明metaclass的强大之处,让我们来尝试编写一个ORM框架吧。
1.编写底层模块的第一步,就是先把调用接口写出来。比如,使用者如果使用这个ORM框架,想定义一个User类来操作对应数据表User,使用者应该写出这样的代码来调用:
class User(Model): #定义类的属性到列的映射 id = IntegerField('id') name = StringField('username') email = StringField('email') password = StringField('password') # 创建一个实例: u = User(id=123, name ='Alice', email='xxx@orm.org', password='xxxx') #保存到数据库 u.save()
其中,父类Model和属性类型StringField、IntegerField由ORM框架提供,剩下的魔术方法如save()全部由metaclas自动完成。这样metaclass的编写虽然会比较复杂,但ORM的使用者调用起来却十分简单。
2.现在就按照上面的接口定义,来实现该ORM,我们先定义一个最基本的Field类用于保存数据库表中的字段名和字段类型:
class Field(object): def __init__(self, name, column_type): self.name = name self.column_type = column_type def __str__(self): return '<%s:%s>' % (self.__class__.__name__, self.name)
3.有了最基本的Field定义,我们就可以扩展定义各种类型的Field了,如StringField、IntegerField等。
class StringField(Field): def __init__(self, name): super(StringField, self).__init__(name,'varchar(100)') #调用父类的__init__方法 class IntegerField(Field): def __init__(self, name): super(IntegerField, self).__init__(name, 'bigint') #调用父类的__init__方法
4.编写ModelMetac元类用于定制基类Model及基类:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
class ModelMetaclass(type): def __new__(cls, name, bases, attrs): if name =='Model': #不对Model类进行修改 return type.__new__(cls, name, bases, attrs) print('Found model: %s' % name) mappings = dict() for k, v in attrs.items(): #在当前类查找出定义的类的所有属性,保存到mappings中 if isinstance(v, Field): print('Found mapping: %s ==> %s' %(k, v)) mappings[k] = v for k in mappings.keys(): #从类的属性中删除该Field属性,否则容易造成运行错误(实例的属性会遮住类的同名属性) attrs.pop(k) attrs['__mappings__'] = mappings #保存属性和列的映射关系 attrs['__table__'] = name #将表名和类名设置成一样的 return type.__new__(cls, name, bases, attrs) 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' % str(args))
5.ok,开始调用:
# 创建一个实例: u = User(id=12345, name='Michael', email='test@orm.org', password='my-pwd') #保存到数据库 u.save() ''' Found model: User Found mapping: id ==> <IntegerField:id> Found mapping: name ==> <StringField:username> Found mapping: email ==> <StringField:email> Found mapping: password ==> <StringField:password> SQL: insert into User (id,username,email,password) values (?,?,?,?) ARGS: [12345, 'Michael', 'test@orm.org', 'my-pwd'] '''
成功了,好像还可以啊(溜了~~~)。
Tips:metaclass是Python非常具有魔术性的对象,他可以改变类创建时的行为,这么牛逼的功能使用起来还是要小心点的。