学习《Python编程轻松进阶》要点总结

本文记述了笔者学习《Python编程轻松进阶》过程中整理出的部分编程要点。

第 1 章 处理错误和请求帮助

1.1 如何理解 Python 错误信息

作为程序员,具备独立搜索答案的能力远比掌握任何算法或数据结构的知识重要。

回溯(Traceback)也被称为调用栈。"most recent call last" 表明将按照调用顺序列出所有函数调用。栈帧对象随着函数的调用而创建,只到函数返回被销毁。回溯显示了导致崩溃的每一个函数调用栈帧的摘要信息。只有导致异常的函数调用的所在行才会被展示在回溯信息中。回溯可以表明程序从哪一行开始出问题,但这一行并不一定是罪魁祸首。

为了帮助网上搜索精确查找:一是使用引号包裹错误信息作为关键字;二是将 python 一起作为关键词。

1.2 借助 linter 避免错误

静态代码格式分析工具(也称为 linter)是通过分析源代码来报告潜在错误的一类程序。IDE 中如果没有 linter,就试一试 pyflakes。

1.3 如何寻求编程帮助

提出一个好的编程问题,要点包括:

  • 提出具体、明确的问题;
  • 提供完整的代码(最小、完整、可复现)和详细的错误信息;
  • 说明已经尝试过的解决方案;
  • 说明操作系统和 Python 版本;

第 2 章 环境设置和命令行

环境设置是指为了编写代码而对计算机所做的设置,包括安装必要的工具并修改它们的设置,以及解决这个过程中遇到的任何问题。

2.1 文件系统

为了使 Python 脚本跨平台兼容,可以使用 pathlib 模块和 / 运算符。导入 pathlib 的典型方法是 from pathlib import Path。主目录 Path.home();当前目录 Path.cwd() 或 os.getcwd();更改目录 os.chdir('...')。注意区分程序中使用的绝对路径和相对路径。

2.2 程序和进程

进程是程序的运行实例。进程间相互独立,同时运行一个 Python 程序产生的多个实例,每个进程中的变量值都可能不同,它们有各自的栈、堆、环境变量等。

2.3 命令行

命令行界面(CLI)能完成比图形用户界面(GUI)更详尽的设置工作。

想要查看单个 Python 指令的结果又不想进入交互式 shell 时,使用 python -c 很方便。

Python 的 subprocess 模块的 run(...) 函数可在 Python 程序内部启动子进程,例如运行 shell 命令。

2.4 环境变量和 PATH

程序在运行时都有一组称为环境变量的变量,每个变量存储着一个字符串。环境变量通常保存的是所有程序都可能使用的系统设置。在进程内创建的子进程会收到父进程的环境变量副本。子进程可以修改它,但不会影响父进程的环境变量,反之亦然。

在当前终端窗口中修改的环境变量,作用于从该终端窗口之后打开的程序。其他的终端窗口不会体现出改动。如果想永久生效,就必须保存到启动文件(如 /.bashrc,/.bash_profile)中。

2.5 不借助命令行运行 Python 程序

(无)

第 3 章 使用 Black 进行代码格式化

代码格式化是指将源代码按照一组规则进行约束,使其具备某种外观。代码格式的可读性对于维护代码至关重要。

3.1 让程序员招人烦的错误做法

编程不是一件容易的事儿,为别人编写容易理解的代码是件功德事。

3.2 风格指南和 PEP 8

Python 改进提案(Python Enhancement Proposal 8)是由 Python 核心开发团队编写的一种风格指南。Black 风格指南改编自 PEP8 风格指南。

3.3 水平间距

  • 使用空格而非 Tab 进行缩进。每一级缩进使用 4 个空格;
  • 在运算符和标识符之间放置空格;
  • 在分隔符后面而非前面添加空格;
  • 不要在句点的前后添加空格;
  • 不要在函数、方法或者容器名称后添加空格;
  • 不要在左括号后或右括号前添加空格;
  • 在行末注释前添加两个空格,最好不把注释放在代码行末;

3.4 垂直间距

  • 不要在在同一行内用分号放置多条语句;
  • 为每个模块单独编写一行导入语句;按照 Python 标准库,第三方,本地程序的顺序导入模块;

3.5 Black:毫不妥协的代码格式化工具

