9.

提供数据功能绑定在一起方法创建创建对象 类型从而能够创建类型 实例实例具有维持自身状态属性具有修改自身状态方法所属定义)。

其他编程语言相比,Python 使用语法语义。Python 有点类似 C++ Modula-3 结合而且支持面向对象编程(OOP)所有标准特性继承机制支持多个派生覆盖方法方法调用中的同名方法对象包含任意数量类型数据模块一样支持 Python 动态特性运行时创建创建可以修改

如果 C++ 术语描述的话成员包括数据成员通常 public (例外情况 私有变量),所有成员函数 virtual 。 Modula-3 一样没有用于对象方法引用对象成员简写形式方法函数声明第一参数代表对象参数方法调用提供 Smalltalk 一样,Python 对象导入重命名提供语义支持 C++ Modula-3 不同,Python 内置类型可以用作用户扩展此外 C++ 一样具有特殊语法内置运算算术运算下标可以实例重新定义

由于缺乏关于公认术语偶尔使用 Smalltalk C++ 术语使用 Modula-3 术语,Modula-3 面向对象语义 C++ 接近 Python,估计听说语言读者
9.1.
名称对象

对象之间相互独立多个名称甚至多个作用多个名称可以绑定同一对象其他语言通常称为别名。Python 初学者通常不容易理解这个概念处理数字字符串元组不可基本类型可以不必理会但是对于涉及可变对象列表字典以及大多数其他类型 Python 代码语义别名可能产生意料之外效果这样通常为了程序受益因为别名某些方面指针例如传递对象代价因为实现传递指针如果函数修改作为参数传递对象调用可以看到更改——无需 Pascal 那样不同机制传参
9.2. Python
作用命名空间

介绍首先介绍 Python 作用规则定义命名空间有一些巧妙技巧了解作用命名空间工作机制有利于加强理解并且即便对于高级 Python 程序员方面知识有用

接下来我们了解一些定义

namespace (
命名空间名称对象映射现在大多数命名空间使用 Python 字典实现除非涉及性能优化我们一般不会关注方面事情而且将来可能改变这种方式命名空间例子内置名称集合包括 abs() 函数以及内置异常名称);模块全局名称函数调用中的局部名称对象属性集合命名空间一种形式关于命名空间重要知识不同命名空间中的名称之间绝对没有关系例如不同模块可以定义 maximize 函数不会造成混淆用户使用函数必须函数前面加上模块

点号之后名称 属性例如表达式 z.real ,real 对象 z 属性严格来说模块名称引用属性引用表达式 modname.funcname ,modname 模块对象,funcname 模块属性模块属性模块定义全局名称之间存在直接映射它们共享相同命名空间! [1]

属性可以只读或者在后一种情况可以属性进行赋值模块属性可以 modname.the_answer = 42 。 可以使用 del 语句删除属性例如,del modname.the_answer 名为 modname 对象移除属性 the_answer。

命名空间不同时刻创建拥有不同生命周期内置名称命名空间 Python 解释器启动创建永远不会删除模块全局命名空间读取模块定义创建通常模块命名空间持续解释器退出脚本文件读取交互读取解释器顶层调用执行语句 __main__ 模块调用一部分拥有自己全局命名空间内置名称实际上模块 builtins 。

函数局部命名空间函数调用创建函数返回抛出函数处理异常删除。(实际上遗忘描述实际发生情况一些。)当然每次递归调用自己局部命名空间

命名空间 作用 Python 代码中的文本区域这个区域直接访问命名空间。“直接访问意思文本区域名称限定引用查找名称范围包括命名空间在内

作用虽然静态确定动态使用执行期间任何时刻都会 3 4 命名空间直接访问嵌套作用

内层作用包含局部名称首先其中进行搜索

那些外层闭包函数作用包含局部全局名称内层那个作用开始向外搜索

倒数第二作用包含当前模块全局名称

外层最后搜索作用内置名称命名空间

如果名称声明全局所有引用赋值直接指向倒数第二作用”,包含模块全局名称作用重新绑定内层作用以外找到变量可以使用 nonlocal 语句如果使用 nonlocal 声明这些变量只读尝试这样变量内层作用创建 局部变量使得同名外部变量保持不变)。

