精通 Python 设计模式——反模式

73 阅读11分钟

在本书最后一章,我们将探讨 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。本节节选部分要点,便于你在编写应用或库时规避。

修复代码风格问题的工具

格式化工具如 Blackblack.readthedocs.io/en/stable/)、isortpycqa.github.io/isort/)和 Ruffdocs.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)

  • 导入应各占一行,并按以下顺序分组:

    1. 标准库导入
    2. 第三方导入
    3. 本地应用/库的特定导入
  • 各组之间用一个空行分隔。

不符合规范:

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) :谨慎使用;与语句至少以两个空格分隔,置于同一行末尾。

不佳示例:

#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()

设有两个类 CustomListACustomListB,它们继承自 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 的选择,并自觉远离反模式。