(略)

第 4 章 选择易懂的名称

简洁而有描述意义的名称能够大大提升程序代码的可读性。

4.1 命名风格

  • 蛇形命名法(snake_case):用下划线分割单词,所有字母都小写,常量名称采用大写;
  • 驼峰命名法(camelCase):从第二个单词开始,每个单词使用首字母大写分隔;
  • Pascal 命名法(PascalCase):第一个字母大写,其后遵循驼峰命名法;

4.2 PEP8 的命名风格

  • 所有字母应是 ASCII 字母;
  • 模块名应该简短,都是小写字母;
  • 类名采用 Pascal 命名法;
  • 常量名使用大写的蛇形命名法;
  • 函数名、方法名和变量名使用小写字母的蛇形命名法;
  • 方法的第一个参数总是小写的 self;
  • 类方法的第一个参数总是小写的 cls;
  • 类中的私有属性总是以下划线开头;
  • 类中的公共属性不要以下划线开头;

4.3 适当的名称长度

因为代码被阅读的次数比被编写的次数要多,所以相比之下,宁愿名称偏长也不要偏短。用到的名称应该在当前上下文中既简短,又有明确含义的。

4.4 起易于搜索的名称

使用编辑器的查找功能是否能快速定位到变量,这是一个简便的判定标准。

4.5 避免笑话、双关语和需要文化背景才能理解的词汇

(无)

4.6 不要覆盖内置名称

(无)

4.7 史上最差的变量名

temp、tmp、data、var

第 5 章 揪出代码的坏味道

代码的坏味道指的是一种揭示潜在问题的代码模式。这种坏味道并不意味着一定存在问题,但它说明该是检查程序的时候了。

5.1 重复的代码

解决重复代码的方法就是去重。简单地说,通过把代码放在一个函数或循环中,使其在代码中只出现一次。

5.2 魔数

泛指在程序中硬编码的内容。更好的做法是使用常量代替这些神奇的内容。

5.3 注释掉的代码和死代码

注释掉的代码不能运行,作为临时手段是可行的。死代码是指无法到达或者逻辑上永远无法运行的代码。需要删除它们,并使用版本控制系统来跟踪变化。

5.4 打印调试

打印调试是指在程序中临时调用 print() 显示变量的值,然后重新运行程序的做法。在导入日志模块并设置基本配置后,就可以将变量信息写入文本文件。

5.5 带有数字后缀的变量

如果一系列变量中使用数字后缀,那么可以考虑用某种数据结构代替它们。

5.6 本该是函数或者模块的类

如果发现创建对象只是为了进行单一的函数调用,或者类中只包含静态方法,那么这些警示我们最好还是编写函数。

5.7 嵌套列表表达式

解析式可以产生简洁的代码,但不要进行多层嵌套。

5.8 空的 except 块和糟糕的错误信息

提示的错误信息是给用户看的,而不是给程序员看的。这些信息应该解释发生了什么以及用户应该怎么做。

5.9 代码坏味道的谬误

  • 谬误:函数应该仅在末尾处有一个 return 语句;
  • 谬误:函数最多只能有一个 try 语句;
  • 谬误:使用 flag 参数不好;
  • 谬误:全局变量不好;
  • 谬误:注释是不必要的;

第 6 章 编写 Python 风格的代码

需要懂的不仅仅是语法和标准库,还要学习它的习惯用法。

6.1 Python 之禅

>>> import this

  • 美丽胜于丑陋;
  • 明确胜于隐含;
  • 简单胜于复杂,复杂胜于更复杂;
  • 扁平胜于嵌套;
  • 稀疏胜于密集;
  • 可读性很重要;
  • 特殊情况并没有特殊到打破规则的地步;
  • 除非必要,否则错误不该被悄无声息地忽略;
  • 面对模棱两可的问题,不要猜测;
  • 应该有一个,最好只有一个明显的方法能使用;
  • 除非你是荷兰人,否则这种方法可能并不那么显而易见;
  • 有总比没有好,然而不经思考就做还不如不做;
  • 如果实现很难解释,它就是个坏主意;如果实现很容易解释,这可能是个好主意;
  • 命名空间是个很棒的主意,可以多用;

6.2 学着喜欢强制缩进

