11. Python 的良好编程规范#
在 Python 编程中,良好的编码习惯可以提高代码的可读性、可维护性和运行效率。Python 官方提供了 PEP 8 作为代码风格指南,本章根据该指南以及经验心得总结了下面几点。
11.1. 代码布局#
Note
一些 Python IDE 软件,有可以将代码布局转化为 PEP 8 风格的选项或快捷键
在 Pycharm 中,快捷键为:
Windows/Linux:
Ctrl
+Alt
+L
Mac:
Cmd
+Option
+L
在 Spyder 中,快捷键为:
Windows/Linux:
Ctrl
+Shift
+I
Mac:
Cmd
+Option
+I
11.1.1. 空格与空行的使用#
使用 4 个空格缩进(避免使用 Tab)
因为不同的编辑器和环境可能对 Tab 的显示方式不同(如 2、4、8 个空格)。使用 4 个空格可以确保代码在所有编辑器、终端和操作系统上保持一致的外观。
适当添加空行
类与类外部的函数之间使用两个空行分割
类内部的函数之间使用一个空行分割
类外部的函数之间使用两个空行分割
import 代码块一般与程序的主代码块空一行或两行
可以(谨慎地)使用额外的空行来分隔相关函数以及函数内部中的不同代码块
import os
import sys
# two blank lines above
class MyClass:
def foo1(self):
pass
# functions inside a class are separated by one blank line
def foo2(self):
pass
# class and the outer function is separated by two blank lines
def main():
my_class = MyClass()
my_class.foo1()
if __name__ == "__main__":
main()
适当使用空格
括号紧后面避免使用空格
在逗号和随后的右括号之间使用一个空格
紧接着逗号或冒号之后使用一个空格
但是,在切片中,冒号的作用类似于二元运算符,并且两边的空格数应相等(将其视为优先级最低的运算符),双冒号放在一起时中间的空格应省略。
对于一些二元计算符:
=
,+=
,-=
,<
,==
,>
,>=
,<=
,!=
,在两边分别留一个空格
# Correct:
spam(ham[1], {eggs: 2})
# Wrong:
spam( ham[ 1 ], { eggs: 2 } )
# Correct:
foo = (0,)
# Wrong:
bar = (0, )
# Correct:
if x == 4:
print(x, y)
# Wrong:
if x == 4 :
print(x , y)
# Correct:
ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:]
ham[lower:upper], ham[lower:upper:], ham[lower::step]
ham[lower+offset : upper+offset]
ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)]
ham[lower + offset : upper + offset]
# Wrong:
ham[lower + offset:upper + offset]
ham[1: 9], ham[1 :9], ham[1:9 :3]
ham[lower : : step]
ham[ : upper]
如果使用具有不同优先级的运算符,请考虑在优先级最低的运算符周围添加空格。请自行判断;但是,切勿使用超过一个空格,并且二元运算符两侧的空格量应始终相同:
避免使用行尾空格
# Correct:
i = i + 1
submitted += 1
x = x*2 - 1
hypot2 = x*x + y*y
c = (a+b) * (a-b)
# Wrong:
i=i+1
submitted +=1
x = x * 2 - 1
hypot2 = x * x + y * y
c = (a + b) * (a - b)
函数调用的参数赋值等号
=
左右不应加空格变量赋值
=
左右应加空格
func(arg=1) # Correct (PEP 8 compliant)
func(arg = 1) # Incorrect
x = 10 # Correct
x=10 # Incorrect
11.1.2. 代码行长度、换行与模块导入#
每行代码长度不超过 79 个字符
应该在二元运算符之前换行,而不是之后
# Wrong:
# operators sit far away from their operands
income = (gross_wages +
taxable_interest +
(dividends - qualified_dividends) -
ira_deduction -
student_loan_interest)
# Correct:
# easy to match operators with operands
income = (gross_wages
+ taxable_interest
+ (dividends - qualified_dividends)
- ira_deduction
- student_loan_interest)
单独导入通常应该放在单独的行,或者使用 from 导入多个 module
# Correct:
import os
import sys
# Wrong:
import sys, os
# Correct:
from subprocess import Popen, PIPE
导入总是放在文件的顶部,紧接着任何模块注释和文档字符串,以及模块全局变量和常量之前。
导入应按以下顺序分组:
标准库导入。
相关第三方进口。
本地应用程序/库特定的导入。
在每组导入之间放置一个空行。
# Standard library imports
import os
import sys
import datetime
# Related third-party imports
import numpy as np
import pandas as pd
import requests
# Local application/library-specific imports
from my_project.utils import helper_function
from my_project.models import UserModel
from my_project.config import settings
11.2. 命名规范#
避免使用 python 的保留关键字命名
变量使用小写+下划线(snake_case)
user_age = 25
max_length = 100
常量通常在模块级别定义,全部用大写字母书写,并用下划线分隔单词
MAX_LIMIT = 500
类名使用大写开头的驼峰命名(PascalCase)
class UserProfile:
pass
模块名称应简短,且全部小写,可以在模块名称中使用下划线提高可读性。Python 包名称也应简短,且全部小写,但不鼓励使用下划线。
如果函数参数的名称与保留关键字或函数外变量名字冲突,通常最好在末尾添加一个下划线
name = "Global Variable"
def greet(name_):
"""Function to greet a user, avoiding causing confusion with the outer variable."""
print(f"Hello, {name_}!")
greet("Alice")
使用有意义的变量名,提高代码可读性
# 不推荐
x = 10
y = 20
# 推荐
width = 10
height = 20
使用
_
作为临时变量(不需要使用的变量)
for _ in range(5):
print("Hello")
长变量名尽量避免缩写,除非是常见缩写
# 不推荐
usr_nm = "Alice"
# 推荐
user_name = "Alice"
始终用 self 作为实例方法的第一个参数, 始终用 cls 作为类方法的第一个参数。
11.3. 注释与文档字符串(Docstring)#
注释应为完整的句子。第一个单词应大写,除非它是一个以小写字母开头的标识符(变量、类、关键字等)的名字。
行内注释应与语句至少相隔两个空格,应以 # 和一个空格开头,可以省略句号
块注释通常由一个或多个由完整句子构成的段落组成,每个句子以句号结尾。
# This function calculates the area of a circle.
# It takes the radius as input and returns the computed area.
def calculate_area(radius):
"""Compute the area of a circle using the formula πr²."""
PI = 3.14159 # Constant value of π, approximated to five decimal places
area = PI * radius * radius # Calculate the area using the formula
return area # Return the computed area.
# The main execution block starts here.
if __name__ == "__main__":
radius = 5 # Define the radius of the circle
area = calculate_area(radius) # Call the function to compute the area
print(f"The area of the circle with radius {radius} is {area:.2f}") # Display the result
为所有公共模块、函数、类和方法编写文档字符串。非公共方法不需要文档字符串,但应该添加注释来描述该方法的作用
对于多行文档字符串,””” 一般单独占一行
def add(a: int, b: int) -> int:
"""
返回两个数的和。
Args:
a (int): 第一个数
b (int): 第二个数
return:
int: 两数之和
"""
return a + b
对于单行文档字符串,请保持 “”” 的开始和结尾在同一行
"""Return an ex-parrot."""
11.4. 其他一些推荐#
11.4.1. 使用 f-string 进行字符串格式化#
相比 %
或 .format()
,f-string
更直观、性能更好:
name = "Alice"
age = 25
print(f"My name is {name} and I am {age} years old.")
My name is Alice and I am 25 years old.
a = 1
b = 4.56
# 第一个数保留 3 位小数,浮点型,第二个数右对齐,宽度 20,保留 2 位小数,百分数显示
f'the numbers are {a:.3f} and {b = :>20.2%}'
'the numbers are 1.000 and b = 456.00%'
11.4.2. 使用 with 语句管理资源#
当操作文件,一些并行计算操作、并行计算或连接数据库时,使用 with 语句,避免手动 close()。
# 不需要手动添加 file.close()
with open("file.txt", "r") as file:
content = file.read()
from multiprocessing import Pool
import os
def worker(n):
return f"Process {os.getpid()} computed {n * n}"
if __name__ == '__main__':
# 使用 with 语句管理进程池
with Pool(processes = 4) as pool:
results = pool.map(worker, range(5))
# 进程池自动关闭,不需要手动 pool.close() 和 pool.join()
for res in results:
print(res)
11.4.3. 使用列表推导式提高代码效率#
列表可以直接在 [] 中使用 for 语句赋值。
# 不推荐
squares = []
for i in range(10):
squares.append(i**2)
# 推荐
squares = [i**2 for i in range(10)]
11.4.4. 使用enumerate()
遍历列表#
为了减少代码以及提高代码的可读性,用enumerate()
遍历替代 range(len(list)) 遍历列表
names = ['lily', 'tom']
# 不推荐
for i in range(len(names)):
print(i, names[i])
# 推荐
for i, name in enumerate(names):
print(i, name)
0 lily
1 tom
0 lily
1 tom
但是,若要获取索引修改列表中的元素,仍然需要 range(len(list))。
names = ['lily', 'tom']
for i in range(len(names)):
names[i] = 'john_' + str(i)
print(i, names[i])
0 john_0
1 john_1
11.4.5. 使用zip()
遍历多个列表#
names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]
# zip() 是 Python 内置函数,它用于并行遍历多个可迭代对象,将其元素逐一配对,返回一个迭代器
for name, age in zip(names, ages):
print(f"{name} is {age} years old.")
Alice is 25 years old.
Bob is 30 years old.
Charlie is 35 years old.
与 enumerate 相似的是,若要修改原始列表中的元素,zip 就不适用了。
11.4.6. 使用__name__ == "__main__"
保护代码#
在 Python 中,if name == “main”: 主要用于防止某些代码在导入时被执行,尤其是在多进程 (multiprocessing)、脚本执行、模块重用等场景下非常重要。
下面这段多进程的代码,如果没有 if name == “main”: 语句,则会出错。
from multiprocessing import Pool
import os
def worker(n):
return f"Process {os.getpid()} computed {n * n}"
if __name__ == '__main__':
# 使用 with 语句管理进程池
with Pool(processes = 4) as pool:
results = pool.map(worker, range(5))
# 进程池自动关闭,不需要手动 pool.close() 和 pool.join()
for res in results:
print(res)
11.5. 开发大型程序时使用logging
代替 print()#
Python 的 logging 模块是一个强大且灵活的日志系统,相比 print(),它能更好地管理日志,尤其适用于生产环境、调试、错误追踪、多线程等场景。
在小型脚本中 print() 可能够用,但在生产环境或大项目中,logging 是更好的选择。
printnt()只能输出到控制台,而 logging
可以:
保存日志到文件
设置不同的日志级别(INFO, ERROR, DEBUG 等)
灵活配置输出格式(时间、文件、行号等)
支持多线程/多进程
import logging
logging.basicConfig(level = logging.INFO)
logging.info("This is an info message")
INFO:root:This is an info message
11.5.1. 支持不同级别的日志#
logging
提供 5 种日志级别:
级别 |
方法 |
用途 |
---|---|---|
|
|
详细调试信息 |
|
|
普通信息 |
|
|
警告(默认级别) |
|
|
错误信息 |
|
|
严重错误 |
import logging
logging.basicConfig(level = logging.DEBUG)
logging.debug("Debugging info")
logging.info("General info")
logging.warning("Warning message")
logging.error("Error message")
logging.critical("Critical issue")
INFO:root:General info
WARNING:root:Warning message
ERROR:root:Error message
CRITICAL:root:Critical issue
11.5.2. 日志可以保存到文件#
在 print() 中,所有信息只能输出到控制台,一旦关闭终端,信息就丢失了。而 logging
可以写入文件,方便后续分析。
import logging
logging.basicConfig(filename="app.log", level=logging.INFO)
logging.info("This is an info message")
logging.error("This is an error message")
日志会被写入 app.log
文件。
11.5.3. 日志格式可自定义#
import logging
logging.basicConfig(
format = "%(asctime)s - %(levelname)s - %(message)s",
level = logging.INFO
)
logging.info("Application started")
logging.error("Something went wrong")
输出类似于:
2025-02-14 12:00:01,234 - INFO - Application started
2025-02-14 12:00:01,235 - ERROR - Something went wrong
日志格式字段说明:
字段 |
说明 |
---|---|
|
时间 |
|
日志级别 |
|
日志内容 |
|
记录日志的文件 |
|
行号 |
11.5.4. logging
可以用于多模块#
在大型项目中,多个 Python 文件可能需要共享日志系统,print() 无法做到这一点,而 logging
可以。
import logging
import module
logging.basicConfig(level=logging.INFO)
logging.info("Starting app...")
module.some_function()
文件 module.py
import logging
def some_function():
logging.info("This is from module.py")
运行 python app.py
后:
INFO:root:Starting app...
INFO:root:This is from module.py
多个模块共享同一个 logging
配置,比 print() 更方便管理。
11.5.5. logging
适用于多线程#
print() 在多线程环境下可能会导致日志混乱,但 logging
是线程安全的。
import logging
import threading
logging.basicConfig(level=logging.INFO, format="%(threadName)s: %(message)s")
def worker():
logging.info("Worker thread executing")
if __name__ == '__main__':
threads = []
for _ in range(5):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
输出:
Thread-1: Worker thread executing
Thread-2: Worker thread executing
Thread-3: Worker thread executing
Thread-4: Worker thread executing
Thread-5: Worker thread executing
日志不会相互覆盖,也不会丢失。