在本书最后一章,我们将探讨 Python 反模式(anti-patterns) 。这些做法未必“错误”,但常常导致代码低效、可读性差、可维护性差。理解这些陷阱,有助于你为 Python 应用编写更整洁、更高效的代码。
本章将涵盖以下主题:
- 代码风格违规(Code style violations)
- 正确性反模式(Correctness anti-patterns)
- 可维护性反模式(Maintainability anti-patterns)
- 性能反模式(Performance anti-patterns)
技术要求
请参见第 1 章中的通用要求。
代码风格违规(Code style violations)
Python 的风格指南 PEP 8(Python Enhancement Proposal No. 8)提供了提高代码可读性与一致性的建议,便于团队协作与长期维护。详见官方页面:peps.python.org/pep-0008。本节节选部分要点,便于你在编写应用或库时规避。
修复代码风格问题的工具
格式化工具如 Black(black.readthedocs.io/en/stable/)、isort(pycqa.github.io/isort/)和 Ruff(docs.astral.sh/ruff/)可帮助快速修复不符合风格指南的代码。这里不展开用法,查阅其官方文档即可在数分钟内上手。
下面进入精选规则。
缩进(Indentation)
- 每一级缩进使用4 个空格;避免混用 Tab 与空格。
最大行宽与空行(Maximum line length and blank lines)
-
建议将所有代码行限制在 79 个字符以内,以提升可读性。
-
空行规则:
- 顶层的函数/类定义之间应以两个空行分隔。
- 类内的方法之间应以一个空行分隔。
错误示例:
class MyClass:
def method1(self):
pass
def method2(self):
pass
def top_level_function():
pass
正确示例:
class MyClass:
def method1(self):
pass
def method2(self):
pass
def top_level_function():
pass
导入(Imports)
-
导入应各占一行,并按以下顺序分组:
- 标准库导入
- 第三方导入
- 本地应用/库的特定导入
-
各组之间用一个空行分隔。
不符合规范:
import os, sys
import numpy as np
from mymodule import myfunction
推荐写法:
import os
import sys
import numpy as np
from mymodule import myfunction
命名约定(Naming conventions)
-
使用有描述性的名称。具体约定:
- 函数/变量(含类属性与方法) :
lower_case_with_underscores - 类名:
CapWords(也称 PascalCase) - 常量:
ALL_CAPS_WITH_UNDERSCORES
- 函数/变量(含类属性与方法) :
不佳示例:
def calculateSum(a, b):
return a + b
class my_class:
pass
maxValue = 100
推荐示例:
def calculate_sum(a, b):
return a + b
class MyClass:
pass
MAX_VALUE = 100
注释(Comments)
-
注释宜为完整句子、首字母大写、清晰简洁。
-
两类注释建议:
- 块注释(block comments) :通常作用于后续的一段代码,与该段代码同级缩进;每行以
#加空格开头。 - 行内注释(inline comments) :谨慎使用;与语句至少以两个空格分隔,置于同一行末尾。
- 块注释(block comments) :通常作用于后续的一段代码,与该段代码同级缩进;每行以
不佳示例:
#This is a poorly formatted block comment.
def foo(): #This is a poorly formatted inline comment.
pass
修正示例:
# This is a block comment.
# It spans multiple lines.
def foo():
pass # This is an inline comment.
表达式与语句中的空白(Whitespace in expressions and statements)
应避免以下多余空白:
-
紧贴圆/方/花括号内侧的空格:如
func( x, y )(应为func(x, y)) -
逗号、分号、冒号前的空格:如
a , b(应为a, b) -
为了对齐而在赋值运算符周围使用多个空格:如
x = 1 # 不推荐 y = 2 # 推荐统一一个空格
以上是最常见、最容易忽视的代码风格问题。前述工具通常会集成到开发流程(如 git 提交钩子、项目 CI/CD)中,能自动检测与修复此类违规,显著提升团队协作效率与代码质量。
正确性反模式(Correctness anti-patterns)
这些反模式若不加以处理,可能导致缺陷或非预期行为。下面讨论常见情形及其推荐替代做法,重点聚焦三类问题:
- 使用
type()进行类型比较 - 可变的默认参数
- 在类外访问受保护成员
说明:使用 VS Code、PyCharm 等 IDE,或 Flake8 等命令行工具,可以帮助你定位此类不良实践。但理解为何不推荐,依然很重要。
使用 type() 函数进行类型比较
在算法中,有时需要比较值的类型。直觉做法是用 type(),但它不考虑子类,灵活度不如 isinstance()。
设有两个类 CustomListA 与 CustomListB,它们继承自 UserList(自定义列表类型时官方推荐继承该类):
from collections import UserList
class CustomListA(UserList):
pass
class CustomListB(UserList):
pass
若用第一种思路,可以写 type(obj) in (CustomListA, CustomListB)。
更好的做法是:直接判断 isinstance(obj, UserList),因为两者都是 UserList 的子类。
示例对比:
def compare(obj):
if type(obj) in (CustomListA, CustomListB):
print("It's a custom list!")
else:
print("It's a something else!")
def better_compare(obj):
if isinstance(obj, UserList):
print("It's a custom list!")
else:
print("It's a something else!")
obj1 = CustomListA([1, 2, 3])
obj2 = CustomListB(["a", "b", "c"])
compare(obj1)
compare(obj2)
better_compare(obj1)
better_compare(obj2)
运行(python ch11/compare_types.py)输出:
It's a custom list!
It's a custom list!
It's a custom list!
It's a custom list!
两种方式都“能用”,但 isinstance() 更简洁、更通用(考虑了继承)。
可变的默认参数(Mutable default argument)
为期望可变值(如 list/dict)的参数提供默认值(如 [] 或 {})会在多次调用间保留修改,产生意外行为。
推荐:默认值设为 None,在函数内部按需创建。
对比示例:
def manipulate(mylist=[]):
mylist.append("test")
return mylist
def better_manipulate(mylist=None):
if not mylist:
mylist = []
mylist.append("test")
return mylist
if __name__ == "__main__":
print("function manipulate()")
print(manipulate())
print(manipulate())
print(manipulate())
print("function better_manipulate()")
print(better_manipulate())
print(better_manipulate())
运行(python ch11/mutable_default_argument.py)输出:
function manipulate()
['test']
['test', 'test']
['test', 'test', 'test']
function better_manipulate()
['test']
['test']
第一种写法会累积此前调用的结果;推荐写法每次都会得到预期的全新列表。
在类外访问受保护成员(Accessing a protected member from outside a class)
直接在类外访问受保护成员(以下划线前缀 _ 的属性)通常会埋下隐患:类的作者并不打算暴露它,后续维护可能更名/改动,从而导致调用方出错。
推荐:将所需信息纳入类的公开接口(方法或只读属性)对外提供。
示例:
class Book:
def __init__(self, title, author):
self._title = title
self._author = author
class BetterBook:
def __init__(self, title, author):
self._title = title
self._author = author
def presentation_line(self):
return f"{self._title} by {self._author}"
if __name__ == "__main__":
b1 = Book(
"Mastering Object-Oriented Python",
"Steven F. Lott",
)
print("Bad practice: Direct access of protected members")
print(f"{b1._title} by {b1._author}")
b2 = BetterBook(
"Python Algorithms",
"Magnus Lie Hetland",
)
print("Recommended: Access via the public interface")
print(b2.presentation_line())
运行(python ch11/ protected_member_of_class.py)输出:
Bad practice: Direct access of protected members
Mastering Object-Oriented Python by Steven F. Lott
Recommended: Access via the public interface
Python Algorithms by Magnus Lie Hetland
两者当前都能打印相同结果,但最佳实践是通过公开方法/属性访问。
此外,良好做法是使用 @property 为受保护成员提供封装后的属性访问(参见第 1 章“封装技巧”)。
可维护性反模式(Maintainability anti-patterns)
这些反模式会让你的代码随着时间推移难以理解与维护。本节讨论若干应当避免的做法,以提升你的 Python 应用或库代码库的质量。重点包括:
- 使用通配符导入(wildcard import)
- 先看再跳(LBYL) vs 求恕而后行(EAFP)
- 过度使用继承与紧耦合
- 用全局变量在函数间共享数据
(同上一节所述,把 Flake8 等工具纳入开发流程,有助于在代码中已有此类问题时及时发现。)
使用通配符导入(wildcard import)
形如 from mymodule import * 的导入会污染命名空间,并让你难以追踪某个变量或函数来自何处,还可能引发命名冲突导致的缺陷。
最佳实践:使用显式导入,或导入模块本身以保持清晰。
LBYL vs EAFP
LBYL(Look Before You Leap) :先检查条件再行动,代码往往更冗长。
EAFP(Easier to Ask for Forgiveness than Permission) :充分利用 Python 的异常机制,代码通常更简洁 Pythonic。
例如,检查文件是否存在再打开(LBYL):
if os.path.exists(filename):
with open(filename) as f:
print(f.text)
推荐的 EAFP 写法:
try:
with open(filename) as f:
print(f.text)
except FileNotFoundError:
print("No file there")
对比示例:
def test_open_file(filename):
if os.path.exists(filename):
with open(filename) as f:
print(f.text)
else:
print("No file there")
def better_test_open_file(filename):
try:
with open(filename) as f:
print(f.text)
except FileNotFoundError:
print("No file there")
filename = "no_file.txt"
test_open_file(filename)
better_test_open_file(filename)
运行(ch11/lbyl_vs_eafp.py)输出相同的:
No file there
No file there
但 try/except 的方式更精炼。
过度使用继承与紧耦合
继承是 OOP 的强大工具,但若为每个细微行为差异都新建子类,会造成紧耦合、层级过深、灵活性差、维护困难。
不推荐的深层继承(简化示例):
class GrandParent:
pass
class Parent(GrandParent):
pass
class Child(Parent):
Pass
更好的做法是小而专一的类,通过**组合(composition)**实现所需行为:
class Parent:
pass
class Child:
def __init__(self, parent):
self.parent = parent
这正是第 1 章“优先组合而非继承”原则所强调的思路。
使用全局变量在函数间共享数据
全局变量在整个程序中可见,常被用于在函数间共享数据(如跨模块的配置、数据库连接等)。
但它们容易引发问题:不同部分可能意外修改全局状态;在多线程环境中更可能出现竞态;也不利于扩展与测试。
不推荐示例:
# Global variable
counter = 0
def increment():
global counter
counter += 1
def reset():
global counter
counter = 0
最佳实践:通过参数传递所需数据,或把状态封装进类,以提升模块化与可测试性。等价的更优实现:
class Counter:
def __init__(self):
self.counter = 0
def increment(self):
self.counter += 1
def reset(self):
self.counter = 0
if __name__ == "__main__":
c = Counter()
print(f"Counter value: {c.counter}")
c.increment()
print(f"Counter value: {c.counter}")
c.reset()
运行(ch11/instead_of_global_variable.py)输出:
Counter value: 0
Counter value: 1
可见,用类管理状态更有效、可扩展、可测试,因此是推荐做法。
性能反模式(Performance anti-patterns)
这些反模式会引发低效,在大规模应用或数据密集型任务中尤为明显。我们将重点讨论以下两类问题:
- 在循环中连接字符串时不使用
.join() - 使用全局变量做缓存
下面开始。
在循环中连接字符串而不使用 .join()
在循环里用 + 或 += 拼接字符串会每次都创建新字符串对象,效率低。更好的做法是使用字符串的 .join(),它专为从序列或可迭代对象高效拼接而设计。
低效写法:
def concatenate(string_list):
result = ""
for item in string_list:
result += item
return result
推荐写法:
def better_concatenate(string_list):
result = "".join(string_list)
return result
测试:
if __name__ == "__main__":
string_list = ["Abc", "Def", "Ghi"]
print(concatenate(string_list))
print(better_concatenate(string_list))
运行(ch11/concatenate_strings_in_loop.py)输出:
AbcDefGhi
AbcDefGhi
两种方式结果相同,但基于 .join() 的写法因性能原因更为推荐。
使用全局变量进行缓存
用全局变量做简单缓存看似快捷,却常导致可维护性差、数据一致性隐患以及缓存生命周期难管理。更稳健的方式是使用专门的缓存库,或 Python 内置的 functools.lru_cache()。
非推荐做法(全局 dict 缓存,ch11/caching/using_global_var.py):
import time
import random
# Global variable as cache
_cache = {}
def get_data(query):
if query in _cache:
return _cache[query]
else:
result = perform_expensive_operation(query)
_cache[query] = result
return result
def perform_expensive_operation(user_id):
time.sleep(random.uniform(0.5, 2.0))
user_data = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"},
3: {"name": "Charlie", "email": "charlie@example.com"},
}
result = user_data.get(user_id, {"error": "User not found"})
return result
if __name__ == "__main__":
print(get_data(1))
print(get_data(1))
运行:
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Alice', 'email': 'alice@example.com'}
虽能工作,但更佳做法是使用 functools.lru_cache() 提供的 LRU(最近最少使用)缓存。它会自动管理缓存大小与条目生命周期,且线程安全,避免多线程同时访问/修改缓存带来的问题;此外实现上通常也经过性能优化。
推荐做法(ch11/caching/using_lru_cache.py):
import random
import time
from functools import lru_cache
@lru_cache(maxsize=100)
def get_data(user_id):
return perform_expensive_operation(user_id)
def perform_expensive_operation(user_id):
time.sleep(random.uniform(0.5, 2.0))
user_data = {
1: {"name": "Alice", "email": "alice@example.com"},
2: {"name": "Bob", "email": "bob@example.com"},
3: {"name": "Charlie", "email": "charlie@example.com"},
}
result = user_data.get(user_id, {"error": "User not found"})
return result
if __name__ == "__main__":
print(get_data(1))
print(get_data(1))
print(get_data(2))
print(get_data(99))
运行输出:
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Alice', 'email': 'alice@example.com'}
{'name': 'Bob', 'email': 'bob@example.com'}
{'error': 'User not found'}
可见,这种方式不仅让缓存机制更稳健,也提升了代码的可读性与可维护性。
小结
理解并规避常见的 Python 反模式,有助于写出更整洁、更高效、可维护的代码。
我们先介绍了代码风格常见违规;随后讨论了可能导致缺陷的正确性反模式;接着覆盖了超越风格层面的可读性与可维护性问题;最后展示了两类应当避免的性能反模式。
记住:好代码不仅要“能跑”,还要“跑得好、易维护”。
至此全书完结:我们从设计原则出发,讲到 Python 中常用的设计模式,最终落到反模式。希望这些思路与示例,能帮助你在面对具体用例时做出更 Pythonic 的选择,并自觉远离反模式。