在 Python 中,代码开头的缩进数量是有意义的,它决定了哪几行代码属于同一个代码块。它还强制代码具有前后一致的可读性。

6.3 使用 timeit 模块衡量性能

Python 标准库的 timeit 模块可以对运行速度进行分析。将代码以字符串的形式传递给 timeit.timeit() 函数,它将告诉你这行代码默认运行 100 万次所需时间的平均值。

编写代码的一个有效准则:先让它可用,再让它更快。

6.4 常被误用的语法

  • 循环处理列表或其他序列时使用 range() 而不是 enumerate();
  • 操作文件时使用 open()/close() 而不是 with;
  • 将某个值与 None 比较时使用 ==,而不是 is None。

6.5 格式化字符串

如果字符串有很多反斜杠,请使用原始字符串。格式化字符串的方法:'+' 运算符;'%s' 转换格式符;字符串的 format() 方法;(Python 3.6 起)使用 f-string 创建嵌套其他字符串的字符串。

6.6 制作列表的浅副本

使用切片语法

shallow_copy = origin[:]

或者使用 copy 模块的 copy 函数

shallow_copy = copy.copy(origin)

6.7 以 Python 风格使用字典

  • 使用 get() 方法,允许当键不存在时返回指定的默认值;
  • 使用 setdefault() 方法,为某个键设置默认值;
  • 使用 collections.defaultdict() 方法,对任意一个件设置默认值;
  • 使用字典代替类 switch 语句;

6.8 条件表达式:Python “丑陋”的三元运算符

valueIfTrue if (condition) else (ValueIfFalse)

以上的条件表达式不要嵌套使用。

6.9 处理变量的值

  • 使用链式比较运算符(<, <=, ==, >=, >)代替 if 语句中的 and 布尔运算符;
  • 赋值运算符(=)同样可以用于链式操作;
  • 使用 in 运算符检查变量的值是否是多个可能值中的一个;

第 7 章 编程术语

7.1 定义

Python 解释器——它是真实存在的软件,用来读取 .py 文件的文本并执行指令。绝大多数情况下指的是用 C 语言编写的 CPython 解释器。如果其他解释器在解释 Python 代码时跟 CPython 存在差异,那么一般认为 CPython 的行为是合规和正确的。

垃圾回收是一种内存自动管理方式,它持续跟踪和判断内存应该何时分配和释放。垃圾回收的作用是释放内存,让新数据使用。

字面量是源代码中显式存在的,表示一个固定值的文本。

关键字是一组被语言本身使用的保留字。,它们不能作为变量名(也就是标识符)使用。

对象是一条数据的呈现。每个对象都有自己的值、身份和数据类型。值是对象所代表的数据。身份是一个唯一的整数,可以通过调用 id() 函数查看。数据类型和对象的身份不会改变,但对象的值可以改变。标识符跟身份并不是强绑定的,因为多个标识符可以用来指代同一个对象。把变量想象成内存中对象的标签更易于理解。

如果对象的值可以改变,它就是可变对象;如果对象的值不能改变,它就是不可变对象。

哈希值是一个整数,它就像是某个值的“指纹”。一个对象的哈希值在这个对象的生命周期内永远不会改变,且具有相同值的对象必须有相同的哈希值。不可变对象都是可哈希的,可变对象是不可哈希的。两个具有相同值的不同对象,身份是不同的,但它们的哈希值是相同的。

序列是容器数据类型的对象,它的元素有序且可以通过整数索引访问。映射也是容器数据类型的对象,它使用键而非索引访问。映射既可以有序也可以无序。

魔术方法是指 Python 中名称以两个下划线开头和结尾的特殊方法。

模块是可以被其他 Python 程序导入,将代码提供给导入者使用的程序。包是模块的集合,通过在文件夹中放置一个名为 __init__.py 的文件,就可以形成一个包。这个文件夹的名称就是包的名称。包可以包含多个模块或其他包。

可调用对象是指实现了可调用运算符的对象。函数是 Python 中的头等对象。这意味着你可以把它们存储在变量中,在函数调用中作为参数传递,作为函数调用的返回值。

7.2 经常被混淆的术语

表达式是由运算符和值组成的指令,可以推导出单一的值。这个值可以是变量(包括值)或者函数调用(返回值)。语句实际上是 Python 中的特有指令。

