class语句是Python主要的OOP工具,但与C++不同的是,Python的class并不是声明式的。迹象def一样,class语句是对象的创建者并且是一个隐含的赋值运算——执行时,它会产生类对象,并把其引用值存储在前面所使用的变量名。此外,像def一样,class语句也是真正的可执行代码。知道Python抵达并运算定义的class语句前,这个类都不存在(一般都是在其所在的模块被导入时,在这之前都不会存在)。
class是复合语句,其缩进语句的主体一般都出现在头一行的下边。在头一行中,超类列在类名称之后的括号内,由逗号相隔。列出一个以上的超类会引起多重继承。以下为class语句的一般形式:
class(superclass,...): # assign to name
data=value # shared class data
def method(self,...):
self.member=value
在class语句内,任何赋值语句都会产生类属性,而且还有特殊名称重载运算符。例如,名为__init__的函数会在实例对象构造时调用(如果定义过的话)。
就像之前见过的那样,类几乎就是命名空间,也就是定义变量名(属性)的工具,把数据和逻辑导出给客户端。就像模块文件,位于class语句主体中的语句会建立其属性。当Pyhon执行class语句时(不是调用类),会从头至尾执行其主体内的所有语句。在这个过程中,进行的赋值运算会在这个类作用域中创建变量名,从而称为对应的类对象内的属性。因此,类就像函数和模块:
类的主要的不同之处在于其命名空间也是Python继承的基础。在类或者实例对象中找不到的所引用的属性,就会在其他类中获取。
因为class是复合语句,所以任何种类的语句都可位于其主体内:pinrt、=、if、def等。当class语句自身运行时(不是稍后调用类来创建实例的时候),class语句内的所有语句都会执行。在class语句内赋值的变量名,会创建类属性,而内嵌的def则会创建类方法,但是,其他赋值语句也可制作属性。
class SharedData:
spam=42 # Generates a class data attribute
x=SharedData() # Make two instances
y=SharedData()
x.spam,y.spam # They inherit and share 'spam'
(42, 42)
如上,因为变量名是在class语句的顶层进行赋值的,因此会附加到类中,从而为所有的实例共享。可以通过类名称来修改它,也可以通过实例或列引用来修改。
SharedData.spam=99
x.spam,y.spam,SharedData.spam
(99, 99, 99)
这种类属性可以用于管理贯穿所有实例的信息。例如,所产生的实例的数目的计数器(31章会扩展这一概念)。现在,如果通过实例而不是类来给变量名spam赋值,看看会发生什么:
x.spam=88
x.spam,y.spam,SharedData.spam
(88, 99, 99)
对实例的属性进行赋值运算会在该实例内创建或修改变量名,而不是在共享的类中。通常情况下,继承搜索只会在属性引用时发生,而不是在赋值运算时发生:对对象属性进行赋值总是会修改该对象,除此之外没有其他影响。例如,y.spam会通过继承而在类中查找,但是,对x.spam进行赋值运算则会把该变量名附加在x本身上。
下面的这个例子,可以更容易地理解这种行为,把相同的变量名储存在两个位置。假设执行下列类:
class MixedNames: # Define class
data='spam' # Assign class attr
def __init__(self,value): # Assign method name
self.data=value # Assign instance attr
def display(self):
print(self.data,MixedNames.data) # Instance attr,class arrt
这个类有两个def,把类属性和方法函数绑定在一起。此外,也包含一个=赋值语句。因为赋值语句是在类中赋值变量名data,该变量名会在这个类的作用域内存在,变成类对象的属性。就像所有类属性,这个data会被继承,从而被所有没有自己的data属性的类的实例所共享。
当创建这个类的实例的时候,变量名data会在构造函数方法内对self.data进行赋值运算,从而把data附加在这些实例上。
当创建这个类的实例的时候,变量名data会在构造函数方法内对self.data进行赋值运算,从而把data附加在这些实例上。
x=MixedNames(1)
y=MixedNames(2)
x.display();y.display()
1 spam
2 spam
结果就是,data存在于两个地方:在实例对象内(由__init__中的slef.data赋值运算所创建)以及在实例继承变量名的类中(由类中的data赋值运算所创建)。类的display方法打印了这两个版本,先以点号运算得到self实例的属性,然后才是类。
利用这些技术把属性储存在不同对象内,可以决定其可见范围。附加在类上时,变量名是共享的;附加在实例上时,变量名是属于每个实例的数据而不是共享的行为或数据。虽然继承搜索会查找变量名,但总是可以通过直接读取所需要的对象,而获得树中任何地方的属性。
如果了解了函数,就了解了类中的方法。方法位于class语句的主体内,是由def语句建立的函数对象。从抽象的视角来看,方法替实例对象提供了要继承的行为。从程序设计的角度来看,方法的工作方式与简单函数完全一致,只是有个重要差异:方法的第一个参数总是接受方法调用的隐性主体,也就是实例对象。
换句话说,Python自动把实例方法的调用对应到类方法函数,如下所示。方法调用需要通过实例,就像这样:
instance.method(args...)
这会自动翻译成以下形式的类方法函数调用:
class.method(instance,args...)
class通过Python继承搜索流程找出方法名称所在之处。事实上,两种调用形式在Python中都有效。
除了方法属性名称是正常的继承外,第一个参数就是方法调用背后唯一的神奇之处。在类方法中,按惯例第一个参数通常称为self(严格地说,只有其位置重要,而不是其名称)。这个参数给方法提供了一个钩子,从而返回调用的主体,也就是实例对象:因为类可以产生许多实例对象,所以需要这个参数来惯例每个实例彼此各不相同的数据。
C++(JAVA也一样)程序员会发现,Pthon的self参数与C++的this指针很像。不过,Python中,self一定要在程序代码中明确地写出:方法一定要通过self来取出或修改由当前方法调用或正在处理的实例的属性。这种让self明确和的本质是有意设计的:这个变量名存在,会让你明确脚本中使用的实例属性名称,而不是本地作用域或全局作用域中的变量名。
假设定义了下面的类:
class NextClass: # Define class
def printer(self,text):
self.message=text
print(self.message)
变量名printer引用了一个函数对象。因为这是在class语句的作用域中赋值的,就会变成类对象的属性,被由这个类创建的每个实例所继承。通常,因为像printer这类方法都是设计成处理实例的,所以得通过实例予以调用。
x=NextClass()
x.printer('instance call')
instance call
x.message
'instance call'
当通过对实例进行点号运算调用它时,printer会先通过继承将其定为,然后它的self参数会自动赋值为实例对象(x)。text参数会获得在调用时传入的字符串(‘instance call’)。注意:i那位Python会自动传递第一个参数给self,实际上只需传递一个参数。在printer中,变量名self是用于读取或设置每个实例的数据的,因为self引用的是当前正在处理的实例。
方法能通过实例或类本身两种方法其中的任意一种进行调用。例如,可以通过类名称调用printer,之傲明确地传递了一个实例给self参数。
NextClass.printer(x,'class call') # direct class call
class call
x.message # and instance changed again
'class call'
通过实例和类的调用具有相同的效果,只要在类实例中传递了相同的实例对象。实际上,在默认的情况下,如果尝试不带任何实例调用的方法时,就会得出错信息。
NextClass.printer('bad call')
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
----> 1 NextClass.printer('bad call')
TypeError: printer() missing 1 required positional argument: 'text'
方法一般是通过实例调用的。不过,通过类调用方法也扮演了一些特殊的角色。常见的场景涉及了构造方法。就像所有属性__init__方法是由继承进行查找的。也就是说,在构造时,Python会找出并且只调用一个__init__。如果要保证子类的构造方法也会执行超类构造时的逻辑,一般必须通过类明确地调用超类的__init__方法。
class Super:
def __init__(self,x):
...default code...
class Sub(Super):
def __init__(self,x,y):
Super.__init__(self,x)
...default code...
I=Sub(1,2)
这是代码有可能直接调用运算符重载方法的环境之一。如果真的想运行超类的构造方法,自然只能用这种方法进行调用:没有这样的调用,子类会完全取代超类的构造方法。这门技术在实际中更显示的介绍,可以参加本章最后的例子。
这种通过类调用方法的模式,是扩展继承方法行为(而不是完全取代)的一般基础。在第31章中,将会遇到Python2.2时新增的选项:静态方法,可以让程序员编写不预期第一参数为实例对象的方法。这类方法可像简单的无实例的函数那样运作,其变量名属于其所在类的作用域,并且可以用来管理类数据。一个相关的概念,类方法,当调用的时候接受一个类而不是一个实例,并且它可以用来管理基于每个类的数据。不过,这是高级的选用扩展功能。通常来说,一定要为方法传入实例,无论通过实例还是类调用都行。
像class语句这样的命名空间工具的重点就是支持变量名继承。本节扩展了Python中关于属性继承的一些机制和角色。
在Python中,当对象进行点号运算时,就发生了继承,而且设计了搜索属性定义树(一个或多个命名空间)。每次使用object.attr形式的表达式时(object是实例或类对象),Python会从头至尾搜索命名空间,先从对象开始,寻找所能找到的第一个attr。这包括在方法中对self属性的引用。因为树种较低的定义会覆盖较高的定义,继承构成了专有化的基础。
通常来说:
刚才谈到了继承树的搜索模式,变成了将系统专有化的最好方式。因为继承会现在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为。实际上,可以把整个系统做成类的层次,再新增外部的子类来对其进行扩展,而不是再原处修改已经存在的逻辑。
重新定义继承变量名的该奶奶引出了各种专有化技术。例如,子类可以完全取代继承的属性,提供超类可以找到的属性,并且通过已覆盖的方法回调超类来扩展超类的方法。再之前已经看到过实际中取代的做法。下面是如何进行扩展的例子:
class Super:
def method(self):
print('in Super.method')
class Sub(Super):
def method(self): # Override method
print('starting Sub.method') # Add actions here
Super.method(self) # Run default action
print('ending Sub.method')
x=Sub()
x.method()
starting Sub.method
in Super.method
ending Sub.method
直接调用超类方法是这里的重点。Sub类以其专有化的版本取代了Super的方法函数。但是,取代时,Sub又回调了Super所导出的版本,从而实现了其默认的行为。换句话说,Sub.method只是扩展了Super.method的行为,而不是完全取代它。这种扩展编码模式常常用于构造函数。例如,参考本章之前的“方法”一节。
扩展知识一种与超类接口的方式。下面展示的specialize.py文件定义了多个类,示范了一些常用的技巧。
class Super:
def method(self):
print('in Super.method')
def delegate(self):
self.action() # Expected to be defined
class Inheritor(Super):
pass
class Replacer(Super):
def method(self):
print('in Replacer.method')
class Extender(Super):
def method(self):
print('starting Extender.method')
Super.method(self)
print('ending Extender.method')
class Provider(Super):
def action(self):
print('in Provider.action')
if __name__=='__main__':
for klass in (Inheritor,Replacer,Extender):
print('\n'+klass.__name__+'...')
klass().method()
print('\nProvider...')
x=Provider()
x.action()
Inheritor...
in Super.method
Provider...
in Provider.action
Replacer...
in Replacer.method
Provider...
in Provider.action
Extender...
starting Extender.method
in Super.method
ending Extender.method
Provider...
in Provider.action
注意上一个例子中的Provider类是如何工作的。当通过Provider实例调用delegate方法时,有两个独立的继承搜索会发生:
这种“填空”的代码结构一般就是OOP的软件框架。至少,从delegate方法的角度来看,这个例子中的超类有时也称作是抽象超类——也就是类的部分行为默认是由其子类所提供的。如果预期的方法没有在子类中定义,当继承搜索失败时,Python会引发未定义变量名的异常。
类的编写者偶尔会使用assert语句,使这种子类需求更为明显,或者引发内置的异常NotImplementError(将在下一部分深入学习可能触发异常的语句时深入讲解)。作为提前介绍,下面是assert方法的实际应用示例:
class Super:
def delegate(self):
self.action()
def action(self):
assert False,'action must be defined!' # if this version is called
X=Super()
X.delegate()
---------------------------------------------------------------------------
AssertionError Traceback (most recent call last)
in ()
6
7 X=Super()
----> 8 X.delegate()
in delegate(self)
1 class Super:
2 def delegate(self):
----> 3 self.action()
4 def action(self):
5 assert False,'action must be defined!' # if this version is called
in action(self)
3 self.action()
4 def action(self):
----> 5 assert False,'action must be defined!' # if this version is called
6
7 X=Super()
AssertionError: action must be defined!
将在32和33章介绍assert。简而言之,如果其表达式运算结构为假,就会引发带有出错信息的异常。在这里,表达式总是为假(0).因此,如果没有方法重新定义,继承就会找到这个版本,触发出错信息。此外,有些类只在该类的不完整方法中直接产生NotImplemented异常。
class Super:
def delegate(self):
self.action()
def action(self):
raise NotImplementedError('action must be defined!')
X=Super()
X.delegate()
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
in ()
6
7 X=Super()
----> 8 X.delegate()
in delegate(self)
1 class Super:
2 def delegate(self):
----> 3 self.action()
4 def action(self):
5 raise NotImplementedError('action must be defined!')
in action(self)
3 self.action()
4 def action(self):
----> 5 raise NotImplementedError('action must be defined!')
6
7 X=Super()
NotImplementedError: action must be defined!
对于子类,除非提供了期待的方法来代替超类中默认的方法,否则将会得到异常。
class SubWrong(Super):pass
Y=SubWrong()
Y.delegate()
---------------------------------------------------------------------------
NotImplementedError Traceback (most recent call last)
in ()
2
3 Y=SubWrong()
----> 4 Y.delegate()
in delegate(self)
1 class Super:
2 def delegate(self):
----> 3 self.action()
4 def action(self):
5 raise NotImplementedError('action must be defined!')
in action(self)
3 self.action()
4 def action(self):
----> 5 raise NotImplementedError('action must be defined!')
6
7 X=Super()
NotImplementedError: action must be defined!
class SubRight(Super):
def action(self):
print('spam')
Y=SubRight()
Y.delegate()
spam
如果要参考这一节中概念的更实际的例子,可以参考第31章结尾的习题8以及第六部分中的解答(在附录B)
在Python2.6和Python3.0中,前一小节的抽象超类(即“抽象基类”),需要由子类填充的方法,它们也可以以特殊的类语法来实现。我们编写代码的这种方法根据版本不同而有所变化。在Python3.0中,可以在class头部使用一个关键字参数,以及特殊的@装饰器语法,这二者都将在稍后更详细地进行学习。
from abc import ABCMeta,abstractmethod
class Super(metaclass=ABCMeta):
@abstractmethod
def method(self,...):
pass
但是在Python2.6中,则使用一个类属性:
class Super:
__metaclass__=ABCMeta
@abstractmethod
def method(self,...):
pass
不管使用哪种方法,效果都是相同的——不能产生一个实例,除非在类树的较低层级定义了该方法。例如,在Python3.0中,与前一小节的例子等价的特殊语法如下:
from abc import ABCMeta,abstractclassmethod
class Super(metaclass=ABCMeta):
def delegate(self):
self.action()
@abstractclassmethod
def action(self):
pass
X=Super()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
8 pass
9
---> 10 X=Super()
TypeError: Can't instantiate abstract class Super with abstract methods action
class Sub(Super):pass
X=Sub()
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
1 class Sub(Super):pass
2
----> 3 X=Sub()
TypeError: Can't instantiate abstract class Sub with abstract methods action
class Sub(Super):
def action(self):
print('spam')
X=Sub()
X.delegate()
spam
按照这种方式编写代码,带有一个抽象方法的类是不能继承的(即,不能通过调用它来创建一个实例),除非其所有的抽象方法都已经在子类中定义了。尽管这需要更多的代码,但这种方法的有点是,当试图产生该类的一个实例的时候,由于没有方法会产生错误,这不会比试图调用一个没有的方法更晚。这一功能可以用来定义一个期待的接口,在客户类中自动验证。
遗憾的是,这种方法也依赖于还没介绍的两种高级语言工具——第31章将要间接、38章将要深入的,以及第31章提及、39章深入介绍的元类声明。
首先要记住的是,点号和无点号的变量名,会用不同的方式处理,而有些作用域是用于对对象命名空间做初始设定的。
无点号的简单变量名遵循第17章中的函数LEGB作用域法则,具体如下:
赋值语句(X=value)
使变量名成为本地变量:在当前作用域内,创建或改变变量名X,除非声明它是全局变量。
引用(X)
在当前作用域内搜索变量名X,之后是在任何以及所有的嵌套的函数中,然后是在当前的全局作用域内搜索,最后是在内置作用域中搜索。
点号的属性名指的是特定对象的属性,并且遵循模块和类的规则。就类和实例对象而言,引用规则增加了继承搜索的这个流程。
赋值语句(object.X=value)
在进行点号运算的对象的命名空间内创建或者修改属性名X,并没有其他作用。继承树的搜索只发生在属性引用时,而不是属性的赋值运算时。
引用(object.X)
就基于类的对象而言,会在对象内搜索属性名X,然后是其上所有可读取的类(使用继承搜索流程)。对于不是基于类的对象而言(例如,模块),则是从对象中直接读取X。
点号和无点号的变量名有不同的搜索流程,再加上两者都有多个搜索层次,有时很难看出变量名最终属于何处。在Python中,赋值变量名的场所相对重要:这完全决定了变量名所在的作用域或对象。以下示范了这条原则是如何变成代码的,并总结了命名空间的概念。
X=11 # Global(module)name/attribute(X,or manynames.X)
def f():
print(X)
def g():
X=22
print(X)
class C:
X=33 # Class attribute(C.X)
def m(self):
X=44 # Local variable in method(X)
self.X=55 # Instance attribute (instance.X)
这里分别五次给相同的变量名X赋值。不过,因为这个名称是在五个不同地方进行赋值的,这个程序中的五个X是完全不同的变量。从上至下,这里对X的赋值语句会产生:模块属性(11),函数内的本地变量(22),类属性(33),方法中的本地变量(44)以及实例属性(55)。虽然这五个都称为X,但事实上它们都是在源代码内的不同位置进行赋值的,或者说是赋值到了不同的对象,因此,使得这些变量名都是独特的变量。
用一段测试代码执行以上的程序看看会得到什么结果,如果能读懂,就完成了Python命名空间的涅槃重生。
if __name__=='__main__':
print(X)
f()
g()
print(X)
obj=C()
print(obj.X)
obj.m()
print(obj.X)
print(C.X)
# print(C.m.X) # FAILS:only visible in method
# print(g.X) # FAILS:only visible in functions
11
11
22
11
33
55
33
注意,可以通过类来读取其属性(C.X),但是无法从def语句外读取函数或方法内的局部变量。局部变量对于在def内的其余代码才是可见的。而事实上,也只有当函数调用或方法执行时,才会存在于内存中。
最后,正如在第17章所了解到的,一个函数在其外部修改名称也是可能的,使用global和(Python3.0中的)nonlocal语句——这些语句提供了写入访问,但是也修改了赋值的命名空间绑定规则:
X=11 # Global in module
def g1():
print(X) # Reference global in module
def g2():
global X # Change global in module
X=22
def h1():
X=33 # Local in function
def nested():
print(X) # Reference local in enclosing scope
def h2():
X=33 # Local in function
def nested():
nonlocal X # Python3.0 statement
X=44 # Change local in enclosing scope
当然,通常来说在脚本内每个变量都不应该使用相同的变量名!但是,就像这个例子所表示的那样,即时这么做,Python的命名空间还是会工作,防止在一个环境中所用的变量名无意中和另一个环境中所使用的变量名发生冲突。
在第19章中学习了模块的命名空间实际上是以字典的形式实现的,并且可以由内置属性__dict__显示这一点。类和实例对象也是如此:属性点号运算其实内部就是字典的索引运算,而属性继承其实就是搜索链接的字典而已。实际上,实例和类对象就是Python中带有链接的字典而已。Python暴露这些字典,还有字典间的链接,以便于在高级角色中使用(例如,编码工具)。
为了了解Python内部属性的工作方式,可以通过交互模式会话加入类,来跟踪命名空间字典的增长方式。在第26章中提供了这种类型的代码的一个简单版本,这里将进一步地介绍它。首先,定义一个超类和一个带方法的子类,而这些方法会在实例中保存数据。
class Super:
def hello(self):
self.data1='spam'
class Sub(Super):
def hola(self):
self.data2='eggs'
X=Sub()
X.__dict__
{}
X.__class__
__main__.Sub
Sub.__bases__
(__main__.Super,)
Super.__bases__
(object,)
Y=Sub()
X.hello()
X.__dict__
{'data1': 'spam'}
X.hola()
X.__dict__
{'data1': 'spam', 'data2': 'eggs'}
Super.__dict__.keys()
dict_keys(['__module__', 'hello', '__dict__', '__weakref__', '__doc__'])
Sub.__dict__.keys()
dict_keys(['__module__', 'hola', '__doc__'])
Y.__dict__
{}
因为属性实际上是Python的字典键,所以其实有两者方式可以读取并对其进行赋值:通过点号运算或者通过键索引运算。
X.data1,X.__dict__['data1']
('spam', 'spam')
X.data3='toast'
X.__dict__
{'data1': 'spam', 'data2': 'eggs', 'data3': 'toast'}
X.__dict__['data3']='ham'
X.data3
'ham'
X.__dict__
{'data1': 'spam', 'data2': 'eggs', 'data3': 'ham'}
不过,这种等效关系只适用于实际中附加在实例上的属性。因为属性点号运算也会执行继承搜索,所以可以存取命名空间字典索引运算无法读取的属性。例如,继承的属性X.hello无法由X.__dict__[‘hello’]读取。
最后,下面是在第4和第15章介绍过的内置函数dir用在类和实例对象上的情况。这个函数能用在任何带有属性的对象上:dir(object)类似于object.__dict__.keys()调用。不过,dir会排序其列表并引入一些系统属性。在Python3.0中,它包含了从所有类的隐含超类object类继承的名称:
X.__dict__,Y.__dict__
({'data1': 'spam', 'data2': 'eggs', 'data3': 'ham'}, {})
list(X.__dict__.keys())
['data1', 'data2', 'data3']
dir(X)
['__class__',
'__delattr__',
'__dict__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__gt__',
'__hash__',
'__init__',
'__init_subclass__',
'__le__',
'__lt__',
'__module__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__str__',
'__subclasshook__',
'__weakref__',
'data1',
'data2',
'data3',
'hello',
'hola']
上一节介绍了“实例和类的特殊属性__class__和__bases__”,但是没有例子说明为什么留意这些属性。简而言之,这些属性可以在程序代码内查看继承层次。例如,可以用它们来显示类树,就像下面的例子展示的那样
"""
Climb inheritance trees using namespace links,displaying higher superclasses with indentation
"""
def classtree(cls,indent):
print('.'*indent+cls.__name__)
for supercls in cls.__bases__:
classtree(supercls,indent+3)
def instancetree(inst):
print('Tree of %s'% inst)
classtree(inst.__class__,3)
def selftest():
class A:pass
class B(A):pass
class C(A):pass
class D(B,C):pass
class E: pass
class F(D,E):pass
instancetree(B())
instancetree(F())
if __name__=='__main__':selftest()
Tree of <__main__.selftest..B object at 0x00000209D645FDA0>
...B
......A
.........object
Tree of <__main__.selftest..F object at 0x00000209D59CC8D0>
...F
......D
.........B
............A
...............object
.........C
............A
...............object
......E
.........object
第15章详细介绍了文档字符串,它是出现在各种结构的顶部的字符串常量,由Python在相应对象的__doc__属性自动保存。它适用于模块文件、函数定义,以及类和方法。
如下提供了一个快速但全面的示例,来概括文档字符串可以在代码中出现的位置。所有这些都可以是三重引号的块。
"""I am:docstr.__doc__"""
def func(args):
'I am:docstr.func.__doc__'
pass
class Spam:
"I am:Spam.__doc__ or docstr.Spam.__doc__"
def method(self,arg):
"I am:Spam.method.__doc__ or self.method.__doc__"
pass
文档字符串的主要优点是,它们在运行时能够保存。因此,如果它们已经编写为文档字符串,可以用其__doc__属性来读取文档:
func.__doc__
'I am:docstr.func.__doc__'
Spam.__doc__
'I am:Spam.__doc__ or docstr.Spam.__doc__'
Spam.method.__doc__
'I am:Spam.method.__doc__ or self.method.__doc__'
文档字符串在运行时可用,但是,它们从语法上比#注释(它可以出现在程序中的任何地方)要缺乏灵活性。两种形式都是有用的工具,并且任何程序文档都是很好的(当然,只要它够准确)。作为首要的最佳实践规则是:针对功能性文档(你的对象做什么)使用文档字符串,针对更加围观的文档(令人费解的表达式是如何工作的)使用#注释
简而言之:
类也支持模块所不支持的额外功能,例如,运算符重载、多实例生成和继承。尽管类和模块都是命名空间,但现在应该能够辨别其实它们是不同的事物。
实际上,“运算符重载”只是意味着在类方法中拦截内置的操作——当类的实例出现在内置操作中,Python自动调用你重载的方法,并且重载方法的返回值变成了相应操作的结果。一些是对重载的关键概念的复习:
如下例子展示了构造方法和减法运算的方法重载:
class Number:
def __init__(self,start):
self.data=start
def __sub__(self,other):
return Number(self.data-other)
X=Number(5)
Y=X-2
Y.data
3
方法 | 重载 | 调用 |
---|---|---|
__init__ | 构造函数 | 对象建立:X=Class(args) |
__del__ | 析构函数 | X对象收回 |
__add__ | 运算符+ | 如果没有_iadd_,X+Y,X+=Y |
__or__ | 运算符|(位OR) | 如果没有_ior_,X |
__repr__,__str__ | 打印、转换 | print(X)、repr(X)、str(X) |
__call__ | 函数调用 | X(*args,**kargs) |
__getattr__ | 点号运算 | X.undefined |
__setattr__ | 属性赋值语句 | X.any=value |
__delattr__ | 属性删除 | del X.any |
__getarrtibute__ | 属性获取 | X.any |
__getitem__ | 索引运算 | X[key],X[i:j],没有__iter__时for循环和其他迭代器 |
__setitem__ | 索引赋值语句 | X[key]=value,X[i:j]=sequence |
__delitem__ | 索引和分片删除 | del X[key],del X[i:j] |
__len__ | 长度 | len(X),如果没有__bool__,真值测试 |
__bool__ | 布尔测试 | bool(X) |
__lt__,__gt__,__le__,__ge__,__eq__,__ne__ | 特定的比较 | X |
__radd__ | 右侧加法 | other+X |
__iadd__ | 实地(增强的)加法 | X+=Y(or else __add__) |
__iter__,__next__ | 迭代环境 | I=iter(X),next(I);for loops, in if no __contains__,all comprehensions,mpa(F,X),其他 |
__contains__ | 成员关系测试 | item in X(任何可迭代的) |
__index__ | 整数值 | hex(X),bin(X),oct(X),O[X],O[X:] |
__enter__,__exit__ | 环境管理器(参见第33章) | with obj as var: |
__get__,__set__,__delete__ | 描述符属性(参加第37章) | X.attr,X.attr=value,del X.attr |
__new__ | 创建(参加第39章) | 在__init__之前创建对象 |
如果在类中定义了(或者继承了)的话,则对于实例的索引运算,会自动调用__getitem__。当实例X出现在X[i]这样的索引运算中时,Python会调用这个实例继承的__getitem__方法(如果有的话),把X作为第一个参数传递,并且方括号内的索引值传给第二个参数。例如,下面的类将返回索引值的平方。
class Indexer:
def __getitem__(self,index):
return index**2
X=Indexer()
print(X[4])
for i in range(10):
print(X[i],end=' ')
16
0 1 4 9 16 25 36 49 64 81
除了索引,对于分片表达式也调用__getitem__。正式地说,内置类型以同样的方式处理分片。例如,下面是一个内置列表上工作的分片,使用了上边界和下边界以及一个stride:
L=[5,6,7,8,9]
print(L[2:4])
print(L[1:])
print(L[:-2])
print(L[::2])
[7, 8]
[6, 7, 8, 9]
[5, 6, 7]
[5, 7, 9]
实际上,分片边界绑定到了一个分片对象中,并且传递给索引的列表实现。事实上,总是可以手动地传递一个分片对象——分片语法主要是一个分片对象进行索引的语法糖:
print(L[slice(2,4)])
print(L[slice(1,None)])
print(L[slice(None,None,2)])
[7, 8]
[6, 7, 8, 9]
[5, 7, 9]
对于带有一个__getitem__的类,这是很重要的——该方法将既针对基本索引调用,又针对分片调用。如下类将会处理分片。当针对所有调用的时候,参数像前面一个类一样是一个整数:
class Indexer:
def __init__(self,sequence):
self.sequence=sequence
def __getitem__(self,index):
print('getitem:',index)
return self.sequence[index]
X=Indexer([5,6,7,8,9])
print(X[0])
print(X[2:4])
print(X[::2])
getitem: 0
5
getitem: slice(2, 4, None)
[7, 8]
getitem: slice(None, None, 2)
[5, 7, 9]
如果使用的话,__setitem__索引赋值方法类似地拦截索引和分片赋值——它为后者接受了一个分片对象,它可能以同样的方式传递到另一个索引赋值中:
def __setitem(self,index,value): # Intercept index or slice assignment
...
self.data[index]=value # Assign index or slice
实际上,__getitem__可能在甚至比索引和分片更多的环境中自动调用,正如下面的小节所介绍的。
初学者可能不见得马上就能领会这里的技巧,但这些技巧都是非常有用的。for语句的作用是从0到更大的索引值,重复对序列进行索引运算,知道检测到超出边界的异常。因此,__getitem__也可以是Python中一种重载迭代的方式。如果定义了这个方法,for循环每次循环时都会调用类的__getitem__,并持续搭配更高的偏移值。这是一种“买一送一”的情况:任何响应索引运算的内置或用户定义的对象,同样会响应迭代。
class stepper:
def __getitem__(self,i):
return self.data[i]
X=stepper()
X.data="spam"
for item in X:
print(item,end=' ')
s p a m
任何支持for循环的类也自动支持Python所有迭代环境,而其中多种环境在之前已经见到过了:
print('p' in X)
print([c for c in X])
print(list(map(str.upper,X)))
(a,b,c,d)=X
print(a,b,d)
True
['s', 'p', 'a', 'm']
['S', 'P', 'A', 'M']
s p m
list(X),tuple(X),''.join(X)
(['s', 'p', 'a', 'm'], ('s', 'p', 'a', 'm'), 'spam')
X
<__main__.stepper at 0x2d393c06940>
在实际应用中,这个技巧可以用于建立提供序列接口的对象,并新增逻辑到内置的序列类型运算。在第31章扩展内置类型时,会再谈到这个观点。
尽管__getitem__技术有效,但它只是一种退而求其次的方法。如今,Python中所有的迭代环境都会先尝试__iter__方法,然后再尝试__getitem__。只有在对象不支持迭代协议的时候,才会尝试索引运算。一般来讲,应该优先使用__iter__,它能够比__getitem__更好地支持一般的迭代环境。
从技术的角度来讲,迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现的,而这种方法应该返回一个迭代器对象。如果已经提供了,Python就会重复调用这个迭代器对象的next方法,知道发生StopIteration异常。如果没有找到这类__iter__方法,Python会改用__getitem__机制,就像之前那样通过偏移量重复索引,直到引发IndexError异常(对于手动迭代来说,一个next内置函数也可以方便地使用:next(I)与I.__next__()是相同的)。
在__iter__机制中,类就是通过实现第14章和第20章介绍的迭代器协议,来实现用户定义的迭代器的。例如,下列代码定义了用户定义的迭代器类来生成平方值。
class Squares:
def __init__(self,start,stop):
self.value=start-1
self.stop=stop
def __iter__(self): # Get iterator object on iter
return self
def __next__(self): # Return a square on each iteration
if self.value==self.stop: # Also called by next built-in
raise StopIteration
self.value+=1
return self.value**2
for i in Squares(1,9):
print(i,end=' ')
1 4 9 16 25 36 49 64 81
迭代器是用来迭代,而不是随机的索引运算。事实上,迭代器根本没有重载索引表达式。
X=Squares(1,5)
X[1]
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
in ()
1 X=Squares(1,5)
----> 2 X[1]
TypeError: 'Squares' object does not support indexing
__iter__机制也是在__getitem__中所见到的其他所有迭代环境的实现方式。然而,和__getitem__不同的是,__iter__只循环依次,而不是循环多次,循环之后就变为空。每次新的循环,都要创建一个新的迭代器对象。
X=Squares(1,5)
print([n for n in X])
print([n for n in X])
print([n for n in Squares(1,5)])
print(list(Squares(1,3)))
[1, 4, 9, 16, 25]
[]
[1, 4, 9, 16, 25]
[1, 4, 9]
注意:如果用生成器函数编写,这个例子可能会更简单一些。
def gsquares(start,stop):
for i in range(start,stop+1):
yield i**2
for i in gsquares(1,5): # or: [x**2 for x in range(1,5)]
print(i,end=' ')
1 4 9 16 25
和类不同的是,这个函数会自动在迭代中存储其状态。当然,这是假设的例子。实际上,可以跳过这两种技术,只用for循环、map或者列表解析,依次创建这个列表。在Python中,完成任务最佳而最快的方式通常也是最简单的方式:
[x**2 for x in range(1,6)]
[1, 4, 9, 16, 25]
然而,在模拟更复杂的迭代对象时,类会比较好用,特别是能够获益于状态信息和继承层次。下一节就要探索这种情况下的使用例子。
之前提到过,迭代器对象可以定义成一个独立的类,有其自己的状态信息,从而能够支持相同数据的多个迭代。考虑一下,当步进到字符串这类内置类型时,会发生什么事情。
S='ace'
for x in S:
for y in S:
print(x+y,end=' ')
aa ac ae ca cc ce ea ec ee
在这里,外层循环调用iter从字符串中取得迭代器,而每个嵌套的循环也做相同的事情来获得独立的迭代器。因为每个激活状态下的迭代器都有自己的状态信息,而不管其他激活状态下的循环是什么状态。
从第14和20章所学可知,生成器函数和表达式以及map和zip这样的内置函数都是单迭代对象,而range内置函数和其他的内置类型(如列表)则支持独立位置的多个活跃迭代器。
当用类来编写用户定义的迭代器时,可由程序员来决定是支持一个单个的或是多个活跃的迭代。要达到多个迭代器的效果,__iter__只需替迭代器定义新的状态对象,而不是返回self。
class SkipIterator:
def __init__(self,wrapped):
self.wrapped=wrapped
self.offset=0
def __next__(self):
if self.offset>=len(self.wrapped):
raise StopIteration
else:
item=self.wrapped[self.offset]
self.offset+=1
return item
class SkipObject:
def __init__(self,wrapped):
self.wrapped=wrapped
def __iter__(self):
return SkipIterator(self.wrapped)
if __name__=='__main__':
alpha='abcdef'
skipper=SkipObject(alpha)
I=iter(skipper)
print(next(I),next(I),next(I))
for x in skipper:
for y in skipper:
for z in skipper:
print(x+y+z,end=' ')
a b c
aaa aab aac aad aae aaf aba abb abc abd abe abf aca acb acc acd ace acf ada adb adc add ade adf aea aeb aec aed aee aef afa afb afc afd afe aff baa bab bac bad bae baf bba bbb bbc bbd bbe bbf bca bcb bcc bcd bce bcf bda bdb bdc bdd bde bdf bea beb bec bed bee bef bfa bfb bfc bfd bfe bff caa cab cac cad cae caf cba cbb cbc cbd cbe cbf cca ccb ccc ccd cce ccf cda cdb cdc cdd cde cdf cea ceb cec ced cee cef cfa cfb cfc cfd cfe cff daa dab dac dad dae daf dba dbb dbc dbd dbe dbf dca dcb dcc dcd dce dcf dda ddb ddc ddd dde ddf dea deb dec ded dee def dfa dfb dfc dfd dfe dff eaa eab eac ead eae eaf eba ebb ebc ebd ebe ebf eca ecb ecc ecd ece ecf eda edb edc edd ede edf eea eeb eec eed eee eef efa efb efc efd efe eff faa fab fac fad fae faf fba fbb fbc fbd fbe fbf fca fcb fcc fcd fce fcf fda fdb fdc fdd fde fdf fea feb fec fed fee fef ffa ffb ffc ffd ffe fff
迭代器的内容比目前所见还要丰富。运算符重载往往是多个层级的:类可以提供特定的方法,或者用作退而求其次选项的更通用的替代方案。
在迭代领域,类通常把in成员关系运算符实现为一个迭代,使用__iter__方法或者__getitem__方法。要支持更加特定的成员关系,类可能编写一个__contains__方法——当出现的时候,该方法优先于__iter__方法,__iter__方法优先于__getitem__方法。__contains__方法应该把成员关系定义为对一个映射应用键(并且可以使用快速查找),以及用于序列的搜索。
考虑如下的类,它编写了所有3个方法和测试成员关系以及应用于一个实例的各种迭代环境。调用的时候,其方法会打印出跟踪信息:
class Iters:
def __init__(self,value):
self.data=value
def __getitem__(self,i):
print('get[%s]:'%i,end='')
return self.data[i]
def __iter__(self):
print('iter=>',end='')
self.ix=0
return self
def __next__(self):
print('next:',end='')
if self.ix==len(self.data):raise StopIteration
item=self.data[self.ix]
self.ix+=1
return item
def __contains__(self,x):
print('contains:',end='')
return x in self.data
X=Iters([1,2,3,4,5])
print(3 in X)
for i in X:
print(i,end='|')
print()
print([i**2 for i in X])
print(list(map(bin,X)))
I=iter(X)
while True:
try:
print(next(I),end='@')
except StopIteration:
break
contains:True
iter=>next:1|next:2|next:3|next:4|next:5|next:
iter=>next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=>next:next:next:next:next:next:['0b1', '0b10', '0b11', '0b100', '0b101']
iter=>next:1@next:2@next:3@next:4@next:5@next:
__getattr__方法是拦截属性点号运算。更确切地说,当通过对未定义(不存在)属性名称和实例进行点号运算时,就会用属性名称作为字符串调用这个方法。如果Python可通过其继承树搜索流程找到这个属性,该方法就不会被调用。因为有这种情况,所以__getattr__可以作为钩子来通过通用的方式响应请求。例子如下:
class Empty:
def __getattr__(self,attrname):
if attrname=='age':
return 40
else:
raise AttributeError(attrname)
X=Empty()
print(X.age)
print(X.name)
40
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
in ()
7 X=Empty()
8 print(X.age)
----> 9 print(X.name)
in __getattr__(self, attrname)
4 return 40
5 else:
----> 6 raise AttributeError(attrname)
7 X=Empty()
8 print(X.age)
AttributeError: name
有个相关的重载方法__setattr__会拦截所有属性的赋值语句。如果定义了这个方法,self.attr=value会变成self.__attr__(‘attr’,value)。这一点技巧性很高,因为在__seltattr__中对任何self属性做赋值,都会再调用__setattr__,导致了无穷递归循环(最后就是堆栈溢出异常)。因此如果想使用这个方法,就要确定时通过对属性字典做索引运算来赋值任何实例属性的(下一节讨论)。也就是说,是使用self.__dict__[‘name’]=x,而不是通过self.name=x。
class Accesscontrol:
def __setattr__(self,attr,value):
if attr=='age':
self.__dict__[attr]=value
else: raise AttributeError(attr+' not allowed')
X=Accesscontrol()
X.age=40
X.age
40
X.name='mel'
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
in ()
----> 1 X.name='mel'
in __setattr__(self, attr, value)
3 if attr=='age':
4 self.__dict__[attr]=value
----> 5 else: raise AttributeError(attr+' not allowed')
6 X=Accesscontrol()
7 X.age=40
AttributeError: name not allowed
为了便于将来参考,还要注意,有其他的方式来管理Python中的属性访问:
由于这些颇有些高级的工具并不是对每个Python程序员都有用,所以将推迟到第31章再介绍这些特性,并且再第37章再详细地介绍所有属性管理技术。
下列程序代码把上一个例子通用化了,让每个子类都有自己的私有变量名列表,这些变量名无法通过其实例进行赋值。
class PrivateExc(Exception):pass
class Privacy:
def __setattr__(self,attrname,value):
if attrname in self.privates:
raise PrivateExc(attrname,self)
else:
self.__dict__[attrname]=value
class Test1(Privacy):
privates=['age']
class Test2(Privacy):
privates=['name','pay']
def __init__(self):
self.__dict__['name']='Tom'
x=Test1()
y=Test2()
x.name='Bob'
y.name='Sue' # Fails
x.age=30 # Fails
y.age=40
---------------------------------------------------------------------------
PrivateExc Traceback (most recent call last)
in ()
20
21 x.name='Bob'
---> 22 y.name='Sue'
23
24 x.age=30
in __setattr__(self, attrname, value)
4 def __setattr__(self,attrname,value):
5 if attrname in self.privates:
----> 6 raise PrivateExc(attrname,self)
7 else:
8 self.__dict__[attrname]=value
PrivateExc: ('name', <__main__.Test2 object at 0x000002D393DF5EF0>)
实际上,这是Python中实现属性私有性(也就是无法在类外对属性名进行修改)的首选方法。虽然Python不支持private声明,但是类似这种技术可以模拟其主要的目的。不过,这只是一部分的解决方案。为使其更有效,必须增强它的功能,让子类也能够设置私有属性,并且使用__getattr__和包装(有时称为代理)来检测对私有属性的读取。
对属性私有性更完整的一个解决方案将推迟到第38章再给出,在哪里,将使用类装饰器来更加通用地拦截和验证属性。即使私有性可以以此方式模拟,但实际应用中几乎不会这么做。不要private声明,Python程序员就可以编写大型的OOP软件框架和应用程序:这是关于访问控制的一般意义上的有趣的发现,超出了在这里所要介绍的范围。
捕捉属性引用值和赋值,往往是很有用的技术。这可支持委托,也是一种设计技术,可以让控制器对象包裹内嵌的对象,增加新行为,并且把其他运算传回包赚的对象(第30章会再谈委托和包装)。
直接上例子:
class Adder:
def __init__(self,value=0):
self.data=value
def __add__(self,other):
self.data+=other
class Addrepr(Adder):
def __repr__(self):
return 'Addrepr(%s)' %self.data
x=Addrepr(2)
x+1
x
Addrepr(3)
print(x)
str(x),repr(x)
Addrepr(3)
('Addrepr(3)', 'Addrepr(3)')
会发现,可以有两种显示方法来进行展示。概括地讲,这是为了进行用户友好的显示。具体来说:
总而言之,__repr__用于任何地方,除了当定义了一个__str__的时候,使用print和str。然而要注意,如果没有定义__str__。打印还是使用__repr__,但反过来并不会成立——其他环境,例如,交互相应模式,只是使用__repr__,并且根本不会尝试__str__。
正是由于这一点,如果想让所有环境都有统一的显示,__repr__是最佳选择。不过,通过分别定义这两个方法,就可在不同环境内支持不同显示。例如,终端用户显示使用__str__,而程序员再开发期间则使用底层__repr__来显示。实际上,__str__只是覆盖了__repr__以得到用户友好的显示环境。
class Addboth(Adder):
def __str__(self):
return '[Value: %s]' % self.data
def __repr__(self):
return 'addboth(%s)'% self.data
x=Addboth(4)
x+1
str(x),repr(x)
('[Value: 5]', 'addboth(5)')
根据一个容器的字符串转换逻辑,__str__的用户友好的显示可能只有当对象出现再一个打印操作顶层的时候才应用,嵌套到较大对象中的对象则可能用其__repr__或默认方法打印。如下的代码说明了这两点:
class Printer:
def __init__(self,val):
self.val=val
def __str__(self):
return str(self.val)
objs=[Printer(2),Printer(3)]
for x in objs:
print(x)
print(objs)
2
3
[<__main__.Printer object at 0x000002D393DC6908>, <__main__.Printer object at 0x000002D393DC6278>]
因此如果为了确保一个定制显示在所有的环境中都显示而不管容器是什么,那么请编写__repr__,而不是__str__。此外,这两种方法都只能返回字符串,因此最好进行类型的强制转换(即使用str内置函数)。
在实际应用中,除了__init__以外,__str__(或其底层的近亲__repr__)似乎是Python脚本中第二个最常用的运算符重载方法。在可以打印对象并且看见定制显示的任何时候,可能就是使用了这两个工具中的一个。
从技术方面来讲,前边例子中出现的__add__方法并不支持+运算符右侧使用实例对象。要实现这类表达式,而支持可互换的运算符,可以一并编写__radd__方法。只有当+右侧的对象是实例而左边对象不是类实例的时候,Python才会调用这个方法。在其他所有时候都是调用__add__。
class Commuter:
def __init__(self,val):
self.val=val
def __add__(self,other):
print('add',self.val,other)
return self.val+other
def __radd__(self,other):
print('radd',self.val,other)
return other+self.val
x=Commuter(88)
y=Commuter(99)
print(x+1)
print(1+y)
print(x+y)
add 88 1
89
radd 99 1
100
add 88 <__main__.Commuter object at 0x000002D393DEDFD0>
radd 99 88
187
注意,x和y是同一个类的实例,当不同类的实例混合出现在表达式时,Python优先选择左侧的那个类。当把两个实例相加的时候,Python运行__add__,它反过来通过简化左边的运算数来触发__radd__。
在更为实际的类中,其中类类型可能需要在结果中传播,事情可能变得更需要技巧:类型测试可能需要辨别它是否能够安全地转换并由此避免嵌套。例如,下面的代码中如果没有isinstance测试,当两个实例相加并且__add__触发__radd__的时候,最终将得到一个Commuter,其val是另一个Commuter:
class Commuter:
def __init__(self,val):
self.val=val
def __add__(self,other):
if isinstance(other,Commuter):other=other.val
return Commuter(self.val+other)
def __radd__(self,other):
return Commuter(other+self.val)
def __str__(self):
return '' % self.val
x=Commuter(88)
y=Commuter(99)
print(x+10)
z=x+y
print(z)
print(z+10)
print(z+z)
为了也实现+=原处扩展相加,可以编写一个__iadd__或者__add__。如果前者空缺的化,使用后者。但是,__iadd__考虑到了更加高效的原处修改:
class Number:
def __init__(self,val):
self.val=val
def __iadd__(self,other):
self.val+=other
return self
x=Number(5)
x+=1
x+=1
x+=1
x.val
8
每个二元运算斗殴类似的右侧和原处重载方法,它们以相同的方式工作(例如,__mul__,__rmul__,__imul__)。右侧方法是一个高级话题,并且实际中很少使用到,只有在需要运算符具有交换性的时候,才会编写它们,并且之后在真值需要支持这样的运算符的时候,才会使用。例如,一个Vector类可能使用这些工具,但一个Employee或Buttom类可能不会。
当调用实例时,使用__call__方法。不,这不是循环定义:如果定义了,Python就会为实例应用函数调用表达式运行__call__方法。这样可以让类实例的外观和用法类似于函数。
class Callee:
def __call__(self,*pargs,**kargs):
print('Called:',pargs,kargs)
C=Callee()
C(1,2,3)
Called: (1, 2, 3) {}
C(1,2,3,x=4,y=5)
Called: (1, 2, 3) {'x': 4, 'y': 5}
更正式地说,在第18章所介绍的所有参数传递方式,__call__方法都支持——传递给实例的任何内容都会传递给该方法,包括通常隐式的实例参数。例如,方法定义:
calss C:
def __call__(self,a,b,c=5,d=6):...
class C:
def __call__(self,*pargs,**kargs):...
class C:
def __call__(self,*pargs,d=6,**kargs):...
直接的效果是,带有一个__call__的类和实例,支持与常规函数和方法完全相同的参数语法和语义。
像这样的拦截调用表达式允许类实例模拟类似函数的外观,但是,也在调用中保持了状态信息以供使用:
class Prod:
def __init__(self,value):
self.value=value
def __call__(self,other):
return self.value*other
x=Prod(2)
x(3)
6
以上示例乍一看可能有点奇怪,毕竟任何一个简单的方法都能提供类似的功能。
然而,当需要为函数的API编写接口时,__call__就变得很有用:这可以编写遵循需要的函数来调用接口对象,同时又能保留状态信息。事实上,这可能是除了__init__构造函数以及__str__和__repr__显示格式方法外,第三个最常用的运算符重载方法了。
作为例子,tkinter GUI工具箱可以把函数注册成实践处理器(也就是回调函数callback)。当事件发生时,tkinter会调用已经注册的对象。如果想让实践处理器保存事件之间的状态,可以注册类的绑定方法(bound method)或者遵循所需接口的实例(使用__call__)。在这一节的代码中,第二个例子中的x.comp和第一个例子中的x都可以用作这种方式作为类似于函数的对象进行传递。
下一章会再介绍绑定方法,这里仅举一个假设的__call__例子,应用于GUI领域。
from tkinter import Button
# 下列类定义了一个对象,支持函数调用接口,但也有状态信息,可记住稍后按下按钮后应该变成什么颜色。
class Callback:
def __init__(self,color):
self.color=color
def __call__(self):
print('turn',self.color)
# 现在,在GUI环境中,即使这个GUI期待的事件处理器是无参数的简单函数,我们还是可以为按钮把这个类的实例注册成事件处理器。
cb1=Callback('blue') # Rember blue
cb2=Callback('green')
B1=Button(command=cb1) # Register handlers
B2=Button(command=cb2)
# 当这个按钮被按下时,就会把实例对象当成简单的函数来调用,就像下面的调用一样。不过,因它把状态保留成实例的属性,所以知道应该做什么。
cb1()
cb2()
turn blue
turn green
实际上,这可能是Python语言中保留状态信息的最好方式,比之前针对函数所讨论的技术更好(全局变量、嵌套函数作用域引用以及默认的可变参数等)。利用OOP,状态的记忆是明确地使用属性赋值运算而实现的。
在继续之前,Python程序员偶尔还会使用其他两种方式,把信息和回调函数联系在一起。其中一个选项是使用lambda函数的默认参数:
cb3=(lambda color='red':'turn '+color)
print(cb3())
turn red
另一种方法是使用类的绑定方法:这种对象记住了self实例以及所引用的函数,使其可以在稍后通过简单的函数调用而不需要实例来实现。
class Callback:
def __init__(self,color):
self.color=color
def changeColor(self):
print('turn',self.color)
cb1=Callback('blue')
cb2=Callback('yellow')
B1=Button(commond=cb1.changeColor) # Reference,but don't call
B2=Button(commond=cb2.changeColor) # Remembers function+self
# 当按下按钮时,就好像是GUI这么做的,启动changeColor方法来处理对象的状态信息:
object=Callback('blue')
cb=object.changeColor
cb()
这种技巧较为简单,但比起__call__重载调用而言就不通用了;统一,有关绑定方法可参考下一章的内容。
第31章将会看到另一个__call__的例子,通过它来实现所谓的函数装饰器的概念:它是可调用对象,在嵌入的函数上多加一层逻辑。因为__call__可把状态信息附加在可调用的对象上,所以自然而然地称为了被一个函数记住并调用了另一个函数的实现技术。
在类中定义比较运算符(<、>、<=、>=、==、!=)时需记住一下限制:
以下是一个快速介绍的例子:
class C:
data='spam'
def __gt__(self,other):
return self.data>other
def __lt__(self,other):
return self.data<other
X=C()
print(X>'ham')
print(X<'ham')
True
False
类也可能定义了赋予其实例布尔特性的方法——在布尔环境中,Python首先尝试__bool__来获取一个直接的布尔值,然后,如果没有该方法,就尝试__len__来根据对象的长度确定一个真值。通常,首先使用对象状态或其他信息来生成一个布尔结果:
class Truth:
def __bool__(self):return True
X=Truth()
if X:print('yes!')
yes!
class Truth:
def __bool__(self):return False
X=Truth()
bool(X)
False
如果没有这个方法,Python退而求其次地求长度,因为一个非空对象看作真:
class Truth:
def __len__(self):return 0
X=Truth()
bool(X)
False
如果两个方法都有,则使用__bool__:
class Truth:
def __init__(self,data):
self.data=data
def __bool__(self):
return True
def __len__(self):
return len(self.data)
X=Truth([])
bool(X),len(X)
(True, 0)
每当实例产生时,就会调用__init__构造函。每当实例空间被回收时(在垃圾收集时),它的对立面__del__,也就是析构函数(destructor method)就会自动执行。
class Life:
def __init__(self,name='unknown'):
print('Hello,',name)
self.name=name
def __del__(self):
print('Goodbye,',self.name)
brian=Life('Brian')
Hello, Brian
del brian # 或者把变量brian赋值成其他对象类型
Goodbye, Brian
基于某些原因,在Python中,析构函数不像其他OOP语言那么常用。原因之一就是因为Python在实例回收时,会自动回收该实例所拥有的所有空间,对于空间管理来说,是不需要析构函数的。原因之二就是无法轻易地预测实例何时回收,通常最好的方法是在有意调用的方法中(或者try/finally语句)编写代码去终止活动。在某种情况下,系统表中可能还在引用该对象,使析构函数无法执行。