用特殊方法定制类
前面我们讲了方法的两个重要方面:首先,方法必须在调用前被绑定(到它们相应类的某个实例中);其次,有两个特殊方法可以分别作为构造器和解构器的功能,分别名为__init__()和__del__()。
事实上,__init__()和__del__()只是可自定义特殊方法集中的一部分。它们中有一些有预定义的默认行为,而其他一些则没有,留到需要的时候去实现。这些特殊方法是Python中用来扩充类的强有力的方式,它们可实现:
模拟标准类型
重载草所附
特殊方法允许类通过重载标准操作符+,*,甚至包括分段下标及映射操作符操作[]来模拟标准类型。如同其他很多保留标识符,这些方法都是以双下划线开始以及结尾的。
以下是方法摘抄:
基本定制型 描述
C.__init__(self[, arg1, ...]) 构造器(带一些可选的参数)
C.__new__(self[, arg1, ...]) 构造器(带一些可选的参数);通常用在设置不变数据类型的子类。
C.__del__(self) 解构器
C.__str__(self) 可打印的字符输出;内建 str()及 print 语句
C.__repr__(self) 运行时的字符串输出;内建 repr() 和‘‘ 操作符
C.__unicode__(self) Unicode 字符串输出;内建 unicode()
C.__call__(self, *args) 表示可调用的实例
C.__nonzero__(self) 为 object 定义 False 值;内建 bool() (从 2.2 版开始)
C.__len__(self) “长度”(可用于类);内建 len()
对象(值)比较
C.__cmp__(self, obj) 对象比较;内建 cmp()
C.__lt__(self, obj) and 小于/小于或等于;对应<及<=操作符
C.__gt__(self, obj) and 大于/大于或等于;对应>及>=操作符
C.__eq__(self, obj) and 等于/不等于;对应==,!=及<>操作符
属性
C.__getattr__(self, attr) 获取属性;内建 getattr();仅当属性没有找到时调用
C.__setattr__(self, attr, val) 设置属性
C.__delattr__(self, attr) 删除属性
C.__getattribute__(self, attr) 获取属性;内建 getattr();总是被调用
C.__get__(self, attr) (描述符)获取属性
C.__set__(self, attr, val) (描述符)设置属性
C.__delete__(self, attr) (描述符)删除属性
定制类/模拟类型
数值类型:二进制操作符
C.__*add__(self, obj) 加;+操作符
C.__*sub__(self, obj) 减;-操作符
C.__*mul__(self, obj) 乘;*操作符
C.__*div__(self, obj) 除;/操作符
C.__*truediv__(self, obj) True 除;/操作符
C.__*floordiv__(self, obj) Floor 除;//操作符
C.__*mod__(self, obj) 取模/取余;%操作符
C.__*divmod__(self, obj) 除和取模;内建 divmod()
C.__*pow__(self, obj[, mod]) 乘幂;内建 pow();**操作符
C.__*lshift__(self, obj) 左移位;<<操作符
C.__*rshift__(self, obj) 右移;>>操作符
C.__*and__(self, obj) 按位与;&操作符
C.__*or__(self, obj) 按位或;|操作符
C.__*xor__(self, obj) 按位与或;^操作符
数值类型:一元操作符
C.__neg__(self) 一元负
C.__pos__(self) 一元正
C.__abs__(self) 绝对值;内建 abs()
C.__invert__(self) 按位求反;~操作符
数值类型:数值转换
C.__complex__(self, com) 转为 complex(复数);内建 complex()
C.__int__(self) 转为 int;内建 int()
C.__long__(self) 转为 long;内建 long()
C.__float__(self) 转为 float;内建 float()
数值类型:基本表示法(String)
C.__oct__(self) 八进制表示;内建 oct()
C.__hex__(self) 十六进制表示;内建 hex()
数值类型:数值压缩
C.__coerce__(self, num) 压缩成同样的数值类型;内建 coerce()
C.__index__(self) 在有必要时,压缩可选的数值类型为整型(比如:用于切片索引等等)
序列类型
C.__len__(self) 序列中项的数目
C.__getitem__(self, ind) 得到单个序列元素
C.__setitem__(self, ind,val) 设置单个序列元素
C.__delitem__(self, ind) 删除单个序列元素
C.__getslice__(self, ind1,ind2) 得到序列片断
C.__setslice__(self, i1, i2,val) 设置序列片断
C.__delslice__(self, ind1,ind2) 删除序列片断
C.__contains__(self, val) 测试序列成员;内建 in 关键字
C.__*add__(self,obj) 串连;+操作符
C.__*mul__(self,obj) 重复;*操作符
C.__iter__(self) 创建迭代类;内建 iter()
映射类型
C.__len__(self) mapping中的项的数目
C.__hash__(self) 散列(hash)函数值
C.__getitem__(self,key) 得到给定键(key)的值
C.__setitem__(self,key,val) 设置给定键(key)的值
C.__delitem__(self,key) 删除给定键(key)的值
C.__missing__(self,key) 给定键如果不存在字典中,则提供一个默认值
基本的定制和对象(值)比较特殊方法在大多数类中都可以被实现,且没有同任何特定的类型模型绑定。延后设置,也就是所谓的福比较,在Python2.1中加入。属性组帮助管理你的类的实例属性。这同样独立于模型。还有一个,__getattribute__(),它仅用在新式类中,我们将在后面章节中对它进行描述。
特殊方法中数值类型部分可以用来模拟很多数值操作,包括那些标准操作符、类型转换、基本表示法及亚索。还有用来模拟序列和映射类型的特殊方法。实现这些类型的特殊方法将会重载操作符,以使它们可以处理你的class类型实例。
表格中,在他们的名字中,用星号通配符标注的数值二进制操作符则表示这些方法有多个版本,在名字上有些许不同。星号可代表在字符串中没有额外的字符,或者一个简单的"r"指明是一个右结合操作。
简单定制
我们之前从Python类型中派生出了派生类RoundFloat。这次我们想创建一个苗条的例子,这样可以对类的定制的工作方法有一个更好的理解。这种类的前提与其他类是一样的:我们只需要一个类来保存浮点型,四舍五入,保留两位小数位。
>>> class RoundFloatManual(object): def __init__ (self, val): assert isinstance(val, float), "Value must be a float!" self.value = round(val, 2)
这个类接受一个浮点值——它断言了传递给构造器的参数必须为一个浮点型——并且将其保存为实例属性值。接下来创建这个类的实例:
>>> rfm = RoundFloatManual(12) Traceback (most recent call last): File "<pyshell#6>", line 1, in <module> rfm = RoundFloatManual(12) File "<pyshell#5>", line 4, in __init__ "Value must be a float!" AssertionError: Value must be a float! >>> rfm = RoundFloatManual(1.23) >>> rfm <__main__.RoundFloatManual object at 0x02918A50> >>> print rfm <__main__.RoundFloatManual object at 0x02918A50>
输入非法时,就不执行程序返回断言错误。如果输入正确,没有输出,但是当我们把这个对象输出时,却得不到我们需要的信息,调用print语句也没有帮助。
print(使用 str()) 和真正的字符串对象表示(使用repr())都没能显示更多有关我们对象的信息。一个好的办法是,去实现__str__()和__repr__()两者之一,或者两者都实现。让我们来添加一个__str__()方法,以覆盖默认的行为。
class RoundFloatManual(object): def __init__ (self, val): assert isinstance(val, float), "Value must be a float!" self.value = round(val,2) def __str__(self): return str(self.value) >>> rfm = RoundFloatManual(4.568) >>> print rfm 4.57
我们还有一些问题,一个问题是在解释器中转储对象是,仍然显示的是默认对象符号,如果我们想修复它,只需要覆盖__repr__()。因为字符串表示法也是Python对象,我们可以让__repr__()和__str__()的输出一致。
我们可以让__repr__()作为__str__()的一个别名:
__repr__ = __str__
现在,可以看到,同时具备了str()和repr()的输出了:
>>> class RoundFloatManual(object): def __init__ (self, val): assert isinstance(val, float), "Value must be a float!" self.value = round(val,2) def __str__(self): return str(self.value) __repr__ = __str__ >>> rfm = RoundFloatManual(4.568) >>> rfm 4.57 >>> print rfm 4.57
在前几天提到的RoundFloat例子里,我们没有担心对象的显示问题,原因是__str__()和__repr__()作为float类的一部分已经为我们定义好了。我们只需要继承他们就行了。而这一次需要做另外的工作,这就是派生的好处。
数值定制
这里将有一个实际的例子,假设我们需要创建一个简单的应用,用于操作时间,精确到小时和分,用于跟踪运行时间等。
我们创建一个Time60类,把整型的小时和分钟作为输入传给构造器:
class Time60(object): 'Time60 - track hours and minutes' def __init__ (self, hr, min): self.hr = hr self.min = min
1.显示
如果我们想有一个有意义的输出,就应该覆盖__str__,有必要还要覆盖__repr__。我们按这样输出时间:
def __str__ (self): return '%d:%d' % (self.hr, self.min) __repr__ = __str__
现在我们就可以用这个类来实例化一些对象了
>>> mon = Time60(10, 30) >>> tue = Time60(11, 15) >>> print mon, tue 10:30 11:15
2.加法
Python的重载操作很简单。重载加号只要重载__add__()方法,如果合适还可以用__radd__()及__iadd__()
实现加法我们只需要把分和小时加载一块儿,处理一下进位操作:
def __add__ (self, other): newhr = self.hr + other.hr newmin = self.min + other.min if newmin >= 60: newmin -= 60 newhr += 1 if newhr >= 24: newhr -= 24 return self. __class__(newhr, newmin)
3.原位加法
有了增量赋值,我们还希望有原位操作,比如说__iadd__(),用来支持像 mon += tue 这样的操作,重载一个__i*__()这样的方法的唯一秘密是必须返回self:
def __iadd__ (self, other): self.hr += other.hr self.min += other.min if self.min >= 60: self.min -= 60 self.hr += 1 if self.hr >= 24: self.hr -= 24 return self
这样这个类可以算是初步完成,但在这个类中,还有很多需要优化和改良的地方。这里我们暂时不继续工作它。
迭代器
我们可以用__iter__()和next()方法,来创建一个迭代器。介绍两个例子。
1.随机序列迭代器
我们给我们的类传入一个初始序列,然后通next()去迭代,这个例子展示了随机且无穷迭代。
from random import choice class RandSeq(object): def __init__ (self, seq): self.data = seq def __iter__ (self): return self def next(self): return choice(self.data)
2.任意项的迭代器
这是代码:
class AnyIter(object): def __init__ (self, data, safe=False): self.safe = safe self.iter = iter(data) def __iter__ (self): return self def next(self, howmany=1): retval = [] for eachItem in range(howmany): try: retval.append(self.iter.next()) except StopIteration: if self.safe: break else: raise return retval
我们给出一个迭代器和一个安全标识符来创建这个对象。如果这个标识符为真,我们将在遍历完这个迭代器前,返回所获取的任意条目,但如果这个标识符为假,则在用户请求过多的条目时,将会引发一个异常。错综复杂的核心在于next(),特别是它如何退出的。
在next()的最后一部分中,我们创建用于返回的一个列表项,并且调用对象的next()方法来获得每一项条目。如果我们遍历完列表,得到一个StopIteration异常,这时则检查安全标识符。如果不安全,则将异常抛还给调用者;否则,退出并返回已经保存过的所有项。
>>> class AnyIter(object): def __init__ (self, data, safe=False): self.safe = safe self.iter = iter(data) def __iter__ (self): return self def next(self, howmany=1): retval = [] for eachItem in range(howmany): try: retval.append(self.iter.next()) except StopIteration: if self.safe: break else: raise return retval >>> a = AnyIter(range(10)) >>> i = iter(a) >>> for j in range(1,5): print j, ':', i.next(j) 1 : [0] 2 : [1, 2] 3 : [3, 4, 5] 4 : [6, 7, 8, 9]
上面程序的运行没有问题,因为迭代器正好符合项的个数。当情况出现偏差,会发生什么呢?让我们首先试试“不安全”的模式,这就是紧随其后创建我们的迭代器:
>>> i = iter(a) >>> i.next(14) Traceback (most recent call last): File "<pyshell#10>", line 1, in <module> i.next(14) File "<pyshell#3>", line 11, in next retval.append(self.iter.next()) StopIteration
因为超出了项的支持量,所以出现了StopIteration异常,并且这个异常还被重新引发回调用者。如果我们以“安全”模式重建迭代器,再次运行一次同一个例子的话,我们就可以在项失控前得到迭代器所得到的元素:
>>> a = AnyIter(range(10), True) >>> i = iter(a) >>> i.next(14) [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
多类型定制
我们创造一个新类,NumStr,由一个数字-字符对组成,相应地,记为n和s,数值类型使用整型。尽管这组顺序对的合适的记号是(n, s)但是我们选用[n::s]来表示它。暂不管这个记号,我们只需要考虑数据元素模型就好了,是一个整体。这个新类有这样的特征:
初始化:对数字和字符串进行初始化,如果其中一(或二)个没有初始化,数字用0,字符串用空字符串作为默认参数。
加法:功能是数字相加,字符串按顺序相连。
乘法:数字相乘,字符串累积相连。
False值:当数字为0且字符串为空是,这个实体有一个False值。
比较:一对NumStr对象比较应该有九种不同的组合(n1>n2 and s1<s2,n1 == n2 and s1>s2等等)。对数字和字符串,我们按标准的数值和字典顺序进行比较,然后将比较的值相加返回结果。
代码如下:
#!/usr/bin/env python class NumStr(object): def __init__(self, num=0, string=''): self.__num = num self.__string = string def __str__(self): # define for str() return '[%d :: %r]' % (self.__num, self.__string) __repr__ = __str__ def __add__(self, other): # define for s+o if isinstance(other, NumStr): return self.__class__(self.__num + other.__num, self.__string + other.__string) else: raise TypeError, 'Illegal argument type for built-in operation' def __mul__(self, num): # define for s*o if isinstance(num, int): return self.__class__(self.__num * num, self.__string * num) else: raise TypeError, 'Illegal argument type for built-in operation' def __nonzero__(self): # reveal tautology return self.__num or len(self.__string) def __norm_cval(self, cmpres): # normalize cmp() return cmp(cmpres, 0) def __cmp__(self, other): # define for cmp() return self.__norm_cval( cmp(self.__num, other.__num)) + self.__norm_cval( cmp(self.__string, other.__string))
至此,定制类暂告一段落,虽然对名称的命名还存在很多疑问,为什么有下划线和双下划线一直让人很困扰。接下来学习私有化。
私有化
默认情况下,属性在Python中都是“public”,类所在模块和导入了类所在模块的其他代码都可以访问到。很多OO语言给数据加上一些可见性,只提供访问函数来访问其值。这就是熟知的实现隐藏,是对象封装的一个关键部分。
大多数OO语言提供“访问控制符”来限定成员函数的访问。
而Python只为类属性的私有性提供了初步的访问形式。
双下划线(__)
Python中,由双下划线开始的属性在运行时被“混淆”,所以直接访问是不允许的。实际上,会在名字前面加上下划线和类名。比如,上例中self.__num属性被“混淆”后,用于访问这个数据值的标识就变成了self._NumStr__num.把类名加上后形成的新的“混淆”结果将可以防止在祖先类或子孙类中的同名冲突。
尽管这样做提供了某种层次上的私有化,但算法处于公共域中并且很容易被“击败”。这更多的是一种对导入源代码无法获得的模块或对同一模块中的其他代码的保护机制。
这种名字混淆的另一个目的是为了保护__XXX变量不与父类名字空间相冲突。如果在类中有一个__XXX属性,它将不会被其子类中的__XXX属性覆盖。而如果父类中仅有一个XXX属性,子类也定义了这个属性,那么子类的XXX就会覆盖父类的XXX。使用__XXX,子类的代码就可以安全地使用__XXX,而不必担心它会影响父类中的__XXX
单下划线(_)
简单的模块级私有化只需要在属性名前使用一个但下划线字符。这就防止模块的属性用“from mymodule import *”来加载这个属性。这是严格给予作用域的,所以这同样适合于函数。
Python2.2中引进的新式类,增加了一套全新的特征,让程序员在类及实例属性提供保护的多少上拥有大量重要的控制权。经管Python没有在语法上把private,protected,friend或protected friend 等特征内建在语言中,但是可以按你的需要严格地定制访问权。