块以缩进开始,当缩进回退时结束。Python 官方文档使用“块”来指以单元形式执行的一段代码,而在语句中通常被称为“子句”。

变量是指向向对象的一个名字。特性则是指“.(点)后面的任何名字”,包括成员变量和方法。

函数是在调用时被执行的代码的集合。方法是与类相关联的函数。

当 for 循环运行时,可迭代对象被传递给内置的 iter() 函数,作为迭代器对象被返回。迭代器对象会一直跟踪循环中使用的可迭代对象的下一项。在循环的每一次迭代中,将迭代器对象传递给内置的 next() 函数皆可以返回可迭代对象的下一项。一个迭代器只能在一个可迭代对象上迭代一次。当使用 class 语句创建自定义的数据类型时,只要实现 __iter()__ 和 __next()__ 这两个特殊方法,就能使用 for 循环遍历对象。

语法错误也被称为解析错误,当 Python 指令不能将源代码的文本解析成有效的指令时,就会发生语法错误。运行时错误是指运行中的程序不能执行某些任务。运行时错误如果没有被处理,程序就会崩溃并显示回溯信息。可以使用 try-except 语句捕获运行时错误并进行错误处理。语义错误也称为逻辑错误,它并不会显示错误信息或者导致崩溃,但计算机执行指令的方式会跟程序员预想的不同。

形参是 def 语句中括号内的变量名称,实参是函数调用中实际传递的值。

机器码由 CPU 指令集(机器内置的指令集合)的指令组成,由 CPU 直接执行。字节码由解释器的指令组成,由解释器执行,而非直接由 CPU 执行。CPython 的字节码是用 C 语言编写的,它将 Python 源代码编译成 Python 字节码,然后执行这些指令。Python 的字节码与 .py 文件存放在同一个文件夹中,文件扩展名为 .pyc。

第 8 章 常见的 Python 陷阱

8.1 循环列表的同时不要增删其中的元素

正确的做法是复制不想删除的项到一个新的列表,再用它替换原来的列表。修改列表中的元素是可以的,修改列表中的元素个数才容易导致问题。

8.2 复制可变值时务必使用 copy.copy() 和 copy.deepcopy()

使用 copy.copy() 复制可变量时,是仅把其中的值 copy 过去。使用 copy.deepcopy() 复制可变量时,如果这个值是一个引用,其引用的对象也会被复制。

8.3 不要使用可变值作为默认参数

默认参数是在函数的 def 语句执行是被创建的,而不是在每次函数调用是被创建。这意味着如果默认参数是对象,那么它仅有一份,函数每次被调用时该对象就会被用到。如果将可变值作为默认参数,Pythoon 的解决方法是将默认参数设置为 None,然后通过代码检查参数是否为 None,如果是,则在函数每次调用时提供一个新的可变参数对象。

8.4 不要通过字符串连接符创建字符串

Python 风格的字符串连接方法是将较小的字符串添加到一个列表中,然后调用列表的 join() 方法将它们连接成一个大字符串。

8.5 不要指望 sort() 按照字母顺序排序

sort() 方法采用 ASCII-betical 排序方法(意为按照字母的 ASCII 序号排序)。

8.6 不要假设浮点数是完全准确的

计算机上的浮点数运算遵循 IEEE 754 标准,而该标准会导致计算机表示和处理浮点数的方式存在舍入错误,所以浮点数的直接运算将与正确结果间会产生细微差异。通常可以用更小单位的整数来代替浮点数。如果需要浮点数计算,可以使用 Python 内置的 decimal 模块。正确做法是将标识符点数的字符串传递给 decimal.Decimal(),然后进行运算。因为整数没有舍入错误,所以把它们直接传递给 decimal.Decimal() 是安全的。默认情况下,decimal.Decimal() 将精确到 28 位,可通过 decimal.getcontext().prec 特性修改。

8.7 不要使用链式 != 运算符

a != b != c 相当于 (a != b) and (b != c)。

8.8 不要忘记在仅有一项的元组中添加逗号

因为表达式添加了括号等同于它自身,所以要表示元组,必须再反括号前加上一个拖尾逗号。

第 9 章 Python 的奇特难懂之处

