Python语言里没有直接的ADT(Abstract Data Type)定义,实现ADT可以采用很多不同的技术。最自然的一种技术是利用class定义实现抽象数据类型。实际上,python语言把内置类型都看作类。在建立这种抽象时,人们不希望暴露其实现的内部细节。由于隐藏抽象的内部信息在软件领域中意义重大,有些编程语言为此提供了专门机制。python没有专门服务于这种需求的机制,只能依靠一些编程约定 。
首先,人们约定在一个类的定义里面,由下划线开头的属性名都当做内部使用的名字,不应该在这个类之外使用。另外,python对类定义里以两个下划线_开头(但不以两个下划线结尾)名字做了特殊处理,使得在类定义之外不能直接使用这个名字访问。例如定义有理数类:
class Rational:
@staticmethod
def _gcd(m, n):
if n == 0:
m, n = n, m
while m != 0:
m, n = n % m, m
return m
def __int__(self, num, den = 1):
if not isinstance(num, int) or not isinstance(den, int):
raise TypeError
if den == 0:
raise ZeroDivisionError
sign = 1
if num < 0:
num, sign = -num, -sign
if den < 0:
den, sign = -den, -sign
g = Rational._gcd(num, den)
self._num = sign * (num//g)
self._den = den //g
在这个类里我们定义了一个局部使用的求最大公约数的静态方法_gcd,在初始化方法里使用。首先,gcd的参数是两个整数,它们不属于被定义的有理数类型。此外,gcd的计算并不依赖任何有理数类的对象,因此其参数表中不应该以表示有理数的self作为第一个参数。但另一方面,这个gcd是为有理数类的实现而需要使用的一种辅助功能,根据信息局部化的原则,局部使用的功能不应该定义为全局函数。综合来看,gcd应该是在有理数类里定义的一个非实例方法。
python把在类里定义的这种方法称为静态方法,描述时需要在函数定义之前加修饰符@staticmethod。静态方法的参数表中不应该有self参数,在其他方面没有任何限制。对于静态方法,可以从其定义所在类的名字出发通过圆点形式调用,也可以从该类的对象出发通过圆点形式调用。本质上说,静态方法就是在类里面定义的普通函数,但也是该类的局部函数。
接下来我们可以考虑有理数类中的其他函数,如果我们想把有理数类的两个属性都当做内部属性,不应该在类之外去引用它们。为满足这种需要,应该定义一对解析操作,也是实力方法。此外,若考虑有理数的运算,我们希望用运算符描述计算过程,写出形式更加自然的计算表达式。python语言支持这种想法,它为所有算数运算符规定了特殊方法名。python中所有特殊的名字都以两个下划线开始,并以两个下划线结束。例如 + 对应的是 add。下面是代码实现:
def num(self):
return self._num
def den(self):
return self._den
def __add__(self, other):
den = self._den * other.den()
num = self._num * other.den() + self._den * other.num()
return Rational(num, den)
def __mul__(self, other):
return Rational(self._num * other.num(), self._den * other.den())
def __floordiv__(self, other):
if other.num() == 0:
raise ZeroDivisionError
return Rational(self._num * other.den(), self._den * other.num())
在每个方法最后用Rational()返回,构造新对象,保证所构造出的对象都能化为最简形式。不需要在每个建立新有理数的地方考虑化简问题。另外,在定义除法时用了整除运算符//。按照python惯例,普通除法/返回的结果是浮点数,对应方法名为truediv,这样可以实现从有理数到浮点数的转换。此外,我们还可以增加等于与小于运算符来比较有理数的大小:
def __eq__(self, other):
return self._num * other.den() == self._den * other.num()
def __lt__(self, other):
return self._num * other.den() < self._den * other.num()
def __ne__(self, other):
return self._num * other.den() != self._den * other.num()
def __le__(self, other):
return self._num * other.den() <= self._den * other.num()
def __gt__(self, other):
return self._num * other.den() > self._den * other.num()
def __ge__(self, other):
return self._num * other.den() >= self._den * other.num()
为了便于输出等目的,人们经常在类里定义一个把该类的对象转换到字符串的方法,便于输出。为了保证系统的str类型转换函数能正确使用,我们可以增加字符串转换方法:
def __str__(self):
return str(self._num) + '/' + str(self._den)
def print(self):
print(self._num, '/', self._den)
类里定义的的另一种方法称为类方法,定义形式是在def行前加修饰符@classmethod。这种方法必须有一个表示其调用类的参数,习惯用cls作为参数名,还可以有任意多个其他参数。类方法也是类对象的属性,可以以属性访问的形式调用。在类方法执行时,调用它的类将自动约束到方法的cls参数,可以通过这个参数访问该类的其他属性。人们通常用类方法实现与本类的所有对象有关的操作。例如Rational中可以设置计数器记录程序运行中创建的该类的实例对象的个数:
class Rational:
counter = 0
def __int__(self, num, den = 1):
if not isinstance(num, int) or not isinstance(den, int):
raise TypeError
if den == 0:
raise ZeroDivisionError
sign = 1
if num < 0:
num, sign = -num, -sign
if den < 0:
den, sign = -den, -sign
g = Rational._gcd(num, den)
self._num = sign * (num//g)
self._den = den //g
Rational.counter += 1
@classmethod
def get_count(cls):
return Rational.counter
为了记录本类创建类的对象个数,Rational类定义了一个数据属性的counter,其初始值为0。每次创建这个类的对象时,初始化方法中会把这个对象计数器加一。
对于函数定义,其中局部名字的作用域自动延伸到内部嵌套的作用域。而对于类定义,情况则不是这样。在类C里定义的名字,其作用域并不自动延伸到C内部嵌套的作用域。因此,如果需要在类中的函数定义里引用这个类的属性,一定要采用基于类名的属性引用方式。
在一些面向对象的语言里,允许把一些实例变量定义为私有变量,只允许在类定义内部访问它们,也就是说,只允许在实例对象的方法函数里访问,不允许在类定义之外使用。实际上,在类之外根本就看不到这种变量,是一种信息隐藏机制。然而,python里没有为定义私有变量提供专门机制,没有办法说明某个属性只能在类的内部访问,只能通过编程约定和良好的编程习惯来保护实例对象里的数据属性。