# 第8章 类基础
> 众里寻他千百度,蓦然回首,那人却在灯火阑珊处
>
> ——辛弃疾
有的资料将这一章内容命名为“面向对象”,虽然没有错误,但这并不很“Pythonic” ,因为 Python 中的函数是第一类对象,在前一章中已经开始“面向对象”了。其实不仅仅是第7章,本书从一开始,就在“面向对象”。那么,本章的类与对象有什么关系?为什么很多自学者会在学到本章的时候遇到困难?如何跨过这个难关?请读者满怀信心地认真学习本章和第9章,严格地执行各项学习建议。“漫卷诗书喜欲狂”的成功愉悦就在不远的将来。
## 8.1 面向对象
在第2章2.4节曾初步了解过“对象”的概念,并且通过前面各章节的学习,读者已经对 Python 中的对象,比如内置对象和作为第一类对象的函数有了初步认识。从现在开始,将要在原有基础上,更深入地理解对象。
### 8.1.1 对象和面向对象
**对象**(Object)虽然是计算机科学中的专业术语,但不同的资料对其表述略有不同,例如《维基百科》中关于“对象”的词条内容是:
> 对象(Object),台湾译作物件,是**面向对象**(Object Oriented)中的术语,既表示客观世界问题空间中的某个具体的事物,又表示软件系统解空间中的基本元素。
计算机科学家 Grandy Booch(被业界尊为“面向对象”领域中的大师)所定义的“对象”包括以下要点:
- **对象**:一个对象有自己的状态、行为和唯一的标识;所有相同类型的对象所具有的结构和行为在它们共同的类中被定义。
- **状态**(State):包括这个对象已有的属性(通常是类里面已经定义好的),再加上对象具有的当前属性值(这些属性往往是动态的)。
- **行为**(Behavior):是指一个对象如何影响外界及被外界影响,表现为对象自身状态的改变和信息的传递。
- **标识**(Identity):是指一个对象所具有的区别于所有其他对象的属性。
将上述要点可以概括为:对象应该具有属性(即“状态”)、方法(即“行为”)和标识。Python 中对象的标识即在创建对象的时候自动在内存空间提供存储地址,所以,平时编写程序主要关注的是属性和方法。在第2章2.4节曾通俗地说明了对象的属性和方法的含义:
- 属性——描述对象“是什么”。
- 方法——描述对象“能干什么”。
再来看“面向对象”是什么意思,这是现在编程的主流思潮。还是引用《维基百科》中词条内容:
> **面向对象程序设计**(Object-oriented Programming,OOP)是一种程序设计范式,同时也是一种程序开发的方法。对象指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。
在此,对“面向对象”的理解姑且局限于这个表述。既然它是一种编程方法,就必须在编程实践中才能理解其真谛——实践出真知。所以,在后续的内容中,我们所学习和练习的,都是以“面向对象”为基本思想和方法——其实前面的学习和练习也如此,只是没有特别强调罢了。
### 8.1.2 类
编程语言中所说的“**类**”,其英文是“class”,“类”是中文翻译名称。对于初学者而言,听到这个名词会感觉怪怪的,因为不是很符合现代汉语的习惯。在汉语中,常说“鸟类”、“人类”等词语。当然,作为专门术语,不是不行,只是不太习惯罢了。诚然,在计算机科学中,类似的翻译还有不少,造成这种现象的原因很多,建议读者以“英汉结合”的方式来理解。
在目前流行的高级编程语言中,类是必须的。借用《维基百科》的定义:
> 在面向对象程序设计中,**类**(class)是一种面向对象计算机编程语言的构造,是创建对象的蓝图,描述了所创建的对象共同的属性和方法。
据此定义,重点理解:
- “蓝图”,一种比喻说法,意思是根据“类”可以得到对象。这就好比一个汽车制造工厂,有了一个生产汽车的设计(包含图纸和生产线),根据这个设计就可以生产出很多汽车。“设计”(或“蓝图”)就相当于“类”,而“汽车”就相当于根据“类”而创建的“对象”——也称之为“实例”,这个过程叫做“实例化”或者“创建实例”(如图8-1-1所示)。
- 在“类”中,要定义“属性”和“方法”。
![](./images/chapter8-1-1.png)
图8-1-1 类与蓝图的对比
“工厂中汽车的设计”绝对不是白纸一张,其中规定很多关于未来要造出来的“汽车对象”的“属性”和“方法”,比如车的颜色、车的性能等。如果抽象来看“类”,也是如此,要在类中规定好将来“造出来”的“实例们”所共有的属性和方法。
例如,把江湖上的一等一的大侠高手们的特点总结一下,发现他们有很多共同之处(以下皆为杜撰,若有雷同,纯属巧合,请勿对号入座):
- 都会“九阴真经”;
- 都吃过毒蛤蟆或者被毒蛇咬过,因此“百毒不侵”;
- 都不是单身;
- 都是男的;
- 都不使用阴招。
假设达到这个标准——“蓝图”——就是大侠,那么在写小说的时候,给某个人物赋予上述各项内容——创建实例,就塑造出来了一位大侠。
为了让生产“大侠”的过程更“数学”化,需要在表述上精准,所以把上述大侠的各项特征写到“类”里面——设计“蓝图”。用类似 Python语言的代码可以这样写:
```python
class 大侠:
性别 = 男
是否单身 = 否
中毒 = 百毒不侵
是否阴狠 = 否
九阴真经()
```
在这个名为“大侠”的类中,有属性——描述大侠的特征,即“是什么”,如:`性别 = 男` 。“性别”是属性,“男”是此属性的值。还有“方法”——描述大侠会什么功夫,即“做什么”,用形如“`九阴真功()`”的方式表示,类似前面学习过的函数,表示它可以被调用——功夫当然是要用来执行的。
接下来就可以用所定义的类“生产”大侠——不是文学作品中塑造大侠,而是用编程语言创建大侠“实例”。
```python
laoqi = 大侠()
```
我们所使用的符号体系与上一章中函数雷同,`大侠` 也是名称——类的名称——后面紧跟一对圆括号 `()` ,表示要执行这个名称所引用的对象,即“执行类”,可以形象地理解为让“工厂的生产线按照蓝图运行起来”,结果就应该是生产出产品了。于是由 `大侠()` 可得到“一个具体的大侠”——实例,并将该实例对象用变量 `laoqi` 引用,也可以简单说成实例 `laoqi` 。
如此,在程序中创建了一个符合“大侠类”中设计规范的“真实的大侠”实例——`laoqi` 。那么这个大侠的属性怎么访问?先想想以前学过的内置对象属性怎么访问?基本格式是“对象.属性”(注意中间的英文状态的符号),然后得到它的值。对实例 `laoqi` 也是如此,例如:
```python
>>> laoqi.中毒 # (1)
百毒不侵
```
注释(1)表示请求属性“中毒”的值,所返回的“百毒不侵”即是类中规定的此属性的值。任何用这个类所创建的大侠,其“中毒”的属性默认值都是“百毒不侵”。
还可以使用大侠具有的武功:
```python
>>> laoqi.九阴真经()
```
上述只是“伪装成”代码来演示。真正在 Python 中定义类,自有其严格规定。
> **自学建议**
>
> 从本章开始,读者会感到所学内容的抽象性更强了,可能略有不适应。“行百里者半九十”,这是最容易放弃的时候,请读者务必坚持,再坚持一会儿,就能在编程之路上跨出一大步。如何坚持学习?以下建议供参考:
>
> - 反复阅读和练习。由于内容更抽象了,不能“阅读”后立即理解,建议对每部分内容反复阅读,对其中的代码反复调试,在反复琢磨中领悟其含义,“欣然忘食”。
> - “发展是硬道理”。即使用了上面的方法,也难免有不理解之处,对这些内容可以暂时“不求甚解”,继续向后学习,很可能后面的知识能有助于解决前面的疑惑。
## 8.2 简单的类
请读者务必注意,本书讲述的是 Python 3 中类的创建方法,与 Python 2 相比,两个版本在定义类的时候稍有差别,如果看到了 Python 2 写的代码,注意区分。
### 8.2.1 创建类
打开你已经熟练使用的 IDE,用真正的 Python 程序实现8.1.2节中的“大侠”。
```python
#coding:utf-8
"""
filename: superman.py
"""
class SuperMan: # (1)
''' # (2)
A class of superman
'''
def __init__(self, name): # (3)
self.name = name # (4)
self.gender = 1 # (5)
self.single = False
self.illness = False
def nine_negative_kungfu(self): # (6)
return "Ya! You have to die."
```
结合代码和图8-2-1,学习简单的、常见的类如何定义。
![](./images/chapter8-2-1.png)
图8-2-1 简单类的结构
注释(1)逻辑行是类的头部,其组成部分是:
- `class` 是定义类的关键词;
- `SuperMan` 是这个类的名称。通常,类名称中每个单词的首字母要大写,如果是多个单词组合,单词之间不插入其它符号。切记,“代码通常是给人看的”,类的名称也尽可能本着“望文生义”的原则命名。
- 类的名称后面紧跟着是英文半角状态下的冒号 “ `:` ”。注意,与定义函数不同,这里没有圆括号。在8.5节会介绍什么时候用到圆括号。
从注释(2)的逻辑行开始是类的语句块,依然是用四个空格的缩进表示语句块。
注释(2)的三引号以及后面的配对三引号,这之间是关于当前类的说明文档,不是必须的。在通常的工程项目中,都要写,原因依然是“代码通常是给人看的”。
注释(3)和(6)定义了**方法**(Method)。它们都是由 `def` 这个关键词定义的,形式上与函数雷同,8.4.1节会将函数与方法给予比较。
以类 `SuperMan` 中的方法 `nine_negative_kungfu()` 为例,其参数有特别的要求,第一个参数必须是 `self` ,而且它必须要有——至少要有一个名为`self` 的参数。另外,并非强制使用 `self` 参数名称,可以用别的名称,但使用 `self` 是惯例。
其他关于编写方法的要求,比如名称的命名、内部语句块的要求等,与函数一样,此处不赘述,读者可以复习第7章关于函数的知识。
比较注释(3)和注释(6)两个逻辑行,虽然都是定义了方法,但形式和命名还有差别。
注意“ `__init__()` ”的名称写法,是以双下划线为前缀和后缀。除了这个方法之外,在后续学习中,还会看到很多其他以双下划线为前缀和后缀的方法名称,Python 语言中将这些方法统称为**特殊方法**(Special Method),或者称为**魔法方法**(Magic Method,第9章中会介绍更多这类方法)。根据英文知识容易知晓,`__init__()` 方法的名称中的 “init” 来自单词 “initial” 。当用类创建实例的时候,会访问这个方法,通过这个方法让实例具有其中所规定的属性。比如注释(4)的逻辑行:
- `self` 表示实例化时创建的实例对象(8.2.2节会详细介绍);
- `self.name` 表示实例对象具有名称为`name` 的属性;
- `self.name = name` 表示实例对象的 `name` 属性(左侧的 `name` )的值是实例化时参数 `name` 的实参(引用对象)。注意,符号 `=` 左侧所表示的实例属性的名称和右侧的参数(变量)名称不是非要求一样的。也可以使用注释(5)所示的那样,直接给实例属性赋值。
由于 `__init__()` 方法是在创建实例时被调用,再结合“initial”这个单词,于是将这个方法称为“**初始化方法**”。
请读者注意,在有的中文资料中,把 `__init__()` 方法称为“构造方法”。本书作者认为这种命名易引起混乱,误导读者。因为在 Python 的类中,真正具有“构造”作用的方法是 `__new__()` 方法,不是 `__init__()` 。因此本书使用“初始化方法”与 `__init__()` 对应,而“构造方法”则是 `__new__()` 的中文名称(参阅第9章9.4节)。
还要提示读者注意,“初始化方法”的最后不要写 return 语句(如果非要写,就写 `return None` )。
注释(6)所定义的是一个普通方法(相对“特殊方法”而言的“普通”,名称的命名上不用双下划线为前缀和后缀),除了参数列表中的 `self` 参数有前述规定和惯例之外,其他方面与第7章学过的函数没有差别。
创建了 `SuperMan` 类之后,就可以用它创建实例——形象地说,“类是实例的工厂”,用它可以塑造无限多个“超人”。
### 8.2.2 实例
承接8.2.1所创建的 `superman.py` 文件中的 `SuperMan` 类,将它实例化,如下述代码:
```python
#coding:utf-8
"""
filename: superman.py
"""
class SuperMan:
'''
A class of superman
'''
def __init__(self, name):
self.name = name
self.gender = 1
self.single = False
self.illness = False
def nine_negative_kungfu(self):
return "Ya! You have to die."
zhangsan = SuperMan("zhangsan") # (7)
print("superman's name is:", zhangsan.name) # (8)
print("superman is:(0-female, 1-male) ",zhangsan.gender) # (9)
result = zhangsan.nine_negative_kungfu() # (10)
print("If superman play nine negative kungfu, the result is:")
print(result)
```
程序中注释(7)的语句即依据 `SuperMan` 类创建一个实例,或说成“**实例化**”。所生成的实例是一个对象,或称为“实例对象”,并用变量 `zhangsan` 引用此对象。
在第7章7.3.1节曾借函数说明了对象后面紧跟圆括号的作用,可概括为“名称引用对象,圆括号才是执行”。对于类 `SuperMan` 而言,它也是一个对象——类也是对象,Python 中万物皆对象。例如:
```python
>>> class Foo:
... def __init__(self):
... self.f = 'foo'
...
>>> Foo # (11)
>>> type(Foo)
>>> id(Foo)
140693620290736
```
定义一个比较简单的类 `Foo` ——`Foo` 是类的名称。观察注释(11)及后续的操作,结合已学知识,可以总结出,与函数的名称引用函数对象类似,类的名称也引用了类对象。
既然如此,如果要在后面增加一个圆括号,就应该表示“执行类”了。“类是实例的工厂”、“类是实例的蓝图”,执行类,就意味着产生实例。
```python
>>> fo = Foo()
>>> fo
<__main__.Foo object at 0x7ff5c9622700>
>>> type(fo)
```
由操作结果可知,`Foo()` 是一个实例对象。
再回到注释(7),执行类 `SuperMan` ,从而得到实例对象。注意,后面的圆括号中要有参数。这是因为 `SuperMan` 类的初始化方法的参数(形参)除了 `self` 之外,还有一个 `name` ,那么实例化(或者说“创建实例”)的时候,要为参数 `name` 传一个对象引用(实参)。
在实例化的时候,不需要给初始化方法中的 `self` 参数传对象引用。注释(7)执行之后,Python 解释器以“隐式传递”的方式,令 `self` 引用刚刚所创立的实例(参阅8.3.3节)。
在执行注释(7)即创建实例时,首先要调用类 `SuperMan` 里面所定义的初始化方法,执行其内部程序。本例中,创建了实例对象的一些属性并完成赋值。例如实例的 `name` 属性值是 `'zhangsan'` ,`gender` 属性值是 `1` 。注释(8)和(9)中,读取了所创建的实例 `zhangsan` 的两个属性 `zhangsan.name` 和 `zhangsan.gender` 的值。
注释(10)的 `zhangsan.nine_negative_kungfu()` 表示调用了实例 `zhangsan` 的 `nine_negative_kungfu()` 方法,调用方式和函数一样。但是要注意参数,在类 `SuperMan` 中,每个方法的第一参数是 `self` ,通过实例调用方法的时候,不需要在圆括号中为 `self` 提供对象引用,这是因为 Python 解释器以“隐式传递”的方式向 `self` 参数传了 `zhangsan` 这个实例对象的引用(严格说法是变量 `zhangsan` 引用的实例对象,参阅8.3.3节)。
很容易理解,以注释(7)的方式,通过修改形参 `name` 的值,还可以创建无数个 `SuperMan` 类的实例,这些实例之间的差别在于其 `name` 属性的值不同。此即“类是实例工厂”的含义,工厂可以根据一个模型生产出很多产品,例如汽车制造厂生产汽车。
构建简单的类之后,下面要对其重要组成部分——属性和方法——分别进行详细说明。
> **自学建议**
>
> 有人将“学问”解释为“学会提问”,在学习过程中,能够将自己不会的东西,整理成别人能理解的问题并准确地表达出来,对学习者的确是一项挑战。特别是通过网络在社交媒体中提问,比如微信群、QQ群、论坛中,大家彼此之间不甚了解,如果问题问得不好,别人即便是有意回答也爱莫能助。那么,如何能提出别人能理解并愿意回答的问题呢?
>
> - 用语要委婉,“会说话的人运气总不会太差”;
> - 问题背景、前因后果要描述清楚;
> - 自己的疑问或者诉求不要太大、不要笼统、不要模糊。比如有人问我“怎么进行数据清洗”,这就不是一两句能说清楚的问题,而社交媒体中也不适合长篇大论,所以这个问题充其量会得到这样的回复:“去看老齐写的《数据准备和特征工程》(电子工业出版社)”——除非提问者就想要这个答案。
>
> 最后特别提醒,会思考的学习者不会在网上“乱喷”和“打唾沫仗”,“不争论”,静下心来敲代码,必能“直挂云帆济沧海”。
## 8.3 属性
Python 语言中对象的属性,可以分为**类属性**(Class Attribute)和**实例属性**(Instance Attribute)。在8.2节所演示的初始化方法中定义的属性,都属于实例属性。本节要对类属性和实例属性分别进行深入阐述。
### 8.3.1 类属性
在交互模式下,创建一个简单的类。
```python
>>> class Foo:
... lang = 'python' # (1)
... def __init__(self, name):
... self.name = name
...
```
这里定义的类 `Foo` 中有一个独立于方法之外的赋值语句(注释(1)所示),这个赋值语句中的变量 `lang` 称为类 `Foo` 的类属性。顾名思义,“类属性”就是“从属于类的属性”,可以通过类名称访问。
```python
>>> Foo.lang
'python'
```
从本质上看,注释(1)就是赋值语句,因此可以理解为 `Foo.lang` 这个变量引用了字符串对象 `'python'` 。
不妨将“类属性”类比于产品的标准配置——每个产品在生产出来之后都具有的特性。在创建实例的时候,类属性会自动配置到每个实例中,即:通过实例也可以访问该属性——但它不是从属于实例的属性,切记!。
```python
# 第一个实例
>>> j = Foo('java')
>>> j.lang
'python'
# 第二个实例
>>> r = Foo('ruby')
>>> r.lang
'python'
```
而在初始化方法 `__init__()` 中所创建的 `self.name` 属性,则会因为实例化时提供不同的实参,其值不相同,即这个属性会随实例而改变,故称之为实例属性——从属于实例的属性。
```python
>>> j.name
'java'
>>> r.name
'ruby'
```
但 `name` 属性不能用类名称访问:
```python
>>> Foo.name
Traceback (most recent call last):
File "", line 1, in
AttributeError: type object 'Foo' has no attribute 'name'
```
类属性可以通过类名称访问,也可以通过类名称进行修改,如:
```python
>>> Foo.lang = 'pascal' # (2)
>>> Foo.lang
'pascal'
```
注释(2)是一个赋值语句——参考注释(1),所谓“修改”,本质上变量 `Foo.lang` 引用了另外一个对象。
通过类名称(或者说类对象)修改了类属性的值,如果再用实例访问这个属性,发现其值也已经改变。
```python
>>> j.lang
'pascal'
>>> r.lang
'pascal'
```
这再次说明,属性 `lang` 不是在实例化的时候创建的,而是随着类的创建存在的。
还可以通过类名称增加类属性。
```python
>>> Foo.author = 'laoqi'
>>> hasattr(Foo, 'author') # 判断对象 Foo 是否有属性 author
True
>>> Foo.author
'laoqi'
>>> j.author
'laoqi'
>>> r.author
'laoqi'
```
如果删除类属性,可以使用 del 语句(参阅第6章6.1.3节)。
```python
>>> del Foo.author
>>> hasattr(Foo, 'author')
False
```
`Foo.author` 属性已经删除,不论是类名称还是实例名称,都不能访问到此属性。
```python
>>> j.author
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Foo' object has no attribute 'author'
```
在 Python 中,不论什么对象,其属性都在该对象的 `__dict__` 属性中—— `__dict__` 名称是双下划线为前缀和后缀。
```python
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', 'lang': 'pascal', '__init__': , '__dict__': , '__weakref__': , '__doc__': None})
```
在返回结果中,以类字典的方式列出了对象 `Foo` 的所有属性——类属性, `'lang': 'pascal'` 也在其中。而如果访问实例的 `__dict__` 属性,所得结果有所不同。
```python
>>> j.__dict__
{'name': 'java'}
```
这里只有在实例化时创建的属性——实例属性,下面就重点研习它。
### 8.3.2 实例属性
继续使用上一节定义的类 `Foo` 及所创建的两个实例 `j` 和 `r`,来探讨实例属性。
在类 `Foo` 实例化时,通过类的初始化方法 `__init__()` 所创建的实例属性,因实例不同而不同,故此属性也称为**动态属性**,对应于类属性的“静态”特征——类属性也称为**静态属性**。
```python
>>> r.name
'ruby'
>>> j.name
'java'
```
若无特别规定(第9章9.2节介绍针对属性的操作),实例属性也能修改和增加、删除。
```python
>>> j.name = 'javascirpt'
>>> j.name
'javascirpt'
```
还是用赋值语句修改 `j.name` 的值。此处修改了 `j.name` 的值,`r.name` 的值是否因此而变化?
```python
>>> r.name
'ruby'
```
因为 `j` 和 `r` 是两个对象,只是从同一个类实例化而得,它们具有同样名称的 `name` 属性罢了,而此属性的值互不影响。
继续使用赋值语句,也能为实例增加属性。
```python
>>> j.__dict__ # 已有属性
{'name': 'javascirpt'}
>>> j.book = 'learn python'
>>> j.__dict__
{'name': 'javascirpt', 'book': 'learn python'}
>>> r.__dict__ # 未受影响
{'name': 'ruby'}
```
使用 del 语句能删除实例属性。
```python
>>> del j.name
>>> j.__dict__
{'book': 'learn python'}
>>> r.__dict__
{'name': 'ruby'}
```
通过实例名称能对该实例的属性进行修改、增加、删除操作。在8.3.1节已经看到,通过实例名称也能访问到类属性名称。那么,是否可以对该类属性的值进行修改呢?比如:
```python
>>> j.lang
'pascal'
```
若做如下操作,结果会如何?
```python
>>> j.lang = 'c++' # (3)
```
未出现异常,说明上述操作是允许进行的。这是不是意味着 `Foo.lang` 的值因此而被修改了呢?非也!
```python
>>> Foo.lang
'pascal'
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', 'lang': 'pascal', '__init__': , '__dict__': , '__weakref__': , '__doc__': None})
```
注释(3)并没有影响到类属性,真实的情况是:
```python
>>> j.__dict__
{'book': 'learn python', 'lang': 'c++'}
```
建立了一个与类属性 `lang` 同名的实例属性 `lang` ,当使用实例名称访问 `lang` 属性的时候,就返回了此实例属性的值。
对于实例而言,它的属性通常按照图8-3-1所示的顺序读取。首先要检查 `j.__dict__` 中是否含有 `lang` 属性,如果有则返回相应的值;否则检查 `Foo.__dict__` 。
![image-20210703134312066](./images/chapter8-3-1.png)
图8-3-1 搜索实例属性
再用 del 语句,将 `j.lang` 删除,若再次读取 `j.lang` ,则会返回 `Foo.__dict__` 中的值。
```python
>>> del j.lang
>>> j.__dict__
{'book': 'learn python'}
>>> Foo.__dict__
mappingproxy({'__module__': '__main__', 'lang': 'pascal', '__init__': , '__dict__': , '__weakref__': , '__doc__': None})
>>> j.lang
'pascal'
```
由以上操作,可以认为,不论类属性还是实例属性,都可以视为“变量”——只不过变量的命名方式有点特别,这些变量引用了某个对象。
还要注意,前面演示中所引用的对象是不可变对象。如果引用了可变对象,结果会不一样。
```python
>>> class Bar:
... lst = []
...
>>> m, n = Bar(), Bar()
>>> m is n
False
>>> Bar.__dict__
mappingproxy({'__module__': '__main__', 'lst': [], '__dict__': , '__weakref__': , '__doc__': None})
>>> m.__dict__
{}
>>> n.__dict__
{}
```
两次实例化类 `Bar` ,分别得到了变量 `m` 和 `n` 引用的两个实例对象,且这两个实例下均没有名为 `lst` 的属性。按照图8-3-1所示的搜索顺序,`m.lst` 的值应该是 `Bar.__dict__['lst']` 的值:
```python
>>> m.lst is Bar.lst
True
```
如果执行:
```python
>>> m.lst.append(9)
```
并不会修改 `m.__dict__` 的值:
```python
>>> m.__dict__
{}
```
但 `Bar.__dict__` 中 `lst` 的值不再是空列表了,即 `Bar.lst` 的值发生了变化。
```python
>>> Bar.__dict__
mappingproxy({'__module__': '__main__', 'lst': [9], '__dict__': , '__weakref__': , '__doc__': None})
>>> Bar.lst
[9]
>>> m.lst
[9]
```
不仅如此,还有受到影响的:
```python
>>> n.__dict__
{}
>>> n.lst
[9]
```
这也不难理解,根据图8-3-1的所示,`n.lst` 的返回值也是 `Bar.__dict__['lst']` 。
遵照“循环上升”学习方法,关于对象属性问题,至此暂告一段,到第9章9.2节还会再次提及它,那将是更高层次的探讨。
### 8.3.3 关于 self
在8.2.1节说明类的基本结构时,特别强调过,若在类里面定义方法,其第一个参数必须是 `self`,并且是不可或缺的(除了8.4.2和8.4.3节的之外)。当然,也可以不使用这个名称,`self` 只是 Python 中的惯例。但是,惯例还是要遵守,这样才能让别人也能容易阅读你的代码。
下面写一个专门研究 `self` 是什么的类。
```python
>>> class P:
... def __init__(self, name):
... self.name = name
... print(self)
... print(type(self))
... print(f'id of self:{id(self)}')
...
```
用类 `P` 创建实例,会同时执行 `__init__()` 方法,就能看到上述 `print()` 函数的执行结果:
```python
>>> a = P('Assembly')
<__main__.P object at 0x7ff5cad34be0> # (4)
# (5)
id of self:140693646560224 # (6)
```
由此结果可知:
- 注释(4)所示的返回值说明 `self` 引用的对象是类 `P` 的实例,其内存地址的十六进制形式是 `0x7ff5cad34be0` ;
- 注释(5)所示的返回值说明 `self` 引用的对象类型是 `P` (类也是类型);
- 注释(6)所示的结果说明 `self` 引用的对象的内存地址的十进制形式是 `140693646560224` 。
```python
>>> a
<__main__.P object at 0x7ff5cad34be0>
>>> type(a)
>>> id(a)
140693646560224
>>> hex(id(a))
'0x7ff5cad34be0'
```
以上显示的是实例 `a` 的有关内容,并将十进制表示的内存地址转换为了十六进制表示。经过对比,可以下结论:`self` 与 `a` 引用了同一个实例对象——类 `P` 的实例。简化地说:“ `self` 就是实例对象。”
当创建实例的时候,实例变量作为第一个参数,被 Python 解释器传给了 `self` ,即8.2.2节所说过的“隐式传递”,所以初始化方法 `__init__()` 中的 `self.name` 是实例的属性。如下 `a.__dict__` 即查看到实例 `a` 的属性。
```python
>>> a.__dict__
{'name': 'Assembly'}
```
重写类 `P` ,增加一个方法,通过此方法的调用进一步理解 `self` 的作用。
```python
>>> class P:
... def __init__(self, name):
... self.name = name
... def get_name(self):
... return self.name
...
>>> a = P('BASIC')
>>> a.get_name()
'BASIC'
```
结合图8-3-2,理解执行 `a.get_name()` 时实例对象通过 `self` “传递”的过程。
![](./images/chapter8-3-2.png)
图8-3-2 关于 self
创建实例的时,`'BASIC'` 传给了参数 `name` (如图示中的①)。实例对象(实例名称所引用)传给了参数 `self` ,如图示中的②所示。当用执行 `a.get_name()` 时,实例也被隐式地作为第一个参数传给该方法,如图示中的③和④所示。
总之,读者应该理解,定义类的时候,参数 `self` 就是预备用来实例化后引用实例的变量(或“参数”)。
下面通过一个练习,理解类在编程实践中的应用。
假设有一个网上书店,商家根据买家购物的金额多少确定快递费(“包邮区”除外)。我们为这个网上书店编写程序,要求能够根据图书的单价、消费者购买数量以及快递费,计算买家应支付总金额——为了突出体现当前已经学过的知识,此问题已被简化。
老生常谈,请读者自行尝试后再看下面的代码示例。
```python
#coding:utf-8
'''
filename: bookshop.py
'''
class Book:
prices = {"A":45.7, "B":56.7, "C":67.8, "D":78.9, "E":90.1} # 书与单价
shipping = 5 # 快递费:5元
def __init__(self, book_name, num, free_ship):
self.book_name = book_name
self.num = num
self.free_ship = free_ship # 免快递费的阈值
# 计算总价
def totals(self):
price = Book.prices.get(self.book_name)
if price:
t = price * self.num
return (t + Book.shipping) if t < self.free_ship else t
return "There is NO this book."
if __name__ == "__main__":
book_a = Book('A', 2, 100) # 购买两本 'A' ,免快递费的阈值 100
a_total = book_a.totals()
print(a_total)
```
程序执行结果参考:
```python
% python bookshop.py
96.4
```
> **自学建议**
>
> 有的读者学到这里,可能会迷茫,“类的基本知识,似乎没什么新东西,但是自己写一个类,不知从何下手”。主要原因在于编写类之前,需要先对问题进行抽象。比如8.1和8.2节所示例中用的“大侠”,我们比较习惯的是“讲大侠的故事”,而不习惯“抽象大侠的属性和方法”。那么,怎样才能让自己习惯于“抽象”呢?现如今的方法就是要多练习。在3.7节和7.1.1节的【自学建议】中都建议过“多练习”,此处再次强调“多练习”,旨在说明所谓“抽象能力”的培养,也是要靠练习,而且是有意识地针对性练习——“学而时习之,不亦说乎”。
## 8.4 方法
类的方法,其基本结构与第7章中学过的函数近似,就普通的方法而言,仿照函数编写即可。然而类里面还会有一些不普通的方法,比如本节将要介绍的“类方法”和“静态方法”。这些方法都是为了让程序更简洁、紧凑而创立的。如果不使用这些方法,也能编写程序,但是用了它们,则锦上添花。
### 8.4.1 比较方法和函数
函数和方法的相似处不少,比如都是使用 `def` 关键词定义,除了某些特殊方法外( `__init__()` 初始化方法 ),普通方法和函数一样,都使用 return 语句作为结束(也可以说,所有方法都以 `return` 语句结束,但是 `__init__()` 中的是 `return None` )。
相同之处容易掌握,区别要特别关注。
函数是由函数名引用的一个独立对象(第一类对象),通过函数名称可以调用这个对象,它不依赖于其他东西。
```python
>>> def func(x): return x+7
...
>>> func
>>> type(func)
>>> func(4)
11
```
在调用函数的时候,如果函数有参数,必须很明确地(或者说是“显式地”)给每个参数提供对象或引用——一个参数也不能少。
而方法,必须要依赖于对象。因为它写在了类里面,如果要调用它,就要使用某个对象。前面已经学习过的知识是使用类的实例对象调用它,即通过实例名称:
```python
>>> class Foo:
... def my_method(self, x):
... return x ** 2
...
>>> f = Foo()
>>> f.my_method(9)
81
```
在类 `Foo` 中定义了方法 `my_method()` ,此方法有两个参数(形参)。根据8.3.3节可知,第一个参数 `self` 总引用类的实例,且通过实例调用方法的时候,不需要显式地为它传入实参。
此外,对于类中的方法,也可以通过类名称调用:
```python
>>> Foo.my_method(f, 9) # (1)
81
```
此时,必须要显式地为 `self` 提供实例参数。
尽管方法必须通过实例名称或者类名称调用,但每个方法在 Python 中也是一个对象,比如:
```python
>>> f.my_method
>
```
像这样的对象在 Python 中叫做**绑定方法**对象,即当前调用的方法绑定在了一个实例上。如果:
```
>>> Foo.my_method
```
显然 `Foo.my_method` 与普通函数无异(如前面编写的函数 `func()` )——其实就是一个函数,注释(1)中的调用方式与函数形式完全一样。那么,这个方法是否可以称为“非绑定方法”——尚未与实例绑定。在 Python 3 中没有这个名词了,因为它本质是函数,只是“函数名称”有点特别罢了。
### 8.4.2 类方法
Python 的内置函数 `classmethod()` 的作用就是在类中以装饰器语法糖的形式定义**类方法**(Class Method)。请读者阅读下面的代码示例:
```python
#coding:utf-8
'''
filename: clssmethod.py
'''
class Message:
msg = "Python is a smart language." # (2)
def get_msg(self):
print("the self is:", self)
print("attrs of class(Message.msg):", Message.msg) # (3)
@classmethod # (4)
def get_cls_msg(cls):
print("the cls is:", cls) # (5)
print("attrs of class(cls.msg):", cls.msg)
if __name__ == "__main__":
mess = Message()
mess.get_msg()
print("-" * 20)
mess.get_cls_msg()
```
先执行程序,再对照结果解释:
```shell
% python classmethod.py
the self is: <__main__.Message object at 0x7ff8ddb32d00>
attrs of class(Message.msg): Python is a smart language.
--------------------
the cls is: # (6)
attrs of class(cls.msg): Python is a smart language.
```
类 `Message` 中定义了类属性 `msg`(如注释(2)所示),然后在普通方法中调用这个类属性,如注释(3)所示,此处使用的是 `Message.msg` ,没有使用 `self.msg` 。如果将 `Message.msg` 改为 `self.msg` ,程序的输出效果是一样。
但是,不提倡使用 `self.msg` 。其原因要从8.3.2节图8-3-1所示的实例属性搜索顺序说起。如果该实例没有 `msg` 属性,则会读取 `Message.__dict__['msg']` 的值。注意前提条件:“实例没有 `msg` 属性”。在简单的程序中,我们能够很容易判断实例是否已经有 `msg` 属性,但在复杂情况下,不能明确地控制实例属性时,在注释(3)的语句中使用 `self.msg` 就会有较大风险(比如实例有与 `msg` 同名的属性,但其值不是注释(2)中的类属性的值)。“稳定压倒一切”,这是编程的基本原则。所以,注释(3)中使用 `Message.msg` 要好于 `self.msg` 。
故事情节必须要进一步“反转”,读者才能感觉有意思——但是,`Message.msg` 这种写法并不好。假如某天开发者一激动,觉得 `class Message` 中所用的类的名称不妥,修改成为了其他名称,但把注释(3)处给忘了——这是常见的(开发者历尽千辛万苦终于发现是此原因导致了 Bug,常常会非常激动,“解决了一个严重的潜在问题”)。那么,程序就会报错。像这种把类名称“写死”的方式,在编程中会称为**硬编码**(Hard Code)。如何避免硬编码?继续看下文。
注释(4)用装饰器装饰了一个名为 `get_cls_msg()` 的方法,这个方法的参数使用了 `cls` ——这也是惯例,使用其它参数名称亦可,不过还是遵守惯例较好。这个方法——被装饰器 `@classmethod` 装饰的方法——中如果调用类属性,不需要“硬编码”,改为 `cls.msg` 的样式。那么,方法中的 `cls` 是什么呢?
注释(5)打印了 `cls` ,其结果显示在注释(6),即 `cls` 引用了对象 `` ——类 `Message`(类也是对象)。所以,从效果上看,`cls.msg` 和`Message.msg` 是一样的,但 `cls.msg` 显然避免了将类名称“写死”的硬编码。能够令 `cls` 引用当前类对象的就是注释(4)的装饰器语法糖。
在 Python 中,通过装饰器 `@classmethod` 装饰的方法称为**类方法**。类方法的参数有且至少有一个,且要置于参数列表的首位,通常命名为 `cls` ,它引用的就是当前所在的类对象。
在上述程序中,类 ` Message` 里面的普通方法 `get_msg()` 通常是通过实例名称调用,如 `mess.get_msg()` ,像这样的方法称为**实例方法**(Instance Method)。
怎么在实际问题中应用类方法?从如下示例中理解。
在定义一个类时,只能有一个初始化方法 `__init__()` 。在某些情况下,会有捉襟见肘之感。比如,有一个名为 `Person` 的类,可以根据姓名( `name` )和年龄( `age` )实例化。如果要求还能用姓名和出生日期( `birthday` )实例化(这个要求是很正常的,因为“年龄”与“出生日期”其实具有一定等价性),应该如何写初始化方法?多写一个吗?肯定不能有重名的方法。
运用类方法就能解决此问题。
```python
#coding:utf-8
'''
filename: agebirth.py
'''
import datetime
class Person:
def __init__(self, name, age):
self.name = name
self.age = age # 用年龄初始化
@classmethod
def by_birth(cls, name, birth_year):
this_year = datetime.date.today().year
age = this_year - birth_year
return cls(name, age) # (7)
def get_info(self):
return "{0}'s age is {1}".format(self.name, str(self.age))
if __name__ == "__main__":
newton = Person('Newton', 26) # (8)
print(newton.get_info())
hertz = Person.by_birth("Hertz", 1857) # (9)
print(hertz.get_info())
```
注释(8)实例化 `Person` 类时,默认首先调用初始化方法 `__init__()` ,并且将参数传给初始化方法。但是,如果用出生年份作为注释(8)的参数,比如 `Person('Hertz', 1857)` 显然是不对的。为了能用年份创建实例,又不破坏已经定义的初始化方法 `__init__()` ,于是使用类方法装饰器,定义了类方法 `by_birth()` 。在这个方法中,计算了 `age` 之后,以注释(7)中的 `cls(name, age)` 创建实例对象。此处不需要使用 `Person` 类名称,而是使用 `cls` 代表当前类名称。注释(9)则直接通过类名称调用类方法创建实例。
特别要注意,注释(9)通过类名称调用类方法,本来在类中所定义的类方法有三个参数,第一个是`cls` ,它引用的就是当前类对象。那么在注释(9)中调用这个方法的时候,不再显式地在参数列表中传入类对象,`Person.by_birth()` 就表示类 `Person` 作为第一个参数传给了 `cls` 。
### 8.4.3 静态方法
先看这样一个问题。
写一个关于猫的类,就正常的猫而言,都有两个耳朵和四条腿,这可以作为其共有的属性,即类属性。不同的猫,颜色可能不同,所以这个属性应该是实例属性。另外,正常的猫都会叫,为此可以定义一个实例方法 `speak()` 实现“猫叫”。但是,如果这样,则没有从方法上体现“不管什么猫,在一般人的听觉中,叫声都一样(假设如此)”的特点。为了解决这种类型的问题,Python 中引入了**静态方法**(Static Method)的编写形式——所谓“静态”,即不因实例而变化,类比于8.3.2节的“静态属性”。
Python 语言的内置函数 `staticmethod()` 为编写静态方法提供了简洁的形式,类似8.4.2节的类方法,所有用 `@staticmethod` 装饰的方法即为静态方法。
```python
#coding:utf-8
'''
filename: catspeak.py
'''
class Cat:
ears = 2
legs = 4
def __init__(self, color):
self.color = color
@staticmethod
def speak(): # (10)
print("Meow, Meow")
if __name__ == "__main__":
black_cat = Cat("black")
white_cat = Cat("white")
black_cat.speak()
white_cat.speak()
if black_cat.speak is white_cat.speak and black_cat.speak is Cat.speak:
print('black_cat.speak, white_cat.speak, Cat.speak are the same objects.')
```
程序执行结果如下:
```python
% python catspeak.py
Meow, Meow
Meow, Meow
black_cat.speak, white_cat.speak, Cat.speak are the same objects.
```
注释(10)所定义的方法,既没有以 `self` 也没有以 `cls` 作为第一个参数,所以这个方法不是实例方法,也不是类方法。如果不用 `@staticmethod` 装饰 `speak()` 方法,在类里面不许可用这种形式定义方法。用 `@staticmethod` 装饰后,就构成了静态方法。
从执行结果可以得知,以 `black_cat.speak` 、`white_cat.speak` 、`Cat.speak` 三种不同方式调用同一个静态方法,该方法是同一个对象——所有猫叫声都一样。
在下面的示例中,综合应用了类方法和静态方法,请读者注意体会它们的应用时机。
```python
#coding:utf-8
'''
filename: judgescore.py
'''
class Score:
def __init__(self, scores):
self.scores = scores
@classmethod
def from_csv(cls, score_csv_str):
scores = list(map(int, score_csv_str.split(',')))
return cls(scores) if cls.validate(scores) else cls(False)
@staticmethod
def validate(scores):
for g in scores:
if g < 0 or g > 100:
return False
return True
if __name__ == '__main__':
# Try out some valid scores
class_scores_valid = Score.from_csv('90, 80, 85, 94, 70')
print('Got scores:', class_scores_valid.scores)
# Should fail with invalid scores
class_scores_invalid = Score.from_csv('92, -15, 99, 101, 77, 65, 100')
print(class_scores_invalid.scores)
```
程序执行结果:
```python
% python judgescore.py
Got scores: [90, 80, 85, 94, 70]
False
```
在 `Score` 类中,三个方法的作用依次是:
- 初始化方法 `__init__()` 只实现了实例属性的赋值;
- 类方法 `from_csv()` 用于创建实例,并且对字符串参数进行转换和判断,如果有不符合要求(小于零或大于一百)的整数,则认为输入数据不合规(返回 `False` )
- 静态方法 `validate()` 用于判断数据是否合规。
在类方法 `from_csv()` 中以 `cls.validate()` 的形式调用了当前类中的静态方法,显然此静态方法不需要与实例绑定。
至此,学习了类中的三种方法:
- 普通的实例方法:最常用的,第一个参数 `self` 是实例,用实例名称调用。
- 类方法:第一个参数 `cls` 是当前的类,必须用 `@classmethod` 装饰。
- 静态方法:不需要引用实例或类的参数,必须用 `@staticmethod` 装饰。
> **自学建议**
>
> 或许已经对类的基本结构有所了解,并且也能编写简单的类。但是,如果将自己写出的程序与“人家的代码”比较,总感觉自己的太幼稚、“人家的代码”显得那么“高深”——乃至于看不懂。对此,不必妄自菲薄,那些能编写“优雅代码”的高手们非天生的,也是经历了从“丑陋”到“优雅”的磨砺过程。根据经验,首先要能够编写最基本形式的类(比如只是用初始化方法和实例方法),经过一段时间学习和练习后,就会觉得所写的程序还有待优化,这就是进步。若还能完成了优化,则是更上一层楼。气定神闲、日拱一卒、坚持不懈,必然能写出“优雅代码”。
## 8.5 继承
**继承**(Inheritance)是 OOP(Object-oriented programming,面向对象程序设计)中的一个重要概念,也是类的三大特性之一(另外两个特性分别是多态和封装)。
OOP 中的“继承”概念和人类自然语言中的“继承”含义相仿。当对象 C 继承了对象 P,C 就具有了对象 P 的所有属性和方法。通常 C 和 P 都是类对象,称 C 为**子类**,称 P 为**父类**。
```python
>>> class P: p = 2
...
>>> class C(P): pass
...
```
定义类 `P` (注意写法,因为代码块只有一行,所以可以如上所示书写),里面只有一个类属性。然后定义类 `C` 。为了能够让类 `C` 实现对类 `P` 的继承,在类 `C` 的名称后面紧跟一个圆括号,圆括号里面写父类 `P` 的名称。
虽然类 `C` 的代码块只有 `pass` ,在其中没有定义任何属性和方法,但是由于它继承了类 `P` ,父类中所定义的类属性 `p` 及其值就会被“代入”到类 `C` :
```python
>>> hasattr(C, 'p')
True
>>> C.p
2
```
当子类继承父类之后,不需要再次编写相同的代码,实现了代码重用——“减少重复代码”是编程的一项基本原则。另外,子类继承父类的同时,也可以重新定义父类中的某些属性或方法,即用同名称的属性和方法覆盖父类的原有的对应部分,使其获得与父类不同的功能。
```python
>>> class D(P):
... p = 222
... q = 20
...
>>> D.p
222
```
类 `D` 继承了类 `P` ,在类 `D` 中定义了类属性 `p = 222` ,与父类 `P` 中所定义的类属性重名,则 `D.p` 的值即为子类中所定义的值。这样的效果称为对父类中属性 `p` 重写或覆盖。
从继承方式而言,Python 中的继承可以分为“单继承”和“多继承”。
### 8.5.1 单继承
所谓**单继承**,就是只从一个父类那里继承。前面列举的继承示例,都是单继承。要想知道类的父类,可以用类对象的属性 `__base__` 查看:
```python
>>> C.__base__
```
类 `C` 的父类是类 `P` ,这是毫无疑问的,前面定义类 `C` 时已经说明了。再看:
```python
>>> P.__base__
```
在定义类 `P` 时,并没有注明它继承什么对象,实际上,它也有父类,那就是类 `object` 。在 Python 3 中所有的类都是 `object` 的子类,所以,就不用在定义类的时候写这个“公共的父类”了(读者在阅读代码的时候,还可能遇到如此定义类的情况:`class MyCls(object)` ,在 Python 3 中,其效果与 `class MyCls` 等同。但是,如果在 Python 2 中,有一种名为“新式类”的定义类的方式,将 `object` 作为定义类时显式继承的对象,即写成 `class MyCls(object)` 的样式。请注意 Python 版本)。
下面列举一个示例,从中既能理解单继承的含义,也能明白为什么要继承——本节开篇未解释为什么,而是单刀直入介绍继承的写法,读者可以通过此示例理解“为什么”。
```python
#coding:utf-8
'''
filename: personinhe.py
'''
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def get_name(self):
return self.name
def get_age(self):
return self.age
class Student(Person):
def grade(self, n):
print(f"{self.name}'s grade is {n}")
if __name__ == "__main__":
stu1 = Student("Galileo", 27) # (1)
stu1.grade(99) # (2)
print(stu1.get_name()) # (3)
print(stu1.get_age()) # (4)
```
执行结果:
```python
% python personinhe.py
Galileo's grade is 99
Galileo
27
```
类 `Student` 继承了类 `Person` ,相当于将类 `Person` 的代码完全搬到了类 `Student` 里,即如同定义了下面的类:
```python
class Student:
def __init__(self, name, age):
self.name = name
self.age = age
def get_name(self):
return self.name
def get_age(self):
return self.age
def grade(self, n):
print(f"{self.name}'s grade is {n}")
```
注释(1)实例化 `Student` 类,其效果即等同于上述代码——参数 `name` 和 `age` 皆来自于父类 `Person` 的初始化方法。
注释(2)的 `grade()` 方法是子类 `Student` 中定义的;注释(3)和(4)的两个实例方法,均在父类中定义。
试想,如果还要定义一个名为 `Girl` 的类,其中也有一部分代码与 `Person` 类相同,就可以继续继承 `Person` 类。如此,`Person` 类中的代码可以被多次重用。这就是继承的意义所在。
继承了之后,子类如果还有个性化的需要,怎么办?例如,子类 `Student` 需要再增加一个实例属性 `school` ,用以说明所属学校。若对子类 `Student` 进行如下修改:
```python
class Student(Person):
def __init__(self, school): # 增加初始化方法
self.school = school
def grade(self, n):
print(f"{self.name}'s grade is {n}")
```
而后实例化类 `Student` ,但是会遇到疑惑。按照此前所说,类 `Student` 继承了类 `Person` ,那么父类中的 `__init__()` 方法也就被“搬运”到子类中,而现在子类中又有了一个同名的 `__init__()` 方法,这就是所谓的重写了父类的该方法,即子类的 `__init__()` 方法覆盖了父类的此方法,那么在子类中,父类的 `__init__()` 方法不再显现。按照此逻辑,实例化应当这样做:
```python
if __name__ == "__main__":
# stu1 = Student("Galileo", 27)
stu1 = Student("Social University")
stu1.grade(99)
print(stu1.get_name())
print(stu1.get_age())
```
再执行程序:
```python
% python personinhe.py
Traceback (most recent call last):
File "/Users/qiwsir/Documents/my_books/codes/personinhe.py", line 27, in
stu1.grade(99)
File "/Users/qiwsir/Documents/my_books/codes/personinhe.py", line 21, in grade
print(f"{self.name}'s grade is {n}")
AttributeError: 'Student' object has no attribute 'name'
```
报错!
在程序开发中,出现错误很正常,这并不可怕,可怕的是没有耐心阅读报错信息。
从输出的异常信息中不难看出,错误在于类 `Student` 中没有属性 `name` 。
从前面的程序中可知,属性 `name` 是类 `Person` 的 `__init__()` 方法中所定义的实例属性,现在新写的类 `Student` 虽然继承了类 `Person` ,但因为重写了父类的 `__init__()` 方法,且子类的该初始化方法中没有定义 `name` 属性,故在实例化 `Student` 类的时候,报出没有此属性的异常。
现在就提出了一个问题,子类重写或覆盖了父类的方法,但还要在子类中继续使用被覆盖的父类方法——颇有些“儿子打算脱离老子独立,但是还想要老子给予财政支持”的味道。在单继承中,为了解决此问题,可以使用 Python 的一个内置 `super()` 函数。再次修改 `Student` 类:
```python
class Student(Person):
def __init__(self, school, name, age): # 增加参数
self.school = school
super().__init__(name, age) # (5)
def grade(self, n):
print(f"{self.name}'s grade is {n}")
```
注释(5)即表示在子类中调用父类中被覆盖的 `__init__()` 方法——注意此处初始化方法的形参,不再显式地写出 `self` 。这样,在实例化 `Student` 类的时候,就需要 `school, name, age` 三个参数——注意类 `Student` 的 `__init__()` 方法中的参数。继续修改程序:
```python
if __name__ == "__main__":
# stu1 = Student("Galileo", 27)
stu1 = Student("Social University", "Galileo", 27)
stu1.grade(99)
print(stu1.get_name())
print(stu1.get_age())
print(stu1.school) # 增加一行
```
执行程序:
```python
% python personinhe.py
Galileo's grade is 99
Galileo
27
Social University
```
注释(5)还有两种替代写法,分别是:
- `super(Student, self).__init__(name, age)`
- `Person.__init__(self, name, age)`
在这两种替代写法中,都使用了父类的名称。对于单继承而言,推荐使用注释(5),在后续的多继承中,会使用到替代写法的形式。
不妨将单继承的知识运用到下面的代码优化中。假设有如下所示的两个类:
```python
#coding:utf-8
'''
filename: rectangle.py
'''
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
def perimeter(self):
return 2 * self.length + 2 * self.width
class Square:
def __init__(self, length):
self.length = length
def area(self):
return self.length * self.length
def perimeter(self):
return 4 * self.length
```
很显然,类 `Rectangle` 和类 `Square` 有很多类似之处——数学上也告诉我们,正方形可以看成是长宽相等的矩形。因此,可以将类 `Square` 利用单继承的知识进行优化——请读者先试试,再看下面的代码。
```python
class Square(Rectangle):
def __init__(self, length):
super().__init__(length, length)
```
顿感简洁,可以发出一声惊叹了。
在此基础上,再设计一个计算正方体(六个面都是正方形,也称立方体、正六面体)的体积和表面积的类,继续使用单继承,当如何编写?思考、尝试后再看参考代码。
```python
class Cube(Square):
def surface_area(self):
face_area = super().area()
return face_area * 6
def volume(self):
face_area = super().area()
return face_area * self.length
```
有了继承,是不是感觉编写程序的工作量减少了很多,再也不会“白头搔更短”了。
### 8.5.2 多继承
顾名思义,“多继承”是指某一个子类的父类不止一个,而是多个。比如:
```python
>>> class P1: p1 = 1
...
>>> class P2: p2 = 2
...
>>> class C(P1, P2): pass
...
>>> dir(C)
['__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__', 'p1', 'p2']
```
子类 `C` 继承了两个父类 `P1` 和 `P2` ,它也就具有了两个父类中所定义的类属性 `p1, p2` 。
继续改造8.5.1节创建的 `rectangle.py` 文件,在其中实现多继承。增加如下两个类:
```python
class Triangle:
def __init__(self, base, height):
self.base = base
self.height = height
def area(self):
return 0.5 * self.base * self.height
class RightPyramid(Triangle, Square):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
def area(self):
base_area = super().area() # (6)
perimeter = super().perimeter() # (7)
return 0.5 * perimeter * self.slant_height + base_area
```
类 `RightPyramid` 表示一个底面是正方形、侧面是三角形的正四棱锥。从几何的角度看,这个锥体由正方形和三角形组成,于是让它继承类 `Triangle` 和 `Square` 也顺理成章,此即为多继承的应用。 `RightPyramid` 类的 `__init__()` 方法的参数 `base` 为组成棱锥的正方形边长,`slant_height` 为组成棱锥的三角形的高。方法 `area()` 计算正四棱锥的表面积。
为了能清晰地观察到类 `RightPyramid` 实例化和调用方法的过程,还是进入到交互模式一步一步地执行。注意,现在要求在 `rectangle.py` 文件所在的目录进入到交互模式中,如图8-5-1所示(在后续内容中,会经常用这种方式对所编写的文件进行调试,并简称为“在文件当前位置进入到交互模式”,请读者知悉)。
![image-20210707111819665](./images/chapter8-5-1.png)
图8-5-1 在当前目录进入交互模式
然后执行下述语句(第11章11.1节会详解)。
```python
>>> from rectangle import *
>>> pyramid = RightPyramid(2, 4)
```
顺利地得到了一个实例 `pyramid` ,再计算它的面积。
```python
>>> pyramid.area()
Traceback (most recent call last):
File "", line 1, in
File "/Users/qiwsir/Documents/my_books/codes/rectangle.py", line 53, in area
base_area = super().area()
File "/Users/qiwsir/Documents/my_books/codes/rectangle.py", line 45, in area
return 0.5 * self.base * self.height
AttributeError: 'RightPyramid' object has no attribute 'height'
```
执行实例的 `area()` 方法后报错,异常信息显示 `RightPyramid` 对象没有 `height` 属性。然而,我们分明在它所继承的一个父类 `Triangle` 中定义了此属性了,为什么这里还会报错?难道没有“成功”地继承?
问题的根源在于**方法解析顺序**(Method Resolution Order,简称 MRO)。
在定义 `RightPyramid` 类时,继承了 `Triangle` 类和 `Square` 类,注释(6)使用 `super()` 调用 `RightPyramid` 的父类,就要根据 MRO 确定按照什么顺序在父类中搜索有关方法和属性。当然,MRO 是 Python 中已经规定好的,可以用对象的 `__mro__` 属性查看:
```python
>>> RightPyramid.__mro__
(, , , , )
```
属性 `RightPyramid.__mro__` 也可以用方法 `RightPyramid.mro()` 替代,两者等效。
上述结果说明,对于类 `RightPyramid` 而言,首先会搜索它自己内部是否有该方法和属性;然后按照继承顺序,分别搜索两个父类 `Triangle` 和 `Square` (注意顺序);如果仍然未果,则继续搜索 `Square` 的父类 `Rectangle` ;还是一无所获,则最终要搜索 `object` 类。
按照上述顺序,执行 `RightPyramid` 类中的注释(6)的 `super().area()` 时,就会在父类 `Triangle` 中搜索到 `Triangle.area(self)` 。这个方法中用到两个实例属性 `self.base` 和 `self.height` ,`self.base` 在实例化的时候已经建立( `self.base = 2` ),但是 `self.height` 没有定义。所以抛出了前述 `AttributeError` 异常。
怎么修改?
注释(6)实则是要计算四棱锥的底面正方形的面积,所以,就可以通过调整父类的顺序,先搜索 `Square` 类,并且通过调用 `Square` 类中的初始化方法,为正方形的边长属性( `self.base` )赋值。在 IDE 中按照下述方式修改代码。
```python
class RightPyramid(Square, Triangle): # 修改父类顺序
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
super().__init__(self.base) # 调用 Square.__init__()
def area(self):
base_area = super().area() # (6)
perimeter = super().perimeter() # (7)
return 0.5 * perimeter * self.slant_height + base_area
```
修改之后,回到交互模式。如果仍然没有退出交互模式,需要将修改之后的模块文件 `rectangle.py` 重新加载——简称“重载”,其方法如下):
```python
>>> import importlib
>>> import rectangle
>>> importlib.reload(rectangle)
```
然后才能使用 `rectangle.py` 文件中更新之后的代码。另外一种更简单的重载方法,就是退出当前交互模式,然后依据前述方式重新进入,再引入模块。
如果在 IDE 中编辑 `rectangle.py` 时,已经退出了交互模式,现在又重新进入交互模式,则不需要上述重加载,直接进入下面的操作(重载之后也执行如下操作)。
```python
>>> from rectangle import *
>>> RightPyramid.__mro__
(, , , , )
```
现在我们看到,MRO 的顺序发生了变化:`RightPyramid -> Square -> Rectangle -> Triangle` 。
```python
>>> pyramid = RightPyramid(2, 4)
>>> pyramid.area()
20.0
```
综上,可以总结一下,在多继承中,如果使用 `super()` 函数调用父类的属性和方法,务必要了解 MRO 的查找顺序。
对于任何类(或类型),都可以通过 `__mro__` 属性查看其“方法解析顺序”——包括但不限于上面的多重继承。例如:
```python
>>> int.__mro__
(, )
>>> float.__mro__
(, )
>>> str.__mro__
(, )
>>> list.__mro__
(, )
>>> dict.__mro__
(, )
```
再观察以上各种类的 MRO,不论是自定义的类还是内置类,都有共同的“祖先” `object` 类——命名为**基类**(Base Class)。在下面的操作中会看到一种很有趣的结果:
```python
>>> bool.__mro__
(, , )
```
`bool` 类继承了 `int` 类,这就揭示了在第3章3.7节曾学到的下述结论的深层原因。
```python
>>> True == 1
True
>>> False == 0
True
>>> 2 + True
3
```
再回到对 `RightPyramid` 类的研究上,如果读者仔细观察,会发现它虽然继承了 `Triangle` 类,实际上并没有通过 `super()` 函数调用 `Triangle` 类中的方法。
对于四棱锥的三角形面积,为什么不使用 `Triangle` 类中的 `area()` 方法计算呢?当然可以,但是由于在类 `Rectangle` 和 `Triangle` 中都有 `area()` 方法,且两个都要在 `RightPyramid` 中调用,如果还用 `super()` ,势必造成混乱。于是可以换一种方法,直接用类名称调用对应方法。
```python
class RightPyramid(Triangle, Square):
def __init__(self, base, slant_height):
self.base = base
self.slant_height = slant_height
Square.__init__(self, self.base)
Triangle.__init__(self, self.base, self.slant_height)
def area(self):
base_area = super(Square, self).area()
tri_area = Triangle.area(self)
return tri_area * 4 + base_area
```
诚然,这也有了“硬编码”的迹象。
如你所见,多继承虽然在编程中很有用,但会导致非常复杂的情况,甚至于让开发者感到困惑。所以,在实践中,用到多继承时都会非常谨慎,尽可能找到其他解决问题的方法,最大程度减少应用多继承的情况。其中,有一种被称为 **mixin** 的技术形式,深受开发者喜欢。
所谓 mixin (或 mix-in),是 OOP 编程语言中的一个类,它包含供其他类使用的方法。例如:
```python
#coding:utf-8
'''
filename: volume.py
'''
class Rectangle:
def __init__(self, length, width):
self.length = length
self.width = width
def area(self):
return self.length * self.width
class Square(Rectangle):
def __init__(self, length):
super().__init__(length, length)
class VolumeMixin: # (8)
def volume(self):
return self.area() * self.height
class Cube(VolumeMixin, Square):
def __init__(self, length):
super().__init__(length)
self.height = length
def surface_area(self):
return super().area() * 6
if __name__ == '__main__':
cube = Cube(2)
print(cube.surface_area())
print(cube.volume()) # (9)
```
程序执行结果:
```python
% python volume.py
24
8
```
总体上与 `rectangle.py` 的程序差不多,区别在于注释(8)定义了一个 mixin 类 `VolumeMixin` ,它的作用即在于向 `Cube` 类提供 `volume` 方法,即注释(9)中实例所调用的方法。
通常,mixin 类不会单独使用(如 `VolumeMixin` ),一个 mixin 类实现一个功能,用于实例化的类继承 mixin 类,可以认为是将若干个功能组装起来,这样使得编程思路清晰,也避免了多继承容易引起的混乱。
为了巩固所学,请读者尝试解决如下问题。
凡是有性生殖的生物体都采用了“多继承”,比如人。人拥有23对不同的染色体,其中有一对染色体决定性别,称为“性染色体”,即 X 染色体和 Y 染色体。女性染色体的组成为 XX,男性染色体的组成为 XY。在自然状态下,夫妇生男生女,就是双方的染色体随机组合的结果。若含 X 染色体的精子与卵子(含有 X 染色体)结合,受精卵性染色体为 XX 型,发育成女胎;若含 Y 染色体的精子与卵子结合,受精卵性染色体为 XY 型,发育成男胎。
将这个生理过程写成一个程序,从而能显示“生男生女”——注意,不是预测。
示例代码如下,供读者参考。
```python
#coding:utf-8
'''
filename: chromosome.py
'''
import random
class Father:
def __init__(self):
self.father_chromosome = 'XY'
def do(self):
print("Make money.")
class Mother:
def __init__(self):
self.mother_chromosome = "XX"
def do(self):
print("Manage money.")
class Child(Father, Mother):
def child_gender(self):
fat = random.choice(self.father_chromosome)
mot = 'X'
chi = fat + mot
if "Y" in chi:
return 1
return 0
if __name__ == "__main__":
p = Child()
if p.child_gender():
print('is a BOY.')
else:
print("is a GIRL.")
```
> **自学建议**
>
> 在第11章11.1节,我们会学到“模块”概念,即每一个 `.py` 文件。如图8-5-1那样进入交互模式后,可以将自己编写的文件作为模块引入,有利于我们通过“分解动作”理解程序的含义。
>
> 如果读者使用的是 Windows 操作系统,也可以打开 cmd,使用 DOS 命令,仿照图8-5-1那样,进入到 `.py` 文件所在目录,并开启 Python 交互模式。
>
> 此外,还可以在 IDE 中实现类似操作,以 VS Code 为例,通过“终端”也能实现类似的操作(如图8-5-2所示)
>
![](./images/chapter8-5-2.png)
>
> 图8-5-2 在 VS Code 中进入交互模式
## 8.6 多态
**多态**(Polymorphism),是 OOP 的一个重要概念。不少学习或使用 Python 的人,特别是曾经了解过 Java ,就会对 Python 中的多态有不同的解读。为了避免人微言轻,在本节将引述一名权威对 Python 语言的多态的阐述,这位大神就是《Thinking in Java》的作者 Bruce Eckel ——将 Java 奉为圭皋的特别注意,这位可真是大神,如果学习 Java 而没有阅读他的书,借用 Java 界的朋友所说,“那就不算学过 Java”。
Bruce Eckel 在2003年5月2日发表了一篇题为《Strong Typing vs. Strong Testing》(https://docs.google.com/document/d/1aXs1tpwzPjW9MdsG5dI7clNFyYayFBkcXwRDo-qvbIk/preview)的博客,将 Java 和 Python 的多态特征进行了比较。
先来欣赏 Bruce Eckel 在文章中所撰写的一段说明多态的 Java 代码:
```java
// Speaking pets in Java:
interface Pet {
void speak();
}
class Cat implements Pet {
public void speak() { System.out.println("meow!"); }
}
class Dog implements Pet {
public void speak() { System.out.println("woof!"); }
}
public class PetSpeak {
static void command(Pet p) { p.speak(); }
public static void main(String[] args) {
Pet[] pets = { new Cat(), new Dog() };
for(int i = 0; i < pets.length; i++)
command(pets[i]);
}
}
```
如果读者没有学习过 Java ,对上述代码理解可能不是很顺畅,这不重要,只要能理解大概意思即可。观察`command(Pet p)` ,这种写法意味着 `command()` 所能接受的参数类型必须是 `Pet` 类型,其他类型不行。所以,必须创建 `interface Pet` 这个接口并且继承 `Cat` 和 `Dog` 类,然后才能用于 `command()` 方法(原文:I must create a hierarchy of Pet, and inherit Dog and Cat so that I can upcast them to the generic command() method)。
然后,Bruce Eckel 又写了一段实现上述功能的 Python 代码:
```python
# Speaking pets in Python:
class Pet:
def speak(self): pass
class Cat(Pet):
def speak(self):
print "meow!"
class Dog(Pet):
def speak(self):
print "woof!"
def command(pet):
pet.speak()
pets = [ Cat(), Dog() ]
for pet in pets:
command(pet)
```
在这段 Python 代码中的 `command()` 函数,其参数 `pet` 并没有要求必须是前面定义的 `Pet`类型(注意区分大小写),仅仅是一个名字为 `pet` 的形参,用其他名称亦可。Python 不关心引用的对象是什么类型,只要该对象有 `speak()` 方法即可。提醒读者注意的是,因为历史原因(2003年),Bruce Eckel 当时写的是针对 Python 2 的“旧式类”,不过适当修改之后在 Python 3 下也能“跑”,例如将 `print "meow!"` 修改为 `print("meow!")` 。
根据已经学习过的知识,不难发现,上面代码中的类 `Pet` 其实是多余的。是的,Bruce Eckel 也这么认为,只是因为此代码是完全模仿 Java 程序而写的。随后,Bruce Eckel 就根据 Python 语言的特性对代码进行了优化。
```python
# Speaking pets in Python, but without base classes:
class Cat:
def speak(self):
print "meow!"
class Dog:
def speak(self):
print "woof!"
class Bob:
def bow(self):
print "thank you, thank you!"
def speak(self):
print "hello, welcome to the neighborhood!"
def drive(self):
print "beep, beep!"
def command(pet):
pet.speak()
pets = [ Cat(), Dog(), Bob() ]
for pet in pets:
command(pet)
```
去掉了多余的类 `Pet` ,增加了一个新的对象类 `Bob` ——人类,这个类根本不是 `Cat` 和 `Dog` 那样的类型,只是它碰巧也有一个名字为 `speak()` 的方法罢了。但是,也依然能够在 `command()` 函数中被调用。
这就是 Python 中的多态特点,大师 Brue Eckel 通过非常有说服力的代码阐述了 Java 和 Python 的区别,并充分展示了 Python 中的多态特征。
诚如前面所述,Python 不检查传入对象的类型,这种方式被称为“隐式类型”(Laten Typing)或者“结构式类型”(Structural Typing),也被通俗地称为**鸭子类型**(Duck Typeing)。其含义在《维基百科》中被表述为:
> 在程序设计中,鸭子类型(Duck Typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口决定,而是由当前方法和属性的集合决定。这个概念的名字来源于由 James Whitcomb Riley 提出的鸭子测试。“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子,游泳起来像鸭子,叫起来也像鸭子时,那么这只鸟就可以被称为鸭子。”
鸭子类型就意味着可以向任何对象发送任何消息,只关心该对象能否接收该消息,不强求该对象是否为某一种特定的类型。这种特征其实在前面函数部分就已经有所体现了。
```python
>>> lam = lambda x, y: x + y
>>> lam(2, 3)
5
>>> lam('python', 'book')
'pythonbook'
```
对于 Python 的这种特征,有一批程序员不接受,他们认为在程序被执行的时候,可能收到错误的对象,而且这种错误还可能潜伏在程序的某个角落。
对于此类争论,大师 Brue Eckel 在上面所提到的博客中,给出了非常明确的回答。下面将原文恭录于此(注:读者如果阅读有困难,可以借用有关工具。之所以不翻译,是避免因个人才疏学浅而导致误传。):
> Strong testing, not strong typing.
>
> So this, I assert, is an aspect of why Python works. C++ tests happen at compile time (with a few minor special cases). Some Java tests happen at compile time (syntax checking), and some happen at run time (array-bounds checking, for example). Most Python tests happen at runtime rather than at compile time, but they do happen, and that's the important thing (not when). And because I can get a Python program up and running in far less time than it takes you to write the equivalent C++/Java/C# program, I can start running the real tests sooner: unit tests, tests of my hypothesis, tests of alternate approaches, etc. And if a Python program has adequate unit tests, it can be as robust as a C++, Java or C# program with adequate unit tests (although the tests in Python will be faster to write).
读完大师的话,犹如醍醐灌顶,豁然开朗,再也不去参与那些浪费口舌的争论了。
对于多态问题,最后还要告诫读者,类型检查是毁掉多态的利器,如 `type()` 、`isinstance()` 及`isubclass()` 这些检查类型的函数,一定要慎用。
> **自学建议**
>
> 本来编程语言是用来解决问题的工具,没有高低贵贱之分。但是,由于用工具的人,时间长了会对自己常用的东西有感情,再加上其他因素,就导致了对编程语言的价值判断。比如,有这样一条编程语言鄙视链(大于号右边的是被左边所鄙视的对象):C > C++ > Java > Python > PHP > HTML 。如果根据学习的难度可以创建一条鄙视链,那么历史上曾经用0、1二进制编码的老前辈是不是也在天上鄙视晚辈后生们呢?
>
> 作为本书读者和志存高远的自学者,不论以哪一种编程语言作为自己开始学习的对象,都要充分理解并应用该语言的特性,而不是用其他语言的特征对某语言进行“点评”。在实际的项目中,我们会根据实际情况,选用不同的编程语言,不是根据当事人会什么语言或者喜好什么语言。
## 8.7 封装和私有化
在程序设计中,**封装**(Encapsulation)是对具体对象的一种抽象,将某些部分“隐藏”起来,在程序外部“看不到”,其含义是其他程序无法调用,而不是“无法用眼睛观察到代码”。如果让代码变成人难以阅读和理解的形式,这种行为称作“代码混淆”(obfuscation)。
### 8.7.1 下划线
Python 中的下划线是一种含义很丰富的符号。
此前的内容中,已经使用过下划线( `_` ),比如变量名称如果是由两个单词构成,中间用下划线连接;再比如类的初始化方法 `__init__()` 是以双下划线为前缀和后缀。现在探讨对象的封装,也可以用下划线实现,方式非常简单,即在准备封装的对象名字前面加“双下划线”。例如:
```python
>>> class Foo:
... __name = "laoqi"
... book = 'python'
...
>>> f = Foo()
>>> f.book
'python'
>>> f.__name
Traceback (most recent call last):
File "", line 1, in
AttributeError: 'Foo' object has no attribute '__name'
```
在类 `Foo` 中有两个类属性,`__name` 是用双下划线作为名称前缀而命名的类属性;`book` 是通常见到的类属性命名。
创建实例 `f` ,`f.book` 能正确地显示属性的值;但是,`f.__name` 则显示了 `AttributeError` 异常。这说明在类 `Foo` 之外,无法调用 `__name` 属性。
```python
>>> Foo.__name
Traceback (most recent call last):
File "", line 1, in
AttributeError: type object 'Foo' has no attribute '__name'
>>> hasattr(Foo, "__name")
False
>>> hasattr(Foo, "book")
True
```
除了用实例无法调用 `__name` 属性,用类名称 `Foo` 也无法调用。在类的外部检测 `Foo` 类是否具有 `__name` 属性时,返回了 `False` ,而检测 `book` 属性,则返回了 `True` 。与 `book` 相比,`__name` 就被“隐藏”了起来,不论是通过实例名称还是类名称,都无法调用它。
```python
>>> class Foo:
... __name = "laoqi"
... book = 'python'
... def get_name(self):
... return Foo.__name
...
```
再给类 `Foo` 增加一个方法 `get_name` ,在这个方法中,通过类名称调用 `__name` 属性。
```python
>>> f = Foo()
>>> f.get_name()
'laoqi'
```
再次实例化之后,执行 `f.get_name()` 后返回了类属性 `__name` 的值,但此属性是在类内部的方法中被调用的。
在 Python 中以双下划线作为名称前缀的属性或方法,都会像 `__name` 那样,只能在类内部调用,在外部无法调用。将这种行为称为**私有化**(Private),亦即实现了对该名称所引用对象的封装。
下面的代码是一个比较完整的示例,请读者认真阅读,并体会“私有化”的作用效果。
```python
# coding=utf-8
'''
filename: private.py
'''
class ProtectMe:
def __init__(self):
self.me = "qiwsir"
self.__name = "laoqi"
def __python(self):
print("I love Python.")
def code(self):
print("What language do you like?")
self.__python()
if __name__ == "__main__":
p = ProtectMe()
p.code()
print(p.me)
p.__python()
```
执行程序,看看效果:
```shell
% python private.py
What language do you like?
I love Python.
qiwsir
Traceback (most recent call last):
File "/Users/qiwsir/Documents/my_books/codes/private.py", line 21, in
p.__python()
AttributeError: 'ProtectMe' object has no attribute '__python'
```
执行到 `p.__python()` 时报 `AttributeError` 异常,说明方法 `__python()` 不能调用,因为它的名称用双下划线为前缀,表明是一个私有化的方法。在 `code()` 方法内,调用了 `__python()` 方法,在执行 `p.code()` 时得到了正确结果,再次表明被封装的对象只能在类的内部调用。
那么,为什么在命名属性或方法时,以双下划线为前缀就能实现封装呢?其原因在于,Python 解释器会对以这种形式命名的对象重命名,在原来的名称前面增加前缀形如 `_ClassName` 的前缀。以在交互模式中创建的 `Foo` 类为例:
```python
>>> Foo.__name
Traceback (most recent call last):
File "", line 1, in
AttributeError: type object 'Foo' has no attribute '__name'
>>> Foo._Foo__name # (1)
'laoqi'
```
`Foo` 的类属性 `__name` 被封装,其实是被 Python 解释器重命名为 `_Foo__name` ( `Foo` 前面是单下划线),若改用注释(1)形式,就可以得到 `Foo` 类的私有化类属性 `__name` 的值。
若在 `Foo` 类内调用 `__name` ,相当于正在写类内部代码块,类对象尚未经 Python 解释器编译。当类的代码块都编写完毕,Python 解释器将其中所有的 `__name` 都更名为 `_Foo__name` ,即可顺利调用其引用的对象。
而在类外面执行 `Foo.__name` 时,Python 解释器没有也不会将 `__name` 解析为 `_Foo__name` ,所以在调用`__name` 时就显示 `AttributeError` 。
读者在这里又看到了另外一种符号:单下划线。
在有的 Python 资料中,并不将上述的方式称为“私有化”——本质是改个名称嘛。而是用单下划线为前缀,“约定”该名称引用的对象作为私有化对象——注意是“约定”。
```python
>>> class Bar:
... _name = "laoqi"
... def get_name(self):
... return self._name
...
```
这里约定 `_name` 只在类内部调用。诚然,如果你不履约,施行“霸权主义”,Python 也不惩戒该行为——没有抛出异常。
```python
>>> Bar._name
'laoqi'
```
因此,也有的开发者认为 Python 并不支持真正的私有化,不能强制某对象私有化。于是将“单下划线”视为该对象宜作为内部使用的标记符。
以上两种私有化的观点和写法,此处并不强制读者采信哪一种,因为这都是编程的形式——不用这些也能写程序。
### 8.7.2 property 装饰器
或许,读者也认为,Python 不能实现真正意义上的对象封装,从上一节内容已经看到,以单下划线为前缀的命名是“君子约定”,以双下划线为前缀的命名是“虚晃一枪”。如果来“真”的,Python 能行吗?
Python 没有像 Java 等某些语言那样,以 `public` 和 `private` 等关键词定义类,可以说所有的类都是 pbulic 的,8.7.1节介绍的以命名“私有化”形式实现封装,也不是 Java 语言中的 `private` 。但是,Python 中有一种方法,能够让程序中的对象更接近“封装”。
在 IDE 中编写名为 `mypassword.py` 的程序文件,其代码如下:
```python
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.password = 'default'
def get_pwd(self):
return self.password
def set_pwd(self, value):
self.password = value
print("you have set your password.")
```
然后从此文件所在目录进入到 Python 交互模式(参阅8.5.2节所示方法,这种调试方法以后会经常用到,请读者务必掌握)。
```python
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.get_pwd()
'default'
>>> laoqi.set_pwd('123')
you have set your password.
>>> laoqi.password
'123'
>>> laoqi.password = '456'
>>> laoqi.get_pwd()
'456'
```
从上面的操作可知,实例 `laoqi` 的密码可以通过属性 `password` 或者方法 `get_pwd()` 读取,也可以通过属性 `password` 或者方法 `set_pwd()` 重置。显然,这样对密码的管理是非常不安全的——要进行适当的“封装”,基本要求是:密码只能通过属性读取,不能通过属性重置,即是只读的。
将 `mypassword.py` 中的文件按照下面方式进行修改。
```python
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.__password = 'default'
@property # (1)
def password(self): # (2)
return self.__password
def set_pwd(self, value):
self.__password = value
print("you have set your password.")
```
为了实现密码只读的需求,使用了注释(1)所示的装饰器 `@property` ——这个装饰器是基于内置函数 `property()` ,并且将原来的方法 `get_pwd()` 更名为 `password()` (如注释(2)所示)。此外,将原来的实例属性 `password` 重命名为 `__password` 。重新载入 `mypassword` 模块(参阅8.5.2节,最简单的方法是在交互模式中执行 `exit()` 函数退出后,在进入交互模式),执行如下操作:
```python
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.password = '123' # (3)
Traceback (most recent call last):
File "", line 1, in
AttributeError: can't set attribute
>>> laoqi.__password = '123' # (4)
>>> laoqi.password
'default'
```
通过实例 `laoqi` 调用 `password` 属性——注意,不是 `laoqi.password()` 方法。虽然注释(2)定义的是 `password()` 方法,但是此方法被 `@property` 装饰之后,就可以用同名的属性形式调用,并得到了默认的密码值。
注释(3)试图通过赋值语句修改密码,结果失败。但,注释(4)貌似成功了,其实这也没有修改 `laoqi.password` 的值,只是为实例 `laoqi` 增加了一个名为 `__password` 的实例属性。如此,实现了密码的“只读”功能。
当然,因为还遗留了 `laoqi.set_pwd()` 方法,调用它还是能重置密码的。
```python
>>> laoqi.set_pwd('456')
you have set your password.
>>> laoqi.password
'456'
```
但是,这样实现重置,有点“太丑”了,还是用 `laoqi.password = '456'` 的方式重置更优雅——注释(3)的执行结果已经说明,不能用赋值语句重置。还有,明码保存是不是太不安全?重置密码之后,最好是能加密保存。
于是有了第二个需求:能够用赋值语句(类似注释(3)那样)重置密码(从用户角度将,重置密码是非常必要的),并且密码要加密保存——否则不称之为“密”码。
根据这些需要,再次修改 `mypassword.py` 文件中的代码。
```python
#coding:utf-8
'''
filename: mypassword.py
'''
class User:
def __init__(self):
self.__password = 'default'
@property
def password(self):
return self.__password
@password.setter # (5)
def password(self, value): # (6)
value = int(value) + 728 # 最低级的加密方法
self.__password = str(value)
print("you have set your password.")
```
注意观察修改内容。注释(5)增加了一个装饰器(注释写法),它的作用就是让注释(6)所定义的方法变成以属性赋值的形式。在注释(6)的方法里面,用了一种最拙劣的加密方法。
重载模块 `mypassword` 后(参阅8.5.2节),在交互模式中继续操作:
```python
>>> from mypassword import *
>>> laoqi = User()
>>> laoqi.password
'default'
>>> laoqi.password = '456' # (7)
you have set your password.
>>> laoqi.password # (8)
'1184'
```
注释(7)实现了用赋值语句重置密码的需求,并且从注释(8)的返回值可知,注释(7)所提交的密码已经被“加密”。
由上述内容,已经初步理解了 `@property` 装饰器的一个作用:将方法转换为属性访问。就凭这个功能,它就能让程序“优雅”很多。比如:
```python
#coding:utf-8
'''
filename: computearea.py
'''
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
if __name__ == "__main__":
rect = Rectangle(7, 8) # (9)
rect_area = rect.area() # (10)
print(f"width = {rect.width}, height = {rect.height}")
print(f"call rect.area() method to rectangle area = {rect_area}")
```
在此程序中,注释(9)创建了一个矩形实例,注释(10)得到了此矩形的面积。虽然这种写法能够实现功能,但是在追求“优雅”的 Python 开发者心目中,注释(10)是“丑陋的”。实例的宽度和长度,分别用属性 `rect.width` 和 `rect.height` 得到,那么面积,也应该是实例的属性,不应该是方法。所以用 `rect.area()` 计算面积,本身就不很“OOP”。如果用 `rect.area` 这样的属性形式得到实例的面积,那才符合 OOP 思想,并体现着 Python 的优雅,更蕴含着开发者的智慧。
用 `@property` 对程序稍作修改:
```python
#coding:utf-8
'''
filename: computearea.py
'''
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property # 增加装饰器
def area(self):
return self.width * self.height
if __name__ == "__main__":
rect = Rectangle(7, 8)
#rect_area = rect.area() # 不再调用方法
print(f"width = {rect.width}, height = {rect.height}")
print(f"call rect.area attribute to rectangle area = {rect.area}")
```
执行结果:
```python
% python computearea.py
width = 7, height = 8
call rect.area attribute to rectangle area = 56
```
装饰器 `@property` 是基于内置函数 `property()` ,这方面类似于8.4.2和8.4.3节使用过的类方法、静态方法装饰器。它不仅能能实现“属性”的读、写,还能实现删除功能。下面的示例中,读者进一步体会 `@property` 的作用。
```python
#coding:utf-8
'''
temperature.py
'''
class Celsius:
def __init__(self, temperature=0):
self.__temperature = temperature
@property
def temperature(self):
return self.__temperature
@temperature.setter
def temperature(self, value):
if value < -273.15: # (11)
raise ValueError("Temperature below -273 is not possible")
self.__temperature = value
@temperature.deleter
def temperature(self):
raise AttributeError("Can't delete attribute")
```
进入到交互模式(参阅8.5.2节),进行如下操作:
```python
>>> from temperature import *
>>> person = Celsius()
>>> person.temperature
0
>>> person.temperature = 36
>>> person.temperature = -300 # (12)
Traceback (most recent call last):
File "", line 1, in
File "/Users/qiwsir/Documents/my_books/codes/temperature.py", line 16, in temperature
raise ValueError("Temperature below -273 is not possible")
ValueError: Temperature below -273 is not possible
>>> del person.temperature # (13)
Traceback (most recent call last):
File "", line 1, in
File "/Users/qiwsir/Documents/my_books/codes/temperature.py", line 21, in temperature
raise AttributeError("Can't delete attribute")
AttributeError: Can't delete attribute
```
重点看注释(12)的操作结果,之所抛出异常,是因为在程序中注释(11)对值的大小进行了判断,如果条件满足,就执行 `raise` 语句(参阅第10章10.3节)。
再看注释(13)的执行结果,抛出了 `AttributeError` 异常,是因为用装饰器 `@temperature.deleter` 所装饰的方法中,执行了 `raise` 语句,即禁止删除 `person.temperature` ,如此对该对象给予“保护”。
> **自学建议**
>
> 学到本章是对读者的最大考验,一般的学习者会止步于本书第7章,对第8章及以后的内容望而却步。为什么?因为从本章开始,不仅要综合运用已学过的知识,还对日常以“直觉感受”为主的思考问题方式提出了挑战。在8.3节的【自学建议】中已经提到了“抽象能力”之于编写类的重要性,并且建议读者要“多练习”。
>
> 在这里进一步建议,要对原有的练习作品,用后续所学去优化。如果读者现在“回头看”从第1章以来做过的各种练习,或许对某些问题又有了新的思考,甚至于认为书中的代码也不怎样——这说明已经有了较高的欣赏和评价能力。除了批判之外,更要自己动手,先把我写的示例代码进行优化——别忘了告诉我,让我和其他读者都能进步。
>
> 还有,要“胆子大一些”,敢于自己设计(或者是“想象”)一些项目,并动手尝试“做一做”,哪怕最后不成功,自己也有了经验。
## 8.8 命名空间
Python 之禅中有这样一句:命名空间是个绝妙的主意,我们应好好利用它(参阅第1章1.4节)。官方文档这样定义**命名空间**(Namespce):A *namespace* is a mapping from names to objects(命名空间是名称和对象的映射集合)。
一般解释“命名空间”的资料中,都会以上述内容开头,然后逐项介绍内置命名空间、全局命名空间和本地命名空间。本节也基本遵循此顺序,但是要提一个小问题,“空间是什么”。这个问题其实很难回答,我们通过直觉能“感受到”空间,比如人和其他物体所在的某个三维范围。对空间进行概念化,成了千古话题,从古希腊开始至今,在哲学、物理学、数学等方面对空间给予了各种定义和研究。亚里士多德认为事物所在场所就是空间。物理学家则把空间和时间合并成“时空”进行研究,最有名的就是相对论了。数学里面的欧几里得空间、线性空间等各种术语也不少。还有更多学科的术语中以“空间”为后缀,比如“外层空间”、“存储空间”、“个人空间”等等。
按照经典物理学的思想,物体存在于一定的空间内,它们之间的相对位置构成了所在的空间。移植此思想,由于 Python 中的对象是用名称来引用(回忆已经学过的变量、函数、类,皆如此),或者说名称与特定对象之间有映射关系,这类对象称为**命名对象**(named object),一些命名对象组成了的集合就形成了**由命名对象构建的空间**,简称**命名空间**。
进入到交互模式,按照下述方式定义 `lst` 、`book` 、`Science` 、`phy` 这些名称以及它们所引用的对象。
```python
>>> lst = ['python', 'math', 'physics']
>>> def book():
... author = "laoqi"
... print(locals())
...
>>> class Science: pass
...
>>> phy = Science()
>>> import math
```
这些命名对象所构成的命名空间称为**全局命名空间**(Global Namespace)。
```python
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': , '__spec__': None, '__annotations__': {}, '__builtins__': , 'lst': ['python', 'math', 'physics'], 'book': , 'Science': , 'phy': <__main__.Science object at 0x7f916b72a6d0>, 'math': }
```
在第7章7.3.3节中, `globals()` 函数的返回值是含有当前作用域全局变量的字典,字典中的成员即表示了命名对象(名称与对象之间的映射),于是此字典也表示了当前的全局命名空间中的所有命名对象。
同样在第7章7.3.3节曾使用过的 `locals()` 内置函数,返回的字典包含了本地(局部)作用域内的命名对象,这些对象构成的命名空间是**局部命名空间**(Local Namespace,或译为**本地命名空间**)。
观察执行函数 `book()` 所得结果,在此函数中即执行了 `locals()` 函数,返回了函数 `book()` 内的名称和对象。
```
>>> book()
{'author': 'laoqi'}
```
在 Python 中,除了全局命名空间和局部命名空间之外,还有**内置命名空间**(Nuilt-in Namespace),其中包含了各种内置命名对象。如下,显示了所有内置命名空间的对象名称。
```
>>> dir(__builtins__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
```
图8-8-1显示了上述三种命名空间之间的关系——近似于第7章图7-3-2。
![](./images/chapter8-8-1.png)
图8-8-1 命名空间的关系
阅读至此,不知读者是否迷惑于命名空间与作用域的关系?的确,很多资料将二者合并一起介绍,还会引用官方文档中的一句话以显示二者差别(A *scope* is a textual region of a Python program where a namespace is directly accessible. 用 Google 翻译为“作用域是 Python 程序的文本区域,可以直接访问命名空间。”)。如果没有深入解读,恐难以从字面上理解这句话的深刻含义。下面以一己拙见,抛砖引玉。
分别编写名为 `namespace_1.py` 和 `namespace_2.py` 的两个文件,其内部代码分别如下。
```python
#coding:utf-8
'''
filename: namespace_1.py
'''
lang = 'Python'
print(globals())
```
```python
#coding:utf-8
'''
filename: namespace_2.py
'''
lang = 'Chinese'
print(globals())
```
进入到交互模式(参阅8.5.2节),将这两个文件视为模块(参阅第11章11.1节),用 `import` 引入,执行 `import` 语句的同时会分别打印出 `globals()` 的执行结果,显示全局命名空间中的所有命名对象。
```python
>>> import namespace_1
{'__name__': 'namespace_1', ...(省略大部分内容), 'lang': 'Python'}
>>> import namespace_2
{'__name__': 'namespace_2', ...(省略大部分内容), 'lang': 'Chinese'}
```
根据命名空间的定义可知,命名空间是命名对象的容器。上述操作中,说明现在有两个全局命名空间的容器,它们彼此是独立的。在这两个命名空间中,都有名称 `lang` ,但是不能用下面的方式访问到:
```python
>>> lang
Traceback (most recent call last):
File "", line 1, in
NameError: name 'lang' is not defined
```
很显然,当前的 `>>>` 环境和引入的模块,虽然都是全局命名空间,但它们之间有边界,或者说每个命名对象都处于一个封闭结构中,这个封闭结构就是**作用域**(Scope)。作用域规定了可以访问特定对象的边界。
```python
>>> namespace_1.lang
'Python'
>>> namespace_2.lang
'Chinese'
```
由于以作用域为边界,命名空间之间实现了彼此独立,即便是同样的名称,也可以使它们之间不发生冲突。图8-8-2显示了作用域和命名空间的关系,并且对应显示了它们所具有的层级特点。
![](./images/chapter8-8-2.png)
图8-8-2 作用域和命名空间的关系
> **自学建议**
>
> 至此关于 Python 语言的最基本知识,已经自学完毕,但并不意味着应用这些基本知识的能力也同步实现,就一般情况而言,还需要学习者通过足量的练习,才能具备解决实际问题的能力。
>
> 练习可以有两类,一类是单项的知识技能训练,比如与本书中提供的练习题目,这类练习的目的在于加强对相关知识的理解和运用。另一类是解决实际问题,这类练习具有实践性、综合性的特征,旨在通过项目实战提升解决实际问题的综合能力。后者是在前者基础上开展的,所以建议学习者务必不要好高骛远。