9.1 为什么 256 是 256,而 257 不是 257

CPython 在程序运行前,提前创建 -5 到 256 的整数对象,它们被称为预分配的整数。在内存中创建一个新的整数对象时,CPython 首先会检查它是否在 -5 和 256 之间。如果是,CPython 就会直接返回现有的整数对象。CPython 对于同一条语句中相同的字面量会重用同一个整数对象。

9.2 字符串驻留

在遇到相同的字符串字面量时,Python 会重用同一个对象而非创建相同字符串的不同副本。但这种优化不会捕捉到所有相同的字符串。CPython 对于各语句中相同的字符串字面量(包括在一条语句内使用 ‘+’ 拼接而成的)会重用同一个对象。

9.3 假的 Python 增量运算符和减量运算符

'++' 和 '--' 运算符实际上不会增加或减少变量的值,对于整数而言这就相当于它本身。另外,一元运算符 '+' 和 '-' 只有放在 Python 的变量前面才有效。

9.4 传递空列表给 all()

内置函数 all() 接受一个序列值。如果该序列中的所有值都是真值,则返回 True,如果其中存在假值,则返回 False。等效于序列中的所有元素做 and 运算。最好把 all([]) 看作在计算”这个列表中不存在假值“,而不是”这个列表中都是真值“。

9.5 布尔值是整数值

布尔值 True 和 False 分别等于 1 和 0。在 Python 中,bool 数据类型是 int 数据类型的一个子类。

9.6 链式使用多种运算符

在一个表达式中链式使用不同种类的运算符,实际上相当于相邻的两个对象之间做运算后的结果之间做 and 运算。

9.7 Python 的反重力特性

>>> import antigravity

会使用内置的 webbrowser 模块,打开网页浏览器,让你看到有关 Python 的经典 XKCD 漫画。类似的还有 Python 之禅。

第 10 章 编写高效的函数

10.1 函数名

函数名应该遵循一般标识符所遵循的管理,通常包括一个动词和一个名词,用于体现操作某种事物。在类和模块中的方法而言,名词不是必须的,但要保证动词能体现其目的,具有明确的描述性。

10.2 函数大小的权衡

函数最好少于 30 行,之多不超过 200 行,应该让函数在合理范围内尽可能短,但不要过分短。(笔者补充:拆分出来的小函数要保证被多次调用,体现其被复用的价值)

10.3 函数的形参和实参

一个合适的函数参数个数准则是保持 0 ~ 3 各参数,参过 6 个可能就偏多了。为函数提供默认参数是降低参数复杂性的一个办法。

* 语法允许你将一个可迭代对象中的项作为参数逐个传入,** 语法允许你将映射对象中的键-值对作为参数逐个传入。在运行中修改列表和字典,就可以为含有 * 和 ** 语法的函数或方法提供可变数量的参数。

在 def 语句中使用 * 语法可以创建可变参数函数或方法,它可以接受不定数量的位置参数。在函数或方法内部,位置形参是一个包含所有位置参数的普通 Python 元组。可以叫任何名字,只要以 * 开头即可,但惯例是将其命名为 args。

如何设计参数取决于我们预测程序员会如何使用我们的代码。如果函数通常接受的是程序运行时创建的数据结构,那么最好设计成接受单个参数。如果通常接受的是程序员在编写代码时指定的参数,那么最好使用 * 语法接受不定数量的参数。

** 语法也可以创建可变参数函数或方法,它表示不定数量的关键字参数。在函数或方法内部,关键字形参是一个包含所有关键字参数的字典。可以叫任何名字,只要以 ** 开头即可,但惯例是将其命名为 kwargs。

* 和 ** 语法的一个常见用途是创建包装函数。包装函数用来将接受的参数做预处理后传递给另一个函数,并或者对被包装函数的返回结果做后处理。

10.4 函数式编程

副作用是指函数对自身代码和局部变量之外的其他部分所做的任何改变。确定性函数是指在给定相同参数的情况下总是返回相同值的函数。非确定性函数则在传递相同参数时不会总是返回相同的值。确定性函数的一个好处是它们的值可以被缓存。无副作用的确定性函数被称为纯函数。函数式程序员尽量在程序中只编写纯函数。编写纯函数的最常见的方法是避免在函数内部使用全局变量,并确保不与文件、互联网、系统时钟、随机数或其他外部资源交互。

