骑麦兜看落日

[Code]Python学习手册_第六部分_类和OOP

字数统计: 32k阅读时长: 133 min
2018/08/14 Share

第二十五章 OOP:宏伟蓝图

  • 本章对类和OOP进行了抽象的学习

要让代码真正归类于面向对象(00),那么对象一般也需要参与到所谓的继承层次中。

本章要开始我们对Python类的探索:类是在Python实现支持继承的新种类的对象的部件。类是Python面向对象程序设计(OOP)的主要工具,而本书这一-部分内容中,我们将会一直讨论OOP的基础内容。OOP提供了一种不同寻常而往往更有效的检查程序的方式,利用这种设计方法,我们分解代码,把代码的冗余度降至最低,并且通过定制现有的代码来编写新的程序,而不是在原处进行修改。

在Python中,类的建立使用了一条新的语句: class语句。正如你将看到的那样,通过class定义的对象,看起来很像本书之前研究过的内置类型。事实上,类其实是只运用并扩展了我们谈到过的一些想法。概括地讲,类就是一些函数的包,这些函数大量使用并处理内置对象类型。不过,类的设计是为了创建和管理新的对象,并且它们也支持继承。

然而,阅读本书这一.部分你会得知,类是Python所提供的最有用的工具之一。合理使
用时,类实际,上可以大量减少开发的时间


为何使用类

类是一种定义新种类的东西的方式,反映了在程序领域中的真实对象

通过类可以建立实际结构和关系的模型

  • 继承

    一种代码定制和复用的机制

    类可以继承其他拥有通用属性的类

  • 组合

    类是一些组件的集合,这些组件以团队的形式共同工作

    每个组件都可以写成类,定义自己的行为以及关系

    类是一个组合的实例,它包含其他对象,这些对象来运作完成相应的指令

继承和组合等一般性OOP概念适用于能够分解成一组对象的任何应用程序

类与函数和模块一样是Python的程序组成单元,类是封装逻辑和数据的另一种方式,类会定义新的命名空间

  • 多重实例

    类基本上就是产生对象的工厂

    每次调用一个类,就会产生一个有独立命名空间的新对象

    每个由类产生的对象都能读取类的属性,并获得自己的命名空间来储存数据,这些数据对于每个对象来说都不同

  • 通过继承进行定制

    类也支持OOP的继承的概念

    我们可以在类的外部重新定义其属性从而扩充这个类

    更通用的是,类可以建立命名空间的层次结构,而这种层次结构可以定义该结构中类创建的对象所使用的变量名

  • 运算符重载

    通过提供特定的协议方法,类可以定义对象来响应在内置类型上的几种运算

    例如,通过类创建的对象可以进行切片、级联和索引等运算

    Python提供了一些可以由类使用的钩子,从而能够中断并实现任何的内置类型运算


概览OOP

属性继承搜索

Python中大多数OOP的表达式为

1
object.attribut

当对class语句产生的对象使用这一表达式时会在Python中启动搜索,搜索对象连接的树来寻找attribute首次出现的地方,先搜索object,然后就是该对象之上的所有类

这种搜索程序是继承,树中位置较低的对象继承了树中位置较高的对象拥有的属性,当从下至上进行搜索时,连接至树中的对象就是树中所有上层对象所定义的所有属性的集合体,直到树的最顶端

  • 类是实例工厂

    类的属性提供了行为(数据以及函数),所有从类产生的实例都继承该类的属性

  • 实例

    代表程序领域中具体的元素,
    实例属性记录数据,而每个特定对象的数据都不同

就搜索树来看,实例从它的类继承属性,而类是从搜索树中所有比它更上层的类中继承属性

通常把树中位置较高的类称为超类(基类),树中位置较低的类称为子类(派生类),超类提供所有子类共享的行为,但是由于搜索是由下而上,子类可能会在书中较低位置重新定义超类的变量名从而覆盖超类定义的行为

类和实例

在Python模型中,类和实例是两种不同的对象类型

在搜索树中,类和实例的主要用途都是用来作为另一种命名空间(变量的封装)

类和实例与模块一样有对其他命名空间对象的自动搜索连接,但类对应的是语句而不是整个文件

类和实例的主要差异在于实例由类中产生

内存中特定模块只有一个实例,而类制作多少实例都可以

从操作的角度来说类通常都有函数,而实例有其他基本的数据项,类的函数中使用了这些数据

类方法调用

调用附属于类的函数时,总会隐含着这个类的实例,运算执行时总是有个主体对象,这个隐含的主体或环境就是将其称之为面向对象模型的一部分

Python把隐含的实例传进方法中的第一个特殊的参数,习惯上将其称为self

编写类树

通过class语句和类调用可以构造一些树和对象

  • 每个class语句会生成一个新的类对象
  • 每次类调用时,就会生成一个新的实例对象
  • 实例自动连接至创建了这些实例的类
  • 类连接至其超类的方式是将超类列在类头部的括号内,其从左至右的顺序会决定树中的次序
1
2
3
4
5
6
class C2: ...			# Make class objects(ovals)
class C3: ...
class C1(C2, C3):... # Linked to superclasses

I1 = C1() # Make instance objects(rectangles)
I2 = C1() # Linked to their classes

运行三个class语句创建了三个类对象,然后通过两次调用C1创建两个实例对象

实例记住了他们来自哪个类,类C1也记住了它所列出的超类

这个例子使用了多重继承,在类树中,类有一个以上的超类class语句中小括号内超类由左至右的次序决定超类搜索的顺序

由于继承搜索的方式,属性附加的对象决定了变量名的作用域,附加在实例上的属性只属于实例,但附加在类上的属性由所有子例及其实例共享

属性附加在类和实例对象的代码

  • 属性通常是在class语句中通过赋值语句添加在类中,而不是嵌入在函数的def语句内
  • 属性通常是在类内,对传给函数的特殊参数(也就是self)做赋值运算添加在实例中的
1
2
3
4
5
6
7
8
9
class C1(C2, C3):			# Make and link class C1
def setname(self, who): # Assign name: C1.setname
self.name = who # Self is either I1 or I2

I1 = C1() # Make two instances
I2 = C1()
I1.setname('bob') # Sets I1.name to 'bob'
I2.setname('mel') # Sets I2.name to 'mel'
print(I1.name) # Prints 'bob'

类通过函数为实例提供行为,由于def会在类中对变量名进行赋值,实际效果就是把属性添加在了类对象中,从而可以由所有实例和子类继承

def出现在这种类的内部通常称为方法,而且会自动接收第一个特殊参数self,这个参数提供了被处理的实例的参照值

类可以生产多个实例,每当需要取出或设定正由某个方法调用所处理的特定的实例的属性时,那些方法通常都会通过这个自动传入的参数self,在之前的代码中,self是用来储存两个实例之一的内部变量名的

OOP是为了代码重用


本章习题

  1. Python的OOP的重要的意义是什么

    OOP就是代码的重用:分解代码、最小化代码的冗余以及对现存的代码进行定制来编写程序,而不是实地修改代码,或者从头开始

  2. 继承搜索在哪里查找属性

    继承搜索会先在实例对象中寻找属性,然后才是创建实例的类,之后是所有较高的超类,由对象树底端到顶端,并且从左侧至右侧(默认)

    当属性首次找到时,搜索就会停止

    因为在此过程中变量名的最低的版本会获胜,类的层次自然而然地支持了通过扩展进行代码的定制,

  3. 类对象和实例对象有什么不同

    类和实例对象都是命名空间( 由作为属性的变量的包)

    两者间主要差别是,类是建立多个实例的工厂

    类也支持运算符重载方法,由实例继承,而且把其中的任何函数视为处理实例的特殊的方法

  4. 为什么类方法函数中的第-一个参数特殊

    类方法函数中的第一个参数之所以特殊,是因为它总是接受将方法调用视为隐含主体的实例对象

    按惯例,通常称为self

    因为方法函数默认总是有这个隐含的主体对象环境,所以我们说这是面向对象,也就是设计用来处理或修改对象的

  5. __init__方法是做什么用的

    如果类中编写了或继承了__ init__方法, 每次类实例创建时,Python会自动调用它

    这也称为构造函数

    除了明确传入类的名称的任何参数外,还会隐性的传入新实例

    这也是最常见的运算符重载方法

    如果没有__init__ 方法.实例刚创建时就是一个简单的空的命名空间

  6. 怎样创建类实例

    你可以调用类名称(就好像函数一样)来创建类实例

    任何传给类名称的参数都要出现在__ init__ 构造函数中第二和其后的参数

    新的实例会记得创建它的类,从而可以实现继承目的

  7. 怎样创建类

    你可以运行class语句来创建类

    就像函数定义一样,这些语句在所在的模块文件导人时,一般就会运行

  8. 怎样定义类的超类

    定义一个类的超类是通过在class语句的圆括号中将其列出,也就是在新的类名称后

    类在圆括号中由左至右列出的顺序,会决定其在类树中由左至右的搜索的顺序


第二十六章 类代码编写基础

  • 本章分析一些实际应用中的基本的类从而详细探讨OOP
  • 研究class语句的语法,了解它是如何用于创建类的继承树的
  • 研究Python如何自动添加方法函数内的第一个参数
  • 研究属性如何通过简单赋值语句把属性加到类树中的对象
  • 研究特殊名称运算符重载方法如何替实例截获并实现内置运算

类产生多个实例对象

类对象提供默认行为,是实例对象的工厂

实例对象是程序处理的实际对象,各自有独立的命名空间,但是继承创建该实例的类中的变量名

类对象来自于语句,实例对象来自于调用

类对象提供默认行为

Python类主要特性

  • class语句创建类对象并将其赋值给变量名

    就像函数def语句,Python class语句也是可执行语句,执行时会产生新的类对象,并将其赋值给class头部的变量名

    就像def应用,class语句一般是在其所在文件导入时执行的

  • class语句内的赋值语句会创建类的属性

    就像模块文件一样,class语句内的顶层的赋值语句会产生类对象中的属性

    从技术角度来讲class语句的作用域会变成类对象属性的命名空间,就像模块的全局作用域一样

    执行class语句后,类的属性可由变量名点号运算获取object.name

  • 类属性提供对象的状态和行为

    类对象的属性记录状态信息和行为,由这个类所创建的所有实例共享

    位于类中的函数def语句会生成方法,方法将会处理实例

实例对象是具体的元素

类的实例

  • 像函数那样调用类对象会创建新的实例对象

    每次类调用时,都会建立并返回新的实例对象,实例代表了程序领域中的具体元素

  • 每个实例对象继承类的属性并获得了自己的命名空间

    由类所创建的实例对象是新命名空间,一开始是空的,但是会继承创建该实例的类对象内的属性

  • 在方法内对self属性做赋值运算会产生每个实例自己的属性

    在类方法函数内,第一个参数self会引用当前调用的实例对象

    self的属性做赋值运算会创建或修改实例内的数据,而不是类的数据

第一个例子

1
2
3
4
5
6
>>> class FirstClass:				# Define a class object
... def setdata(self, value): # Define class methods
... self.data = value # self is the instance
... def display(self):
... print(self.data) # self.data:per instance
...

def建立函数相同,class语句在执行时创建类对象

class语句首行列出类的名称,:后接一个或多个内嵌语句的主体

class语句内所有位于顶层的赋值语句会产生附加在类上的属性,def语句是赋值运算,位于类中的函数称为方法

在方法函数中,调用时第一个参数自动接收隐含的实例对象即调用的主体

1
2
3
4
>>> x = FirstClass()			# Make twi instances
>>> y = FirstClass() # Each is a new namespace
>>> x.setdata("King arthur") # Call methods:self is x
>>> y.setdata(3.14159) # Runs:FitstClass.setdata(3.14159)

调用类产生的实例对象可以读取类属性的命名空间,类和实例是类树中通过继承搜索的相连的命名空间

继承是在对对象.运算时发生的,只与查找相连各个对象内的变量名有关,对实例以及类对象内的属性名称进行.运算可以通过继承搜索从实例或类中取得变量名

1
2
3
4
>>> x.display()		# self.data differs in each instance
King arthur
>>> y.display()
3.14159

在方法中,self自动引用正在处理的实例,赋值语句会把值储存在实例的命名空间而不是类的命名空间

不同实例的self.data值不同,但display方法相同,都继承自类

实例属性(成员)在首次赋值后存在,接受任何对象类型

1
2
3
4
5
>>> x.data = "New value"	# Can get/set attributes
>>> x.display() # Oytside the class too
New value

>>> x.anothername = "spam" # Can set new attributes here too!

可以在方法内对self赋值运算修改实例属性,或者在类外通过对实例对象赋值运算修改属实例性

可以在实例命名空间内产生全新的属性,实例对象的任何类方法都可以使用该对象

类通过self参数进行赋值运算建立实例的所有属性


类通过继承进行定制

类可以引入子类来进行修改,而不对现有组件进行原地的修改

编写类层次结构可以通过让类继承其他类实现,在阶层较低的地方覆盖现有的属性,让行为特定化

属性继承机制

  • 超类列在了类开头的括号中

    要继承另一个类的属性需要把该类列在class语句开头的括号中

    含有继承的类称为子类,而子类所继承的类就是其超类

  • 类从其超类中继承属性

    就像实例继承其类中所定义的属性名一样,类也会继承其超类中定义的所有属性名称

    当读取属性时,如果属性不存在于子类中,Python会自动搜索父类中的这个属性

  • 实例会继承所有可读取类的属性

    每个实例会从创建它的类及该类的超类中获取变量名

    寻找变量名时,Python会检查实例,然后是它的类,最后是所有超类

  • 每个object.attribute都会开启新的独立搜索

    Python会对每个属性取出表达式进行对类树的独立搜索,包括在class语句外对实例和类的引用(例如,X.attr)

    方法中的每个self.attr表达式都会开启对self及其上层中类的attr属性的搜索

  • 逻辑的修改是通过创建子类,而不是修改超类

    在树中层次较低的子类中重新定义超类的变量名,子类就可取代并定制所继承的行为

类树搜索是由于类支持程序的分解和定制,且可以把程序的冗余度降到最低

第二个例子

1
2
3
4
5
6
7
8
9
10
11
>>> class FirstClass:				# Define a class object
... def setdata(self, value): # Define class methods
... self.data = value # self is the instance
... def display(self):
... print(self.data) # self.data:per instance
...

>>> class SecondClass(FirstClass): # Inherits setdata
... def display(self): # Changes display
... print('Current value = "%s"' % self.data)
...

在树中较低处发生的重新定义的、取代属性的动作称为重载,类树搜索中同名的属性会在树中较低位置处首先被找到,所以会覆盖父类中的同名属性

1
2
3
4
5
6
7
8
>>> x = FirstClass()
>>> x.data = "New value" # Can get/set attributes
>>> z = SecondClass()
>>> z.setdata(42) # Finds setdata in FirstClass
>>> z.display() # Finds overridden method in SecondClass
Current value = "42"
>>> x.display() # x is still a FirstClass instance
New value

子类及其实例会继承父类的非同名属性

子类属性的专有化是在父类外部完成的,不会影响到父类对象

类是模块内的属性

class语句执行时将类对象赋值给类名

