文章目录
1. 使用__slots__
# 定义一个class:
class Student(object):
pass
# 尝试给实例绑定一个属性:
>>> s = Student()
>>> s.name = 'Michael' # 动态给实例绑定一个属性
>>> print(s.name)
Michael
# 尝试绑定一个方法:
>>> 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
# 给一个实例绑定的方法,对另外一个实例是不起作用的:
>>> s2 = Student() # 创建新的实例
>>> s2.set_age(25) # 尝试调用方法
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'set_age'
# 使用给class绑定方法,可以给所有实例都绑定方法
>>> def set_score(self, score):
... self.score = score
...
>>> Student.set_score = set_score
>>> s.set_score(100) # 给class绑定后,所有实例均可调用
>>> s.score
100
>>> s2.set_score(99)
>>> s2.score
99
- 通常情况下,上面的
set_score
方法可以直接定义在class中,但动态绑定允许我们在程序运行中的过程中动态给class加上功能,这在静态语言中很难实现;
1.1 使用__slots__
- 怎样限制实例的属性?
# 比如只允许对Student实例添加name和age属性,通过在定义class的时候,定义一个特殊的__slots__变量,来达到限制的功能:
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称
>>> s = Student()
>>> s.name = 'Michael'
>>> s.age = 25
>>> s.score = 99 # 由于score没有被放到__slots__中,所以不能绑定score属性,故报错
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'
- 使用
__slots__
要注意,__slots__
定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
>>> class GraduateStudent():
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999
- 除非在子类中也定义
__slots__
,这样,子类就允许定义的属性就是自身的__slots__
加上父类的__slots__
;
2. 使用@property
- 在绑定属性时,直接把属性暴露出去,虽然写起来简答,但是没办法检查函数,导致可以把成绩随便修改:
s = Student()
s.score = 9999
# 这显然不合逻辑,可通过以下方法来限制score的范围:
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 > 1000:
raise ValueError('score must between 0 ~ 1000!')
self._score = value
# 这样就可以随心所欲设置score了:
>>> s = Student()
>>> s.set_score(60)
>>> s.get_score()
60
>>> s.set_score(9999)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "E:/4_Programe/1_Python/3_Code/1_LiaoDaDa/@property.py", line 9, in set_score
raise ValueError('score must between 0 ~ 1000!')
ValueError: score must between 0 ~ 1000!
- 但是这种方法有有点复杂,没有直接用属性这么简单,有没有既能检查参数,又能用类似属性这种简单的方式来访问类的变量呢?
# 前面装饰器(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 # 实际上转化为s.set_score(60)
>>> s.score # 实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "E:\4_Programe\1_Python\3_Code\1_LiaoDaDa\property.py", line 28, in score
raise ValueError('score must between 0 ~ 100!')
ValueError: score must between 0 ~ 100!
- 这个
@property
,我们对实例属性操作的时候,就知道该属性不是直接暴露的,而是通过getter和settter方法来实现的
# 还可以定义只读属性:只定义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和当前时间计算出来;
2.1 练习
- 请利用
@property
给一个Screen
对象加上width
和height
属性,以及一个只读属性resolution
:
class Screen(object):
@property
def width(self):
return self._width
@property
def height(self):
return self._height
@width.setter
def width(self, value):
self._width = value
@height.setter
def height(self, value):
self._height = value
@property
def resolution(self):
return self._width * self._height
# 测试:
s = Screen()
s.width = 1024
s.height = 768
print('resolution =', s.resolution)
if s.resolution == 786432:
print('测试通过!')
else:
print('测试失败!')
resolution = 786432
测试通过!
3. 多重继承
-
回忆一下
Animal
类层次的设计,假设我们要实现下面4中动物:- Dog - 狗狗
- Bat - 蝙蝠
- Parrot - 鹦鹉
- Ostrich - 鸵鸟
-
按照哺乳动物和鸟类归类,可以设计出这样的类的层次:
-
按照‘能跑’和‘能飞’归类,可以设计出这样的类的层次:
-
如果把上面的分类都包括进来,就得设计很多的层次:
- 哺乳类:能跑的哺乳类,能飞的哺乳类;
- 鸟类:能跑的鸟类,能飞的鸟类。
- 如果再增加宠物类和非宠物类,类的数量就会呈现指数级增长,这样设计明显是不行的;
# 正确的方法是采用多重继承,首先,主要的类层次依旧按照哺乳类和鸟类设计
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
# 给动物加上Runnable和Flyable的功能:
class Runnable(object): # 只需先定义好Runnable和Flyable的类:
def run(self):
print('Running...')
class Flyable(object):
def fly(self):
print('Flying...')
# 对于需要Runnable功能的动物,就多继承一个Runnable,如:
class Dog(Mammal, Runnable):
pass
# 对于需要Flyable功能的动物,就多继承一个Flyable,如:
class Bat(Mammal, Flyable):
pass
# 通过多重继承,一个子类就可以同时拥有多个父类的所有功能
3. 1 Mixln
- 通常设计类的继承关系时,主线都是单一继承下来的,如:
Ostrich
继承自Bird
,但是多重继承,可以让Ostrich
除了继承自Bird
外,再同时继承Runnable
。这种设计通常称之为MixIn。 - 可以把
Runnable
和Flyable
改为RunnableMixIn
和FlyableMixIn
。类似的,还可以定义出肉食动物CarnivorousMixIn
和植食动物HerbivoresMixIn
,让某个动物同时拥有好几个MixIn:
class Dog(Mammal, RunnableMixIn, CarnivorousMixIn):
pass
- MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。
- Python自带的很多库也使用了MixIn。如:Python自带了
TCPServer
和UDPServer
这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn
和ThreadingMixIn
提供。通过组合,我们就可以创造出合适的服务来。
# 编写一个多进程的TCP服务:
class MyTCPServer(TCPServer, ForkingMixIn):
pass
# 编写一个多线程模式的UDP服务:
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
# 编写一个协程模型:
class MyTCPServer(TCPServer, CoroutineMixIn):
pass
- 这样不需要复杂而庞大的继承链,只需选择组合不同的类的功能,就可以快速的构造出所需的子类
4. 定制类
- 看到类似
__slots__
这种形如__xxx__
的变量名或者函数名就要注意,在Python中他们是有特殊用途的,python的class中还有很多这样有特殊用途的函数,来帮助我们定制类;
4.1 __str__
# 先定义一个Student类,打印一个实例:
>>> class Student(object):
... def __init__(self, name):
... self.name = name
...
>>> print(Student('Michael'))
<__main__.Student object at 0x000001B73BE877F0> # 打印出一大堆这个东西,并不好看
# 定义__init__()方法,返回一个好看的字符串
>>> 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 # 这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者的区别就是前者返回的是用户看待的字符串,后者返回的是开发者看到的字符串,也就是说__repr__()是为调试服务的
<__main__.Student object at 0x000001B73BE877F0>
# 解决办法就是再定义一个__repr__(),但是通常这两个代码是一样的,但是有个偷懒的方法:
class Student(object):
def __init__(self, name):
self.name = name
def __str__(self):
return 'Student object (name=%s)' % self.name
__repr__ = __str__
4.2 __iter__
- 如果一个类想要被用于
for...in
循环,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用带迭代对象的__next__()
方法拿到循环的下一个值,知道遇到StopIteration
错误时退出循环。
# 使用__iter__方法,以斐波那契数列,写一个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 StopAsyncIteration()
return self.a # 返回下一个值
#试试将Fib实例作用于for循环:
>>> for n in Fib():
... print(n)
...
1
1
2
3
5
...
46368
75025
4.3 __getitem__
# Fib实例作用于for循环,看起来和list有点像,但是当做list来使用还真不行:
>>> Fib()[5] # 取第5个元素
Traceback (most recent call last):
File "E:\1_Install_Total\8_Anaconda\lib\site-packages\IPython\core\interactiveshell.py", line 2961, in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
File "<ipython-input-3-14290f619470>", line 1, in <module>
Fib()[5]
TypeError: 'Fib' object does not support indexing
# 通过__getitem__方法,来实现像list按照下标取出元素:
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[100]
573147844013817084101
- list中有个神奇的切片,对于Fib却报错,原因是
__getiten__
传入的可能是一个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
>>> f = Fib()
>>> f[0:5]
[1, 1, 2, 3, 5]
>>> f[:10]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
# 但是没有对step参数做处理:
>>> f[:10:2]
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
# 也没有对负数进行处理,所以要正确实现一个__getitem__还有很多工作要干
- 此外,如果把对象看成
dict
,__getitem__()
的参数也可能是一个可以作key的object,例如str
。 - 与之对应的是
__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。 - 总之,通过上面的方法,我们自己定义的类表现的和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的鸭子类型,不需要强制继承某个接口
4.4 __getattr__
# 当我们调用不存在的类的方法或者属性时,就会报错,如定义Student类
class Student(object):
def __init__(self):
self.name = 'Michael'
>>> s = Student() # 调用name属性,没问题
>>> print(s.name)
Michael
>>> print(s.score) # 调用不存在的score属性,报错
Traceback (most recent call last):
...
AttributeError: 'Student' object has no attribute 'score'
- 要避免这个错误,除了加上一个
score
属性外,还可以另外写一个__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
# 调用方式变为:
>>> a.age()
25
# 注意到任意调用s.abc都会返回None,这是因为我们定义的__getattr__默认返回就是None,要让class只响应特定的几个属性,就要抛出AttributeError的错误:
class Student(object):
def __getattr__(self, attr):
if attr == 'age':
return lambda: 25
raise AttributeError('\'Student\' has no attribute \'%s\'' % attr)
- 这实际上可以把一个类的所有属性和方法调用全部动态化处理了,那这种完全动态调用的处理有什么实际作用呢?那就是可以针对完全动态的情况作调用;
4.5 __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()函数,就可以判断一个对象是否是“可调用”对象
>>> callable(Student())
True
>>> callable(max)
True
>>> callable([1, 2, 3])
False
>>> callable(None)
False
>>> callable('str')
False
5. 使用枚举类
- 定义常量时,方法之一是用大写变量通过整数定义,如下,好处是简单,但缺点是类型是
int
,并且仍然是变量:
JAN = 1
FEB = 2
MAR = 3
…
NOV = 11
DEC = 12
- 更好的方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了
Enum
类来实现这个功能:
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开始计数
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
# 同时如果需要更精确的枚举类型,可以从Enum派生出自定义类:
from enum import Enum, unique
@unique # @unique装饰器可以帮助我们检查保证没有重复值
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
# 访问这些枚举类型可以有很多种方法:
>>> 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的值获得枚举常量
5.1 练习
- 把
Student
的gender
属性改造为枚举类型,可以避免使用字符串:
# -*- coding: utf-8 -*-
from enum import Enum, unique
@unique
class Gender(Enum):
Male = 0
Female = 1
class Student(object):
def __init__(self, name, gender):
self.name = name
self.gender = gender
# 测试:
bart = Student('Bart', Gender.Male)
if bart.gender == Gender.Male:
print('测试通过!')
else:
print('测试失败!')
6. 使用元类
6.1 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
是一个实例,他的类型就是calssHello
。- calss的定义是运行时通过
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
上
6.1 metaclass
- 当定义类以后,可以根据这个类创建出实例,即:先定义类,后创建实例;
- 如果想创建出类,就是:先定义metaclass,然后创建类;即:先定义metaclass,创建类,然后创建实例;