高阶函数可以接受函数作为参数或者使用函数作为返回值。lambda 函数也被称为匿名函数或者无名函数,其代码通常仅包含一条返回语句。在将函数作为参数传递给其他函数时,经常会用到 lambda 函数。映射(map)可以根据原列表的值创建新列表。过滤(filter)则可以创建一个只包含原列表中符合某种标准的值的新列表。使用 map() 和 filter() 函数来创建映射列表和过滤列表已经是 Python 的过时做法了。更好的方法是使用列表推导式创建。

10.5 返回值的数据类型应该不变

为了让函数具备更好的可预测性,应该尽量仅返回单一数据类型的值。Python 中函数可以返回不同数据类型的值,但返回的数据类型越少,函数就越简单,也越不容易出错。

10.6 抛出异常和返回错误码

错误码是从函数中返回的值,说明代码有问题。使用”异常“一词的好处是,函数返回值只与函数的目的有关,而不必同时用来表明存在错误。当异常表示一个实际错误时,异常类的名称往往以 Error 结尾。

第 11 章 注释、文档字符串和类型提示

11.1 注释

以 # 开始,直到行尾的所有文本都是单行注释。使用三引号的多行字符串可以用于多行注释。多行的注释可以连续使用多个单行注释。单行注释应该在 # 符号后保留一个空格。注释的缩进水平应该跟被注释的代码一致。跟在代码行内的注释被称为”内联注释“,代码和注释之间应该至少保留两个空格。内联注释的一个常见且适宜的用途是解释变量的作用,或为其提供其他背景信息。

注释的内容应该是大小写正确且带有标点符号的完整句子。应该解释为什么代码要这样写,这代表了代码背后的意图。在工程代码甚至个人项目的代码的注释要有一定水准的专业性,用礼貌、直接、严肃的语气编写注释。

可以在注释中总是大写某个标签和问题描述,方便日后查找。

11.2 文档字符串

出现在模块的 .py 文件顶部或者在类或 def 语句之后的多行注释,提供关于被定义的模块、类、函数或方法的文档。必须使用三引号的多行注释,且始终使用双引号。尽管模块的文档字符串应该是模块中出现的第一个字符串,但还是应该在 #!... 注释之后。文档字符串反映在模块/类/函数名.__doc__ 属性中。

一般而言,文档字符串应该包含一个用于概述模块、类或者函数的行,后面有一个空行,空行后再提供更详细的信息。

11.3 类型提示

Python 是动态类型,变量、参数和返回值可以是任意数据类型,甚至可以在程序运行时改变数据类型。

Python 解释器不会因为有类型提示而做任何运行时的类型检查,类型提示只是提供给静态类型检查工具使用的。通过类型提示,静态代码工具就能有更多的信息发现程序中的潜在错误。类型提示适用于指定类型的所有子类。为了更好地利用类型检查器,需要配置 IDE 或 文本编辑器,使类型检查器在后台运行。

虽然可以在行尾添加 # type: ignore 来禁用类型提示的警告,但当遇到警告时,绝大多数情况下应该重写代码以避免警告。

从内置的 typing 模块导入 Union,以指定多类型的类型提示。若需要在普通类型之外还包括 NoneType,需要在 Union 中添加 None,或者把 Union 换为 typing.Optional。用 typing.Any 类型提示或 object 来指定任意的数据类型。

为了具体指定容器类型中值的数据类型,必须使用 typing 模块的类型别名。

第 12 章 通过 Git 管理项目

(略)

第 13 章 性能测试和大 O 算法分析

(略)

第 14 章 项目实战

(略)

第 15 章 面向对象编程和类

将变量和函数组合成的新的数据类型称之为类。基于类可以创建对象。通过将代码组织成类可以将一个庞大的程序分解成更小的部分。Python 的 OOP 特性是可选的。

15.1 拿现实世界打比方:填写表格

类是事物的抽象,对象则是事物本身。类中包含的信息应该取决于程序的需要。

15.2 基于类创建对象

15.3 创建一个简单的类--WizCoin

按照惯例,模块名是小写的,而类名的首字母大写。