1
2
3
4
5
6
7
from modulename import FirstClass			# Copy name into my scope
class SecondClass(FirstClass): # Use class name directly
def display(self): ...

import modulename # Access the whole module
class SecondClass(modulename.FirstClass): # Qualify to reference
def display(self): ...

类对象可以用任何普通表达式引用,可以在交互模式下输入或写在文件中将其导入

1
2
3
4
5
6
7
8
9
10
# food.py
var = 1 # food.var
def func(): # food.func
...
class spam: # food.spam
...
class ham: # food.ham
...
class eggs: # food.eggs
...

类存在于模块,遵循模块相关的规则

  • 单一模块文件内可以有一个以上的类
  • class语句导入时执行,类名会变成独立的模块属性
  • 每个模块可以任意混合任意数量的变量、函数以及类,而模块内的所有变量名的行为都相同
1
2
3
4
5
6
7
8
9
# person
class person:
...

% python
import person # import module
x = person.person() # Class within module
from person import person # Get class from module
x = person() # Use class name

模块名与类名相同时以同样方式导入

Python通用惯例指出,类名应该以一个大写字母开头使得它们更为清晰


类可以截获Python运算符

运算符重载就是让用类写成的对象可截获并响应用在内置类型上的运算,包括加法、切片、打印和点号运算

运算符重载是表达式方法的自动分发机制,表达式和其他内置运算流程要经过类的实现来控制,可以自己在类对象中以任何方式解释运算符

运算符重载是可选的功能,主要应用于开发Python工具

  • 以双下划线命名的方法__X__

    Python运算符重载的实现是提供特殊命名的方法来拦截内置运算

    Python语言替每种运算和特殊命名的方法之间定义了固定不变的映射关系,如+对应类的__add__方法

  • 当实例出现在内置运算时,这类方法会自动调用

    例如,如果实例对象继承了__add__方法,当对象出现在+表达式内时,该方法就会自动调用,该方法的返回值会变成相应+表达式的结果

  • 类可覆盖多数内置类型运算

    有几十种特殊运算符重载的方法的名称,几乎可截获并实现内置类型的所有运算,它不仅包括了表达式,而且像打印和对象建立这类基本运算也包括在内

  • 运算符覆盖方法没有默认值,而且也不需要

    如果类没有定义或继承运算符重载方法,相应的运算在类实例中并不支持

    例如,如果类中没有__add__, +表达式对对象运算就会引发异常

  • 运算符可让类与Python的对象模型相集成

    重载类型运算使以类实现的用户定义对象的行为会像内置对象一样,提供了对象接口一致性以及与预期接口的兼容性

第三个例子

定义类ThirdClass实现三个特殊名称的属性

  • 当新的实例构造时,会调用__ init__
  • ThirdClass实例出现在+表达式中时会调用__add__
  • 当打印一个对象的时候会运行__str__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
>>> class FirstClass:				# Define a class object
... def setdata(self, value): # Define class methods
... self.data = value # self is the instance
... def display(self):
... print(self.data) # self.data:per instance
...
>>> class SecondClass(FirstClass): # Inherits setdata
... def display(self): # Changes display
... print('Current value = "%s"' % self.data)
...
>>> class ThirdClass(SecondClass): #Inherit from SecondClass
... def __init__(self, value): # On "ThirdClass(value)"
... self.data = value
... def __add__(self, other): # On "self + other"
... return ThirdClass(self.data + other)
... def __str__(self): # On "print(self)", "str()"
... return '[ThirdClass: %s]' % self.data
... def mul(self, other): # In-place change:named
... self.data *= other
...
>>> a = ThirdClass('abc') # __init__ called
>>> a.display() # Inherited method called
Current value = "abc"
>>> print(a) # __str__:returns display string
[ThirdClass: abc]
>>> b = a + 'xyz' # __add__:makes a new instance
>>> b.display() # b has all ThirdClass methods
Current value = "abcxyz"
>>> print(b) # __str__:returns display string
[ThirdClass: abcxyz]
>>> a.mul(3) # mul:changes instance in-place
>>> print(a)
[ThirdClass: abcabcabc]

__init__运算符重载方法也称为构造函数方法,实例化类时传递的参数会传给__init__构造函数内的value参数,用于初始化对象的状态

+操作会把左侧的实例对象传给__add__中的self参数,把右边的值传给other,返回的值作为+表达式的结果,返回时会创建这个类新的实例对象

print中要打印的对象会被传递给__str__中的self,返回的字符串作为对象的打印字符串

__init____add____str__等特殊命名的方法与其他变量名一样会由子类和实例继承

运算符重载方法的名称不是内置变量或保留字,只是当对象出现在不同的环境时Python会去搜索的属性,Python会自动调用或可以由程序代码调用

为什么要使用运算符重载

如果省略运算符重载方法且不从超类中继承该方法,实例就不支持相应的运算,会抛出异常

只有在实现本质为数学的对象时才会用到许多运算符重载方法

需要传递用户定义的对象给预期的内置类型(例如,列表或字典)一个可用的运算符的函数,可能需要使用运算符重载.在类中实现同一组运算符,可以保证对象会支持相同的预期的对象接口从而与这个函数兼容

每一个实际的类都会出现一个重载方法__init__构造函数,可以让类立即在其新建的实例内添加属性


世界上最简单的Python类

类的基本继承模型就是在连接的对象树中搜索属性

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class rec: pass		# Empty namespace object
...
>>> rec.name = 'Bob' # Just objects with attributes
>>> rec.age = 40

>>> x = rec() # Instances inherit class names
>>> y = rec() # name is stored on the class only
>>> x.name, y.name
('Bob', 'Bob')

>>> x.name = 'Sue' # But assignment changes x only
>>> rec.name, x.name, y.name
('Bob', 'Sue', 'Bob')

建立的类可以没有附加的属性,需要添加pass语句

建立类后可以通过赋值变量名直接为增加属性而不需要实例化类,因为类本身也是对象,有独立完备的命名空间,可以在任何时刻设定或修改其属性

实例化类时实例是空的命名空间对象,可以继承并获取附加在类上的属性,但是实例本身没有属性

如果把一个属性赋值给一个实例,就会在该实例对象内创建或修改该属性,但不会因属性的引用而启动继承搜索,因为属性赋值运算只会影响属性赋值所在的类或实例对象

1
2
3
4
5
6
7
8
9
10
11
>>> rec.__dict__.keys()
dict_keys(['__module__', '__dict__', '__weakref__', '__doc__', 'name', 'age'])
>>> list(x.__dict__.keys())
['name']
>>> list(y.__dict__.keys())
[]

>>> x.__class__
<class '__main__.rec'>
>>> rec.__bases__
(<class 'object'>,)

命名空间对象的属性通常是以字典的形式实现的,而继承树只是连接至其他字典的字典

__dict__属性是大多数基于类的对象的命名空间字典(一些类也可能在__slots__中定义属性)

通过 __class__属性可以查看实例继承的类,__base__属性显示类的超类的元组,这两个属性是Python在内存中类树常量的表示方式

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def upperName(self):
... return self.name.upper() # Still needs a self
...
>>> upperName(x) # Call as a simple fucntion
'SUE'

>>> rec.method = upperName
>>> x.method() # Run method to process x
'SUE'
>>> y.method() # Same, but pass y to self
'BOB'
>>> rec.method(x) # Can call through instance or class
'SUE'

Python的类模型相当动态,类和实例只是命名空间对象,能引用树中任何一个对象的任意位置处都可以通过通过赋值语句动态建立属性

函数也可以独立地在任意类的外部创建类的属性

将类外部的函数赋值为类的属性,就会变成方法,可以由任何实例调用

通常情况下,类是由class语句填充,而实例的属性则通过在类的方法函数内对self属性进行赋值运算创建

Python中的OOP实际上是在已连接命名空间对象内寻找属性

类与字典的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> rec = {}
>>> rec['name'] = 'mel' # Dictionary-based record
>>> rec['age'] = 45
>>> rec['job'] = 'trainer/writer'
>>> print(rec['name'])
mel

>>> class rec: pass
...
>>> rec.name = 'mel' # Class-based record
>>> rec.age = 45
>>> rec.job = 'trainer/writer'
>>> print(rec.age)
45

>>> class rec: pass
...
>>> pers1 = rec() # Instance-based records
>>> pers1.name = 'mel'
>>> pers1.job = 'trainer'
>>> pers1.age = 40
>>> pers2 = rec()
>>> pers2.name = 'vls'
>>> pers2.job = 'developer'
>>> pers1.name, pers2.name
('mel', 'vls')

字典可以记录程序中实体的属性

类可以像字典一样打包信息,使用键来记录属性,对类属性赋值来填充它

对于每一条不同的记录,我们可以实例化空的类,对实例的属性赋值来填充记录,不同的实例有不同的名称空间所以有不同的属性字典

1
2
3
4
5
6
7
8
9
10
11
>>> class Person:
... def __init__(self, name, job): # Class = Data + Logic
... self.name = name
... self.job = job
... def info(self):
... return (self.name, self.job)
...
>>> rec1 = Person('mel', 'trainer')
>>> rec2 = Person('vls', 'developer')
>>> rec1.job, rec2.info()
('trainer', ('vls', 'developer'))

也可以以方法的形式绑定处理逻辑,在构建的时候初始化实例并把属性收集到一个元组中


本章习题

  1. 类和模块之间有什么关系

    总是位于模块中,类是模块对象的属性

    模块都是命名空间

    类对应于语句而不是整个文件,而且支持多个实例、继承以及运算符重载这些OOP概念

    模块就像是单个的实例类,没有继承,而且模块对应于整个文件的代码。

  2. 实例和类是如何创建的

    类是通过运行class语句创建的

    实例是像函数那样调用类来创建的

  3. 类属性是在哪里创建的,是怎样创建的

    类属性的创建是通过把属性赋值给类对象实现的

    类属性通常是由class语句中的顶层赋值语句而产生的,class语句的作用域会变成类对象属性的命名空间
    可以在任何引用类对象的地方(class语句外)对其属性赋值从而创建类属性

  4. 实例属性是在哪里创建的,是怎样创建的

    实例属性是通过对实例对象赋值属性来创建的

    实例属性一般是在class语句中的类方法函数中对self参数赋值属性而创建的

    可以在任何引用了实例的地方通过赋值语句来创建属性,包括class语句外

    一般来说,所有实例属性都是在__ init__构造函数中初始化的,这样的话,之后的方法调用都可假设属性已经存在

  5. Python类中的self有什么意义

    self通常是类方法函数中第一个参数的名称,Python会自动为其填入实例对象(也就是方法调用的隐含主体)

    这个参数不一定叫self,其位置才是重点

  6. Python类中如何编写运算符重载

    Python类中的运算符重载是用特定名称的方法写成的,这些方法的开头和结尾都是双下划线,但不是内置函数或保留字

    当实例出现在相应的运算中时,Python就会自动执行特定名称的方法

    Python为这些运算和特殊方法的名称定义了对应关系

  7. 什么时候可能在类中支持运算符重载

    运算符重载可用于实现模拟内置类型的对象(例如,序列或像矩阵这样的数值对象),以及模拟代码中所预期的内置类型接口

    模拟内置类型的接口可让你传入具有状态信息(也就是记住操作调用之间数据的属性)的类实例

    当简单命名的方法就够用时,不应该使用运算符重载

  8. 哪个运算符重载方法是最常用的

    __init__构造函数是最常用的

    几乎每个类都使用这个方法为实例属性进行初始化,以及执行其他的启动任务

  9. Python OOP程序代码中最重要的两个概念是什么

    方法函数中的特殊self参数和__init__构造函数是Python中OOP的两个基石


第二十七章 更多实例

  • 展示关于类的一些更加实际的例子
  • 介绍Python类和OOP的所有基础知识的实际应用
  • 提供关于Python面向对象程序设计的教程
  • 介绍多种减少冗余性和最小化维护代价的构建代码的方式
  • 简单地介绍了使用GUI和数据库这样的应用程序工具来拓展代码的方式

步骤1:创建实例

编写主类Person

1
2
3
# File person.py(start)

class Person:

在Python中,按照惯例,模块名使用小写字母,而类名使用一个大写字母开通

在Python中的单个模块文件可以编写任意多个函数和类

当模块拥有一个单一、一致的用途时工作得更好

编写构造函数

1
2
3
4
5
6
7
# Add record field initialization

class Person():
def __init__(self, name, job, pay): # Constructor takes 3 arguments
self.name = name # Fill out fields when created
self.job = job # self is the nw instance object
self.pay = pay

通常通过给类方法函数中的self属性赋值来实例对象属性

赋给实例属性第一个值的通常方法是在__init__构造函数方法中将它们赋值给self

创建实例时传入的参数会作为构造函数方法的参数,可以将参数赋值给self对应的属性,self就是新创建的实例对象,传入的参数称为状态信息

1
2
3
4
5
6
7
# Add defaults for constructor arguments

class Person():
def __init__(self, name, job=None, pay=0): # Normal function args
self.name = name
self.job = job
self.pay = pay

构造函数方法__init__是一个常规的函数,支持所有的函数特性,可以为参数提供默认值

在产生一个实例时会自动调用构造函数,它包含了每次创建一个实例的时候Python会自动运行的代码

在进行中测试

Python编程是一种增量原型,可以编写一些代码、测试它编写更多代码、再次测试,以此类推

Python交互式会话可以在进行中多次测试代码,但是每次新的测试会话都需要重新导入模块和重新输入测试实例

1
2
3
4
5
6
7
8
9
10
11
12
# Add incremental self-test  code

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay

bob = Person('Bob Smith') Test the class
sue = Person('Sue Jones', job='dev', pay=100000) # Runs __init__ automatically
print(bob.name, bob.pay) # Fetch attached attributes
print(sue.name, sue.pay) # sue's and bob's attrs differ

可以在包含测试对象文件的底部编写代码来进行更多的大量测试且更容易再次运行

关键字参数可以根据位置或关键字传递参数,关键字参数可以用来提醒数据的作用且没有顺序

1
2
3
% python person.py
Bob Smith 0
Sue Jones 100000

测试结果证明每一个实例都是一个命令空间对象,拥有各自类所创建的状态信息的独立副本

类的每一个实例都有自己的一组self属性,类通过这种方式记录多个对象的信息

以两种方式使用代码

在其他地方导入该类使用它时仍会看到其测试代码的输出

可以把测试代码分割到不同的文件中,但是与被测试项目相同的文件中编写代码更加方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Allow this file to be imported as wel as run/tested

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay

if __name__ == '__main__': # When run for testing only
# self-test code
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob.name, bob.pay)
print(sue.name, sue.pay)

__name__检查模块可以在文件运行时而非导入时运行测试语句

1
2
3
4
5
6
7
% python person.py
Bob Smith 0
Sue Jones 100000

% python
>>> import person
>>>

导入时,文件定义了类,但是没有使用它

直接运行时,文件创建了类的两个实例并打印属性


步骤2:添加行为方法

