数据封装, 继承和多态只是面向对象程序设计中最基础的 3 个概念. 在 Python 中, 面向对象还有很多高级特性, 如: 多重继承, 定制类, 元类等概念.
_slots_
作用: 限制实例的属性.
Python 允许在定义 class 的时候, 定义一个特殊的__slots__变量, 来限制该 class 实例能添加的属性:
- class Student(object):
- __slots__ = ('name', 'age') # 用 tuple 定义允许绑定的属性名称
- >>> s = Student() # 创建新的实例
- >>> s.name = 'Michael' # 绑定属性'name'
- >>> s.age = 25 # 绑定属性'age'
- >>> s.score = 99 # 绑定属性'score'
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- AttributeError: 'Student' object has no attribute 'score'
由于'score'没有被放到__slots__中, 所以不能绑定 score 属性, 试图绑定 score 将得到 AttributeError 的错误.
使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用, 对继承的子类是不起作用的:
- >>> class GraduateStudent(Student):
- ... pass
- ...
- >>> g = GraduateStudent()
- >>> g.score = 9999
除非在子类中也定义__slots__, 这样, 子类实例允许定义的属性就是自身的__slots__加上父类的__slots__.
@property
装饰器 (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 ~ 100!')
- self._score = value
把一个 getter 方法变成属性, 只需要加上 @property 就可以了, 此时,@property 本身又创建了另一个装饰器 @score.setter, 负责把一个 setter 方法变成属性赋值, 于是, 我们就拥有一个可控的属性操作:
- >>> s = Student()
- >>> s.score = 60 # OK, 实际转化为 s.set_score(60)
- >>> s.score # OK, 实际转化为 s.get_score()
- 60
- >>> s.score = 9999
- Traceback (most recent call last):
- ...
ValueError: score must between 0 ~ 100!
注意到这个神奇的 @property, 我们在对实例属性操作的时候, 就知道该属性很可能不是直接暴露的, 而是通过 getter 和 setter 方法来实现的.
还可以定义只读属性, 只定义 getter 方法, 不定义 setter 方法就是一个只读属性:
- 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 和当前时间计算出来.
多重继承
在设计类的继承关系时, 通常, 主线都是单一继承下来的, 例如, Ostrich 继承自 Bird. 但是, 如果需要 "混入" 额外的功能, 通过多重继承就可以实现, 比如, 让 Ostrich 除了继承自 Bird 外, 再同时继承 Runnable. 这种设计通常称之为 MixIn.
为了更好地看出继承关系, 我们把 Runnable 和 Flyable 改为 RunnableMixIn 和 FlyableMixIn. 类似的, 你还可以定义出肉食动物 CarnivorousMixIn 和植食动物 HerbivoresMixIn, 让某个动物同时拥有好几个 MixIn:
- class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
- pass
MixIn 的目的就是给一个类增加多个功能, 这样, 在设计类的时候, 我们优先考虑通过多重继承来组合多个 MixIn 的功能, 而不是设计多层次的复杂的继承关系.
定制类
到类似__slots__这种形如__xxx__的变量或者函数名就要注意, 这些在 Python 中是有特殊用途的.__slots__我们已经知道怎么用了,__len__()方法我们也知道是为了能让 class 作用于 len()函数. 除此之外, Python 的 class 中还有许多这样有特殊用途的函数, 可以帮助我们定制类.
- _str_
- >>> class Student(object):
- ... def __init__(self, name):
- ... self.name = name
- ... def __str__(self):
- ... return 'Student object (name: %s)' % self.name
- ...
- >>> print(Student('Michael'))
- Student object (name: Michael)
这样打印出来的实例, 不但好看, 而且容易看出实例内部重要的数据. 但是直接敲变量不用 print, 打印出来的实例还是不好看:
- >>> s = Student('Michael')
- >>> s
- <__main__.Student object at 0x109afb310>
这是因为直接显示变量调用的不是__str__(), 而是__repr__(), 两者的区别是__str__()返回用户看到的字符串, 而__repr__()返回程序开发者看到的字符串, 也就是说,__repr__()是为调试服务的.
解决办法是再定义一个__repr__(). 但是通常__str__()和__repr__()代码都是一样的, 所以, 有个偷懒的写法:
- class Student(object):
- def __init__(self, name):
- self.name = name
- def __str__(self):
- return 'Student object (name=%s)' % self.name
- __repr__ = __str__
- _iter_
如果一个类想被用于 for ... in 循环, 类似 list 或 tuple 那样, 就必须实现一个__iter__()方法, 该方法返回一个迭代对象, 然后, Python 的 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> 100000: # 退出循环的条件
- raise StopIteration()
- return self.a # 返回下一个值
现在, 试试把 Fib 实例作用于 for 循环:
- >>> for n in Fib():
- ... print(n)
- ...
- 1
- 1
- 2
- 3
- 5
- ...
- 46368
- 75025
- _getitem_
像 list 那样按照下标取出元素, 需要实现__getitem__()方法:
- class Fib(object):
- def __getitem__(self, n):
- a, b = 1, 1
- for x in range(n):
a, b = b, a + b
return a
现在, 就可以按下标访问数列的任意一项了:
- >>> f = Fib()
- >>> f[0]
- 1
- >>> f[1]
- 1
- >>> f[2]
- 2
- >>> f[3]
- 3
- >>> f[10]
- 89
- >>> f[100]
- 573147844013817084101
但是 list 有个神奇的切片方法:
- >>> list(range(100))[5:10]
- [5, 6, 7, 8, 9]
对于 Fib 却报错. 原因是__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
- _getattr_
Python 有一个机制, 那就是写一个__getattr__()方法, 动态返回一个属性:
- class Student(object):
- def __init__(self):
- self.name = 'Michael'
- def __getattr__(self, attr):
- if attr=='score':
- return 99
当调用不存在的属性时, 比如 score,Python 解释器会试图调用
__getattr__(self, 'score')
来尝试获得属性, 这样, 我们就有机会返回 score 的值:
- >>> s = Student()
- >>> s.name
- 'Michael'
- >>> s.score
- 99
返回函数也是完全可以的:
- class Student(object):
- def __getattr__(self, attr):
- if attr=='age':
- return lambda: 25
只是调用方式要变为:
>>> s.age() 25
注意, 只有在没有找到属性的情况下, 才调用__getattr__, 已有的属性, 比如 name, 不会在__getattr__中查找.
此外, 注意到任意调用如 s.abc 都会返回 None, 这是因为我们定义的__getattr__默认返回就是 None. 要让 class 只响应特定的几个属性, 我们就要按照约定, 抛出 AttributeError 的错误:
- class Student(object):
- def __getattr__(self, attr):
- if attr=='age':
- return lambda: 25
- raise AttributeError('\'Student\'object has no attribute \'%s\'' % attr)
- _call_
一个对象实例可以有自己的属性和方法, 当我们调用实例方法时, 我们用 instance.method()来调用.
同样的, 任何类, 只需要定义一个__call__()方法, 就可以直接对实例进行调用. 请看示例:
- class Student(object):
- def __init__(self, name):
- self.name = name
- def __call__(self):
- print('My name is %s.' % self.name)
- >>> s = Student('Michael')
- >>> s() # self 参数不要传入
My name is Michael.
__call__()还可以定义参数. 对实例进行直接调用就好比对一个函数进行调用一样, 所以你完全可以把对象看成函数, 把函数看成对象, 因为这两者之间本来就没啥根本的区别.
如果你把对象看成函数, 那么函数本身其实也可以在运行期动态创建出来, 因为类的实例都是运行期创建出来的, 这么一来, 我们就模糊了对象和函数的界限.
那么, 怎么判断一个变量是对象还是函数呢? 其实, 更多的时候, 我们需要判断一个对象是否能被调用, 能被调用的对象就是一个 Callable 对象, 比如函数和我们上面定义的带有__call__()的类实例:
- >>> callable(Student())
- True
- >>> callable(max)
- True
- >>> callable([1, 2, 3])
- False
- >>> callable(None)
- False
- >>> callable('str')
- False
枚举类
枚举类Enum 是为枚举类型定义一个 class 类型, 然后, 每个常量都是 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 装饰器可以帮助我们检查保证没有重复值.
访问这些枚举类型可以有若干种方法:
- >>> 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
- >>> print(day1 == Weekday(1))
- True
- >>> Weekday(7)
- Traceback (most recent call last):
- ...
- 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
可见, 既可以用成员名称引用枚举常量, 又可以直接根据 value 的值获得枚举常量.
元类
type()
动态语言和静态语言最大的不同, 就是函数和类的定义, 不是编译时定义的, 而是运行时动态创建的.
比方说我们要定义一个 Hello 的 class, 就写一个 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.
- >>> print(type(Hello))
- <class 'type'>
- >>> print(type(h))
- <class 'hello.Hello'>
type()函数可以查看一个类型或变量的类型, Hello 是一个 class, 它的类型就是 type, 而 h 是一个实例, 它的类型就是 class 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 个参数:
class 的名称;
继承的父类集合, 注意 Python 支持多重继承, 如果只有一个父类, 别忘了 tuple 的单元素写法;
class 的方法名称与函数绑定, 这里我们把函数 fn 绑定到方法名 hello 上.
通过 type()函数创建的类和直接写 class 是完全一样的, 因为 Python 解释器遇到 class 定义时, 仅仅是扫描一下 class 定义的语法, 然后调用 type()函数创建出 class.
正常情况下, 我们都用 class Xxx... 来定义类, 但是, type()函数也允许我们动态创建出类来, 也就是说, 动态语言本身支持运行期动态创建类, 这和静态语言有非常大的不同, 要在静态语言运行期创建类, 必须构造源代码字符串再调用编译器, 或者借助一些工具生成字节码实现, 本质上都是动态编译, 会非常复杂.
metaclass
除了使用 type()动态创建类以外, 要控制类的创建行为, 还可以使用 metaclass.
metaclass, 直译为元类, 简单的解释就是:
当我们定义了类以后, 就可以根据这个类创建出实例, 所以: 先定义类, 然后创建实例.
但是如果我们想创建出类呢? 那就必须根据 metaclass 创建出类, 所以: 先定义 metaclass, 然后创建类.
连接起来就是: 先定义 metaclass, 就可以创建类, 最后创建实例.
所以, metaclass 允许你创建类或者修改类. 换句话说, 你可以把类看成是 metaclass 创建出来的 "实例".
来源: http://www.jianshu.com/p/8be53552f413