方法是与某个类的对象相关联的函数。__init__() 方法并非类的必要条件。它通常被用来设置特性的初始值。方法的第一个参数并不是必须命名为 self,但使用 self 是惯例。int()、str()、float() 和 bool() 其实都是构造函数,可以分别返回新的整数、字符串、浮点值和布尔对象。

特性是与对象相关联的变量。所有特性和方法都可以被公开访问,类外的代码可以访问和修改类的任何对象的任何特性。Python 的惯例是为私有特性或方法的名字使用下划线前缀,但从技术上说,这并不能阻止类外的代码访问私有特性和方法。

15.4 函数 type() 和特性 __qualname__

从 type() 函数返回的对象是类型对象。所有类型对象都有 __qualname__ 特性,它是对象类型的字符串。

15.5 非 OOP 和 OOP 的例子:井字棋

(无)

15.6 为现实世界设计类是一件难事

(无)

第 16 章 面向对象编程和继承

继承是一种用于类的代码重用技术,它将两个类绑定为父子关系,其中子类继承了父类方法的副本。

16.1 继承的原理

一个类总是知道它的父类,但不知道它的子类。父类和子类表现了“是一个”的关系。继承的主要目的是重用代码,如果程序的某个类的方法是其它类的方法的超集,就可以使用继承以避免复制粘贴代码。

子类重写的方法与父类的方法具有相同的名称。不使用继承,就只能在方法中到处使用 if-else 语句,使代码的复杂性急剧上升。

重写方法时,可能会将父类方法的部分代码粘贴到子类方法中。为了防止这种重复的代码,可以通过内置的 super() 函数在重写的方法中调用父类中的原始方法。

组合是设计类的一种技巧,它在类中包含对象,而不是继承这些对象的类。

继承的主要缺点是对父类所做的任何改变都会被子类所继承。

16.2 函数 isinstance() 和 issubclass()

如果要对对象进行类型检查,可使用更灵活的 isinstance() 内置函数。

issubclass() 可以识别作为第一个参数的类是否是第二个参数的类对象的子类。

16.3 类方法

类方法是与一个类相绑定的,而非像普通方法那样与单个对象绑定。标记类方法:def 语句前的 @classmethod 装饰符和使用 cls 作为第一个参数。

类方法中的代码不能访问某个具体对象的特性或者调用具体对象的常规方法。类方法只能调用其它类方法或者访问类的特性,也可以通过该类的任何对象调用。

子类可以继承父类的类方法,如果需要的话还可以重写。cls 应该被视为“当前的类对象”。

16.4 类特性

类特性是从属于类而非对象的变量,它的创建位置是在类的声明语句之中、所有方法之外。

16.5 静态方法

静态方法本质上只是一个普通函数,因为它们不能访问类或对象的特性和方法。标记静态方法:def 语句前的 @staticmethod 装饰符

16.6 何时应该使用类和静态的面向对象特征

(无)

16.7 面向对象的行话

封装的第一个定义是:封装是将相关的数据和代码捆绑成一个单元;第二个定义是:封装是一种隐藏信息的技术,对象借助它隐藏其复杂工作原理的实现细节。

多态性允许将一种类型的对象当作另一种类型的对象来对待。多态性有时侯也指能处理特定的多种类型或运算符重载,也就是运算符可以根据它们所运算的对象类型的不同而有不同行为。

16.8 何时不应该使用继承

有时没有必要创建类或使用继承,例如:

  • 如果类的所有方法从未使用过 self 参数或 cls 参数,那么可以删除这个类,用函数代替这些方法。
  • 如果创建了一个只有单个子类的父类,且从未创建这个父类的对象,那么可以将父类和子类组合成一个类。
  • 如果创建了三四层以上的继承体系,那么很可能是在滥用继承。请做整合,减少子类的数量。

16.9 多重继承

多重继承支持多个父类。

16.10 方法解析顺序

方法解析顺序(method resolution order,MRO)是 Python 在继承方法或者在方法中调用 super() 函数时查找类的有序列表。调用子类的 mro() 方法以查看 MRO。

Python 内置的 super() 函数名称中的 “super” 实际上有误导性。它并不总是指向父类,而是 MRO 中的下一个类。

第 17 章 Python 风格的面向对象编程:属性和魔术方法