尽管类添加了结构的一个额外的层级,但最终还是通过嵌入和处理列表及字符串这样的基本核心数据类型来完成大部分工作,类是最小的结构性拓展

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Process embedded built-in types: strings, mutability

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob.name, bob.pay)
print(sue.name, sue.pay)
print(bob.name.split()[-1]) # Extract object's last name
sue.pay *= 1.10
print(sue.pay)

可以对实例对象的属性应用对Python核心对象的基本操作,但是目标对象和类结构中的属性联系起来了

像这样在类之外的硬编码操作可能会导致未来的维护问题

编写方法

封装的思想是把操作逻辑包装到界面之后,这样每种操作在程序里只编码一次,需要修改时只需要修改一处且修改时不会影响到使用它的代码

我们需要操作对象的代码位于类方法中而不是分散在整个程序中,可以构造代码以删除冗余并优化维护,把操作放入方法中还会使得这些操作应用于类的任何实例

方法只是附加给类并旨在处理那些类的实例的常规函数,实例是方法调用的主体,并且会自动传递给方法self参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Add methods to encapsulate operations for maintainability

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self): # Behavior methods
return self.name.split()[-1] # self is implied subject
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent)) # Must change here only

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob.name, bob.pay)
print(sue.name, sue.pay)
print(bob.lastName(), sue.lastName()) # Use the new methods
sue.giveRaise(.10) # instead of hardcoding
print(sue.pay)

对于舍入问题,可以在方法中调用内置的int函数把结果转换为整数,使用内置的round(N,2)来舍入并保留分币,使用decimal类型来修改精度或存储为一个完整的浮点数并使用%.2f{0:.2f}格式化字符串显示

Python自动把实例传递给第一个参数,从而告诉一个方法应该处理哪个实例,通常这个参数叫做self


步骤3:运算符重载

实例对象的默认显示格式为对象的类名及其在内存中的地址,必须手动地接受和打单个的属性

提供打印显示

通过运算符重载,编写一个在类的实例运行时将其截获并处理内置的操作的方法

__str__方法在实例转换为其可打印字符串的时候自动运行,打印一个对象会显示对象的__str__方法所返回的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Add __str__ overload method for printing object

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self):
return self.name.split()[-1]
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent))
def __str__(self):
return '[Person: %s, %s]' % (self.name, self.pay)

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)

__str__中使用%格式构建字符串,类使用这样的内置类型对象和操作来完成任务

所有内置类型函数的内容都适用于基于类的代码,类很大程度上只是添加了额外的一层结构把函数和数据包装在一起并且支持拓展


步骤4:通过子类定制行为

之前编写的类有效地把数据和逻辑一起包装到一个单个的、自包含的结构中,使得将来能够很容易地定位代码并很直接地修改代码

继承是基于已完成的工作的定制来促进一种编码模式,可以显著地缩减开发时间

编写子类

1
class Manager(Person):		# Define a subclass of Person

通过创建一个类继承超类,并且可以添加一些定制

1
2
class Manager(Person):							# Inherit  Person attrs
def giveRaise(self, percent, bonus=.10): # Redefine to customize

通过重新定义方法有效地替代了该方法,从而定制了该操作

拓展方法:不好的方式

1
2
3
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
self.pay = int(self.pay * (1 + percent + bonus)) # Bad:cut-and-paste

不好的编写定制方法的方式是复制和粘贴超类的代码,然后进行修改

复制粘贴会使未来的维护工作倍增,当需要修改代码时必须修改两个地方的代码

拓展方法:好的方式

类方法总是可以在一个实例中调用或者通过类来调用,通过类调用需要手动地传递实例

1
instance.method(args...)

自动地转换为

1
class.method(instance, args...)

包含要运行的方法的类,由该方法的继承搜索规则确定

可以在脚本中以任何一种形式编写,通过类名调用的方式需要自己给self发送一个实例

1
2
3
class Manager(Person):
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus) # Good:augment original

通过使用拓展的参数来直接调用其超类的版本来拓展方法

通过类直接调用有效地扰乱了继承,并且把调用沿着类树向上传递一运行一个特定的版本

这样对未来的代码维护意义重大

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# Add customization of one behavior in a subclass

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self):
return self.name.split()[-1]
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent))
def __str__(self):
return '[Person: %s, %s]' % (self.name, self.pay)

class Manager(Person):
def giveRaise(self, percent, bonus=.10): # Redefine at this level
Person.giveRaise(self, percent + bonus) # Call Person's version

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 'mgr', 50000) # Make a Manager:__init__
tom.giveRaise(.10) # Runs custom version
print(tom.lastName()) # Runs inherited method
print(tom) # Runs inherited __str__

子类继承了超类的__str____init__方法

多态的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if __name__ == '__main__':
bob = person('Bob Smith')
sue = person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 'mgr', 50000)
tom.giveRaise(.10)
print(tom.lastName())
print(tom)
print('--All three--')
for object in (bob, sue, tom): # Process objects generically
object.giveRaise(.10) # Run this object's giveRaise
print(object) # RUn the common __str__

根据所传递的对象的类型自动运行相应的版本就是多态

继承、定制和扩展

类可以继承、定制和扩展超类中已有的代码

可以在子类中添加一个独特的、超类中没有的方法进行扩展,这样的扩展只能在子类中使用

OOP:大思路

在OOP中可以通过定制来编程而不是复制和修改已有的代码

类所隐藏的编程风格会显著地减少开发时间

可以用类来构建可定制层级,为随时间而发展的软件提供更好的解决方案

  • 从头编写全新的、独立的类需要重新实现原来类中相同的行为
  • 直接原处修改已有的类可能会使需要原来的操作的地方无法满足
  • 在子类中复制父类的代码会引入代码冗余性,使未来维护的工作量倍增

步骤5:定制构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Add __str__ overload method for printing object

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self):
return self.name.split()[-1]
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent))
def __str__(self):
return '[Person: %s, %s]' % (self.name, self.pay)

class Manager(Person):
def __init__(self, name, pay): # Redefine constructor
Person.__init__(self, name, 'mgr', pay) # Run otiginal with 'mgr'
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus)

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 50000) # Job name not needed:
tom.giveRaise(.10) # Implied/set by class
print(tom.lastName())
print(tom)

通过类名直接调用并显式地传递self实例运行超类的版本来扩展__init__构造函数

在构造的时候,Python自身使用类树来查找并调用唯一的也是最低的一个__init__方法,必须通过超类的名称手动调用来运行更高的__init__方法

OOP比我们认为的要简单

Python的OOP机制中的概念

  • 实例创建–填充实例属性
  • 行为方法—在类方法中封装逻辑
  • 运算符重载–为打印这样的内置操作提供行为
  • 定制行为–重新定义子类中的方法以使其特殊化
  • 定制构造函数—为超类步骤添加初始化逻辑

这些概念基于3个简单的思路:在对象树中继承,查找属性、方法中特殊的self参数以及运算符重载对方法的自动派发

通过这种方法可以使自己的代码在未来易于修改,通过定制方法减少冗余

组合类的其他方法

Python中的OOP基本机制很简单,但较大的程序中的一些技术在于组合类的方式

组合类的方式可以是继承,可以是把对象彼此嵌套以组成复合对象

当嵌入的对象比直接定制隐藏需要与容器之间有更多有限的交互时,对象嵌入以及基于其的设计模式还是很合适的

对象嵌入适用于想要跟踪或验证对另一个对象方法的调用的控制器,或可以聚合其他对象当做一个集合对待的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Embedding-based Manager alternative

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self):
return self.name.split()[-1]
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent))
def __str__(self):
return '[Person: %s, %s]' % (self.name, self.pay)

class Manager():
def __init__(self, name, pay):
self.person = Person(name, 'mgr', pay) # Embed a Person object
def giveRaise(self, percent, bonus=.10):
self.person.giveRaise(percent + bonus) # Intercept and delegate
def __getattr__(self, attr):
return getattr(self.person, attr) # Delegate all other attrs
def __str__(self):
return str(self.person) # Must overload again

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 50000)
tom.giveRaise(.10)
print(tom.lastName())
print(tom)

使用__getattr__运算符重载来拦截未定义属性的访问,并将它们委托给嵌入的对象

委托是一种基于复合的结构,它管理一个包装的对象并且把方法调用传递给对象

__str__这样的运算符重载方法必须重新定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# Add __str__ overload method for printing object

class Person():
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self):
return self.name.split()[-1]
def giveRaise(self,percent):
self.pay = int(self.pay * (1 + percent))
def __str__(self):
return '[Person: %s, %s]' % (self.name, self.pay)

class Manager(Person):
def __init__(self, name, pay):
self.person = Person(name, 'mgr', pay)
def giveRaise(self, percent, bonus=.10):
self.person.giveRaise(percent + bonus)
def __getattr__(self, attr):
return getattr(self.person, attr)
def __str__(self):
return str(self.person)

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 50000)
tom.giveRaise(.10)
print(tom.lastName())
print(tom)

class Department:
def __init__(self, *args):
self.members = list(args)
def addMember(self, person):
self.members.append(person)
def giveRaise(self, percent):
for person in self.members:
person.giveRaise(percent)
def showAll(self):
for person in self.members:
print(person)

development = Department(bob, sue) # Embed objects in a composite
development.addMember(tom)
development.giveRaise(.10) # Runs embedded objects' giveRaise
development.showAll() # Runs embedded objects' __str__s

这里的代码使用了继承和复合,Department是嵌入并控制其他对象的聚合的一个复合体,但是嵌入的PersonManager对象自身使用继承来定制

在Python 3.0中捕获内置属性

基于委托的类无法截取并委托像__str__这样的运算符重载方法属性

通用的属性管理器__getattr____getattribute__无法截获被隐式调用的__str__等运算符重载方法


步骤6:使用内省工具

编写的类存在的两个问题

  • 打印一个实例对象会显示继承树中顶层的类的名称,然而尽可能地用最低层的类来显示对象可能会更准确些
  • 当前的显示格式只是显示了包含在__str__中的属性而非全部属性,没有考虑未来的目标,未来在代码中引入冗余性时必须做潜在的额外工作

特殊类属性

Python的内省工具是特殊的属性和函数,允许访问对象实现的一些内部工具

  • 内置的instance.__class__属性提供了一个从实例到创建它的类的链接

    类有__name__属性提供类的名称,__bases__序列提供了超类的访问,可以使用这些来打印创建一个实例的类的名字,而不是通过硬编码来做到

  • 内置的object.__dict__ 属性提供了一个字典,带有一个键/值对,以便每个属性都附加到一个命名空间对象(包括模块、类和实例)

    由于它是字典,因此可以获取键的列表、按照键来索引、迭代其键等从而广泛地处理所有的属性,使用这些来打印出任何实例的每个属性,而不是在定制显示中硬编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> from person import Person
>>> bob = Person('Bob Smith')
>>> print(bob) # Show bob's __str__
[Person: Bob Smith, 0]
>>> bob.__class__ # Show bob's class and its name
<class 'person.Person'>
>>> bob.__class__.__name__
'Person'
>>> list(bob.__dict__.keys()) # Attributes are really dict keys
['name', 'job', 'pay'] # Use list to force list in 3.0
>>> for key in bob.__dict__:
... print(key, '=>', bob.__dict__[key]) # Index manually
...
name => Bob Smith
job => None
pay => 0
>>> for key in bob.__dict__:
... print(key, '=>', getattr(bob, key)) # obj.attr but attr is a var
...
name => Bob Smith
job => None
pay => 0

如果实例的类定义了__slots__而实例没有存储在__dict__字典中,实例的一些属性也是可以访问的,因为属性存储在__slots__

一种通用显示工具

在超类中把接口投入使用,以显示准确的类名并格式化显示任何实例的所有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File classtools.py
"Assorted class utilities and tools"

class AttrDisplay():
"""
Provides an inheritable print overload method that displays
instances with their class names and a name=value pair for
each attribute stored on the instance itself (but not attrs
inherited from its classes). Can be mixed into any class,
and will work on any instance.
"""
def gatherAttrs(self):
attrs = []
for key in sorted(self.__dict__):
attrs.append('%s=%s' % (key, getattr(self, key)))
return ', '.join(attrs)
def __str__(self):
return '[%s: %s]' % (self.__class__.__name__, self.gatherAttrs())

if __name__ == '__main__':
class TopTest(AttrDisplay):
count = 0
def __init__(self):
self.attr1 = TopTest.count
self.attr2 = TopTest.count+1
TopTest.count += 2
class SubTest(TopTest):
pass

X, Y = TopTest(), SubTest()
print(X) # Show all instance attrs
print(Y) # Show lowest class name

文档字符串可以放在简单函数和模块的顶部,也可以防止类及其方法的开始处供help函数和PyDoc工具提取和显示

由于__str__,print重载用于通用的内省工具,对任何实例有效,不管实例的属性集合是什么

由于这是一个类,可以自动变成一个公用的工具,得益于继承,可以混合到想要使用它显示格式的任何类中

如果想要改变实例的显示,只需要修改这个类,继承其__str__的每一个类都将自动选择新的格式

实例与类属性的关系

self__dict__中包含了附加到继承树底部self对象中的属性,而没有包含继承的类的属性

继承的类属性只是附加到了类,而没有向下复制到实例

1
2
3
4
5
6
7
8
>>> from person import Person
>>> bob = Person('Bob Smith')
>>> list(bob.__dict__.keys())
['name', 'job', 'pay']
>>> list(bob.__class__.__dict__.keys())
['__module__', '__init__', 'lastName', 'giveRaise', '__str__', '__dict__', '__weakref__', '__doc__']
>>> dir(bob)
['__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__', 'giveRaise', 'job', 'lastName', 'name', 'pay']

若想包含继承属性,可以通过__class__链接爬升到实例的类,使用这里的__dict__去获取类属性,然后迭代类的__bases__属性爬升到更高的超类

在实例上运行内置的dir调用有同样的效果,因为dir结果在排序的结果列表中包含了继承的名称

Python 3.0的dict.keys不是一个列表,需要list调用

Python 3.0的dir返回的更多关于运算符重载的名称

工具类的命名考虑

通用性工具的类必须注意与客户类潜在的无意的命名冲突

1
2
3
4
5
6
7
8
class TopTest(AttrDisplay):
count = 0
def __init__(self):
self.attr1 = TopTest.count
self.attr2 = TopTest.count+1
TopTest.count += 2
def gatherAttrs(self): # Replaces method in AttrDisplay!
return 'Spam'

若子类定义了相同名称的类可能会覆盖当前的类

为了减少像这样名称冲突的机会,对于不想做其他用途的方法可以添加一个单个下划线的前缀

在方法名前使用两个下划线符号会被自动拓展为包含类的名称,称为伪私有类属性

类的最终形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File person.py (final)

from classtools import AttrDisplay # Use generic display tool

class Person(AttrDisplay):
"""
Create and process person records
"""
def __init__(self, name, job=None, pay=0):
self.name = name
self.job = job
self.pay = pay
def lastName(self): # Assumes last is last
return self.name.split()[-1]
def giveRaise(self,percent): # Percent must be 0..1
self.pay = int(self.pay * (1 + percent))

class Manager(Person):
"""
A customized Person with special requirments
"""
def __init__(self, name, pay):
Person.__init__(self, name, 'mgr', pay)
def giveRaise(self, percent, bonus=.10):
Person.giveRaise(self, percent + bonus)