通常当前局部作用域字面文本引用当前函数局部名称函数之外局部作用域引用全局作用域一致命名空间模块命名空间定义局部命名空间放置另一命名空间

划重点作用字面文本确定模块定义函数全局作用域就是模块命名空间无论函数什么地方什么别名调用另一方面实际名称搜索运行时动态完成但是,Python 正在朝着编译静态名称解析方向发展因此不要过于依赖动态名称解析!(局部变量已经静态确定。)

Python
特殊规定如果存在生效 global nonlocal 语句名称赋值总是进入内层作用赋值不会复制数据只是名称绑定对象删除如此语句 del x 局部作用域引用命名空间移除 x 绑定所有引入名称操作使用局部作用域尤其是 import 语句函数定义局部作用域绑定模块函数名称

global
语句用于表明特定变量全局作用域全局作用域重新绑定;nonlocal 语句表明特定变量在外作用在外作用重新绑定
9.2.1.
作用命名空间示例

演示如何引用不同作用名称空间以及 global nonlocal 变量绑定影响

def scope_test():
def do_local():
spam = "local spam"

def do_nonlocal():
nonlocal spam
spam = "nonlocal spam"

def do_global():
global spam
spam = "global spam"

spam = "test spam"
do_local()
print("After local assignment:", spam)
do_nonlocal()
print("After nonlocal assignment:", spam)
do_global()
print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

示例代码输出

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

注意局部 赋值默认状态不会改变 scope_test spam 绑定。 nonlocal 赋值改变 scope_test spam 绑定 global 赋值改变模块层级绑定

而且,global 赋值没有 spam 绑定
9.3.
初探

引入一点语法对象类型一些语义
9.3.1.
定义语法

简单定义形式如下

class ClassName:
<
语句-1>
.
.
.
<
语句-N>

函数定义 (def 语句) 一样定义必须执行才能生效定义 if 语句分支函数内部

实践定义语句通常函数定义可以其他语句部分内容稍后讨论函数定义一般特殊参数列表方法调用约定规范指明 --- 同样稍后解释

进入定义创建命名空间用作局部作用域 --- 因此所有局部变量赋值这个命名空间之内特别函数定义绑定这里函数名称

(结尾) 正常离开定义创建 对象基本上围绕定义创建命名空间包装我们在下了解有关对象信息原始 (进入定义之前有效) 作用重新生效对象这里定义名称进行绑定 (这个示例 ClassName)。
9.3.2. Class
对象

对象支持操作属性引用实例

属性引用 使用 Python 所有属性引用使用标准语法: obj.name。 有效属性名称对象创建存在命名空间中的所有名称因此如果定义这样:

class MyClass:
"""
简单示例"""
i = 12345

def f(self):
return 'hello world'

那么 MyClass.i MyClass.f 就是有效属性引用分别返回整数函数对象属性可以赋值因此可以通过赋值改变 MyClass.i 。 __doc__ 有效属性返回所属文档字符串: "A simple example class"。

实例 使用函数表示可以对象视为返回实例不带参数函数举例来说假设使用上述):

x = MyClass()

创建 实例 对象分配局部变量 x。

实例操作 (“调用对象) 创建对象许多希望创建对象实例根据特定初始状态定制因此可能定义名为 __init__() 特殊方法这样:

def __init__(self):
self.data = []

定义 __init__() 方法实例自动创建实例唤起 __init__()。 因此这个例子可以通过以下语句获得初始化实例:

x = MyClass()

当然,__init__() 方法还有一些参数用于实现灵活性这种情况提供实例运算参数传递 __init__()。 例如
>>>

class Complex:

def __init__(self, realpart, imagpart):

self.r = realpart

self.i = imagpart


x = Complex(3.0, -4.5)

x.r, x.i
(3.0, -4.5)

9.3.3.
实例对象

现在我们实例对象什么实例对象所能理解唯一操作属性引用有效属性名称数据属性方法

数据属性 对应 Smalltalk 中的实例变量”,以及 C++ 中的数据成员”。 数据属性需要声明局部变量一样它们首次赋值产生举例来说如果 x 上面创建 MyClass 实例以下代码打印数值 16,保留任何追踪信息:

x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter

一种实例属性引用称为 方法方法属于对象函数