17.1 属性

属性是指定了 getter、setter 和 deleter 方法的特性,这些方法可以控制特性如何被读取、更新和删除。

将 someAttribute 特性重写为属性:

  1. 使用下划线前缀重命名特性 _someAttribute;
  2. 创建一个带有 @property 装饰符的 someAttribute 方法。这个 getter 方法和所有方法一样都有 self 参数;
  3. 创建一个名为 someAttribute 且带有 @property.setter 装饰符的方法。这个方法的参数为 self 和 value;
  4. 创建一个名为 someAttribute 且带有 @property.deleter 装饰符的方法。这个 getter 方法和所有方法一样都有 self 参数;

在这种情况下,_someAttribute 特性被称为幕后字段或者幕后变量,是属性所基于的特性。Python 会在下列情况下自动调用这些方法:

  • 当 Python 访问属性时,它会在幕后调用 getter 方法并使用它的返回值;
  • 当 Python 运行赋值语句为属性赋值时,它会在幕后调用 setter 方法,传递新值作为 setter 方法的 value 参数;
  • 当 Python 运行 del 语句删除属性时,它会在幕后调用 deleter 方法。

如果属性的 getter、setter 和 deleter 方法内的代码还是访问该属性,就会导致方法再次调用其本身,循环往复产生递归调用直至程序崩溃。

尽管定义了属性,但同样还是可以直接操作带下划线前缀的幕后变量。

所有 Python 对象都会自动有一个内置的 __class__ 属性,指向该对象的类对象。换言之,obj.__class__ 返回和 type(obj) 相同的类对象。该类对象有一个 __qualname__ 特性,是表示类名称的字符串。

如果不定义属性的 setter 和 deleter 方法,该属性就是一个只读属性。

可以先使用特性,在后续有必要时将其转换为属性,而不必破坏类外的任何代码。在使用特性的名字创建属性时,只需要使用下划线来重命名特性。

17.2 Python 的魔术方法

以双下划线开头和结尾的特殊方法被称为魔术方法(或称为 dunder 方法或特殊方法)。这些魔术方法常被用于运算符重载,是类的对象与 Python 运算符、内置函数结合进行运算。魔术方法在使用运算符或内置函数操作对象时,在幕后被调用。

repr 字符串是一串 Python 代码,会在运行时创建对象的副本,常用于技术环境中,如错误信息或日志文件;str 字符串是人类可读的字符串,它提供了关于对象的清晰、有用的信息,常用于用户显示。Python 通过 __repr__() 和 __str__() 创建对象的 repr 和 str 字符串表示。__repr__() 和 __str__() 方法指定了当对象被传递给内置函数 repr() 和 str() 时返回的 repr 和 str 字符串。如果 repr() 和 str() 不知道如何为对象创建 repr 和 str 字符串,会显示一怪兽尖括号包裹、包含对象内存地址和类名的字符串。包含在 f 字符串大括号中的对象将会隐式调用 str() 疑惑的对象的 str 字符串。

强烈建议在自己编写的类中实现 __repr__() 和 __str__() 方法。

数值魔术方法或数学魔术方法重载了 Python 的数学运算符。如果错误的对象类型被传递给了魔术方法,则魔术方法不应该抛出异常,而应该返回内置的 NotImplemented。

当对象在数学运算符的左边时,Python 会调用数值魔术方法。而当对象在数学运算符的右边时,Python 会调用反射数值魔术方法(或称反向魔术方法、右手魔术方法)。任何符合交换律的运算都可以在调用反射数值魔术方法时直接调用原始的数值魔术方法。

不应该在数值魔术方法或反射数值魔术方法中改变对象本身,而应该创建并返回一个新对象。只要对象是可变的,由增强赋值运算符调用的原地魔术方法(或称原地赋值方法)在原地改变对象本身。例外是在不可变对象的情况下,原地魔术方法应该创建一个新的对象并返回。如果没有实现原地魔术方法,Python 将调用数值魔术方法。

当对象被放在带有比较运算符的表达式中时,Python 会在幕后调用比较魔术方法。由于比较关系的对称性,不需要实现反射比较魔术方法。

(完)

posted @ 2026-10-14 16:19  green-cnblogs  阅读(5)  评论(0)    收藏  举报