if __name__ == '__main__':
bob = Person('Bob Smith')
sue = Person('Sue Jones', 'dev', 100000)
print(bob)
print(sue)
print(bob.lastName(), sue.lastName())
sue.giveRaise(.10)
print(sue)
tom = Manager('Tom Jones', 50000)
tom.giveRaise(.10)
print(tom.lastName())
print(tom)

从模块中导入通用工具以使用它,使用继承将其混合到顶层类中,并删除__str__方法

1
2
3
4
5
6
7
% python person.py
[Person: job=None, name=Bob Smith, pay=0]
[Person: job=dev, name=Sue Jones, pay=100000]
Smith Jones
[Person: job=dev, name=Sue Jones, pay=110000]
Jones
[Manager: job=mgr, name=Tom Jones, pay=60000]

通用工具将显示对象的所有属性

每个对象都显示其类树中最低的类的名称


步骤7(最后一步):把对象存储到数据库中

创建的对象并不是真正的数据库记录而是内存中的临时性对象,关闭Python后实例将消失

使用Python一项叫做对象持久化的功能可以让对象在创建他们的程序退出后依然存在

PIckle和Shelve

对象持久化通过3个标准的库模块来实现

  • pickle

    任意Python对象和字节串之间的序列化

  • dbm

    实现一个可通过键访问的文件系统以存储字符串

  • shelve

    使用另两个模块按照键把Python对象存储到一个文件中

pickle模块是一种通用的对象格式化工具,对于内存中几乎任何的Python对象都能转换为字节串,这个字节串可以随后用来在内存中重新构建最初的对象

通过在文件中存储一个对象的pickle字符串可以有效地使其持久化,随后载入到程序并进行unpickle操作可以重新创建最初的对象

shelve模块提供了一个额外的层结构,允许按照键来存储pickle处理后的对象

shelve使用pickle把一个对象转换为其pickle化的字符串,并将其存储在一个dbm文件中的之下,随后载入的时候,shelve通过获取pickle化的字符串,并用pickle在内存中重新创建最初的对象

shelvepickle化的对象类似于字典,通过键索引来访问、指定键来存储,区别在于开始必须打开shelve兵器修改后必须关闭它

在shelve数据库中存储对象

编写脚本,把类的对象存储到shelve

1
2
3
4
5
import person					# Load class with import
bob = person.Person(...) # Go through module name

from person import Person # Load class with from
bob = Person(...) # Use name directly

通过两种方式导入类以便创建一些实例来存储

1
2
3
4
5
6
7
8
9
10
11
12
# File makedb.py: store Person objects on  a shelve database

from person import Person, Manager # Load our classes
bob = Person('Bob Smith') # Re-create objects to be stored
sue = Person('Sue Jones', job='dev', pay=100000)
tom = Manager('Tom Jones', 50000)

import shelve
db = shelve.open('persondb') # Filename where objects are stored
for object in (bob, sue, tom): # Use object's name attr as key
db[object.name] = object # Store object on shelve by key
db.close() # Close after making changes

导入shelve模块,用一个外部文件名打开一个新的shelve对象,把对象赋给shelve中的键,操作完毕后关闭shelve对象

shelve中,是唯一的,可以是任何的字符串,针对每个键只存储一个值,这个值可以是几乎任何类型的Python对象,包括内置对象和用户定义的类实例,以及所有这些嵌套式的组合

交互地探索shelve

运行后当前目录下会出现一个或多个persondb开头的文件作为数据库,这是一个二进制散列文件,其中内容对于shelve模块以外的环境没有太大的意义

与内置open函数一样,shelve.open()中的文件名也是相对于当前工作目录的,除非它包含了一个目录路径

文件实现为一个通过键访问的文件,其中包含了Python对象的pickle化的表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Directory listing module: verify files are present

>>> import glob
>>> glob.glob('person*')
['person.py', 'persondb.db']

# Type the file: text mode for string,binary mode for bytes

>>> print(open('persondb.db').read())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/codecs.py", line 321, in decode
(result, consumed) = self._buffer_decode(data, self.errors, final)
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd2 in position 11: invalid continuation byte
>>> print(open('persondb.db','rb').read())
b'\x00\x06\x15a\x00\x00\x00\x02\x00\x00\x04\xd2\x00\x00\x10\x00\x00\x00\x00\x0c\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x08\x00\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00(\x00\x00\x00\x03\x00\x00\x00\x01\x95n}\xe3'

这些文件在不同的平台上有所不同,并且不是一个用户友好的数据库界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
>>> import shelve
>>> db = shelve.open('persondb') # Reopen the shelve
>>> len(db) # Three 'records' stored
3
>>> list(db.keys()) # keys is the index
['Bob Smith', 'Sue Jones', 'Tom Jones'] # list to make a list in 3.0
>>> bob = db['Bob Smith'] # Fetch bob by key
>>> print(bob) # Runs __str__ from AttrDisplay
[Person: job=None, name=Bob Smith, pay=0]
>>> bob.lastName() # Runs lastName from Person
'Smith'
>>> for key in db: # Iterate, fetch, print
... print(key, '=>', db[key])
...
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]
>>> for key in sorted(db): # Iterate by sorted keys
... print(key, '=>', db[key])
...
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]

在这里交互提示模式有效地成为数据库客户端

由于shelve是包含了Python对象的对象,可以用常规的Python语法和开发模式来处理

载入或使用存储的对象时不一定必须导入所属的类,即使作用域中没有相关的类也可以调用对象相关的方法,这是因为Python对一个类实例进行了pickle操作时记录了self实例属性以及实例所属的类的名字和类的位置,当随后从shelve中获取实例并对其unpickle时,Python将自动地重新导入该类并将实例连接到它

这种方法的结果是导入类实例时会自动获取其所有的类行为,只有在创建新实例时才必须导入类

  • 缺点

    当随后载入一个实例的时候,类及其模块的文件都必须导入

    更正式地说,可以pickle的类必须在一个模块文件的顶部编码,而这个模块文件可以通过sys.path模块的查找路径所列出的目录来访问

    由于这一外部模块文件的需求,一些应用程序选择pickle更简单的对象

  • 优点

    该类的实例再次载入时,对类的源代码文件的修改会自动选取,并改变存储的对象的行为

更新shelve中的对象

编写程序,在每次运行的时候更新一个实例

1
2
3
4
5
6
7
8
9
10
11
12
# File updatedb.py: update Person object on database

import shelve
db = shelve.open('persondb') # Reopen shelve with same filename

for key in sorted(db): # Iterate to display database objects
print(key, '\t=>', db[key]) # Printes with custom format

sue = db['Sue Jones'] # Index by key to fetch
sue.giveRaise(.10) # Updata in memory using class method
db['Sue Jones'] = sue # Assign to key to updata in shelve
db.close() # Close after making changes

数据库中的实例可以直接使用__str__重载方法打印对象,调用giveRaise方法增加之前写入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
% python updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=100000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]

% python updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=110000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]

% python updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=121000]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]

% python updatedb.py
Bob Smith => [Person: job=None, name=Bob Smith, pay=0]
Sue Jones => [Person: job=dev, name=Sue Jones, pay=133100]
Tom Jones => [Manager: job=mgr, name=Tom Jones, pay=50000]

运行几次后可以看到对象值的改变

1
2
3
4
5
6
7
8
9
>>> import shelve
>>> db = shelve.open('persondb') # Reopen database
>>> rec = db['Sue Jones'] # Fetch object by key
>>> print(rec)
[Person: job=dev, name=Sue Jones, pay=146410]
>>> rec.lastName()
'Jones'
>>> rec.pay
146410

可以在交互模式中验证脚本的作用


未来方向

本章看到了Python的OOP所有基本机制的实际运作,并且学习了在代码中避免冗余性及相关可维护性问题的方法,还构建了功能完备的类来完成实际的工作,还通过把对象存储到Python的shelve中创建正事的数据库记录使信息持久地存在

Python的工具

  • GUI

    目前能够使用交互提示的基于命令行的界面来处理数据库和脚本,可以扩展对象数据库的易用性,添加一个图形化的用户界面来浏览和更新数据库记录

    可以构建能够移植到Python的tkinter的GUI或者可以移植到WxPythonPyQt第三方工具的GUI

    tkinter是Python自带的,可以快速地构建简单的GUI,并且是学习GUI编程技巧的理想工具

    WxPythonPyQt使用起来更加复杂,但是往往能创建更高级的GUI

  • Web站点

    可以实现一个Web站点来浏览和更新记录,而不是使用GUI和交互式提示

    Web站点可以用Python自带的基本CGI脚本编程工具来构建,也可以用像DjangoTurboGearsPylonsweb2PyZopeGoogle's App Engine这样的全功能的第三方Web开发框架来完成

    在Web上,数据可以仍然存储在shelve.pickle文件或其他基于Python的媒介中,处理它的脚本直接自动在服务器上运行,以响应来自Web浏览器和其他客户端的请求,并且生成HTML来与一个用户交互,而不是直接或通过框架API与用户交互

  • Web服务

    尽管Web客户端往往可以解析来自Web站点的回复中的信息,还可以用更直接的方式来从Web获取记录:通过像
    SOAPXML-RPC这样的一个Web服务接口来调用Python自身或第三方开源域所支持的API

    这样的API以一种更加直接的形式返回数据,而不是嵌入一个回应页面的HTML中

  • 数据库

    如果数据库变得更大或者更关键,可以将其从shelve转移到开源的ZODB面向对象数据库系统(OODB),或者是像MySQLOraclePostgreSQLSQLite这样的一个更传统的基于SQL的数据库系统中

    Python自身带有一个内置的SQLite数据库,但是其他的开源选项也可以从Web上免费获得

    ZODB类似于Python的shelve,但是它解决了很多局限性,支持较大的数据库、并发更新、事务处理和内存中修改自动写人

    MySQL这样基于SQL的系统,为数据库存储提供了企业级的工具,并且可以在一个Python脚本中直接使用

  • ORM

    如果需要迁移到关系数据库中进行存储,不一定要牺牲Python的OOP工具,像SQLObjectSQLAlchemy这样的对象关系映射器(ORM)可以自动实现关系表和行与Python的类和实例之间的映射,这样就可以使用常规的Python类语法来处理存储的数据

    这种方法为shelveZODB提供了一个到OODB的替代,使得可以利用关系数据库和Python的类模型的双重威力


本章习题

  1. 当我们从shelve获取一个Manager对象并打印它的时候,显示格式逻辑来自何处

    在类的最终版本中,Manager最终从单独的classtools模块的AttrDisplay继承其__str__打印方法 Manager自己没有一个这样的方法,因此,继承查找爬升到其Person超类,由于那里也没有__str__,查找继续向上爬升,并在AttrDisplay中找到它

    类语句的标题行中的圆括号中列出的类名,提供了到更高的超类的链接

  2. 当从一个shelve获取一个Person对象而没有导入其模块的时候,该对象如何知道它有一个giveRaise方法可供调用

    当实例稍后载入内存中的时候,shelve 使用pickle自动地把该实例重新连接到创建它的类

    Python从其模块内部重新导入该类,创建一个带有其存储的属性的实例,并且把实例的__class__连接设置到指向其最初的类
    通过这种方式,载入实例自动获取所有其他最初方法(如lastNamegiveRaise__str__),即便没有把实例的类导入作用域

  3. 为什么把处理放入方法中而不是在类之外硬编码如此重要

    把处理放入方法中很重要,这样一来,未来只有一个副本需要修改,并且方法可以在任何实例之上运行

    这就是Python封装的概念,把逻辑封装到接口背后,更好地支持未来的代码维护

    如果没有这么做,就会产生代码冗余性,将来代码修改的时候工作就会加倍

  4. 为什么通过子类而不是复制并修改最初的代码来定制会更好

    用子类定制可以减少开发工作

    我们可以通过编写新的子类利用已有的方法来很容易地扩展以前的工作

    这比每次从头开始编写要好很多,也好过引入多个冗余的代码副本,它们未来可能全部都必须更新

  5. 为什么回调一个超类的方法来运行默认操作而不是在子类中复制和修改其代码要更好

    不管是什么样的情况,复制和修改代码会使未来的潜在工作翻倍

    复制代码会产生冗余性,当代码改进的时候这是一个主要的问题

  6. 为什么使用__dict__ 这样的工具来允许一般性地处理对象,而不是为类的每个类型编写更多定制代码要更好

    通用性工具可以避免硬编码解决方案,而后者必须随着时间推移和类的改进保持与类的其他部分同步

    一个通用的打印方法只由所有出现的类继承,并且只需要在通用版本中修改,从通用类继承的所有类都会保持同步

    删除代码冗余性会减少未来的开发工作,这是类带来的主要好处之一

  7. 一般来说,何时可以选择使用对象嵌入和组合而不是继承

    与直接定制相比较,继承是最佳的代码扩展

    对于多个对象聚合到一个完整的对象,并且由一个控制器层类主导的情况,组合非常实用
    继承向上传递调用以实现复用,组合向下传递以实现委托

    继承和组合不是互斥的,嵌入一个控制器中的对象,往往其本身是基于继承来定制的

  8. 如何修改本章中的类,从而在Python中实现一个个人联络信息数据库

    本章中的类可以用作样本代码来实现各种类型的数据库

    基本上,可以修改构造函数来记录不同的属性,并提供各种适用于目标应用程序的方法,从而改变用途
    例如,可以使用诸如name、address、birthday、phone、email等属性来构建一个联系人数据库,并且可以采用适合这一用途的方法

    这里的大多数shelve数据库代码都可以用来存储对象,只需要稍作修改即可


第二十八章 类代码编写细节

  • 深入探索OOP机制
  • 深入学习类、方法和继承
  • 讲解抽象超类、类数据属性、命名空间和链接、超类方法和构造函数手动调试的概念,并进行扩展
  • 总结Python中命名空间的概念

class语句

class语句是对象的创建者并且是一个隐含的赋值运算,执行时它会产生类对象并把其引用值存储在变量名中

class语句是真正的可执行代码,直到Python抵达并运行定义的class语句前类都不存在

一般形式

class是复合语句,超类列在类名称之后的括号内,由逗号相隔,其缩进语句的主体出现在头一行下边

1
2
3
4
class <name>(superclass, ...):	# Assign to name
data = value # Shared class data
def method(self, ...): # Methods
slef.member = value # Per-instance data

class语句内,任何赋值语句都会产生类属性,还有特殊名称方法重载运算符

例子

类是命名空间,也是定义变量名的工具,把数据和逻辑导出给客户端

当Python执行class语句时,从头至尾执行其主体内的所有语句,进行的赋值运算会在类作用域中创建变量名,成为对应的类对象内的属性

  • class语句是本地作用域,由内嵌的赋值语建立的变量名就存在于这个本地作用域内
  • class语句内赋值的变量名会变成类对象的属性

类的命名空间也是Python继承的基础,类或实例对象找不到所引用的属性时呼从其他类中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> class SharedData:
... spam = 42 # Generates a class data attribute
...
>>> x = ShareData() # Make two instances
>>> y = ShareData()
>>> x.spam, y.spam # They inherit and share 'spam'
(42, 42)

