类动态绑定方法与限定实例属性
类动态绑定方法
前面我们说了如何给类动态的添加属性,那么如何动态绑定方法呢?如下示例:
class A(object):
pass
# 第一种方式给一个实例绑定,但是对另外一个实例不起作用
a = A()
def add(self,a):
self.a = a
from types import MethodType
# 给实例绑定一个方法
a.add = MethodType(add,a)
a.add(1)
print(a.a)
def reduce(self,b):
self.b = b
# 为了给所有实例都绑定方法,可以给class绑定方法
A.reduce = reduce
# 测试
a1 = A()
a2 = A()
a1.reduce(10)
a2.reduce(20)
print(a1.b)
print(a2.b)
# 结果
1
10
20
限定实例属性
我们想要限制实例的属性怎么办?比如,只允许对A实例添加name和age属性。
为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的slots变量,来限制该class实例能添加的属性,使用slots要注意,slots定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:
class A(object):
__slots__ = ('a','b')
a = A()
a.a = '12306'
a.b = 120
a.c = 156
# 结果:
Traceback (most recent call last):
File "F:/Pythonproject/test_dir/__init__.py", line 674, in <module>
a.c = 156
AttributeError: 'A' object has no attribute 'c'
@property为属性检查参数
在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,导致可以把属性值随便改,这个时候,我们可以用set和get方法来做检查:
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
s = Student()
# 现在,对任意的Student实例进行操作,就不能随心所欲地设置score了
s.set_score(999)
上面实现了这个功能,但是我如果要做到既能检查参数,又可以用类似属性这样简单的方式来访问类的变量,可以用@property这个内置的装饰器,装饰器(decorator)可以给函数动态加上功能:
class Student(object):
# 把一个getter方法变成属性,只需要加上@property就可以了,这个时候它就是一个属性
@property
def score(self):
return self._score
# @property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值
@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
s = Student()
# 现在就做到了既能检查参数,又可以用类似属性这样简单的方式来访问类的变量
s.score = 999
# 还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性
# birth是可读写属性,而age就是一个只读属性,因为age可以根据birth和当前时间计算出来。
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
多继承
在Java里面我们知道是没有多继承的,但是可以通过接口的多实现来弥补这个空缺,在Python中我们就有多继承,那么为什么需要多继承呢?我们可以这么思考:蝙蝠,我们知道它是鸟类,我们设计蝙蝠这么一个类让他继承鸟类,但是我们还知道它是哺乳动物,我们可以能会这么设计,鸟类下面分哺乳类鸟类,哺乳类鸟类下面分有羽毛的和无羽毛的。。。这样一直下去,类的数量会呈指数增长,很明显这样设计是不行的。所以,就出现了多继承,蝙蝠这个类继承多种不同的类,拥有了不同的功能。多继承可以如下:
class A(object):
pass
class B(object):
pass
class C(A,B):
pass
MixIn
多继承的时候可能会出现这么一个问题,蝙蝠这个类是无羽毛会飞的哺乳动物,设计这个类的时候回继承鸟类,有羽毛类,哺乳类这三个类。但是在有时候会一不小心的出现继承的这三个类中,其中有一个类的功能不属于蝙蝠,那么问题就来了。如何避免呢?我们在设计类的时候尽量在继承关系上针对蝙蝠这个对象的特征去继承相应的类。
在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。
Python自带的很多库也使用了MixIn。举个例子,Python自带了TCPServer和UDPServer这两类网络服务,而要同时服务多个用户就必须使用多进程或多线程模型,这两种模型由ForkingMixIn和ThreadingMixIn提供。通过组合,我们就可以创造出合适的服务来。
# 多进程模式的TCP服务
class MyTCPServer(TCPServer, ForkingMixIn):
pass
# 多线程模式的UDP服务
class MyUDPServer(UDPServer, ThreadingMixIn):
pass
对象的常用方法
str
在Java中我们可以使用obj.toString的方法打印对象,并且可以重写这个方法来规范打印的格式,在Python中如何操作呢?我们有__str__
方法:
class A:
def __init__(self, name):
self.name = name
# 不重写这个方法的时候打印<__main__.A object at 0x000000000067CF28>
def __str__(self):
return "A的名字是:" + self.name
# __str__()和__repr__(),两者的区别是__str__()返回用户看到的字符串,
# 而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。
__repr__ = __str__
print(A('李白'))
# 结果
A的名字是:李白
iter与getitem
如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()
方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()
方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环:
class Fib:
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 > 1000:
raise StopIteration
return self.a
# 要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:
def __getitem__(self, item):
# 判断item是索引
if isinstance(item, int):
a, b = 1, 1
for x in range(item):
a, b = b, a + b
return a
# item是切片
if isinstance(item, slice):
start = item.start
stop = item.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
print(Fib()[1])
print(Fib()[0:5])
for n in Fib():
print(n)
上面代码没有对step参数作处理,也没有对负数作处理,所以,要正确实现一个__getitem__()
还是有很多工作要做的。
此外,如果把对象看成dict,__getitem__()
的参数也可能是一个可以作key的object,例如str。
与之对应的是__setitem__()
方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()
方法,用于删除某个元素。
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
getattr
我们在调用对象中不存在的属性的时候回报错,要避免这个错误,除了可以加上一个score属性外,Python还有另一个机制,那就是写一个__getattr__()
方法,动态返回一个属性。
class A:
def __getattr__(self, item):
if item == 'a':
return 100
if item == 'b':
return lambda: 88 # 返回函数也可以
# 注意,只有在没有找到属性的情况下,才调用__getattr__,已有的属性,比如a,不会在__getattr__中查找。
print(A().a)
print(A().b)
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。
这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。
比如这种情况,我们要拼凑URL的时候,给每个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__
# 因为没有以下status user timeline等这些变量,所以不停的调用__getattr__方法
print(Chain().status.user.timeline.list)
#结果
/status/user/timeline/list
call
一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。任何类,只需要定义一个__call__()
方法,就可以直接对实例进行调用
class A:
def __call__(self, *args, **kwargs):
print('我是call')
class B:
pass
a = A()
# __call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,
# 所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。
a()
# 怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,
# 能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例
print(callable(A()))
print(callable(B()))
枚举
来看看如何定义枚举,又如何使用枚举:
from enum import Enum, unique
Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))
# value属性则是自动赋给成员的int常量,默认从1开始计数。
for name, member in Month.__members__.items():
print(name, '=>', member, ',', member.value)
# 如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:
# @unique装饰器可以帮助我们检查保证没有重复值。
@unique
class Weekday(Enum):
Sun = 0 # Sun的value被设定为0
Mon = 1
Tue = 2
Wed = 3
Thu = 4
Fri = 5
Sat = 6
# 既可以用成员名称引用枚举常量,又可以直接根据value的值获得枚举常量。
print(Weekday.Sun)
print(Weekday['Wed'])
print(Weekday.Fri.value)
print(Weekday(1))
网友评论