40. 编写整洁且可读的代码
在本书中,你已经学习了 Python 的语法、数据结构(data structure)、控制流(control flow)、函数(function)、类(class)以及许多其他编程概念。你现在可以编写能够运行的程序了。但能“运行”的代码与“可维护”的代码之间存在关键差异——可维护的代码是指你和其他人能够在几个月甚至几年后理解、修改和调试(debug)的代码。
本章聚焦于编写整洁、可读的代码。你将学习让 Python 代码专业且可维护的约定(convention)与实践(practice)。这些并不是随意的规则——它们是经过实战检验的指南,能让协作更容易、减少 bug,并帮助你在之后回头看代码时理解自己写过的内容。
40.1) 为什么风格重要:阅读 vs. 编写代码
40.1.1) 代码被阅读的次数远多于被编写的次数
当你编写代码时,你会花几分钟或几小时把它写出来。但这段代码会被阅读很多次:当你调试它时、当你添加功能时、当其他开发者与它一起工作时,以及当你几个月后回来试图回忆它做什么时。
请看这段能正常工作但风格很差的代码:
# 警告:风格很差——仅用于演示
def c(l):
t=0
for i in l:
t=t+i
return t/len(l)
data=[85,92,78,90,88]
result=c(data)
print(result) # Output: 86.6这段代码完全能正常运行。它计算一个数字列表的平均值。但要理解它在做什么,需要仔细分析。现在把它与下面这个版本对比一下:
def calculate_average(numbers):
"""Calculate the arithmetic mean of a list of numbers."""
total = 0
for number in numbers:
total = total + number
return total / len(numbers)
test_scores = [85, 92, 78, 90, 88]
average_score = calculate_average(test_scores)
print(average_score) # Output: 86.6第二个版本为什么更好?
- 函数名(
calculate_average)清楚说明了目的 - 变量名(
numbers、total、test_scores)具有描述性 - docstring 解释了函数做什么
- 合理的空格让结构更清晰
- 任何人都能在不研究它的情况下理解这段代码
两个版本产生完全相同的结果,但第二个版本是一眼就能理解的。
关键洞察:你只写一次代码,但你会读它几十次甚至上百次。为清晰的命名与格式多花几秒钟,可以在之后节省数小时的困惑。
40.1.2) 可读性减少 bug
清晰的代码更容易调试,因为你可以快速理解每一部分的作用。当变量名具有描述性、结构整洁时,你更容易发现逻辑错误。
# 很难调试——这些变量代表什么?
# 警告:风格很差——仅用于演示
def process(x, y):
if x > y:
return x * (1 - y)
return x
result = process(100, 0.1)# 很容易调试——很清楚发生了什么
def apply_discount(price, discount_rate):
"""Calculate price after applying discount rate (0.0 to 1.0)."""
discount_amount = price * discount_rate
final_price = price - discount_amount
return final_price
original_price = 100
discount = 0.1 # 10% 折扣
final_price = apply_discount(original_price, discount)
print(f"Final price: ${final_price}")
# Output: Final price: $90.0在第二个版本中,你可以立刻看懂逻辑:“我们在计算折扣金额,然后从价格中减去它。”而在第一个版本中,你必须在脑中跟踪 x 和 y 代表什么,并弄清楚 x * (1 - y) 的含义。
40.1.3) 一致性让协作成为可能
当团队中每个人都遵循同样的风格约定时,代码会变得可预测。你不会浪费心力去解读不同的格式风格——你可以专注于理解逻辑。
Python 有一份官方风格指南,叫 PEP 8(Python Enhancement Proposal 8)。PEP 8 定义了以下方面的约定:
- 如何命名变量、函数和类
- 如何格式化代码(空格、行长度、缩进)
- 何时使用注释与 docstring
- 如何组织 import
遵循 PEP 8 意味着你的代码对其他 Python 程序员来说会很熟悉,从而让协作更顺畅。我们将在接下来的小节中覆盖关键的 PEP 8 指南。
40.2) 命名约定:变量、函数与类(PEP 8)
40.2.1) 通用命名原则
好名字应该具有描述性且不含歧义。它们应当在不需要你阅读实现细节的情况下,告诉你某个东西代表什么或做什么。
关键原则:
- 使用完整单词,不要用缩写(除非是非常常见的缩写,如
id、url、html) - 更具体:
user_count比count更好,calculate_total_price比calculate更好 - 避免单字母命名,除非是非常短的循环(loop)或数学公式
- 不要在名称中包含类型信息(Python 是动态类型的)
# 糟糕的名字——不清楚它们代表什么
# 警告:风格很差——仅用于演示
# 'n' 是什么?一个数字?一个名字?一个节点?
# 'd' 是什么?一个日期?一个距离?一个时长?
# 'l' 是什么?看起来像数字 1!
n = "Alice"
d = 25
l = [1, 2, 3]
calc = lambda x: x * 2
# 好的名字——清晰且有描述性
student_name = "Alice"
age_in_years = 25
test_scores = [1, 2, 3]
double_value = lambda x: x * 2例外:短循环变量
# 可接受:非常短,且上下文清晰
for i in range(10):
print(i)
for x, y in coordinates:
distance = (x**2 + y**2) ** 0.5
# 但为了清晰,最好使用更具描述性的名字
for student_index in range(len(students)):
print(students[student_index])
for point_x, point_y in coordinates:
distance = (point_x**2 + point_y**2) ** 0.540.2.2) 变量与函数名:snake_case
在 Python 中,变量和函数使用 snake_case:全部小写,单词之间用下划线分隔。
# 变量
user_name = "Bob"
total_price = 99.99
is_valid = True
max_retry_count = 3
# 函数
def calculate_tax(amount, rate):
"""Calculate tax on a given amount."""
return amount * rate
def send_email_notification(recipient, message):
"""Send an email to the specified recipient."""
print(f"Sending to {recipient}: {message}")
# 使用函数
tax_amount = calculate_tax(100, 0.08)
send_email_notification("user@example.com", "Welcome!")为什么用 snake_case? 它非常易读。下划线形成清晰的单词边界,使名称更容易扫读。对比 calculatetotalprice(难读)与 calculate_total_price(一眼清楚)。
40.2.3) 常量名:UPPER_SNAKE_CASE
常量(constant)——在程序运行期间不应改变的值——使用 UPPER_SNAKE_CASE:全部大写并用下划线分隔。
# 模块级常量
MAX_LOGIN_ATTEMPTS = 3
DEFAULT_TIMEOUT_SECONDS = 30
PI = 3.14159
DATABASE_URL = "postgresql://localhost/mydb"
def validate_password_length(password):
"""Check if password meets minimum length requirement."""
MIN_PASSWORD_LENGTH = 8 # 函数内常量
return len(password) >= MIN_PASSWORD_LENGTH
# 使用常量
if login_attempts > MAX_LOGIN_ATTEMPTS:
print("Account locked")重要:Python 没有内置的常量语法。不同于一些语言(如 JavaScript 的 const 或 Java 的 final),Python 没有办法声明变量不可更改。
相反,Python 程序员使用命名约定来表达意图:
UPPER_SNAKE_CASE的含义是:“我希望它是常量——不要修改它”- 这是一种程序员之间的沟通工具,不是语言特性
# Python 没有常量语法——这只是一个普通变量
MAX_LOGIN_ATTEMPTS = 3
# Python 不会阻止你修改它
MAX_LOGIN_ATTEMPTS = 5 # ❌ 技术上可行,但违反约定
# 命名约定是在表达意图:
# “我用全大写命名,是为了表示我不希望它被更改”最佳实践:如果某个值确实需要在程序执行期间改变,就不要把它命名为常量:
# 这个值会改变——使用小写
max_login_attempts = 3
max_login_attempts = 5 # ✅ OK——名字表明它可以改变
# 这个值不应该改变——使用大写
MAX_LOGIN_ATTEMPTS = 3
# 不要在后续代码里重新赋值这个约定能帮助程序员理解你的意图并避免 bug。看到 MAX_LOGIN_ATTEMPTS 时,你就知道不该去修改它。
40.2.4) 类名:PascalCase
类名使用 PascalCase(也叫 CapWords):每个单词首字母大写,不使用下划线。
# 类定义
class Student:
"""Represent a student with name and grades."""
def __init__(self, name):
self.name = name
self.grades = []
class ShoppingCart:
"""Manage items in a shopping cart."""
def __init__(self):
self.items = []
def add_item(self, item):
"""Add an item to the cart."""
self.items.append(item)
class DatabaseConnection:
"""Handle database connection and queries."""
def __init__(self, url):
self.url = url
# 创建实例(注意:实例使用 snake_case 变量名)
student = Student("Alice")
shopping_cart = ShoppingCart()
db_connection = DatabaseConnection("localhost")为什么类用 PascalCase? 它在视觉上能把类与函数、变量区分开来。看到 Student(),你会立刻知道这是在创建某个类的实例(instance)。看到 calculate_average(),你就知道这是在调用一个函数。
40.2.5) 私有与内部名称:前导下划线
以单个下划线开头的名称(_name)表示内部使用——它们用于模块或类的内部,而不是给外部代码使用。
Python 没有语法把方法或属性标记为“私有”(private)(不像 Java 或 C++ 的 private)。相反,Python 用带前导下划线(_name)的命名约定来表达意图。
_name 的含义:
- “这是仅供内部使用的”
- “我做这个是为了在这个类/模块内部使用,而不是给外部代码用”
- “未来版本中它可能随时改变——不要依赖它”
class BankAccount:
"""Represent a bank account with balance tracking."""
def __init__(self, account_number, initial_balance):
self.account_number = account_number
self._balance = initial_balance # 内部属性
def deposit(self, amount):
"""Add money to the account."""
if self._validate_amount(amount): # 内部方法
self._balance += amount
def _validate_amount(self, amount):
"""Internal helper to validate transaction amounts."""
return amount > 0
def get_balance(self):
"""Return the current balance."""
return self._balance
# 使用该类
account = BankAccount("12345", 1000)
account.deposit(500)
print(account.get_balance()) # Output: 1500
# 技术上可行,但违反约定
print(account._balance)
# Output: 1500 (works, but you shouldn't do this!)
# 技术上可行,但违反约定
result = account._validate_amount(100)
# Output: True (works, but you shouldn't do this!)关键点:Python 无法阻止你访问 _balance 或调用 _validate_amount()。下划线是程序员之间的信号,不是安全特性。
为什么会有这个约定
由于 Python 无法强制隐私性,前导下划线就是类作者表达意图的方式:
下划线传达的信号:
- “这是内部实现细节——未来版本中可能改变”
- “请使用公共方法——它们保证保持稳定”
- “如果你依赖内部细节,当我更新库时你的代码可能会坏掉”
这个约定形成了一种契约:类作者可以自由修改内部实现(任何带 _ 的东西),但必须保持公共接口稳定。这让库可以在不破坏用户代码的情况下演进。
40.2.6) 特殊名称:双下划线
前后带双下划线的名称(__name__)是 Python 定义的特殊方法或魔术方法。不要用这种模式创建你自己的名字——这保留给 Python 使用。
class Point:
"""Represent a 2D point."""
def __init__(self, x, y): # 特殊方法:初始化
self.x = x
self.y = y
def __str__(self): # 特殊方法:字符串表示
return f"Point({self.x}, {self.y})"
def __add__(self, other): # 特殊方法:加法运算符
return Point(self.x + other.x, self.y + other.y)
p1 = Point(1, 2)
p2 = Point(3, 4)
print(p1) # Output: Point(1, 2)
print(p1 + p2) # Output: Point(4, 6)正如我们在第 31 章学到的,这些特殊方法让运算符重载(operator overloading)以及与 Python 内置函数的集成成为可能。
40.2.7) 命名总结表
| 类型 | 约定 | 示例 |
|---|---|---|
| 变量 | snake_case | user_name、total_count |
| 函数 | snake_case | calculate_tax()、send_email() |
| 常量 | UPPER_SNAKE_CASE | MAX_SIZE、DEFAULT_TIMEOUT |
| 类 | PascalCase | Student、ShoppingCart |
| 内部/私有 | _leading_underscore | _balance、_validate() |
| 特殊/魔术 | double_underscore | __init__、__str__ |
40.3) 代码布局:缩进、空格与空行
40.3.1) 缩进:四个空格
Python 使用缩进定义代码块。始终每级缩进使用 4 个空格——不要用 Tab,也不要混用 Tab 和空格。
def calculate_grade(score):
"""Determine letter grade from numeric score."""
if score >= 90:
return "A"
elif score >= 80:
return "B"
elif score >= 70:
return "C"
else:
return "F"
# 嵌套缩进:每一层 4 个空格
def process_students(students):
"""Process a list of student records."""
for student in students:
if student["active"]:
grade = calculate_grade(student["score"])
print(f"{student['name']}: {grade}")
students = [
{"name": "Alice", "score": 92, "active": True},
{"name": "Bob", "score": 78, "active": True}
]
process_students(students)
# Output:
# Alice: A
# Bob: C为什么是 4 个空格? 这是 Python 社区标准。你遇到的大多数 Python 代码都使用 4 个空格,因此遵循该约定能让你的代码与生态系统保持一致。
配置你的编辑器:现代代码编辑器通常可以设置为在你按下 Tab 键时插入 4 个空格。这样既能享受 Tab 键的便利,又能保持 4 空格标准。
40.3.2) 最大行长度:79 个字符
PEP 8 建议将每行限制为 79 个字符(docstring 和注释最多可到 99 个字符)。这看起来可能很严格,但它有实际好处:
- 在较小屏幕上代码仍然易读
- 你可以并排查看两个文件
- 它鼓励把复杂表达式拆成更简单的部分
注意:许多现代项目使用略长的限制(88、100 或 120 个字符)。关键是在项目内保持一致。选择一个限制并坚持下去。
# 太长——难读
# 警告:风格很差——仅用于演示
def calculate_monthly_payment(principal, annual_rate, years):
return principal * (annual_rate / 12) * (1 + annual_rate / 12) ** (years * 12) / ((1 + annual_rate / 12) ** (years * 12) - 1)
# 更好——拆成可读的多行
def calculate_monthly_payment(principal, annual_rate, years):
"""Calculate monthly loan payment using amortization formula."""
monthly_rate = annual_rate / 12
num_payments = years * 12
numerator = principal * monthly_rate * (1 + monthly_rate) ** num_payments
denominator = (1 + monthly_rate) ** num_payments - 1
return numerator / denominator
payment = calculate_monthly_payment(200000, 0.045, 30)
print(f"Monthly payment: ${payment:.2f}") # Output: Monthly payment: $1013.37拆分长行:当你需要拆行时,使用括号、方括号或花括号内的隐式续行:
# 很长的函数调用
result = some_function(
first_argument,
second_argument,
third_argument,
fourth_argument
)
# 很长的列表
colors = [
"red", "green", "blue",
"yellow", "orange", "purple",
"pink", "brown", "gray"
]
# 很长的字符串
message = (
"This is a very long message that needs to be broken "
"across multiple lines for readability. Python automatically "
"concatenates adjacent string literals."
)
print(message)
# Output: This is a very long message that needs to be broken across multiple lines for readability. Python automatically concatenates adjacent string literals.40.3.3) 运算符两侧与逗号后加空格
在运算符两侧以及逗号后使用空格以提升可读性:
# 糟糕的空格——拥挤且难读
# 警告:风格很差——仅用于演示
x=5
y=x*2+3
result=calculate_tax(100,0.08)
data=[1,2,3,4,5]
# 良好的空格——清晰且可读
x = 5
y = x * 2 + 3
result = calculate_tax(100, 0.08)
data = [1, 2, 3, 4, 5]
# 表达式中的空格
total = (price * quantity) + shipping_cost
is_valid = (age >= 18) and (has_license == True)
# 函数定义中的空格
def calculate_discount(price, discount_rate, minimum_purchase=0):
"""Calculate discounted price if minimum purchase is met."""
if price >= minimum_purchase:
return price * (1 - discount_rate)
return price例外:在关键字参数或默认参数值中,不要在 = 两侧加空格:
# 关键字参数的正确空格
result = calculate_discount(price=100, discount_rate=0.1, minimum_purchase=50)
# 默认参数的正确空格
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"40.3.4) 用空行进行逻辑分隔
使用空行来分隔代码的不同逻辑部分:
顶层函数与类之间使用两行空行:
def first_function():
"""First function."""
pass
def second_function():
"""Second function."""
pass
class MyClass:
"""A class definition."""
pass类中方法之间使用一行空行:
class Student:
"""Represent a student with grades."""
def __init__(self, name):
self.name = name
self.grades = []
def add_grade(self, grade):
"""Add a grade to the student's record."""
self.grades.append(grade)
def get_average(self):
"""Calculate the student's grade average."""
if not self.grades:
return 0
return sum(self.grades) / len(self.grades)函数内部用空行分隔逻辑步骤:
def process_order(order_items, customer):
"""Process a customer order and calculate total."""
# 计算小计
subtotal = 0
for item in order_items:
subtotal += item["price"] * item["quantity"]
# 应用客户折扣
discount = 0
if customer["is_premium"]:
discount = subtotal * 0.1
# 计算税费
tax = (subtotal - discount) * 0.08
# 计算最终总额
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}这些空行相当于视觉上的“段落”,让代码结构一目了然。
40.3.5) 避免行尾空白字符
不要在行尾留下空格——它们不可见,但可能会在版本控制系统以及某些编辑器中引发问题。
# 不好——不可见的行尾空格(用 · 表示以便说明)
# 警告:风格很差——仅用于演示
def calculate(x):···
return x * 2···
# 好——没有行尾空格
def calculate(x):
return x * 2大多数现代编辑器都可以配置为在保存文件时自动移除行尾空白字符。
40.4) 文档:编写有用的注释与 Docstring
40.4.1) 何时写注释
注释解释的是代码为什么要这么做,而不是做了什么。命名良好的变量与函数应当让“做了什么”一目了然。
# 糟糕的注释——在陈述显而易见的事
# 警告:风格很差——仅用于演示
x = x + 1 # 给 x 加 1
# 好的注释——解释原因
x = x + 1 # 针对从 0 开始的索引进行调整
# 糟糕的注释——与代码重复
# 警告:风格很差——仅用于演示
# Check if age is greater than or equal to 18
if age >= 18:
print("Adult")
# 好的注释——解释业务逻辑
# 美国的法定饮酒年龄
if age >= 21:
print("Can purchase alcohol")注释有价值的场景:
- 解释复杂算法:
def binary_search(sorted_list, target):
"""Search for target in sorted list using binary search."""
left = 0
right = len(sorted_list) - 1
while left <= right:
# 计算中点,避免整数溢出
# (right + left) // 2 在索引非常大时可能溢出
mid = left + (right - left) // 2
if sorted_list[mid] == target:
return mid
elif sorted_list[mid] < target:
left = mid + 1 # Target is in right half
else:
right = mid - 1 # Target is in left half
return -1 # Target not found- 澄清不明显的业务规则:
def calculate_shipping_cost(weight, distance):
"""Calculate shipping cost based on weight and distance."""
base_cost = 5.00
# 针对重物的免运费促销(截至 2024 年的公司政策)
# 这鼓励批量下单,并降低单位运输成本
if weight > 50:
return 0
# 标准费率:每磅 $0.50 外加每英里 $0.10
# 基于 2024 年第一季度谈判得到的承运商合同
return base_cost + (weight * 0.50) + (distance * 0.10)- 记录变通方案或临时解决方案:
def process_data(data):
"""Process incoming data records."""
# TODO: 这是针对格式错误记录的临时修复
# 一旦在上游实现数据校验就移除
if not isinstance(data, list):
data = [data]
for record in data:
# 处理每条记录
pass40.4.2) 编写有效的 Docstring
Docstring 是一种特殊注释,用于为模块、类和函数编写文档。它们用三引号包围,并作为定义中的第一条语句出现。
def calculate_bmi(weight_kg, height_m):
"""
Calculate Body Mass Index (BMI).
BMI is calculated as weight in kilograms divided by the square of height in meters.
Args:
weight_kg: Weight in kilograms (float or int)
height_m: Height in meters (float or int)
Returns:
float: The calculated BMI value
Example:
>>> calculate_bmi(70, 1.75)
22.857142857142858
"""
return weight_kg / (height_m ** 2)
# 访问 docstring
print(calculate_bmi.__doc__)
# Output:
# Calculate Body Mass Index (BMI).
#
# BMI is calculated as weight in kilograms divided by the square of height in meters.
# ...简单函数使用单行 docstring:
def square(x):
"""Return the square of x."""
return x * x
def is_even(n):
"""Return True if n is even, False otherwise."""
return n % 2 == 0复杂函数使用多行 docstring:
def find_prime_factors(n):
"""
Find all prime factors of a positive integer.
This function returns a list of prime numbers that, when multiplied
together, equal the input number. The factors are returned in ascending order.
Args:
n: A positive integer greater than 1
Returns:
list: Prime factors in ascending order
Raises:
ValueError: If n is less than 2
Example:
>>> find_prime_factors(12)
[2, 2, 3]
>>> find_prime_factors(17)
[17]
"""
if n < 2:
raise ValueError("n must be at least 2")
factors = []
divisor = 2
while n > 1:
while n % divisor == 0:
factors.append(divisor)
n = n // divisor
divisor += 1
return factors类 docstring:
class BankAccount:
"""
Represent a bank account with deposit and withdrawal operations.
This class maintains an account balance and provides methods for
depositing and withdrawing money. All transactions are validated to prevent negative balances.
Attributes:
account_number: Unique identifier for the account
balance: Current account balance in dollars
"""
def __init__(self, account_number, initial_balance=0):
"""
Initialize a new bank account.
Args:
account_number: Unique account identifier (string)
initial_balance: Starting balance (default: 0)
"""
self.account_number = account_number
self.balance = initial_balance
def deposit(self, amount):
"""
Add money to the account.
Args:
amount: Amount to deposit (must be positive)
Raises:
ValueError: If amount is not positive
"""
if amount <= 0:
raise ValueError("Deposit amount must be positive")
self.balance += amount40.4.3) Docstring 约定
第一行:对函数/类做简要总结。应能放在一行内。
空行:将摘要与详细描述分隔开。
详细描述:解释函数做什么、任何重要细节以及如何使用。
Args/Parameters:列出每个参数的类型与用途。
Returns:描述函数返回什么及其类型。
Raises:记录函数可能抛出的异常(exception)。
Example:展示典型用法(可选但有帮助)。
def calculate_compound_interest(principal, rate, time, compounds_per_year=1):
"""
Calculate compound interest on an investment.
Uses the compound interest formula: A = P(1 + r/n)^(nt)
where A is the final amount, P is principal, r is annual rate,
n is compounds per year, and t is time in years.
Args:
principal: Initial investment amount (float)
rate: Annual interest rate as decimal (e.g., 0.05 for 5%)
time: Investment period in years (float)
compounds_per_year: Number of times interest compounds annually
(default: 1 for annual compounding)
Returns:
float: Final amount after compound interest
Example:
>>> calculate_compound_interest(1000, 0.05, 10, 12)
1647.0095
"""
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * time)40.4.4) 用 TODO 注释标记未来工作
使用 TODO 注释标记需要未来关注的区域:
def process_payment(amount, payment_method):
"""Process a payment transaction."""
# TODO: 添加对加密货币支付的支持
# TODO: 实现反欺诈检测检查
if payment_method == "credit_card":
return process_credit_card(amount)
elif payment_method == "paypal":
return process_paypal(amount)
else:
raise ValueError(f"Unsupported payment method: {payment_method}")许多编辑器可以搜索 TODO 注释,让你很容易找到需要处理的地方。
40.5) 组织你的代码:Imports、常量、函数与 Main
40.5.1) 标准模块结构
一个组织良好的 Python 模块通常遵循以下结构:
- 模块 docstring:描述模块做什么
- Imports:先标准库,再第三方,再本地导入
- 常量:模块级常量
- 函数与类:主要代码
- 主执行块:当脚本被执行时运行的代码
"""
student_manager.py
Manage student records including grades and GPA calculations.
This module provides functions for adding students, recording grades,
and calculating grade point averages.
"""
# 标准库导入
import sys
from datetime import datetime
# 第三方导入(如果有)
# import requests
# 本地导入(如果有)
# from .database import save_student
# 常量
MAX_GRADE = 100
MIN_GRADE = 0
PASSING_GRADE = 60
# 函数
def calculate_gpa(grades):
"""
Calculate GPA from a list of numeric grades.
Args:
grades: List of numeric grades (0-100)
Returns:
float: GPA on 4.0 scale
"""
if not grades:
return 0.0
average = sum(grades) / len(grades)
# 转换为 4.0 量表
if average >= 90:
return 4.0
elif average >= 80:
return 3.0
elif average >= 70:
return 2.0
elif average >= 60:
return 1.0
else:
return 0.0
def validate_grade(grade):
"""
Check if a grade is within valid range.
Args:
grade: Numeric grade to validate
Returns:
bool: True if grade is valid, False otherwise
"""
return MIN_GRADE <= grade <= MAX_GRADE
# 主执行
if __name__ == "__main__":
# 当脚本被直接执行时运行的代码
test_grades = [85, 92, 78, 88]
gpa = calculate_gpa(test_grades)
print(f"GPA: {gpa}") # Output: GPA: 3.040.5.2) Import 的组织方式
把 import 分为三组,并用空行分隔:
- 标准库导入:Python 内置模块
- 第三方导入:安装的包(如
requests、numpy) - 本地导入:你自己的模块
# 标准库导入
import os
import sys
from datetime import datetime, timedelta
from pathlib import Path
# 第三方导入
import requests
from flask import Flask, render_template
# 本地应用导入
from myapp.database import connect_db
from myapp.models import User, Product
from myapp.utils import format_currencyImport 风格:
# 导入整个模块
import math
result = math.sqrt(16) # Output: 4.0
# 导入特定对象
from math import sqrt, pi
result = sqrt(16) # Output: 4.0
# 使用别名导入
import numpy as np
array = np.array([1, 2, 3])
# 导入多个对象
from os import path, getcwd, listdir避免通配符导入(from module import *)——它会让名称来源不清晰:
# 不好——不清楚 sqrt 来自哪里
# 警告:风格很差——仅用于演示
from math import *
result = sqrt(16)
# 好——显式导入
from math import sqrt
result = sqrt(16)40.5.3) 组织常量
将模块级常量放在靠上位置,紧跟在 imports 之后:
"""Configuration settings for the application."""
import os
# 应用常量
APP_NAME = "Student Manager"
VERSION = "1.0.0"
DEBUG_MODE = True
# 数据库配置
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///students.db")
MAX_CONNECTIONS = 10
# 业务规则
MAX_STUDENTS_PER_CLASS = 30
PASSING_GRADE = 60
GRADE_WEIGHTS = {
"homework": 0.3,
"midterm": 0.3,
"final": 0.4
}
def calculate_final_grade(homework, midterm, final):
"""Calculate weighted final grade."""
return (
homework * GRADE_WEIGHTS["homework"] +
midterm * GRADE_WEIGHTS["midterm"] +
final * GRADE_WEIGHTS["final"]
)40.5.4) 函数的逻辑顺序
按逻辑顺序组织函数:
- 先放公共函数:给其他模块使用的函数
- 再放辅助函数:支撑公共函数的内部函数
- 相关函数放在一起:把协同工作的函数分组
"""Order processing module."""
# 公共 API 函数
def process_order(order_items, customer):
"""
Process a customer order.
This is the main entry point for order processing.
"""
subtotal = _calculate_subtotal(order_items)
discount = _calculate_discount(subtotal, customer)
tax = _calculate_tax(subtotal - discount)
total = subtotal - discount + tax
return {
"subtotal": subtotal,
"discount": discount,
"tax": tax,
"total": total
}
def validate_order(order_items):
"""Validate that an order contains valid items."""
if not order_items:
return False
for item in order_items:
if not _validate_item(item):
return False
return True
# 内部辅助函数
def _calculate_subtotal(items):
"""Calculate order subtotal (internal use)."""
total = 0
for item in items:
total += item["price"] * item["quantity"]
return total
def _calculate_discount(subtotal, customer):
"""Calculate customer discount (internal use)."""
if customer.get("is_premium"):
return subtotal * 0.1
return 0
def _calculate_tax(amount):
"""Calculate sales tax (internal use)."""
TAX_RATE = 0.08
return amount * TAX_RATE
def _validate_item(item):
"""Validate a single order item (internal use)."""
required_fields = ["name", "price", "quantity"]
return all(field in item for field in required_fields)注意公共函数(process_order、validate_order)在前面,而辅助函数(前缀为 _)在后面。这会清晰地表明哪些函数是主要 API。
40.5.5) 模块内的类组织方式
当一个模块包含类时,要有逻辑地组织它们:
"""User management system."""
# 常量
DEFAULT_ROLE = "user"
ADMIN_ROLE = "admin"
# 先定义基类
class User:
"""Base user class."""
def __init__(self, username, email):
self.username = username
self.email = email
self.role = DEFAULT_ROLE
def can_edit(self, resource):
"""Check if user can edit a resource."""
return resource.owner == self.username
# 再定义派生类
class AdminUser(User):
"""Administrator with elevated privileges."""
def __init__(self, username, email):
super().__init__(username, email)
self.role = ADMIN_ROLE
def can_edit(self, resource):
"""Admins can edit any resource."""
return True
# 将相关类放在一起
class Resource:
"""Represent a resource that can be owned and edited."""
def __init__(self, name, owner):
self.name = name
self.owner = owner
# 与类相关的工具函数
def create_user(username, email, is_admin=False):
"""Factory function to create appropriate user type."""
if is_admin:
return AdminUser(username, email)
return User(username, email)类组织原则:
- 基类在派生类之前(读者需要先理解基类)
- 相关类放在一起(User 和 Resource 相关)
- 与类配套的工具函数放在类定义之后
- 每个类都应该有清晰的 docstring 来说明其用途
40.6) if __name__ == "__main__" 模式
40.6.1) 理解这种模式
每个 Python 文件都有一个内置变量,叫 __name__。Python 会根据文件的使用方式自动设置这个变量的值:
- 当你直接运行一个文件(例如
python my_script.py)时,Python 会把__name__设为"__main__" - 当你把文件作为模块导入(import) 时,Python 会把
__name__设为模块的名字(文件名去掉.py)
这让你可以编写仅在文件被直接执行时运行的代码,而在被导入时不运行:
"""math_utils.py - Mathematical utility functions."""
def add(a, b):
"""Add two numbers."""
return a + b
def multiply(a, b):
"""Multiply two numbers."""
return a * b
# 这段代码只会在文件被直接执行时运行
if __name__ == "__main__":
# 测试这些函数
print(f"5 + 3 = {add(5, 3)}") # Output: 5 + 3 = 8
print(f"5 * 3 = {multiply(5, 3)}") # Output: 5 * 3 = 15当你运行 python math_utils.py 时,你会看到输出。但当你在另一个文件里导入它时:
# another_file.py
from math_utils import add, multiply
result = add(10, 20)
print(result) # Output: 30
# math_utils.py 中的测试代码不会运行注意测试代码(位于 if __name__ == "__main__": 内)在被导入时不会运行!
40.6.2) 为什么这种模式很重要
这种模式有几个重要用途:
1. 测试与演示:你可以在同一个文件中包含函数的示例用法:
"""temperature.py - Temperature conversion utilities."""
def celsius_to_fahrenheit(celsius):
"""Convert Celsius to Fahrenheit."""
return (celsius * 9/5) + 32
def fahrenheit_to_celsius(fahrenheit):
"""Convert Fahrenheit to Celsius."""
return (fahrenheit - 32) * 5/9
if __name__ == "__main__":
# 演示这些函数
print("Temperature Conversion Examples:")
print(f"0°C = {celsius_to_fahrenheit(0)}°F") # Output: 0°C = 32.0°F
print(f"100°C = {celsius_to_fahrenheit(100)}°F") # Output: 100°C = 212.0°F
print(f"32°F = {fahrenheit_to_celsius(32)}°C") # Output: 32°F = 0.0°C2. 可复用模块:同一个文件既可以是独立脚本,也可以是可导入模块:
"""data_processor.py - Process and analyze data files."""
import sys
def load_data(filename):
"""Load data from a file."""
with open(filename) as f:
return [line.strip() for line in f]
def analyze_data(data):
"""Perform analysis on data."""
return {
"count": len(data),
"average_length": sum(len(item) for item in data) / len(data)
}
if __name__ == "__main__":
# 作为脚本运行时,处理命令行参数
if len(sys.argv) < 2:
print("Usage: python data_processor.py <filename>")
sys.exit(1)
filename = sys.argv[1]
data = load_data(filename)
results = analyze_data(data)
print(f"Processed {results['count']} items")
print(f"Average length: {results['average_length']:.2f}")你可以将其作为脚本运行:
$ python data_processor.py data.txt
Processed 42 items
Average length: 15.23或者在另一个文件中导入它:
# my_analysis.py
from data_processor import load_data, analyze_data
my_data = load_data("myfile.txt")
results = analyze_data(my_data)
print(f"Found {results['count']} items")40.6.3) Main 块的常见模式
模式 1:简单测试用例
"""calculator.py - Basic calculator operations."""
def add(a, b):
"""Add two numbers."""
return a + b
def subtract(a, b):
"""Subtract b from a."""
return a - b
if __name__ == "__main__":
# 快速测试
assert add(2, 3) == 5
assert subtract(10, 4) == 6
print("All tests passed!") # Output: All tests passed!模式 2:main 函数
对于更复杂的脚本,定义一个 main() 函数:
"""report_generator.py - Generate reports from data."""
import sys
def load_data(filename):
"""Load data from file."""
# Implementation here
pass
def generate_report(data):
"""Generate report from data."""
# Implementation here
pass
def save_report(report, output_file):
"""Save report to file."""
# Implementation here
pass
def main():
"""Main entry point for the script."""
if len(sys.argv) < 3:
print("Usage: python report_generator.py <input> <output>")
return 1
input_file = sys.argv[1]
output_file = sys.argv[2]
try:
data = load_data(input_file)
report = generate_report(data)
save_report(report, output_file)
print(f"Report saved to {output_file}")
return 0
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
# 以 main 的状态码退出(0 = 成功,1 = 错误)
sys.exit(main())这种模式有几个优势:
main()函数可以被独立测试- 脚本有清晰的入口点
- 正确的退出码(0 表示成功,非 0 表示错误)
- 脚本逻辑与模块函数之间有清晰分离
40.6.4) Main 块的最佳实践
让 main 块保持聚焦:if __name__ == "__main__" 内的代码应主要负责脚本执行协调,而不应包含复杂逻辑:
# 不好——在 main 块里写复杂逻辑
# 警告:风格很差——仅用于演示
if __name__ == "__main__":
data = []
for i in range(100):
if i % 2 == 0:
data.append(i * 2)
result = sum(data) / len(data)
print(result)
# 好——把逻辑放到函数里,main 块负责协调
def generate_even_doubles(limit):
"""Generate doubled even numbers up to limit."""
return [i * 2 for i in range(limit) if i % 2 == 0]
def calculate_average(numbers):
"""Calculate average of numbers."""
return sum(numbers) / len(numbers)
if __name__ == "__main__":
data = generate_even_doubles(100)
result = calculate_average(data)
print(result) # Output: 99.0复杂脚本使用 main() 函数:如前所示,定义 main() 会让脚本更容易测试、更有组织。
记录脚本用法:如果脚本接收命令行参数,在模块 docstring 中记录它们:
"""
file_processor.py - Process text files with various operations.
Usage:
python file_processor.py <input_file> <output_file> [--uppercase]
Arguments:
input_file: Path to input file
output_file: Path to output file
--uppercase: Convert text to uppercase (optional)
"""
import sys
def process_file(input_path, output_path, uppercase=False):
"""Process file with specified options."""
with open(input_path) as f:
content = f.read()
if uppercase:
content = content.upper()
with open(output_path, 'w') as f:
f.write(content)
if __name__ == "__main__":
if len(sys.argv) < 3:
print(__doc__) # 打印模块 docstring
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
uppercase = "--uppercase" in sys.argv
process_file(input_file, output_file, uppercase)
print(f"Processed {input_file} -> {output_file}")编写整洁、可读的代码是一项需要通过练习逐步提升的技能。本章中的约定与模式并非随意规则——它们是被证明有效的实践,能让代码更容易理解、维护与调试。随着你编写更多 Python 代码,这些模式会变得自然而然。
请记住:代码被阅读的次数远多于被编写的次数。你多花几秒钟选择清晰的名称、添加有帮助的注释,或正确组织 imports,都能在以后节省数小时的困惑——对你自己以及所有与你的代码一起工作的人都是如此。
在下一章中,我们将探索建立在这些整洁代码实践之上的调试与测试技术,帮助你写出不仅可读,而且正确、可靠的代码。