>>> x.spam, y.spam, ShareData.spam
(99, 99, 99)

>>> x.spam = 88
>>> x.spam, y.spam, ShareData.spam
(88, 99, 99)

class内赋值的变量名会创建类属性,内嵌的def语句会创建类方法,其他赋值语句也可制作属性

可通过类名称修改它或是通过实例或类引用它

对实例的属性进行赋值运算会在该实例内创建或修改变量名而不是在共享的类中,继承搜索只会在属性引用时发生,而不是在赋值运算时发生

继承搜索会查找变量名,也可以通过直接读取所需要的对象获得树中任何地方的属性

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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 attr
...
>>> x = MixedNames(1) # Make two instance
>>> y = MixedNames(2) # Each has its own data
>>> x.display(); y.display() # self.data differs, MixedNames.data is the same
1 spam
2 spam

类中的赋值语句创建了类作用域内的变量名,成为类对象的属性

类属性会被继承,从而被所有没有该属性的实例对象共享

data变量名存在于实例对象内以及在实例继承的类中

把属性储存在不同对象内决定其可见范围,附加在类上时变量名是共享的,附加在实例上时变量名属于每个实例


方法

方法是由def语句建立的函数对象,位于class语句的主体内,替实例对象提供了要继承的行为

方法的工作方式与简单函数完全一致,但是方法的第一个参数总是接受方法调用的隐性主体

1
instance.method(args, ...)

python会自动把实例对象的调用对应到类方法函数

1
class.method(instance, args, ...)

class通过Python继承搜索流程找出方法名称所在之处

在类方法中,按惯例第一个参数通常称为self,这个参数给方法提供了一个钩子从而返回调用的主体,需要在程序代码中明确写出

例子

1
2
3
4
class NextClass:				# Define class
def printer(self, text): # Define method
self.message = text # Change instance
print(self.message) # Access instance

printer引用函数对象

由于printer是在class语句的作用域中赋值,所以附加为类对象的属性,由类创建的每个实例所继承

1
2
3
4
5
>>> x = NextClass()				# Make instance
>>> x.printer('instance call') # Call its method
instance call
>>> x.message # Instance changed
'instance call'

当通过实例进行点号运算调用它时,printer会先通过继承将其定位,然后它的self参数会自动赋值为实例对象,text参数获得调用时传入的字符串

1
2
3
4
5
6
7
8
9
>>> NextClass.printer(x, 'class call')		# Direct class call
class call
>>> x.message # Instance changed again
'class call'

>>> NextClass.printer('bad call')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: printer() missing 1 required positional argument: 'text'

方法能通过实例或类本身两种方式其中的任意一种进行调用

如果尝试不带任何实例调用的方法,就会得到错误

调用超类构造函数

如果要保证子类的方法也会执行超类方法的逻辑,一般要通过子类明确地调用超类的方法

1
2
3
4
5
6
7
8
class Super:
def __init__(self, x):
...default code...

class Sub(Super):
def __init__(self, x, y):
Super.__init__(self, x) # Run superclass __init__
...custom code... # Do my init actions

没有这样的调用子类会完全取代超类的构造函数

其他方法调用的可能性

通过类调用方法的模式是扩展继承方法行为的一般基础

通过静态方法可以编写第一参数可以不是实例对象的方法,方法名属于所在类的作用域,可以用来管理类数据

类方法调用时接受一个类而不是一个实例,可以用来管理基于每个类的数据


继承

class语句的重点是支持变量名继承,继承构成了专有化的基础

使用object.attr形式的表达式时,Python会搜索命名空间树,先从当前对象开始,寻找所能找到的第一个attr

在方法中对self属性的引用通用会搜索以self所引用的对象开始的继承树

属性树的构造

命名空间树的构造以及填入变量名的方式

  • 实例属性是由对方法内self属性进行赋值运算而生成的
  • 类属性是通过class语句内的赋值语句生成的
  • 超类的连接是通过class语句首行的括号内列出的类生成的

结果就是连接实例的属性命名空间到产生它的类,再到类首行中所列出的所有超类

每次以点号运算从实例对象取出属性名称时,Python会向上搜索树,从实例直到超类

继承方法的专有化

继承树搜索模式变成了将系统专有化的最好方式,因为继承会先在子类寻找变量名,然后才查找超类,子类就可以对超类的属性重新定义来取代默认的行为

子类可以完全取代继承的属性,提供超类可以找到的属性,并且通过已覆盖的方法回调超类来扩展超类的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> class Super:
... def method(self):
... print('in Super.method')
...
>>> class Sub(Super):
... def method(self): # Overrrde method
... print('starting Sub.method') # Add actions here
... Super.method(self) # Run default action
... print('ending Sub.method')
...
>>> x = Super() # Make a Super instance
>>> x.method() # Runs Super.method
in Super.method
>>> x = Sub() # Make a Sub instance
>>> x.method() # Runs Sub.method, calls Super.method
starting Sub.method
in Super.method
ending Sub.method

子类可以以专有化的版本取代超类的方法函数,通过回调超类的版本可以实现默认的行为,这样实现了扩展超类的行为

类接口技术

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Super:
def method(self):
print('in Super.method') # Default behavior
def delegate(self):
self.action() # Expected to be defined

class Inheritor(Super): # Inherit method verbatim
pass

class Replacer(Super): # Replace method completely
def method(self):
print('in Replacer.method')

class Extender(Super): # Extend method behavior
def method(self):
print('starting Extender.method')
Super.method(self)
print('ending Extender.method')

class Provider(Super): # Fill in a required method
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.delegate()

类作为对象可以放在元组中,并通过通用方式创建实例

类有__name__属性,默认为类首行中的 类名称的字符串

这里示范了一些与超类接口方式的常用技巧

  • Super

    定义一个method函数以及在子类中期待一个actiondelegate

  • Inheritor

    没有提供任何新的变量名,因此会获得Super中定义的一切内容

  • Replacer

    用自己的版本覆盖Supermethod

  • Extender

    覆盖并回调默认method,从而定制Supermethod

  • Provider

    实现Superdelegate方法预期的action方法

抽象超类

当通过Provider实例调用delegate方法时有两个独立的继承搜索会发生

  • 在最初的x.delegate的调用中Python会搜索Provider实例和它上层的对象,直到在Super中找到delegate方法,实例x会传递给self参数
  • Super.delegate方法中,self.action会对self以及它上层的对象启动新的独立继承搜索

这个例子的超类也称作抽象超类,也就是类的部分行为默认是由其子类所提供的

若预期的方法没有在子类中定义,当继承搜索失败时Python会引发未定义变量名的异常,使用assert语句或引发内置的异常NoImplementedError可以使子类的需求更加明确

1
2
3
4
5
6
7
8
9
10
11
class Super:
def delegate(self):
self.action()
def action(self):
assert Flase, 'action must be define!' # If this version is called

% python
>>> X = Super()
>>> X.delegate()
Traceback (most recent call last):
AssertionError: action must be define!

assert语句在其表达式运算结果为假时引发带有出错信息的异常

子类没有定义时继承会找到这里的版本触发出错消息

1
2
3
4
5
6
7
8
9
10
11
class Super:
def delegate(self):
self.action()
def action(self):
raise NotImplementedError('action must be define!')

% python
>>> x = Super()
>>> x.delegate()
Traceback (most recent call last):
NotImplementedError: action must be define!

在类的方法中直接产生NotImplemented异常

1
2
3
4
5
6
7
8
9
10
11
12
>>> class Sub(Super): pass
...
>>> X = Sub()
>>> X.delegate()
Traceback (most recent call last):
NotImplementedError: action must be define!
>>> class Sub(Super):
... def action(self): print('spam')
...
>>> X = Sub()
>>> X.delegate()
spam

除非子类提供了期待的方法来替代超类中的默认方法,否则会得到异常

Python 2.6和Python 3.0的抽象超类

抽象超类可以以特殊的语法来实现

1
2
3
4
5
class Super:
__metaclass__ = ABCMeta
@abstractmethod
def method(self, ...):
pass

在Python 2.6中,使用一个类属性,以及特殊的@装饰器语法

1
2
3
4
5
6
from abc import ABCMeta, abstractmethod

class Super(metaclass=ABCMeta):
@abstractmethod
def method(self, ...):
pass

在Python 3.0中,在一个class头部使用一个关键字参数,以及特殊的@装饰器语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> from abc import ABCMeta, abstractmethod
>>> class Super(metaclass=ABCMeta):
... def delegate(self):
... self.action()
... @abstractmethod
... def action(self):
... pass
...
>>> X = Super()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Super with abstract methods action
>>> class Sub(Super): pass
...
>>> X = Sub()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Sub with abstract methods action
>>> class Sub(Super):
... def action(self): print('spam')
...
>>> X = Sub()
>>> X.delegate()
spam

带有一个抽象方法的类是不能继承的,除非其所有的抽象方法都已经在子类中定义了

这种方法可以在试图产生该类的一个实例的时候产生错误,可以用来定义一个期待的接口,在客户类中自动验证


命名空间:完整的内容

  • 无点号运算的变量名与作用域相对应
  • 点号的属性名使用的是对象的命名空间
  • 模块和类的作用域会对对象的命名空间进行初始化

简单变量名:如果赋值就不是全局变量

无点号的简单变量名遵循LEGB作用域法则

  • 赋值语句X = value

    使变量名成为本地变量:在当前作用域内,创建或改变变量名,除非声明它为全局变量

  • 引用X

    依次从当前作用域、所有的嵌套的函数中、当前的全局作用域、内置作用域中搜索

属性名称:对象命名空间

点号的属性名指的是特定对象的属性,并且遵循模块和类的规则

就类和实例对象而言,引用规则增加了继承搜索这个流程

  • 赋值语句object.X = value

    在进行点号运算的对象的命名空间内创建或修改属性名,继承树的搜索只发生在属性引用时而不是属性的赋值运算时

  • 引用object.X

    就基于类的对象而言,会在对象内搜索属性名,然后是其上所有可读取的类

    对于不是基于类的对象而言,是从对象中直接读取属性名

Python命名空间的”禅”:赋值将变量名分类

在Python中,赋值变量名的场所决定了变量名所在的作用域或对象

通常在脚本内每个变量都不应该使用相同的变量名,但是即使这么做Python的命名空间还是会工作,防止在一个环境中所用的变量名无意中和另一个环境中所使用的变量名发生冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# manynames.py

X = 11 # Global (module) name/attribute(X, or manynames.X)

def f():
print(X) # Access global X

def g():
X = 22 # Local (function) variable (X, hides module X)
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的赋值语句会产生:模块属性(11)、函数内的本地变量(22)、类属性(33)、方法中的本地变量(44)以及实例属性(55)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# manynames.py, continued

X = 11

def f():
print(X)

def g():
X = 22
print(X)

class C:
X = 33
def m(self):
X = 44
self.X = 55

if __name__ == '__main__':
print(X) # 11: module(a.k.a. manynames.X outside file)
f() # 11: global
g() # 22: local
print(X) # 11: module name unchanged

obj = C() # Make instance
print(obj.X) # 33: class name inherited by instance

obj.m() # Attach attribute name X to instance now
print(obj.X) # 55: instance
print(C.X) # 33 class(a.k.a. obj.X if no X in instance)

#print(C.m.X) # FAILS: only visible in method
#print(g.X) # FAILS: only visible in function

可以通过类来读取其属性,但无法从def语句外读取函数或方法内的局部变量,局部变量只对于在def内的代码可见,也只有当函数调用或方法执行时才会存在于内存中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# otherfile.py

import manynames

X = 66
print(X) # 66: the global here
print(manynames.X) # 11: globals bnecome attributes after imports

manynames.f() # 11: manynames's X, not the one here!
manynames.g() # 22: local in other file's function

print(manynames.C.X) # 33: attribute of class in other module
I = manynames.C()
print(I.X) # 33: still from class here
I.m()
print(I.X) # 55: now from instance!

可以通过导入文件看到其他模块内的变量名

作用域是由源代码中的赋值语句位置来决定的,不会受到导入关系的影响

实例的属性与变量一样,赋值后才会存在,通常实例属性在__init__构造函数内进行赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
X = 11				# Global in module

def g1():
print(X) # Reference global in module

def g2():
global X
X = 22 # Change global in module

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 # Python 3.0 statement
X = 44 # Change local in enclosing scope

一个函数也可以在其外部修改名称

使用globalnonlocal语句提供了写入访问,但是也修改了赋值的命名空间绑定规则

命名空间字典

模块的命名空间是以字典的形式实现的,可以通过内置属性__dict__显示

类和实例对象就是带有连接的字典,属性点号运算内部就是字典的索引运算,属性继承就是搜索链接的字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
>>> class super:
... def hello(self):
... self.data1 = 'spam'
...
>>> class sub(super):
... def hola(self):
... self.data2 = 'eggs'
...

>>> X = sub()
>>> X.__dict__ # Instance namespace dict
{}
>>> X.__class__ # Class of instance
<class '__main__.sub'>
>>> sub.__bases__ # Superclasses of class
(<class '__main__.super'>,)
>>> super.__bases__ # () empty tuple in Python 2.6
(<class 'object'>,)

>>> Y = sub()
>>> X.hello()
>>> X.__dict__
{'data1': 'spam'}
>>> X.hola()
>>> X.__dict__
{'data1': 'spam', 'data2': 'eggs'}
>>> sub.__dict__.keys()
dict_keys(['__module__', 'hola', '__doc__'])
>>> super.__dict__.keys()
dict_keys(['__module__', 'hello', '__dict__', '__weakref__', '__doc__'])
>>> Y.__dict__
{}

>>> 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'

定义一个超类和一个带方法的子类

创建子类的实例时得到一个空的命名空间字典,但是会有连接指向它的类,让继承搜索能顺着寻找

实例中的__class__属性链接到它的类,类中的__base__属性是一个元组,包含了通往更高的超类的链接

类为self赋值会填入实例对象的属性命名空间内,每个实例都有独立的命名空间字典,可以记录与同一个类的其他势力命名空间字典中的属性完全不同的属性

属性可以通过点号运算或者通过键索引运算,但是键索引运算无法读取继承的属性

1
2
3
4
5
6
7
8
9
10
>>> 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']
>>> dir(sub)
['__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__', 'hello', 'hola']
>>> dir(super)
['__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__', 'hello']

dir函数能用在任何带有属性的对象上,dir(object)类似于object.__dict__.keys()调用

dir会排序属性列表,并引入一些系统属性,还包含了从所有类的隐含超类object类继承的名称

命名空间链接

__class____bases__属性可以在程序代码内查看继承层次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# classtree.py

"""
Climb inheritance trees using namespace links,
displaying higher superclasses with indentation
"""

def classtree(cls, indent):
print('.' * indent + cls.__name__) # Print class name here
for supercls in cls.__bases__: # Recur to all superclasses
classtree(supercls, indent+3) # May visit super > once

def instancetree(inst):
print('Tree of %s' % inst) # Show instance
classtree(inst.__class__, 3) # Climb to its class

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()

classtree函数是递归的,通过调用自身运行到超类,直到空的__bases__属性组时停止