实例对象有效方法名称依赖所属根据定义所有函数对象属性定义其实相应方法因此我们示例,x.f 有效方法引用因为 MyClass.f 函数 x.i 不是方法因为 MyClass.i 不是函数但是 x.f MyClass.f 不是一回事 --- 方法对象不是函数对象
9.3.4.
方法对象

通常方法绑定立即调用:

x.f()

MyClass 示例返回字符串 'hello world'。 但是方法不是必须立即调用: x.f 方法对象可以保存起来以后调用例如:

xf = x.f
while True:
print(xf())

持续打印 hello world,直到结束

方法调用究竟发生什么可能已经注意尽管 f() 函数定义指定参数上面调用 x.f() 没有参数这个参数发生什么事需要参数函数附带任何参数情况调用 Python 肯定引发异常 --- 即使参数实际上没有使用...

实际上可能已经到了答案方法特殊在于实例对象作为函数第一参数传入我们示例调用 x.f() 其实相当于 MyClass.f(x)。 总之调用具有 n 参数方法相当于调用参数对应函数这个参数方法所属实例对象位置其他参数之前

总而言之方法运作方式如下实例数据属性引用搜索实例所属如果名称表示属于函数对象有效属性指向实例对象函数对象引用打包方法对象传入参数列表调用方法对象基于实例对象参数列表构造参数列表传入这个参数列表调用相应函数对象
9.3.5.
实例变量

一般来说实例变量用于实例唯一数据变量用于所有实例共享属性方法:

class Dog:

kind = 'canine' #
变量所有实例共享

def __init__(self, name):
self.name = name #
实例变量实例独有

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind #
所有 Dog 实例共享
'canine'
>>> e.kind #
所有 Dog 实例共享
'canine'
>>> d.name #
d 独有
'Fido'
>>> e.name #
e 独有
'Buddy'

正如 名称对象 讨论共享数据可能涉及 mutable 对象例如列表字典时候导致令人惊讶结果例如以下代码中的 tricks 列表应该用作变量因为所有 Dog 实例共享单独列表:

class Dog:

tricks = [] #
变量错误用法

def __init__(self, name):
self.name = name

def add_trick(self, trick):
self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks #
预期所有 Dog 实例共享
['roll over', 'play dead']

正确设计应该使用实例变量:

class Dog:

def __init__(self, name):
self.name = name
self.tricks = [] #
Dog 实例新建列表

def add_trick(self, trick):
self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4.
补充说明

如果同样属性名称同时出现实例属性查找优先选择实例:
>>>

class Warehouse:

purpose = 'storage'

region = 'west'


w1 = Warehouse()

print(w1.purpose, w1.region)
storage west

w2 = Warehouse()

w2.region = 'east'

print(w2.purpose, w2.region)
storage east

数据属性可以方法以及对象普通用户(“客户端”)引用换句话说不能用于实现抽象数据类型实际上 Python 没有任何东西强制隐藏数据 --- 完全基于约定。 (另一方面 C 语言编写 Python 实现可以完全隐藏实现细节必要控制对象访问特性可以通过 C 编写 Python 扩展使用。)

客户端应当谨慎使用数据属性 --- 客户端可能通过直接操作数据属性方式破坏方法维护固定变量注意客户端可以实例对象添加他们自己数据属性不会影响方法只要保证避免名称冲突 --- 再次提醒在此使用命名约定可以省去许多令人头痛麻烦

方法内部引用数据属性其他方法!)没有简便方式发现实际上提升方法可读性浏览方法代码不会存在混淆局部变量实例变量机会

方法第一参数常常名为 self。 不过就是约定: self 名称 Python 绝对没有特殊含义但是注意遵循约定使得代码其他 Python 程序员缺乏可读性而且可以想像 浏览器 程序编写可能依赖这样约定

任何作为属性函数实例定义相应方法函数定义文本并非必须包含定义之内函数对象赋值局部变量可以例如:

#
之外定义函数
def f1(self, x, y):
return min(x, x+y)

class C:
f = f1

def g(self):
return 'hello world'

h = g

现在 f、g h C 指向函数对象属性因此它们 C 实例方法 --- 其中 h g 完全等价注意这种做法通常使程序读者感到迷惑

方法可以通过使用 self 参数方法属性调用其他方法:

