搞懂这27个问题,让你秒变 Python 高手

📅 2026/6/30 15:29:34
搞懂这27个问题,让你秒变 Python 高手
01. 为什么使用缩进来分组语句Guido van Rossum 认为使用缩进进行分组非常优雅并且大大提高了普通 Python 程序的清晰度。大多数人在一段时间后就学会并喜欢上这个功能。由于没有开始/结束括号因此解析器感知的分组与人类读者之间不会存在分歧。偶尔 C 程序员会遇到像这样的代码片段:if (x y)x;y--;z;如果条件为真则只执行 x 语句但缩进会使你认为情况并非如此。即使是经验丰富的 C 程序员有时会长时间盯着它想知道为什么即使 x y y 也在减少。因为没有开始/结束括号所以 Python 不太容易发生编码式冲突。在 C 中括号可以放到许多不同的位置。如果您习惯于阅读和编写使用一种风格的代码那么在阅读或被要求编写另一种风格时您至少会感到有些不安。许多编码风格将开始/结束括号单独放在一行上。这使得程序相当长浪费了宝贵的屏幕空间使得更难以对程序进行全面的了解。理想情况下函数应该适合一个屏幕例如20--30 行。20 行 Python 可以完成比 20 行 C 更多的工作。这不仅仅是由于缺少开始/结束括号 -- 缺少声明和高级数据类型也是其中的原因 -- 但缩进基于语法肯定有帮助。02. 为什么简单的算术运算得到奇怪的结果请看下一个问题。03. 为什么浮点计算不准确用户经常对这样的结果感到惊讶: 1.2 - 1.00.19999999999999996并且认为这是 Python 中的一个 bug。其实不是这样。这与 Python 关系不大而与底层平台如何处理浮点数字关系更大。CPython 中的 float 类型使用 C 语言的 double 类型进行存储。float 对象的值是以固定的精度通常为 53 位存储的二进制浮点数由于 Python 使用 C 操作而后者依赖于处理器中的硬件实现来执行浮点运算。这意味着就浮点运算而言Python 的行为类似于许多流行的语言包括 C 和 Java。许多可以轻松地用十进制表示的数字不能用二进制浮点表示。例如在输入以下语句后: x 1.2为 x 存储的值是与十进制的值 1.2 (非常接近) 的近似值但不完全等于它。在典型的机器上实际存储的值是1.0011001100110011001100110011001100110011001100110011(binary)它对应于十进制数值:1.1999999999999999555910790149937383830547332763671875(decimal)典型的 53 位精度为 Python 浮点数提供了 15-16 位小数的精度。要获得更完整的解释请参阅 Python 教程中的 浮点算术 一章。04. 为什么 Python 字符串是不可变的有几个优点。一个是性能知道字符串是不可变的意味着我们可以在创建时为它分配空间并且存储需求是固定不变的。这也是元组和列表之间区别的原因之一。另一个优点是Python 中的字符串被视为与数字一样“基本”。任何动作都不会将值 8 更改为其他值在 Python 中任何动作都不会将字符串 8 更改为其他值。05. 为什么必须在方法定义和调用中显式使用“self”这个想法借鉴了 Modula-3 语言。出于多种原因它被证明是非常有用的。首先更明显的显示出使用的是方法或实例属性而不是局部变量。阅读 self.x 或 self.meth() 可以清楚地表明即使您不知道类的定义也会使用实例变量或方法。在 C 中可以通过缺少局部变量声明来判断假设全局变量很少见或容易识别 —— 但是在 Python 中没有局部变量声明所以必须查找类定义才能确定。一些 C 和 Java 编码标准要求实例属性具有 m_ 前缀因此这种显式性在这些语言中仍然有用。其次这意味着如果要显式引用或从特定类调用该方法不需要特殊语法。在 C 中如果你想使用在派生类中重写基类中的方法你必须使用 :: 运算符 -- 在 Python 中你可以编写 baseclass.methodname(self,)。这对于 init() 方法非常有用特别是在派生类方法想要扩展同名的基类方法而必须以某种方式调用基类方法时。最后它解决了变量赋值的语法问题为了 Python 中的局部变量根据定义在函数体中赋值的那些变量并且没有明确声明为全局赋值就必须以某种方式告诉解释器一个赋值是为了分配一个实例变量而不是一个局部变量它最好是通过语法实现的出于效率原因。C 通过声明来做到这一点但是 Python 没有声明仅仅为了这个目的而引入它们会很可惜。使用显式的 self.var 很好地解决了这个问题。类似地对于使用实例变量必须编写 self.var 意味着对方法内部的非限定名称的引用不必搜索实例的目录。换句话说局部变量和实例变量存在于两个不同的命名空间中您需要告诉 Python 使用哪个命名空间。06. 为什么不能在表达式中赋值许多习惯于 C 或 Perl 的人抱怨他们想要使用 C 的这个特性while (line readline(f)) {// do something with line}但在 Python 中被强制写成这样:while True:line f.readline()if not line:break... # do something with line不允许在 Python 表达式中赋值的原因是这些其他语言中常见的、很难发现的错误是由这个结构引起的if (x 0) {// error handling}else {// code that only works for nonzero x}错误是一个简单的错字x 0 将 0 赋给变量 x 而比较 x 0 肯定是可以预期的。已经有许多替代方案提案。大多数是为了少打一些字的黑客方案但使用任意或隐含的语法或关键词并不符合语言变更提案的简单标准它应该直观地向尚未被介绍到这一概念的人类读者提供正确的含义。一个有趣的现象是大多数有经验的 Python 程序员都认识到 while True 的习惯用法也不太在意是否能在表达式构造中赋值; 只有新人表达了强烈的愿望希望将其添加到语言中。有一种替代的拼写方式看起来很有吸引力但通常不如while True解决方案可靠line f.readline()while line:... # do something with line...line f.readline()问题在于如果你改变主意例如你想把它改成 sys.stdin.readline() 如何知道下一行。你必须记住改变程序中的两个地方 -- 第二次出现隐藏在循环的底部。最好的方法是使用迭代器这样能通过 for 语句来循环遍历对象。例如 file objects 支持迭代器协议因此可以简单地写成:for line in f:... # do something with line...07 为什么 Python 对某些功能例如 list.index()使用方法来实现而其他功能例如 len(List)使用函数实现正如 Guido 所说(a) 对于某些操作前缀表示法比后缀更容易阅读 -- 前缀和中缀运算在数学中有着悠久的传统就像在视觉上帮助数学家思考问题的记法。比较一下我们将 x*(ab) 这样的公式改写为 x_ax_b 的容易程度以及使用原始 OO 符号做相同事情的笨拙程度。”(b) 当读到写有 len(X)的代码时就知道它要求的是某件东西的长度。这告诉我们两件事结果是一个整数参数是某种容器。相反当阅读 x.len()时必须已经知道 x 是某种实现接口的容器或者是从具有标准 len()的类继承的容器。当没有实现映射的类有 get()或 key()方法或者不是文件的类有 write()方法时我们偶尔会感到困惑。—https://mail.python.org/pipermail/python-3000/2006-November/004643.html”08. 为什么 join()是一个字符串方法而不是列表或元组方法从 Python 1.6 开始字符串变得更像其他标准类型当添加方法时这些方法提供的功能与始终使用 String 模块的函数时提供的功能相同。这些新方法中的大多数已被广泛接受但似乎让一些程序员感到不舒服的一种方法是, .join([1, 2, 4, 8, 16])结果如下:1, 2, 4, 8, 16反对这种用法有两个常见的论点。第一条是这样的“使用字符串文本(String Constant)的方法看起来真的很难看”答案是也许吧但是字符串文本只是一个固定值。如果在绑定到字符串的名称上允许使用这些方法则没有逻辑上的理由使其在文字上不可用。第二个异议通常是这样的“我实际上是在告诉序列使用字符串常量将其成员连接在一起”。遗憾的是并非如此。出于某种原因把 split() 作为一个字符串方法似乎要容易得多因为在这种情况下很容易看到:1, 2, 4, 8, 16.split(, )是对字符串文本的指令用于返回由给定分隔符分隔的子字符串或在默认情况下返回任意空格。join() 是字符串方法因为在使用该方法时您告诉分隔符字符串去迭代一个字符串序列并在相邻元素之间插入自身。此方法的参数可以是任何遵循序列规则的对象包括您自己定义的任何新的类。对于字节和字节数组对象也有类似的方法。09. 异常有多快如果没有引发异常则 try/except 块的效率极高。实际上捕获异常是昂贵的。在 2.0 之前的 Python 版本中通常使用这个习惯用法:try:value mydict[key]except KeyError:mydict[key] getvalue(key)value mydict[key]只有当你期望 dict 在任何时候都有 key 时这才有意义。如果不是这样的话你就是应该这样编码if key in mydict:value mydict[key]else:value mydict[key] getvalue(key)对于这种特定的情况您还可以使用 value dict.setdefault(key, getvalue(key))但前提是调用 getvalue()足够便宜因为在所有情况下都会对其进行评估。10. 为什么 Python 中没有 switch 或 case 语句你可以通过一系列 if... elif... elif... else.轻松完成这项工作。对于 switch 语句语法已经有了一些建议但尚未就是否以及如何进行范围测试达成共识。有关完整的详细信息和当前状态请参阅 PEP 275 。对于需要从大量可能性中进行选择的情况可以创建一个字典将 case 值映射到要调用的函数。例如def function_1(...):...functions {a: function_1,b: function_2,c: self.method_1, ...}func functions[value]func()对于对象调用方法可以通过使用 getattr() 内置检索具有特定名称的方法来进一步简化def visit_a(self, ...):......def dispatch(self, value):method_name visit_ str(value)method getattr(self, method_name)method()建议对方法名使用前缀例如本例中的 visit_ 。如果没有这样的前缀如果值来自不受信任的源攻击者将能够调用对象上的任何方法。11. 难道不能在解释器中模拟线程而非得依赖特定于操作系统的线程实现吗答案 1不幸的是解释器为每个 Python 堆栈帧推送至少一个 C 堆栈帧。此外扩展可以随时回调 Python。因此一个完整的线程实现需要对 C 的线程支持。答案 2幸运的是 Stackless Python 有一个完全重新设计的解释器循环可以避免 C 堆栈。12. 为什么 lambda 表达式不包含语句Python 的 lambda 表达式不能包含语句因为 Python 的语法框架不能处理嵌套在表达式内部的语句。然而在 Python 中这并不是一个严重的问题。与其他语言中添加功能的 lambda 表单不同Python 的 lambdas 只是一种速记符号如果您懒得定义函数的话。函数已经是 Python 中的第一类对象可以在本地范围内声明。因此使用 lambda 而不是本地定义的函数的唯一优点是你不需要为函数创建一个名称 -- 这只是一个分配了函数对象(与 lambda 表达式生成的对象类型完全相同)的局部变量13. 可以将 Python 编译为机器代码C 或其他语言吗Cython 将带有可选注释的 Python 修改版本编译到 C 扩展中。Nuitka 是一个将 Python 编译成 C 代码的新兴编译器旨在支持完整的 Python 语言。要编译成 Java可以考虑 VOC 。14. Python 如何管理内存Python 内存管理的细节取决于实现。Python 的标准实现 CPython 使用引用计数来检测不可访问的对象并使用另一种机制来收集引用循环定期执行循环检测算法来查找不可访问的循环并删除所涉及的对象。gc 模块提供了执行垃圾回收、获取调试统计信息和优化收集器参数的函数。但是其他实现(如 Jython 或 PyPy ))可以依赖不同的机制如完全的垃圾回收器 。如果你的 Python 代码依赖于引用计数实现的行为则这种差异可能会导致一些微妙的移植问题。在一些 Python 实现中以下代码在 CPython 中工作的很好可能会耗尽文件描述符:for file in very_long_list_of_files:f open(file)c f.read(1)实际上使用 CPython 的引用计数和析构函数方案 每个新赋值的 f 都会关闭前一个文件。然而对于传统的 GC这些文件对象只能以不同的时间间隔可能很长的时间间隔被收集和关闭。如果要编写可用于任何 python 实现的代码则应显式关闭该文件或使用 with 语句无论内存管理方案如何这都有效for file in very_long_list_of_files:with open(file) as f:c f.read(1)15. 为什么 CPython 不使用更传统的垃圾回收方案首先这不是 C 标准特性因此不能移植。(是的我们知道 Boehm GC 库。它包含了 大多数 常见平台但不是所有平台的汇编代码尽管它基本上是透明的但也不是完全透明的; 要让 Python 使用它需要使用补丁。)当 Python 嵌入到其他应用程序中时传统的 GC 也成为一个问题。在独立的 Python 中可以用 GC 库提供的版本替换标准的 malloc()和 free()嵌入 Python 的应用程序可能希望用 它自己 替代 malloc()和 free()而可能不需要 Python 的。现在CPython 可以正确地实现 malloc()和 free()。16. CPython 退出时为什么不释放所有内存当 Python 退出时从全局命名空间或 Python 模块引用的对象并不总是被释放。如果存在循环引用则可能发生这种情况 C 库分配的某些内存也是不可能释放的例如像 Purify 这样的工具会抱怨这些内容。但是Python 在退出时清理内存并尝试销毁每个对象。如果要强制 Python 在释放时删除某些内容请使用 atexit 模块运行一个函数强制删除这些内容。17. 为什么有单独的元组和列表数据类型虽然列表和元组在许多方面是相似的但它们的使用方式通常是完全不同的。可以认为元组类似于 Pascal 记录或 C 结构它们是相关数据的小集合可以是不同类型的数据可以作为一个组进行操作。例如笛卡尔坐标适当地表示为两个或三个数字的元组。另一方面列表更像其他语言中的数组。它们倾向于持有不同数量的对象所有对象都具有相同的类型并且逐个操作。例如 os.listdir(.) 返回表示当前目录中的文件的字符串列表。如果向目录中添加了一两个文件对此输出进行操作的函数通常不会中断。元组是不可变的这意味着一旦创建了元组就不能用新值替换它的任何元素。列表是可变的这意味着您始终可以更改列表的元素。只有不变元素可以用作字典的 key因此只能将元组和非列表用作 key。18. 列表如何在 CPython 中实现CPython 的列表实际上是可变长度的数组而不是 lisp 风格的链表。该实现使用对其他对象的引用的连续数组并在列表头结构中保留指向该数组和数组长度的指针。这使得索引列表 a[i] 的操作成本与列表的大小或索引的值无关。当添加或插入项时将调整引用数组的大小。并采用了一些巧妙的方法来提高重复添加项的性能; 当数组必须增长时会分配一些额外的空间以便在接下来的几次中不需要实际调整大小。19. 字典如何在 CPython 中实现CPython 的字典实现为可调整大小的哈希表。与 B-树相比这在大多数情况下为查找目前最常见的操作提供了更好的性能并且实现更简单。字典的工作方式是使用 hash() 内置函数计算字典中存储的每个键的 hash 代码。hash 代码根据键和每个进程的种子而变化很大例如Python 的 hash 值为-539294296而python(一个按位不同的字符串)的 hash 值为 1142331976。然后hash 代码用于计算内部数组中将存储该值的位置。假设您存储的键都具有不同的 hash 值这意味着字典需要恒定的时间 -- O(1)用 Big-O 表示法 -- 来检索一个键。20. 为什么字典 key 必须是不可变的字典的哈希表实现使用从键值计算的哈希值来查找键。如果键是可变对象则其值可能会发生变化因此其哈希值也会发生变化。但是由于无论谁更改键对象都无法判断它是否被用作字典键值因此无法在字典中修改条目。然后当你尝试在字典中查找相同的对象时将无法找到它因为其哈希值不同。如果你尝试查找旧值也不会找到它因为在该哈希表中找到的对象的值会有所不同。如果你想要一个用列表索引的字典只需先将列表转换为元组用函数 tuple(L) 创建一个元组其条目与列表 L相同。元组是不可变的因此可以用作字典键。已经提出的一些不可接受的解决方案哈希按其地址对象 ID列出。这不起作用因为如果你构造一个具有相同值的新列表它将无法找到例如:mydict {[1, 2]: 12}print(mydict[[1, 2]])会引发一个 KeyError 异常因为第二行中使用的 [1, 2] 的 id 与第一行中的 id 不同。换句话说应该使用 来比较字典键而不是使用 is 。使用列表作为键时进行复制。这没有用的因为作为可变对象的列表可以包含对自身的引用然后复制代码将进入无限循环。允许列表作为键但告诉用户不要修改它们。当你意外忘记或修改列表时这将产生程序中的一类难以跟踪的错误。它还使一个重要的字典不变量无效d.keys() 中的每个值都可用作字典的键。将列表用作字典键后应标记为其只读。问题是它不仅仅是可以改变其值的顶级对象你可以使用包含列表作为键的元组。将任何内容作为键关联到字典中都需要将从那里可到达的所有对象标记为只读 —— 并且自引用对象可能会导致无限循环。如果需要可以使用以下方法来解决这个问题但使用它需要你自担风险你可以将一个可变结构包装在一个类实例中该实例同时具有 eq() 和 hash() 方法。然后你必须确保驻留在字典或其他基于 hash 的结构中的所有此类包装器对象的哈希值在对象位于字典或其他结构中时保持固定。class ListWrapper:def __init__(self, the_list):self.the_list the_listdef __eq__(self, other):return self.the_list other.the_listdef __hash__(self):l self.the_listresult 98767 - len(l)*555for i, el in enumerate(l):try:result result (hash(el) % 9999999) * 1001 iexcept Exception:result (result % 7777777) i * 333return result注意哈希计算由于列表的某些成员可能不可用以及算术溢出的可能性而变得复杂。此外必须始终如此如果 o1 o2 即 o1.eq(o2) is True 则 hash(o1) hash(o2)即o1.hash() o2.hash() 无论对象是否在字典中。如果你不能满足这些限制字典和其他基于 hash 的结构将会出错。对于 ListWrapper 只要包装器对象在字典中包装列表就不能更改以避免异常。除非你准备好认真考虑需求以及不正确地满足这些需求的后果否则不要这样做。请留意。21. 为什么 list.sort() 没有返回排序列表在性能很重要的情况下仅仅为了排序而复制一份列表将是一种浪费。因此 list.sort() 对列表进行了适当的排序。为了提醒您这一事实它不会返回已排序的列表。这样当您需要排序的副本但也需要保留未排序的版本时就不会意外地覆盖列表。如果要返回新列表请使用内置 sorted() 函数。此函数从提供的可迭代列表中创建新列表对其进行排序并返回。例如下面是如何迭代遍历字典并按 keys 排序:for key in sorted(mydict):... # do whatever with mydict[key]...22. 如何在 Python 中指定和实施接口规范由 C和 Java 等语言提供的模块接口规范描述了模块的方法和函数的原型。许多人认为接口规范的编译时强制执行有助于构建大型程序。Python 2.6 添加了一个 abc 模块允许定义抽象基类 (ABCs)。然后可以使用isinstance() 和 issubclass() 来检查实例或类是否实现了特定的 ABC。collections.abc 模块定义了一组有用的 ABCs 例如 Iterable Container , 和 MutableMapping对于 Python通过对组件进行适当的测试规程可以获得接口规范的许多好处。还有一个工具 PyChecker可用于查找由于子类化引起的问题。一个好的模块测试套件既可以提供回归测试也可以作为模块接口规范和一组示例。许多 Python 模块可以作为脚本运行以提供简单的“自我测试”。即使是使用复杂外部接口的模块也常常可以使用外部接口的简单“桩代码stub”模拟进行隔离测试。可以使用 doctest和 unittest 模块或第三方测试框架来构造详尽的测试套件以运行模块中的每一行代码。适当的测试规程可以帮助在 Python 中构建大型的、复杂的应用程序以及接口规范。事实上它可能会更好因为接口规范不能测试程序的某些属性。例如 append() 方法将向一些内部列表的末尾添加新元素接口规范不能测试您的 append() 实现是否能够正确执行此操作但是在测试套件中检查这个属性是很简单的。编写测试套件非常有用您可能希望设计代码时着眼于使其易于测试。一种日益流行的技术是面向测试的开发它要求在编写任何实际代码之前首先编写测试套件的各个部分。当然Python 允许您草率行事根本不编写测试用例。23. 为什么没有 goto可以使用异常捕获来提供 “goto 结构” 甚至可以跨函数调用工作的 。许多人认为异常捕获可以方便地模拟 CFortran 和其他语言的 go 或 goto 结构的所有合理用法。例如:class label(Exception): pass # declare a labeltry:...if condition: raise label() # goto label...except label: # where to gotopass...但是不允许你跳到循环的中间这通常被认为是滥用 goto。谨慎使用。24. 为什么原始字符串r-strings不能以反斜杠结尾更准确地说它们不能以奇数个反斜杠结束结尾处的不成对反斜杠会转义结束引号字符留下未结束的字符串。原始字符串的设计是为了方便想要执行自己的反斜杠转义处理的处理器(主要是正则表达式引擎)创建输入。此类处理器将不匹配的尾随反斜杠视为错误因此原始字符串不允许这样做。反过来允许通过使用引号字符转义反斜杠转义字符串。当 r-string 用于它们的预期目的时这些规则工作的很好。如果您正在尝试构建 Windows 路径名请注意所有 Windows 系统调用都使用正斜杠:f open(/mydir/file.txt) # works fine!如果您正在尝试为 DOS 命令构建路径名请尝试以下示例dir r\this\is\my\dos\dir \\dir r\this\is\my\dos\dir\ [:-1]dir \\this\\is\\my\\dos\\dir\\25. 为什么 Python 没有属性赋值的“with”语句Python 有一个 with 语句它封装了块的执行在块的入口和出口调用代码。有些语言的结构是这样的:with obj:a 1 # equivalent to obj.a 1total total 1 # obj.total obj.total 1在 Python 中这样的结构是不明确的。其他语言如 ObjectPascal、Delphi 和 C 使用静态类型因此可以毫不含糊地知道分配给什么成员。这是静态类型的要点 -- 编译器 总是 在编译时知道每个变量的作用域。Python 使用动态类型。事先不可能知道在运行时引用哪个属性。可以动态地在对象中添加或删除成员属性。这使得无法通过简单的阅读就知道引用的是什么属性局部属性、全局属性还是成员属性例如采用以下不完整的代码段:def foo(a):with a:print(x)该代码段假设 a 必须有一个名为 x 的成员属性。然而Python 中并没有告诉解释器这一点。假设 a 是整数会发生什么如果有一个名为 x 的全局变量它是否会在 with 块中使用如您所见Python 的动态特性使得这样的选择更加困难。然而Python 可以通过赋值轻松实现 with 和类似语言特性减少代码量的主要好处。代替:function(args).mydict[index][index].a 21function(args).mydict[index][index].b 42function(args).mydict[index][index].c 63写成这样:ref function(args).mydict[index][index]ref.a 21ref.b 42ref.c 63这也具有提高执行速度的副作用因为 Python 在运行时解析名称绑定而第二个版本只需要执行一次解析。26. 为什么 if/while/def/cblass 语句需要冒号冒号主要用于增强可读性(ABC 语言实验的结果之一)。考虑一下这个:if a bprint(a)与if a b:print(a)注意第二种方法稍微容易一些。请进一步注意在这个 FAQ 解答的示例中冒号是如何设置的这是英语中的标准用法。另一个次要原因是冒号使带有语法突出显示的编辑器更容易工作他们可以寻找冒号来决定何时需要增加缩进而不必对程序文本进行更精细的解析。27. 为什么 Python 在列表和元组的末尾允许使用逗号Python 允许您在列表元组和字典的末尾添加一个尾随逗号:[1, 2, 3,](a, b, c,)d {A: [1, 5],B: [6, 7], # last trailing comma is optional but good style}有几个理由允许这样做。如果列表元组或字典的字面值分布在多行中则更容易添加更多元素因为不必记住在上一行中添加逗号。这些行也可以重新排序而不会产生语法错误。不小心省略逗号会导致难以诊断的错误。例如:x [fee,fiefoo,fum]这个列表看起来有四个元素但实际上包含三个 : fee, fiefoo 和 fum 。总是加上逗号可以避免这个错误的来源。