递归时一个函数的每个活动层级都获取自己的本地作用域副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
% python classtree.py
Tree of <__main__.selftest.<locals>.B object at 0x101fa7160>
...B
......A
.........object
Tree of <__main__.selftest.<locals>.F object at 0x101fa7160>
...F
......D
.........B
............A
...............object
.........C
............A
...............object
......E
.........object

在Python 3.0下运行时包含了隐含的object超类的树会自动添加到独立的类上

1
2
3
4
5
6
7
8
9
10
11
>>> class Emp: pass
...
>>> class Person(Emp): pass
...
>>> bob = Person()
>>> import classtree
>>> classtree.instancetree(bob)
Tree of <__main__.Person object at 0x104210320>
...Person
......Emp
.........object

可以在任何想很快得到类树显示的的地方导入模块


回顾文档字符串

文档字符串是出现在各种结构的顶部的字符串场了,由对象的__doc__属性自动保存,适用于模块文件、函数定义、类和方法

文档字符串在运行时可用,但是比#注释缺乏灵活性

1
2
3
4
5
6
7
8
9
10
11
"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

这里概括文档字符串可以在代码中出现的位置

1
2
3
4
5
6
7
8
9
>>> import docstr
>>> docstr.__doc__
'I am: docstr.__doc__'
>>> docstr.func.__doc__
'I am: docstr.func.__doc__'
>>> docstr.spam.__doc__
'I am: spam.__doc__ or docstr.spam.__doc__'
>>> docstr.spam.method.__doc__
'I am: spam.method.__doc__ or self.method.__doc__'

文档字符串可以在运行时保持,通过__doc__属性获取文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
>>> help(makedb)

Help on module docstr:

NAME
docstr - I am: docstr.__doc__

CLASSES
builtins.object
spam

class spam(builtins.object)
| I am: spam.__doc__ or docstr.spam.__doc__
|
| Methods defined here:
|
| method(self, arg)
| I am: spam.method.__doc__ or self.method.__doc__
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)

FUNCTIONS
func(args)
I am: docstr.func.__doc__

FILE
/docstr.py

(END)

可以使用PyDoc工具查看格式化报表的字符串

在Python 3.0中还显示隐含object超类继承来的额外属性


类与模块的关系

模块与类都与命名空间有关

  • 模块
    • 是数据/逻辑包
    • 通过编写Python文件或C扩展来创建
    • 通过导入来使用
    • 实现新的对象
    • 由class语句创建
    • 通过调用来使用
    • 总是位于一个模块中
    • 支持运算符重载、多实例生成和继承

本章习题

  1. 什么是抽象超类

    抽象类是会调用方法的类,但没有继承或定义该方法,而是期待该方法由子类填补

    当行为无法预测,非得等到更为具体的子类编写才知道时,通常可用这种方式把类通用化

    OOP软件框架也使用这种方式作为客户端定义、可定制的运算的实现方法

  2. 当简单赋值语句出现在class语句顶层时,会发生什么

    当简单赋值语句出现在类语句的顶层时,就会把数据属性附加在这个类上,由所有的实例共享

    数据属性并不是可调用的方法函数

  3. 类为什么可能会需要手动调用超类中的__init__方 法

    如果类定义自身的__init__构造函数,但是也必须启用超类的构建其代码,就必须手动调用超类的__init__方法

    Python本身只会自动执行树中最低的构造函数

    超类的构造函数通过类名称来调用,手动传入self实例

  4. 怎样增强(而不是完全取代)继承的方法

    要增强继承的方法而不是完全替代,还得在子类中进行重新定义,但是要从子类的新版方法中手动回调超类版本的这个方法

    就是把self实例手动传给超类的版本的这个方法: Superclass .method(self, ...)


第二十九章 运算符重载

  • 深入介绍类机制
  • 关注运算符重载

基础知识

运算符重载会拦截内置的操作,调用定义的特殊方法,将方法的返回值作为相应操作的结果

重载的关键概念

  • 运算符重载让类拦截常规的Python运算
  • 类可重载所有Python表达式运算符
  • 类也可重载打印、函数调用、属性点号运算等内置运算
  • 重载使类实例的行为像内置类型
  • 重载是通过提供特殊名称的类方法来实现的

运算符重载方法不是必须的,也不是默认的,如果没有编写或继承一个运算符重载方法只是不支持相应的操作

构造函数和表达式:__init____sub__

1
2
3
4
5
6
7
8
9
10
11
class Number:                               # On Number(start)
def __init__(self, start):
self.data = start # On instance - other
def __sub__(self, other): # Result is a new instance
return Number(self.data - other)

>>> from number import Number # Fetch class from module
>>> X = Number(5) # Number.__init__(X, 5)
>>> Y = X - 2 # Number.__sub__(X, 2)
>>> Y.data # Y is new Number instance
3

__init__方法拦截实例的构造函数

__sub__方法捕捉减法表达式

常见的运算符重载方法

在类中,对内置对象所能做的事几乎都有相应的特殊名称的重载方法

所有重载方法的名称前后都有两个下划线字符,便于把同一类中定义的变量名区别开来

特殊方法名称和表达式或运算的映射关系是Python语言预先定义好的

没有定义运算符重载方法时可以继承自超类

运算符重载方法是可选的,如果没有编写或继承一个方法,类不支持相关运算,在试图使用相关运算时引发异常

多数重载方法用在需要对象行为表现得像是内置类型一样的程序中,大多数类中使用__init__构造函数

方法 重载 调用
__init__ 构造函数 对象建立:X = Class(Args)
__del__ 析构函数 X对象收回
__add__ 运算符+ 如果没有_iadd_,X + Y, X += Y
__or__ 运算符` ,位OR`
__repr__,__str__ 打印、切换 print(X), repr(X), str(X)
__call__ 函数调用 X(*args, **kargs)
__getattr__ 点号运算 X.undefined
__setattr__ 属性赋值语句 X.any = value
__delattr__ 属性删除 del X.any
__getattribute__ 属性获取 X.any
__getitem__ 索引运算 X[key], X[i:j],没__iter__时的for循环和其他迭代器
__setitem__ 索引赋值语句 X[key] = value, X[i:j] = sequence
__delitem__ 索引和分片删除 del X[key], X[i:j]
__len__ 长度 len(X), 如果没有__bool__,真值测试
__bool__ 布尔测试 bool(X), 真测试
__lt__,__gt__ 特定的比较 X < Y, X > Y, X <= Y, X >= Y
__le__, __ge__ X == Y
__eq__,__ne__ X != Y
__radd__ 右侧加法 Other + X
__iadd__ 实地加法 X += Y
__iter__, __next__ 迭代环境 I=iter(X), next(I), for loops, in if no __contains__,all comprehensions,map(E, X)
__contains__ 成员关系测试 item in X
__index__ 整数值 hex(X), bin(X), oct(X), O[X], O[X:]
__enter__, __exit__ 环境管理器 with obj as var:
__get__, __set__, __delete__ 描述符属性 X.attr, X.attr = value, del X.attr
__new__ 创建 __init__之前创建对象

索引和分片:__getitem____setitem__

如果在类中定义或继承了__getitem__方法会在对实例的索引运算自动调用

1
2
3
4
5
6
7
8
9
10
11
>>> class Indexer:
... def __getitem__(self, index):
... return index ** 2
...
>>> X = Indexer()
>>> X[2] # X[i] calls X.__getitem__(i)
4
>>> for i in range(5):
... print(X[i], end=' ') # Runs __getitem__(X, i) each time
...
0 1 4 9 16

方法的第一个参数为实例对象,第二个参数为索引值

拦截分片

__getitem__方法既针对基本索引调用,又针对分片调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> L = [5, 6, 7, 8, 9]
>>> L[2:4] # Slice with slice syntax
[7, 8]
>>> L[1:]
[6, 7, 8, 9]
>>> L[:-1]
[5, 6, 7, 8]
>>> L[::2]
[5, 7, 9]

>>> L[slice(2, 4)] # Slice with slice object
[7, 8]
>>> L[slice(1, None)]
[6, 7, 8, 9]
>>> L[slice(None, -1)]
[5, 6, 7, 8]
>>> L[slice(None, None, 2)]
[5, 7, 9]

内置类型以同样的方式处理索引和分片

可以手动地传递一个分片对象,分片语法主要是用一个分片对象slice进行索引的语法,分片边界绑定到了一个分片对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
>>> class Indexer:
... data = [5, 6, 7, 8, 9]
... def __getitem__(self, index): # Called for index or slice
... print('getitem:', index)
... return self.data[index] # Perform index or slice
...
>>> X = Indexer()
>>> X[0] # Indexing sends __getitem__ an integer
getitem: 0
5
>>> X[1]
getitem: 1
6
>>> X[-1]
getitem: -1
9

>>> X[2:4] # Slicing sends __getitem__ a slice object
getitem: slice(2, 4, None)
[7, 8]
>>> X[1:]
getitem: slice(1, None, None)
[6, 7, 8, 9]
>>> X[:-1]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
>>> X[::2]
getitem: slice(None, None, 2)
[5, 7, 9]

当针对索引调用时参数是一个整数

当针对分片调用时方法接收一个分片对象,它在一个新的索引表达式中直接传递给嵌套的列表索引

分片表达式用包含边界的一个分片对象来触发__getitem__,既针对内置类型,也针对用户定义的类

1
2
3
def __setitem__(self, index, value):        # Intercept index or slice assignment
...
self.data[index] = value # Assign index or slice

__setitem__索引赋值方法类似地拦截索引和分片赋值,为后者接收一个分片对象,以同样的方式传递到另一个索引赋值中

Python 2.6中的分片和索引

在Python 3.0之前,类可以定义__getslice____setslice__方法来专门拦截分片获取和赋值


索引迭代:__getitem__

for语句不断迭代索引值,对序列重复索引运算,直到检测到超出边界的异常

__getitem__可以是Python中重载迭代的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> class stepper:
... def __getitem__(self, i):
... return self.data[i]
...
>>> X = stepper() # X is stepper object
>>> X.data = "Spam"
>>> X[1] # Indexing calls __getitem__
'p'
>>> for item in X: # for loops call __getitem__
... print(item, end=' ') # for indexes item 0..N
...
S p a m
>>> 'p' in X # Al call __getitem__ too
True
>>> [c for c in X] # List comprehension
['S', 'p', 'a', 'm']
>>> list(map(str.upper, X)) # map calls
['S', 'P', 'A', 'M']
>>> (a, b, c, d) = X # Sequence assignments
>>> a, c, d
('S', 'a', 'm')
>>> list(X), tuple(X), ''.join(X)
(['S', 'p', 'a', 'm'], ('S', 'p', 'a', 'm'), 'Spam')
>>> X
<__main__.stepper object at 0x103f1dd68>

任何响应索引运算的内置或用户定义的对象,同样会响应所有迭代环境


迭代器对象:__iter____next__

Python中所有的迭代环境都先尝试__iter__方法,再尝试__getitem__,只有在对象不支持迭代协议的时候才会尝试索引运算

迭代环境是通过调用内置函数iter去尝试寻找__iter__方法来实现,Python重复调用这个方法返回的迭代器对象的next方法,直到发生StopIteration异常

如果没有找到__iter__方法,Python会改用__getitem__机制,通过偏移量重复索引,直到引发IndexError异常

用户定义的迭代器

__iter__机制中,类通过实现迭代器协议来实现用户定义的迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Squares:
def __init__(self, start, stop): # Save state when created
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

% python
>>> from iters import Squares
>>> for i in Squares(1, 5): # for calls iter,which calls __iter__
... print(i, end=' ') # Each iteration calls __next__
...
1 4 9 16 25

>>> X = Squares(1, 5) # Iterate manually:what loops do
>>> I = iter(X) # iter calls __iter__
>>> next(I) # next calls __next__
1
>>> next(I)
4
>>> next(I)
9
>>> next(I)
16
>>> next(I)
25
>>> next(I) # Can catch this in try statement
Traceback (most recent call last):
raise StopIteration
StopIteration

>>> X = Squares(1, 5)
>>> X[1]
Traceback (most recent call last):
TypeError: 'Squares' object does not support indexing

迭代器对象就是实例self,以raise语句发出信号表示迭代结束

手动迭代对内置类型也有效

__iter__对象会在调用过程中明确地保留状态信息,所以__getitem__具有更好的通用性

迭代器没有重载索引表达式

1
2
3
4
5
6
7
8
9
>>> X = Squares(1, 5)
>>> [n for n in X] # Exhausts items
[1, 4, 9, 16, 25]
>>> [n for n in X] # Now it's empty
[]
>>> [n for n in Squares(1, 5)] # Make a new iterator object
[1, 4, 9, 16, 25]
>>> list(Squares(1, 3))
[1, 4, 9]

__getitem__不同,__iter__只循环一次,循环之后变为空,每次新的循环需要创建一个新的迭代器对象

1
2
3
4
5
6
7
8
9
10
11
>>> 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

>>> [x ** 2 for x in range(1, 6)]
[1, 4, 9, 16, 25]

使用函数生成器编写更为简单,和类不同的是生成器函数会自动在迭代中存储其状态

可以通过for循环、map或是列表解析一次性创建列表

有多个迭代器的对象

迭代器对象可以定义为一个独立的类,有其自己的状态信息,从而能够支持相同数据的多个迭代

1
2
3
4
5
6
>>> S = 'abc'
>>> for x in S:
... for y in S:
... print(x + y, end=' ')
...
aa ab ac ba bb bc ca cb cc

外层循环调用iter从字符串中取得迭代器,嵌套的循环也同样获得独立的迭代器,每个激活状态下的迭代器都有自己的状态信息,而不管其他激活状态下的循环是什么样子

生成器函数和表达式以及mapzip这样的内置函数,都证明是单迭代对象,range内置函数和其他的内置类型支持独立位置的多个活跃迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SkipIterator:
def __init__(self, wrapped):
self.wrapped = wrapped # Iterator state information
self.offset = 0
def __next__(self):
if self.offset >= len(self.wrapped): # Terminate iterations
raise StopIteration
else:
item = self.wrapped[self.offset] # else return and skip
self.offset += 2
return item

class SkipObject:
def __init__(self, wrapped): # Save item to be used
self.wrapped = wrapped
def __iter__(self):
return SkipIterator(self.wrapped) # New iterator each time

if __name__ == '__main__':
alpha = 'abcdef'
skipper = SkipObject(alpha) # Make container object
I = iter(skipper) # Make an iterator on it
print(next(I), next(I), next(I)) # Visit offsets 0, 2, 4

for x in skipper: # for calls __iter__ automatically
for y in skipper: # Nested fors call __iter__ again each time
print(x + y, end=' ') # Each iterator has its own state, offset

用类编写用户定义的迭代器时,要达到多个迭代器的效果,只需__iter__替迭代器定义新的状态对象,而不是返回self

因为每个循环都会获得独立的迭代器对象来记录自己的状态信息,所以每个激活状态下的循环都有自己在字符串中的位置

1
2
3
4
5
6
>>> S = 'abcdef'
>>> for x in S[::2]:
... for y in S[::2]: # New objects on each iteration
... print(x + y, end=' ')
...
aa ac ae ca cc ce ea ec ee