class Bag:
def __init__(self):
self.data = []

def add(self, x):
self.data.append(x)

def addtwice(self, x):
self.add(x)
self.add(x)

方法可以通过普通函数相同方式引用全局名称方法相关全局作用域就是包含方法定义语句模块。(永远不会用作全局作用域。)尽管一个人理由方法使用全局作用域中的数据全局作用域依然存在许多合理使用场景例子导入全局作用域函数模块可以方法使用定义全局作用域中的函数一样通常包含方法本身定义全局作用域在下我们将会发现为何有些时候方法需要引用所属

对象因此具有 称为 类型),并存 object.__class__ 。
9.5.
继承

当然如果支持继承语言特性不值得称为”。派生定义语法如下:

class DerivedClassName(BaseClassName):
<
语句-1>
.
.
.
<
语句-N>

名称 BaseClassName 必须定义包含派生定义作用访问命名空间作为名称替代允许使用其他任意表达式例如定义另一模块中时这就有用:

class DerivedClassName(modname.BaseClassName):

派生定义执行过程相同构造对象记住信息用来解析属性引用如果请求属性找不到搜索转往进行查找如果本身派生其他规则递归应用

派生实例没有任何特殊: DerivedClassName() 创建实例方法引用以下方式解析搜索相应属性必要继承逐步向下查找如果产生函数对象方法引用生效

派生可能方法因为方法调用同一对象其他方法没有特殊权限所以方法尝试调用调用同一定义另一方法可能实际上调用派生定义方法。( C++ 程序员提示:Python 所有方法实际上 virtual 方法。)

派生中的方法实际上可能想要扩展简单替换同名方法一种方式可以简单直接调用方法调用 BaseClassName.methodname(self, arguments)。 有时客户端有用。 (注意全局作用域 BaseClassName 名称访问使用方式。)

Python
内置函数用于继承机制

使用 isinstance() 检查实例类型: isinstance(obj, int) obj.__class__ int 派生 int True。

使用 issubclass() 检查继承关系: issubclass(bool, int) True,因为 bool int 但是,issubclass(float, int) False,因为 float 不是 int

9.5.1.
多重继承

Python
支持一种多重继承带有多个定义语句如下:

class DerivedClassName(Base1, Base2, Base3):
<
语句-1>
.
.
.
<
语句-N>

对于多数目的简单情况可以认为搜索从父继承属性操作深度优先层次结构存在重叠不会同一搜索因此如果属性 DerivedClassName 找不到 Base1 搜索然后递归 Base1 搜索如果那里找不到 Base2 搜索类推

真实情况这个复杂一些方法解析顺序动态改变支持 super() 协同调用这种方式某些其他多重继承语言称为后续方法调用继承语言中的 super 调用强大

动态调整顺序必要因为所有多重继承情况都会显示菱形关联至少上级可通过多路径底层访问)。 例如所有继承 object,因此任何多重继承情况提供以上路径可以通向 object。 为了确保不会访问一次以上动态法会一种特殊方式搜索顺序线性保留指定左至右顺序调用上级一次并且保持单调可以被子影响优先顺序)。 总而言之这些特性使得设计具有多重继承可靠扩展成为可能了解细节参阅 Python 2.3 方法解析顺序
9.6.
私有变量

那种对象内部访问私有实例变量 Python 并不存在但是大多数 Python 代码遵循这样约定带有下划线名称 (例如 _spam) 应该当作 API 公有部分 (无论函数方法或是数据成员)。 应当视为实现细节可能通知加以改变

由于存在对于私有成员有效使用场景例如避免名称定义名称冲突),因此存在机制有限支持称为 名称改写任何形式 __spam 标识至少带有前缀下划线至多后缀下划线文本替换 _classname__spam,其中 classname 除了前缀下划线当前名称这种改写考虑标识句法位置只要出现定义内部进行

参见

私有名称调整规范说明 了解相关详情特例

名称改写有助于方法破坏方法调用例如:

class Mapping:
def __init__(self, iterable):
self.items_list = []
self.__update(iterable)

def update(self, iterable):
for item in iterable:
self.items_list.append(item)

__update = update #
原始 update() 方法私有副本

class MappingSubclass(Mapping):