使用内置工具可以达到类似的的效果

迭代器与内置工具并不完全相同,首先分片表达式把结果列表存储在内存中,而迭代器一次产生一个值,其次分片产生新对象而没有对同一个对象进行多处的循环

1
2
3
4
5
6
7
8
9
>>> S = 'abcdef'
>>> S = S[::2]
>>> S
'ace'
>>> for x in S:
... for y in S: # Same object, new iterators
... print(x + y, end=' ')
...
aa ac ae ca cc ce ea ec ee

这里对一个对象进行了多处循环,但仍是一次性把分片结果存储在内存中


成员关系:__contains____iter____getitem__

运算符重载往往是多个层级的,类可以提供特定的方法或通用的方案

  • 比较先尝试一个特定的__lt__,没有则使用通用的__cmp__
  • 布尔测试先尝试一个特定的__bool__,没有则使用通用的__len__

类实现in成员关系运算符使用特定的成员关系__contains__优先于迭代__iter__方法优先于索引__getitem__方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Iters:
def __init__(self, value):
self.data = value
def __getitem__(self, i): # Fallback for iteration
print('get[%s]:' % i, end='') # Also for index, slice
return self.data[i]
def __iter__(self): # Preferred for iteration
print('iter=> ', end='') # Allows only 1 active iterator
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): # Preferred for 'in'
print('contains: ', end='')
return x in self.data

X = Iters([1, 2, 3, 4, 5]) # Make instance
print(3 in X) # Membership
for i in X: # For loops
print(i, end=' | ')

print()
print([i ** 2 for i in X]) # Other iteration contexts
print(list(map(bin, X)))

I = iter(X) # Manual iteration(what other contexts do)
while True:
try:
print(next(I), end=' @ ')
except StopIteration:
break

__contains__方法把成员关系定义为一个映射应用键以及用于序列的搜索

1
2
3
4
5
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:[Finished in 0.1s]

这段代码输出特定的__contains__拦截成员关系,通用的__iter__捕获其他的迭代环境以至__next__重复地被调用,而__getitem不会被调用

1
2
3
4
5
iter=> next:next:next: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:[Finished in 0.2s]

如果去掉__contains__方法后成员关系路由到了通用的__iter__

1
2
3
4
5
get[0]:get[1]:get[2]:True
get[0]:1 | get[1]:2 | get[2]:3 | get[3]:4 | get[4]:5 | get[5]:
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100', '0b101']
get[0]:1 @ get[1]:2 @ get[2]:3 @ get[3]:4 @ get[4]:5 @ get[5]:[Finished in 0.2s]

如果去掉__contains____iter__方法后调用索引__getitem__方法,针对成员关系和其他迭代环境使用连续较高的索引

1
2
3
4
5
6
7
8
9
10
11
12
>>> from iters import Iters
>>> X = Iters('spam') # Indexing
>>> X[0] # __getitem__(0)
get[0]:'s'
>>> 'spam'[1:] # Slice syntax
'pam'
>>> 'spam'[slice(1, None)] # Slice object
'pam'
>>> X[1:] # __getitem__(slcie(..))
get[slice(1, None, None)]:'pam'
>>> X[:-1]
get[slice(None, -1, None)]:'spa'

__getitem__方法更加通用,除了迭代还呼拦截显示索引和分片

但是在并非序列的、迭代的用例中__iter__方法更容易编写,因为它不必管理一个整数索引

__contains__可以考虑到作为一种特殊情况优化成员关系


属性引用:__getattr____setattr__

__getattr__方法拦截未定义的属性点号运算,可以作为钩子来通过通用的方式响应属性请求

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> class empty:
... def __getattr__(self, attrname):
... if attrname == "age":
... return 40
... else:
... raise AttributeError(attrname)
...
>>> X = empty()
>>> X.age
40
>>> X.name
Traceback (most recent call last):
AttributeError: name

未定义的属性名称和实例进行点号运算时,若通过继承树搜索流程没有找到该属性时,调用__getattr__方法

调用__getattr__方法后,self赋值为实例类,attrname赋值为未定义的属性名称字符串,返回值作为点号表达式的结果,得到一个动态计算的属性

对于类不知道如何处理的属性,可以引发内置的AttributeError异常告诉Python一个未定义属性名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  >>> 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 # Calls __setattr__
>>> X.age
40
>>> X.name = 'mel'
Traceback (most recent call last):
AttributeError: name not allowed
__setattr__`会拦截所有属性的**赋值语句**,点号表达式`self.attr = value`变成`self__setattr__('attr', value)

__setattr__中对任何self属性赋都会再次调用__setattr__,导致无穷递归循环,需要通过属性字典__dict__做索引运算来赋值实例的属性

其他属性管理工具

  • __getattribute__方法了拦截所有的属性,需要小心地避免循环
  • Property内置函数允许将方法和特定类属性上的获取和设置操作关联起来
  • 描述符提供了一个协议把一个类的__get____set__方法与对特定类属性的访问关联起来

模拟实例属性的私有性:第一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class PrivateExc(Exception): pass               # More on exceptions later

class Privacy:
def __setattr__(self, attrname, value): # On self.attrname = value
if attrname in self.privates:
raise PrivateExc(attrname, self)
else:
self.__dict__[attrname] = value # slef.attrname = value loops!

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

Y.age = 30
x.age = 40 # Fails

通过通用化拦截属性,让每个拥有自己的私有变量名列表的子类无法通过其实例进行赋值

这是Python中实现属性私有性的首选方法

可以让子类也能够设置私有属性,并且使用__getattr__和包装来检测对私有属性的读取

使用类装饰器可以更加通用地拦截和验证属性


__repr____str__会返回字符串表达式形式

如果定义了的话,当类的实例打印或转换成字符串时__repr____str__就会自动调用,这些方法可替对象定义更好的显示,而不是使用默认的实例显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
>>> class adder:
... def __init__(self, value=0):
... self.data = value # Initialize data
... def __add__(self, other):
... self.data += other # Add other in-place (bad!)
...
>>> x = adder() # Default displays
>>> print(x)
<__main__.adder object at 0x1048d9f60>
>>> x
<__main__.adder object at 0x1048d9f60>

>>> class addrepr(adder): # Inherit __init__,__add__
... def __repr__(self): # Add string representation
... return 'addrepr(%s)' % self.data # Convert to as-code string
...
>>> x = addrepr(2) # Runs __init__
>>> x + 1 # Runs __add__
>>> x # Runs __repr__
addrepr(3)
>>> print(x) # Runs __repr__
addrepr(3)
>>> str(x), repr(x) # Runs __repr__ for both
('addrepr(3)', 'addrepr(3)')

>>> class addstr(adder):
... def __str__(self): # __str__ but no __repr
... return '[Value: %s]' % self.data # Convert to nice string
...
>>> x = addstr(3)
>>> x + 1
>>> x # Default __repr__
<__main__.addstr object at 0x1048c5cf8>
>>> print(x) # Runs __str__
[Value: 4]
>>> str(x), repr(x)
('[Value: 4]', '<__main__.addstr object at 0x1048c5cf8>')

>>> class addboth(adder):
... def __str__(self):
... return '[Value: %s]' % self.data # User-friendly string
... def __repr__(self):
... return 'addboth(%s)' % self.data # As-code string
...
>>> x = addboth(4)
>>> x + 1
>>> x # Runs __repr__
addboth(5)
>>> print(x) # Runs __str__
[Value: 5]
>>> str(x), repr(x)
('[Value: 5]', 'addboth(5)')

实例对象的默认显示既无用也不好看

两种显示方法可以进行用户友好的显示

  • 打印操作会首先尝试__str__str内置函数,通常返回一个用户友好的显示
  • __repr__用于交互提示模式以及repr函数等其他没有__str__时的任何环境,通常返回一个编码字符串,可以用来重新创建对象或给开发者一个详细的显示
  • printstr在定义__str__时调用__str__,否则调用__repr__

如果想让所有环境都有统一的显示,__repr__是最佳选择,通过分别定义两个方法可以在不同环境内支持不同显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> class Printer:
... def __init__(self, val):
... self.val = val
... def __str__(self): # Used for instance itself
... return str(self.val) # COnvert to a string result
...
>>> objs = [Printer(2), Printer(3)]
>>> for x in objs: print(x) # __str__ run when instance printed
... # But not when instance in a list!
2
3
>>> print(objs)
[<__main__.Printer object at 0x1048c57b8>, <__main__.Printer object at 0x1048c57f0>]
>>> objs
[<__main__.Printer object at 0x1048c57b8>, <__main__.Printer object at 0x1048c57f0>]

>>> class Printer:
... def __init__(self, val):
... self.val = val
... def __repr__(self): # __repr__ used by print if no __str__
... return str(self.val) # __repr__ used if echoed or nested
...
>>> objs = [Printer(2), Printer(3)]
>>> for x in objs: print(x) # No __str__: runs __repr__
...
2
3
>>> print(objs) # Runs __repr__, not __str__
[2, 3]
>>> objs
[2, 3]

__str____ repr__都必须返回字符串, 其他的结果类型不会转换并会引发错误

根据一个容器的字符串转换逻辑,__str__的用户友好的显示是当对象出现在一个打印操作顶层的时候才应用,嵌套到其他对象中的对象用其__repr__或默认方法打印


右侧加法和原处加法:__radd____iadd__

__add__方法只支持+运算符左侧使用实例对象

__radd__放在只支持+运算符右侧使用实例对象,而左侧不是实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> 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)
>>> x + 1 # __add__:instance + noninstance
add 88 1
89
>>> 1 + y # __radd__:noninstance + instance
radd 99 1
100
>>> x + y # __add__:instance + instance, triggers __radd__
add 88 <__main__.Commuter object at 0x1048c5908>
radd 99 88
187

当不同类的实例混合出现在表达式时,Python优先选择左侧的类

当把两个实例相加的时候,Python会运行__add__,通过简化左边的运算数来触发__radd__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> 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 '<Commuter: %s>' % self.val
...
>>> x = Commuter(88)
>>> y = Commuter(99)
>>> print(x + 10) # Result is another Commuter instance
<Commuter: 98>
>>> print(10 + y)
<Commuter: 109>

>>> z = x + y # Not nested: doesn't recur to __radd__
>>> print(z)
<Commuter: 187>
>>> print(z + 10)
<Commuter: 197>
>>> print(z + z)
<Commuter: 374>

若需要返回类的类型,需要类型测试来辨别是否能够安全地转换并由此避免嵌套

原处加法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+=`原处扩展相加会优先调用更加高效的`iadd`,空缺时调用`__add__
>>> class Number:
... def __init__(self, val):
... self.val = val
... def __iadd__(self, other): # __iadd__ explicit: x += y
... self.val += other # Usually returns self
... return self
...
>>> x = Number(5)
>>> x += 1
>>> x += 1
>>> x.val
7

>>> class Number:
... def __init__(self, val):
... self.val = val
... def __add__(self, other): # __add__ fallback: x = (x + y)
... return Number(self.val + other) # Propagates class type
...
>>> x = Number(5)
>>> x += 1
>>> x += 1
>>> x.val
7

每一个二元运算符都有类似的右侧和原处重载方法,以相同的方式工作


Call表达式:__call__

定义Python后会为实例应用函数调用表达式,调用时实例时运行__call__方法

1
2
3
4
5
6
7
8
9
>>> class Callee:
... def __call__(self, *pargs, **kargs): # Intercept instance calls
... print('Called:', pargs, kargs) # Accept arbitrary arguments
...
>>> C = Callee()
>>> C(1, 2, 3) # C is a callable object
Called: (1, 2, 3) {}
>>> C(1, 2, 3, x=4, y=5)
Called: (1, 2, 3) {'x': 4, 'y': 5}

__call__支持所有函数参数传递方式,传递实例的任何内容都会传递给该方法,包括隐式的实例参数

1
2
3
4
5
6
7
8
9
10
11
>>> class Prod:
... def __init__(self, value): # Accept just one argument
... self.value = value
... def __call__(self, other):
... return self.value * other
...
>>> x = Prod(2) # "Remembers" 2 in state
>>> x(3) # 3 (passed) * 2 (state)
6
>>> x(4)
8

拦截调用表达式允许类实例模拟类似函数的外观,也在调用中保持了状态信息以供使用

函数接口和回调代码

如果想让事件处理器保存事件之间的状态,可以注册类的绑定方法或者遵循所需接口的实例(__call__)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Callback:
def __init__(self, color): # Function + state information
self.color = color
def __call__(self): # Support calls with no arguments
print('turn', self.color)

cb1 = Callback('blue')
cb2 = Callback('green')

B1 = Button(command=cb1)
B2 = Button(command=cb2)

cb1() # On events: prints 'bule'
cb2() # Prints 'green'

类对象支持函数调用接口,也有状态信息

即使GUI期待的事件处理器是无参数的简单函数,还是可以为按钮把这个类的实例注册成事件处理器

当按钮按下时,将实例对象作为简单的函数调用,但是这个调用保存了以前的状态

1
cb3 = (lambda color='red': 'turn' + color)  # Or:defaults

还可以使用lambda函数的默认参数把信息和回调函数联系起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Callback:
def __init__(self, color): # Class with state information
self.color = color
def changeColor(self): # A normal named method
print('turn', self.color)

cb1 = Callback('blue')
cb2 = Callback('yellow')

B1 = Button(command=cb1.changeColor) # Reference, but don't
B2 = Button(command=cb2.changeColor) # Remembers function + self

object = Callback('blue') # Registered event handler
cb = object.changeColor # On event prints 'blue'
cb()

使用类的绑定方法,可以保存self实例以及所引用的函数,通过函数调用来实现


比较:__lt____gt__和其他方法

类调用的方法可以捕获六种比较运算符:<><=>===!=

相关限制:

  • 与前面讨论的__add__ /__radd__对不同,比较方法没有右端形式相反 当只有一个运算数支持比较的时候,使用其对应方法(例如,__lt____gt__互为对应)

  • 比较运算符没有隐式关系

    ==并不意味着!=是假的,因此,__eq___ne__应该定义为确保两个运算符都正确地作用

1
2
3
4
5
6
7
8
9
10
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') # True(rusn __gt__)
print(X < 'ham') # False(rusn __lt__)

拦截并实现了比较表达式

Python 2.6的__cmp__方法(已经从Python 3.0中移除了)

1
2
3
4
5
6
7
8
class C:
data = 'spam' # 2.6 only
def __cmp__(self, other): # __cmp__not used in 3.0
return cmp(self.data, other) # cmp not defined in 3.0

X = C()
print(X > 'ham') # True (runs __cmp__)
print(X < 'ham') # False (runs __cmp__)

__cmp__用来计算正在运行的运算符

1
2
3
4
class C:
data = 'spam'
def __cmp__(self, other):
return (self.data > other) - (self.data < other)

模拟cmp调用


布尔测试:__bool____len__

在布尔环境中,Python首先尝试__bool__来获取一个直接的布尔值,如果没有该方法,就尝试__len__类根据对象的长度确定一个真值

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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

通常使用对象状态或其他信息来生成一个布尔结果

1
2
3
4
5
6
7
>>> class Truth:
... def __len__(self): return 0
...
>>> X = Truth()
>>> if not X: print('no!')
...
no!

当没有__bool__方法时Python会调用__len__方法,非空对象看作是真

1
2
3
4
5
6
7
8
>>> class Truth:
... def __bool__(self): return True # 3.0 tries __bool__ first
... def __len__(self): return 0 # 2.6 tries __len__first
...
>>> X = Truth()
>>> if X: print('yes!')
...
yes!

当两个方法都有时优先调用__bool__,因为它更具体

1
2
3
4
5
6
>>> class Truth:
... pass
...
>>> X = Truth()
>>> bool(X)
True

当没有定义两个方法时,将对象看作是真

Python 2.6中的布尔

Python 2.6中的__bool____nonzero__,以相同的方式工作

在Python 2.6中__bool__为普通的自定义方法


对象析构函数:__del__

当实例产生时调用__init__构造函数,当实例空间被收回时,调用__del__析构函数

1
2
3
4
5
6
7
8
9
10
11
>>> 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
>>> brian = 'loretta'
Goodbye Brian

当类实例的最后一个引用失去时会触发其析构函数

通常在Python中析构函数不像使用其他OOP语言那么常用

  • Python在实例收回时,会自动收回该实例所拥有的所有空间,对于空间管理来说,是不需要析构函数的
  • 无法轻易地预测实例何时收回,通常最好是在有意调用的方法中(或者try/finally)编写代码去终止活动,在某种情况下,系统表中可能还在引用该对象使析构函数无法执行

本章习题

  1. 哪两种运算符重载方法可以用来支持类中的迭代

    类可以通过定义(或继承)__getitem____iter__来支持迭代

    在所有的迭代环境中,Python首先尝试使用__iter__ ,它返回支持迭代协议的一个对象,该对象带有一个__next__方法)

    如果在继承搜索中没有找到__iter__,Python调用__getitem__索引方法,它可以重复地调用,使用连续较高的索引

  2. 哪两种运算符重载方法处理打印,并且在何种环境下处理

    __str____ repr__方法实现对象打印显示

    前者由printstr内置函数调用 后者总是由repr内置函数、交互式响应和嵌套的出现,如果没有__str__方法也可以由printstr调用

    __str__通常用于用户友好的显示,__ repr__ 给出额外的细节,或者对象的编码形式

  3. 如何在类中拦截分片操作

    分片由__getitem__索引方法捕获:它用一个分片对象调用,而不是一个简单的索引

    在Python 2.6中,__getslice__ 也可以使用

  4. 如何在类中捕获原处加法

    原处加法首先尝试__iadd__,其次用__add__赋值

    同样的模式对于所有的二进制运算也是如此

    __radd__方法可用于右端相加

  5. 何时应该提供运算符重载

    当一个类自然地匹配的或者需要模拟一个内置类型接口的时候,例如,集合可能模拟序列或映射接口

    如果表达式运算符没有自然地映射对象的时候,我们通常不应该实现表达式运算符,而应该使用常规命名的方法


第三十章 类的设计

  • 介绍一些核心的OOP概念,以及一些比目前展示过的例子更实际的额外例子
  • 编写常用的OOP设计模式:继承(从其他类中获取行为)、组合(控制嵌入的对象)、委托(把对象包装在代理类内)和工厂
  • 介绍一些类设计的概念:伪私有属性、多继承和边界方法

Python和OOP

通过调用标记进行重载(或不要)


OOP和继承:”是一个”关系


OOP和组合:”有一个”关系

重访流处理器

为什么要在意:类和持续性


OOP和委托:”包装”对象


类的为私有属性

变量名压缩概览

为什么使用伪私有属性


方法是对象:绑定或无绑定

在Python 3.0中,无绑定方法是函数

绑定方法和其他可调用对象

为什么要在意:绑定方法和回调函数


多重继承:”混合”类

编写混合显示类


类是对象:通用对象的工厂

为什么有工厂


与设计相关的其他话题


本章习题

  1. 什么是多重继承

    当类从一个以上超类继承时,就发生了多重继承

  2. 什么是委托

    委托涉及把对象包装在代理类中,这样代理类会增加额外的行为,而把其他运算传给被包装的对象

    代理类包含了被包装的对象的接口

  3. 什么是组合

    组合是一种技术,让控制器类嵌入和引导一群对象,并自行提供接口

    是利用类创建较大结构的方式

  4. 什么是绑定方法

    绑定方法结合实例和方法函数

    调用时,不用刻意传入实例对象,因为原始的实例依然可用

  5. 为什么使用伪私有属性

    伪私有属性__X用来把名称本地化到类中,包括像定义在类中的方法以及在类中赋值的self实例属性

    这样的名称扩展来包含类名称,类名称使得它们独特


第三十一章 类的高级主题

  • 研究如何加了你内置类型的子类、新式类的变化和拓展、静态方法和类方法、函数装饰器等
  • 介绍类的相关陷阱

拓展内置类型

通过嵌入扩展类型

通过子类扩展类型


新式类


新式类变化

类型模式变化

钻石继承变动


新式类的扩展

slots实例

类特性

__getattrbute__和描述符

元类


静态方法和类方法

为什么使用特殊方法

Python 2.6和Python 3.0中的静态方法

静态方法替代方案

使用静态和类方法

使用静态方法统计实例

用类方法统计实例


装饰器和元类:第一部分

函数装饰器基础

装饰器例子

类装饰器和元类

更多详细信息


类陷阱

修改类属性的副作用

修改可变的类属性也可能产生副作用

多重继承:顺序很重要

类、方法以及嵌套作用域

Python中基于委托的类:__getattr__和内置函数

“过度包装”


本章习题

  1. 列举出两种能够扩展内置对象类型的方法

    你可以在包装类中内嵌内置对象,或者直接做内置类型的子类

    后者显得更简单,因为大多数原始的行为都被自动继承了

  2. 函数修饰器是用来做什么的

    函数修饰器通常是用来给现存的函数增加函数每次被调用时都会运行的一层逻辑
    它们可以用来记录函数的日志或调用次数、检查参数的类型等

    它们同样可以用做”静态方法”(一个在类中的函数,不需要传人实例)

  3. 怎样编写新式类

    可以通过对对象的内置类(或者其他的内置类型)继承来编写新式类

    在Python 3.0中,所有的类都将会自动成为新式类,因此不需要这么派生

    在Python 2.6中,这样派生出来的类是新式类,那些没有派生的类是”经典类”

  4. 新式类与经典类有何不同

    新式类与多重继承树中的钻石搜索模式有所不同,它们实际上是以广度优先(横向)进行搜索的,而不是深度优先(向上)

    新式类还针对实例和类修改了type内置函数的结果,针对内置操作方法,没有运行__getattr__这样的通用属性获取方法,并且支持包括特性、描述符和__slots__实例属性列表这样的一组高级额外工具

  5. 正常方法和静态方法有何不同

    正常(实例)方法会接受第一个self参数(隐含的实例),但是静态方法不是这样

    静态方祛只是嵌套在类对象中的简单函数

    为了使一个方法成为静态方法,它必须可以通过特殊的内置函数运行,或者使用装饰器进行装饰

    Python 3.0允许通过类而没有这个步骤就调用类中的简单函数,但是,通过实例调用仍然需要静态方法声明

CATALOG
  1. 1. 第二十五章 OOP:宏伟蓝图
    1. 1.1. 为何使用类
    2. 1.2. 概览OOP
      1. 1.2.1. 属性继承搜索
      2. 1.2.2. 类和实例
      3. 1.2.3. 类方法调用
      4. 1.2.4. 编写类树
      5. 1.2.5. OOP是为了代码重用
    3. 1.3. 本章习题
  2. 2. 第二十六章 类代码编写基础
    1. 2.1. 类产生多个实例对象
      1. 2.1.1. 类对象提供默认行为
      2. 2.1.2. 实例对象是具体的元素
      3. 2.1.3. 第一个例子
    2. 2.2. 类通过继承进行定制
      1. 2.2.1. 第二个例子
      2. 2.2.2. 类是模块内的属性
    3. 2.3. 类可以截获Python运算符
      1. 2.3.1. 第三个例子
      2. 2.3.2. 为什么要使用运算符重载
    4. 2.4. 世界上最简单的Python类
      1. 2.4.1. 类与字典的关系
    5. 2.5. 本章习题
  3. 3. 第二十七章 更多实例
    1. 3.1. 步骤1:创建实例
      1. 3.1.1. 编写构造函数
      2. 3.1.2. 在进行中测试
      3. 3.1.3. 以两种方式使用代码
    2. 3.2. 步骤2:添加行为方法
      1. 3.2.1. 编写方法
    3. 3.3. 步骤3:运算符重载
      1. 3.3.1. 提供打印显示
    4. 3.4. 步骤4:通过子类定制行为
      1. 3.4.1. 编写子类
      2. 3.4.2. 拓展方法:不好的方式
      3. 3.4.3. 拓展方法:好的方式
      4. 3.4.4. 多态的作用
      5. 3.4.5. 继承、定制和扩展
      6. 3.4.6. OOP:大思路
    5. 3.5. 步骤5:定制构造函数
      1. 3.5.1. OOP比我们认为的要简单
      2. 3.5.2. 组合类的其他方法
      3. 3.5.3. 在Python 3.0中捕获内置属性
    6. 3.6. 步骤6:使用内省工具
      1. 3.6.1. 特殊类属性
      2. 3.6.2. 一种通用显示工具
      3. 3.6.3. 实例与类属性的关系
      4. 3.6.4. 工具类的命名考虑
      5. 3.6.5. 类的最终形式
    7. 3.7. 步骤7(最后一步):把对象存储到数据库中
      1. 3.7.1. PIckle和Shelve
      2. 3.7.2. 在shelve数据库中存储对象
      3. 3.7.3. 交互地探索shelve
      4. 3.7.4. 更新shelve中的对象
    8. 3.8. 未来方向
    9. 3.9. 本章习题
  4. 4. 第二十八章 类代码编写细节
    1. 4.1. class语句
      1. 4.1.1. 一般形式
      2. 4.1.2. 例子
    2. 4.2. 方法
      1. 4.2.1. 例子
      2. 4.2.2. 调用超类构造函数
      3. 4.2.3. 其他方法调用的可能性
    3. 4.3. 继承
      1. 4.3.1. 属性树的构造
      2. 4.3.2. 继承方法的专有化
      3. 4.3.3. 类接口技术
      4. 4.3.4. 抽象超类
      5. 4.3.5. Python 2.6和Python 3.0的抽象超类
    4. 4.4. 命名空间:完整的内容
      1. 4.4.1. 简单变量名:如果赋值就不是全局变量
      2. 4.4.2. 属性名称:对象命名空间
      3. 4.4.3. Python命名空间的”禅”:赋值将变量名分类
      4. 4.4.4. 命名空间字典
      5. 4.4.5. 命名空间链接
    5. 4.5. 回顾文档字符串
    6. 4.6. 类与模块的关系
    7. 4.7. 本章习题
  5. 5. 第二十九章 运算符重载
    1. 5.1. 基础知识
      1. 5.1.1. 构造函数和表达式:__init__和__sub__
      2. 5.1.2. 常见的运算符重载方法
    2. 5.2. 索引和分片:__getitem__和__setitem__
      1. 5.2.1. 拦截分片
      2. 5.2.2. Python 2.6中的分片和索引
    3. 5.3. 索引迭代:__getitem__
    4. 5.4. 迭代器对象:__iter__和__next__
      1. 5.4.1. 用户定义的迭代器
      2. 5.4.2. 有多个迭代器的对象
    5. 5.5. 成员关系:__contains__、__iter__和__getitem__
    6. 5.6. 属性引用:__getattr__和__setattr__
      1. 5.6.1. 其他属性管理工具
      2. 5.6.2. 模拟实例属性的私有性:第一部分
    7. 5.7. __repr__和__str__会返回字符串表达式形式
    8. 5.8. 右侧加法和原处加法:__radd__和__iadd__
      1. 5.8.1. 原处加法
    9. 5.9. Call表达式:__call__
      1. 5.9.1. 函数接口和回调代码
    10. 5.10. 比较:__lt__、__gt__和其他方法
      1. 5.10.1. Python 2.6的__cmp__方法(已经从Python 3.0中移除了)
    11. 5.11. 布尔测试:__bool__和__len__
      1. 5.11.1. Python 2.6中的布尔
    12. 5.12. 对象析构函数:__del__
    13. 5.13. 本章习题
  6. 6. 第三十章 类的设计
    1. 6.1. Python和OOP
      1. 6.1.1. 通过调用标记进行重载(或不要)
    2. 6.2. OOP和继承:”是一个”关系
    3. 6.3. OOP和组合:”有一个”关系
      1. 6.3.1. 重访流处理器
      2. 6.3.2. 为什么要在意:类和持续性
    4. 6.4. OOP和委托:”包装”对象
    5. 6.5. 类的为私有属性
      1. 6.5.1. 变量名压缩概览
      2. 6.5.2. 为什么使用伪私有属性
    6. 6.6. 方法是对象:绑定或无绑定
      1. 6.6.1. 在Python 3.0中,无绑定方法是函数
      2. 6.6.2. 绑定方法和其他可调用对象
      3. 6.6.3. 为什么要在意:绑定方法和回调函数
    7. 6.7. 多重继承:”混合”类
      1. 6.7.1. 编写混合显示类
    8. 6.8. 类是对象:通用对象的工厂
      1. 6.8.1. 为什么有工厂
    9. 6.9. 与设计相关的其他话题
    10. 6.10. 本章习题
  7. 7. 第三十一章 类的高级主题
    1. 7.1. 拓展内置类型
      1. 7.1.1. 通过嵌入扩展类型
      2. 7.1.2. 通过子类扩展类型
    2. 7.2. 新式类
    3. 7.3. 新式类变化
      1. 7.3.1. 类型模式变化
      2. 7.3.2. 钻石继承变动
    4. 7.4. 新式类的扩展
      1. 7.4.1. slots实例
      2. 7.4.2. 类特性
      3. 7.4.3. __getattrbute__和描述符
      4. 7.4.4. 元类
    5. 7.5. 静态方法和类方法
      1. 7.5.1. 为什么使用特殊方法
      2. 7.5.2. Python 2.6和Python 3.0中的静态方法
      3. 7.5.3. 静态方法替代方案
      4. 7.5.4. 使用静态和类方法
      5. 7.5.5. 使用静态方法统计实例
      6. 7.5.6. 用类方法统计实例
    6. 7.6. 装饰器和元类:第一部分
      1. 7.6.1. 函数装饰器基础
      2. 7.6.2. 装饰器例子
      3. 7.6.3. 类装饰器和元类
      4. 7.6.4. 更多详细信息
    7. 7.7. 类陷阱
      1. 7.7.1. 修改类属性的副作用
      2. 7.7.2. 修改可变的类属性也可能产生副作用
      3. 7.7.3. 多重继承:顺序很重要
      4. 7.7.4. 类、方法以及嵌套作用域
      5. 7.7.5. Python中基于委托的类:__getattr__和内置函数
      6. 7.7.6. “过度包装”
    8. 7.8. 本章习题