def update(self, keys, values):
#
update() 提供签名
#
不会破坏 __init__()
for item in zip(keys, values):
self.items_list.append(item)

上面示例即使 MappingSubclass 引入 __update 标识情况不会出错因为 Mapping 替换 _Mapping__update MappingSubclass 替换 _MappingSubclass__update。

注意改写规则设计主要为了避免意外冲突访问修改视为有的变量仍然可能特殊情况甚至有用例如调试

注意传递 exec() eval() 代码不会唤起视作当前类似 global 语句效果因此这种效果限于同时经过字节编译代码同样限制适用 getattr(), setattr() delattr(),以及对于 __dict__ 直接引用
9.7.
杂项说明

有时具有类似 Pascal "record" C "struct" 数据类型有用一些名称数据捆绑在一起实现目标理想方式使用 dataclasses:

from dataclasses import dataclass

@dataclass
class Employee:
name: str
dept: str
salary: int

>>>

john = Employee('john', 'computer lab', 1000)

john.dept
'computer lab'

john.salary
1000

期望使用特定抽象数据类型 Python 代码通常可以通过传入模拟数据类型方法作为替代例如如果基于文件对象格式化某些数据函数可以定义带有 read() readline() 方法以便字典缓冲获取数据作为参数传入

实例方法对象 具有属性: m.__self__ 就是带有 m() 方法实例对象 m.__func__ 就是方法对应 函数对象
9.8.
迭代

到目前为止可能已经注意大多数容器对象可以使用 for 语句:

for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')

这种访问风格清晰简洁方便迭代使用非常普遍使得 Python 成为统一整体幕后,for 语句容器对象调用 iter()。 函数返回定义 __next__() 方法迭代对象方法逐一访问容器中的元素元素用尽,__next__() 引发 StopIteration 异常通知终止 for 循环可以使用 next() 内置函数调用 __next__() 方法这个例子显示运作方式:
>>>

s = 'abc'

it = iter(s)

it
<str_iterator object at 0x10c90e650>

next(it)
'a'

next(it)
'b'

next(it)
'c'

next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
next(it)
StopIteration

了解迭代协议背后机制可以轻松添加迭代为了定义 __iter__() 方法用于返回带有 __next__() 方法对象如果定义 __next__(),那么 __iter__() 可以简单返回 self:

class Reverse:
"""
序列执行反向循环迭代。"""
def __init__(self, data):
self.data = data
self.index = len(data)

def __iter__(self):
return self

def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]

>>>

rev = Reverse('spam')

iter(rev)
<__main__.Reverse object at 0x00A1DB50>

for char in rev:

print(char)


m
a
p
s

9.9.
生成

生成 用于创建迭代简单强大工具它们写法类似标准函数它们返回数据使用 yield 语句每次生成上调 next() 上次离开位置恢复执行记住上次执行语句所有数据)。 显示如何非常容易创建生成示例如下:

def reverse(data):
for index in range(len(data)-1, -1, -1):
yield data[index]

>>>

for char in reverse('golf'):

print(char)


f
l
o
g

可以生成完成任何功能同样可以通用描述基于迭代完成生成写法更为紧凑因为自动创建 __iter__() __next__() 方法

另一关键特性在于局部变量执行状态每次调用之间自动保存使得函数相比使用 self.index self.data 这种实例变量方式编写更为清晰

除了自动创建方法保存程序状态生成终结它们自动引发 StopIteration。 这些特性结合在一起使得创建迭代编写常规函数一样容易
9.10.
生成表达式

某些简单生成可以简洁表达式代码语法类似列表推导外层圆括号方括号这种表达式设计用于生成立即外层函数使用情况生成表达式相比完整生成紧凑不灵相比列表推导更为节省内存

示例:
>>>

sum(i*i for i in range(10)) #
平方
285

xvec = [10, 20, 30]

yvec = [7, 5, 3]

sum(x*y for x,y in zip(xvec, yvec)) #

260

unique_words = set(word for line in page for word in line.split())

valedictorian = max((student.gpa, student.name) for student in graduates)

data = 'golf'

list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

备注
[1]

存在例外模块对象秘密只读属性名为 __dict__,返回用于实现模块命名空间字典名称 __dict__ 属性不是全局名称显然使用违反命名空间实现抽象应当用于事后调试之类情况