开发环境与规范
# 第 8 章 开发环境与规范
# 目录介绍
# 8.1 虚拟环境
# 8.1.1 虚拟环境
假设你有两个项目:
- 项目 A(老项目):依赖
Django==3.2 - 项目 B(新项目):依赖
Django==5.0
如果你把它们全装到全局 Python——只有一个 Django 能活下来。这就是"依赖地狱"。
🔑 虚拟环境就是为每个项目创建一个隔离的 Python 空间——每个项目有自己的 python、自己的 pip、自己的依赖包,互不干扰:
全局 Python (/usr/bin/python3)
├── 虚拟环境 A (.venv_a/) ← Django 3.2 + requests 2.28
│ ├── python → /usr/bin/python3(符号链接)
│ └── lib/site-packages/
│ ├── django==3.2
│ └── requests==2.28
│
├── 虚拟环境 B (.venv_b/) ← Django 5.0 + httpx 0.27
│ └── lib/site-packages/
│ ├── django==5.0
│ └── httpx==0.27
│
└── 全局 site-packages/ ← 保持干净,不装项目依赖
2
3
4
5
6
7
8
9
10
11
12
13
# 8.1.2 venv 使用
Python 3.3+ 自带 venv——零安装成本:
# ===== 创建虚拟环境 =====
# 进入项目目录
cd my_project
# 创建虚拟环境(名字通常叫 .venv 或 venv)
python3 -m venv .venv # 点号开头——Git 默认忽略
# 目录结构:
# .venv/
# ├── bin/ (activate, python, pip...)
# ├── lib/ (site-packages 在这里)
# └── pyvenv.cfg
# ===== 激活虚拟环境 =====
# Linux / macOS
source .venv/bin/activate
# Windows
.venv\Scripts\activate
# 激活后终端前会出现 (.venv) 提示符
# (.venv) $ ← 表示当前在虚拟环境中
# ===== 确认环境 =====
which python # 应该指向 .venv/bin/python
which pip # 应该指向 .venv/bin/pip
python --version # 虚拟环境中的 Python 版本
# ===== 退出虚拟环境 =====
deactivate
# ===== 删除虚拟环境(简单粗暴——删目录就行) =====
rm -rf .venv # 不留任何痕迹
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
🔑 .venv 目录不要提交到 Git——加到 .gitignore:
# .gitignore
.venv/
venv/
env/
__pycache__/
*.pyc
.env
2
3
4
5
6
7
VS Code 配置——自动识别 .venv:
// .vscode/settings.json
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.terminal.activateEnvironment": true
}
2
3
4
5
# 8.1.3 pip 依赖
# ===== 基本操作 =====
pip install requests # 安装最新版
pip install requests==2.31.0 # 指定版本
pip install "Django>=4.2,<5.0" # 版本范围
pip install -r requirements.txt # 批量安装
pip uninstall requests -y # 卸载(-y 跳过确认)
pip list # 列出已安装
pip show requests # 查看某个包的详细信息
pip check # 检查依赖冲突
# ===== 导出 / 恢复依赖 =====
pip freeze > requirements.txt # 导出当前环境所有包
pip install -r requirements.txt # 在新环境恢复一模一样的依赖
# requirements.txt 示例:
# Django==5.0.6
# requests==2.31.0
# pandas==2.2.2
# openpyxl==3.1.5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
⚠️ pip freeze 的陷阱——它会导出所有安装的包,包括间接依赖:
# pip freeze 导出的内容——包含了"你没手动装但 A 需要 B 所以自动装了"的包
asgiref==3.8.1 ← Django 的依赖——不是你手动装的
Django==5.0.6 ← ✅ 你手动装的
sqlparse==0.5.1 ← Django 的依赖
tzdata==2024.1 ← 系统时区数据
# 更好的做法:手动维护 requirements.in(顶层依赖)
# Django==5.0.6 ← 只写你直接依赖的包
# requests==2.31.0
# 然后用 pip-compile 自动生成完整的 requirements.txt(见 pip-tools)
2
3
4
5
6
7
8
9
10
🔑 更好的实践——pip-tools:
pip install pip-tools
# 1. 手动维护 requirements.in(只写顶层依赖)
echo "Django>=4.2" > requirements.in
echo "requests" >> requirements.in
# 2. pip-compile 自动生成完整的 requirements.txt(含所有子依赖 + 锁定版本)
pip-compile requirements.in
# 3. 安装
pip-sync requirements.txt # 和 pip install -r 的区别:会卸载不在文件中的包!
2
3
4
5
6
7
8
9
10
11
# 8.1.4 依赖冲突
场景复现:
pip install package-a==1.0 # package-a 需要 requests>=2.25,<2.30
pip install package-b==2.0 # package-b 需要 requests>=2.30
# ❌ ERROR: Cannot install package-a and package-b
# 因为 requests 版本范围冲突:<2.30 vs >=2.30
2
3
4
解决方案:
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
| ① 升级/降级版本 | 能找到兼容版本时 | 低 |
| ② 用虚拟环境隔离 | 两个项目完全独立 | 低 |
③ pip install --force-reinstall | 强制覆盖冲突包(危险) | 中 |
④ 用 pip check 定位冲突 | 快速诊断 | 低 |
# 诊断工具
pip check # 检查当前环境是否有冲突
pipdeptree # 可视化依赖树 ← pip install pipdeptree
pipdeptree -r # 反向依赖树:哪些包依赖了 requests?
2
3
4
# 8.1.5 现代方案
pip + requirements.txt 是传统方案——pipenv 和 poetry 提供了锁定文件 + 自动激活等现代能力:
# ===== pipenv =====
pip install pipenv
pipenv install requests # 安装 + 自动更新 Pipfile
pipenv install --dev pytest # 开发依赖
pipenv shell # 激活虚拟环境(自动创建)
pipenv lock # 生成 Pipfile.lock(确定版本)
pipenv install --deploy # 生产环境:严格按 lock 安装
exit # 退出
# Pipfile 示例:
# [[source]]
# url = "https://pypi.org/simple"
# [packages]
# requests = "*"
# [dev-packages]
# pytest = "*"
# ===== poetry =====(最推荐——全功能项目管理) =====
pip install poetry
poetry new my_project # 创建新项目(含目录结构)
poetry add requests # 添加依赖
poetry add --dev pytest black # 开发依赖
poetry install # 安装所有依赖
poetry shell # 激活虚拟环境
poetry run python main.py # 不激活也能跑
poetry lock # 锁定版本
poetry update # 更新依赖
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
🔑 poetry 比 pip 多了什么:
| 功能 | pip + venv | poetry |
|---|---|---|
| 虚拟环境 | 手动 python -m venv | 自动创建和管理 |
| 依赖声明 | requirements.txt(手动写) | pyproject.toml(结构化) |
| 依赖锁定 | requirements.txt(需手动更新) | poetry.lock(自动锁定子依赖版本) |
| 发布到 PyPI | 手动 setup.py + twine | poetry publish 一条命令 |
| 冲突解决 | 基础 | 约束求解器——更准确地发现/解决冲突 |
# poetry 的 pyproject.toml 示例
# [tool.poetry]
# name = "my_project"
# version = "0.1.0"
#
# [tool.poetry.dependencies]
# python = "^3.10"
# requests = "^2.31"
# pandas = "^2.2"
#
# [tool.poetry.dev-dependencies]
# pytest = "^8.0"
# black = "^24.0"
2
3
4
5
6
7
8
9
10
11
12
13
📌 建议:个人脚本用
pip + venv + requirements.txt(最简);团队项目用 poetry(pyproject.toml + lock)。
# 8.2 代码规范
# 8.2.1 PEP 8 规则
PEP 8 是 Python 官方风格指南——全 Python 社区遵循同一份规范:
# ===== 一、缩进:4 个空格——不用 Tab =====
def calculate_total(items):
total = 0
for item in items: # 4 空格
if item.price > 0: # 8 空格
total += item.price # 12 空格
# ===== 二、行长度:≤ 79 字符(文档 ≤ 72)=====
# ❌ 太长
users = session.query(User).filter(User.age > 18).order_by(User.name.desc()).all()
# ✅ 用括号隐式续行
users = (
session.query(User)
.filter(User.age > 18)
.order_by(User.name.desc())
.all()
)
# ===== 三、空行:函数间 2 行,类内方法间 1 行 =====
import os # import 后 2 行空行
import sys
def func_a(): # 顶层函数间 2 行空行
pass
def func_b():
pass
class MyClass:
def method_a(self): # 类内方法间 1 行空行
pass
def method_b(self):
pass
# ===== 四、空格规则 =====
x = 1 # ✅ 等号两边空格
y = x * 2 + 3 # ✅ 运算符两边空格
d = {"a": 1, "b": 2} # ✅ 冒号后空格,前不要
func(a=1, b=2) # ✅ 关键字参数等号两边无空格
lst = [1, 2, 3] # ✅ 逗号后空格
# x=1 # ❌ 无空格
# ===== 五、import 顺序:标准库 → 第三方 → 本地 =====
# ① 标准库
import os
import sys
from datetime import datetime
# ② 第三方库
import numpy as np
import requests
from openpyxl import load_workbook
# ③ 本地模块
from my_project.models import User
from my_project.utils import helpers
# ===== 六、逗号尾随 =====
# 多行列表最后加逗号——方便用 Git 看每次只改一行
config = {
"host": "localhost",
"port": 8080,
"debug": True, # ← 这个逗号让 git diff 更干净
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 8.2.2 命名约定
| 类型 | 约定 | 示例 |
|---|---|---|
| 变量 / 函数 | snake_case | user_name, calculate_total() |
| 类名 | CamelCase | StudentRecord, BankAccount |
| 常量 | UPPER_SNAKE_CASE | MAX_SIZE, DEFAULT_TIMEOUT |
| 模块 / 文件 | snake_case | user_service.py, data_loader.py |
| 包 / 目录 | snake_case(无下划线更好) | models, data_analysis |
| 私有(约定) | _leading_underscore | _internal_method(), _cache |
| 名称改写 | __dunder(双下划线开头) | __private_attr |
| 避免 | 单字符 l, O, I | 和数字 1/0 难以区分 |
# ✅ PEP 8 风格
class UserProfile:
MAX_LOGIN_ATTEMPTS = 5 # 类常量
def __init__(self, user_name: str, email: str):
self.user_name = user_name # 实例属性
self._login_attempts = 0 # 保护属性
self.__reset_token = None # 名称改写(§4.1.3)
def is_account_locked(self) -> bool: # 布尔方法用 is_ 前缀
return self._login_attempts >= self.MAX_LOGIN_ATTEMPTS
# ❌ 不规范——混合了多种命名风格
class userProfile: # 类名应为 CamelCase
maxLoginAttempts = 5 # 常量应为 UPPER_CASE
def IsAccountLocked(self): # 函数应为 snake_case
...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
🔑 命名不是美学问题——是维护成本问题:
calculate_total_tax_for_order()看一眼就知道做什么cttfo()猜 30 秒——然后钻进去读代码才知道
# 8.2.3 文档规范
Python 的 docstring 用 """...""",写在函数/类/模块的第一行:
def connect_database(host: str, port: int = 5432,
user: str = "admin",
database: str = "main") -> "DatabaseConnection":
"""连接到 PostgreSQL 数据库。
本函数会尝试连接三次,如果全部失败则抛出 DatabaseError。
Args:
host: 数据库主机地址。
port: 端口号,默认 5432。
user: 用户名。
database: 数据库名。
Returns:
DatabaseConnection: 连接对象,可以执行 SQL 查询。
Raises:
ConnectionError: 三次重试后仍然无法连接。
Examples:
>>> conn = connect_database("db.example.com", user="reader")
>>> conn.execute("SELECT COUNT(*) FROM users")
1523
"""
... # 实现略
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
🔑 三种主流风格:
| 风格 | 适用 | 示例 |
|---|---|---|
| Google 风格 | 企业项目 | Args: / Returns: / Raises: ——可读性最好 |
| Sphinx 风格 | 文档生成 | :param: / :return: ——Sphinx 原生 |
| Numpy 风格 | 科学计算 | Parameters\n----------\n |
本教程统一使用 Google 风格——因为它简单、清晰、IDE 支持最好。
# 8.2.4 类型注解
Python 3.5+ 支持类型注解——不会被运行时强制检查,但 IDE 和 mypy 能发现类型错误:
# 基本类型注解
def greet(name: str, age: int) -> str: # 参数类型:冒号后;返回类型:箭头后
return f"{name} 今年 {age} 岁"
# 集合类型(Python 3.9+)
def process_scores(scores: list[int]) -> dict[str, float]:
return {
"max": max(scores),
"avg": sum(scores) / len(scores),
}
# 可选类型
from typing import Optional
def find_user(user_id: int) -> Optional[dict]:
"""可能查不到——返回 None"""
...
# 联合类型(Python 3.10+)
def parse_value(raw: str) -> int | float | str:
try:
return int(raw)
except ValueError:
try:
return float(raw)
except ValueError:
return raw
# 自定义类型别名
from typing import TypeAlias
UserId: TypeAlias = int
JsonDict: TypeAlias = dict[str, "JsonValue"] # 递归类型需加引号
# 实际开发中的完整注解
from dataclasses import dataclass
@dataclass
class Order:
order_id: int
customer: str
amount: float
items: list[str] # 不是 list——IDE 能推断出是字符串列表
paid: bool = False
def get_high_value_orders(orders: list[Order], threshold: float = 1000.0) -> list[Order]:
return [o for o in orders if o.amount >= threshold]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
🔑 类型注解的价值:
# 没有类型注解——IDE 无法推断
def process(data):
data. # ← IDE 无法自动补全!不知道 data 是什么类型
# 有类型注解——IDE 知道一切
def process(data: list[Order]):
data[0].amou # ← IDE 自动补全 .amount!
2
3
4
5
6
7
静态检查——mypy:
pip install mypy
mypy my_project/ # 静态分析——不运行代码就发现类型错误
# mypy 输出示例:
# main.py:15: error: Argument 1 to "greet" has incompatible type "int"; expected "str"
2
3
4
5
# 8.2.5 格式化工具
人工遵守 PEP 8 太费脑——用工具自动格式化:
pip install black flake8 isort mypy pre-commit
# ===== black:代码格式化(不可协商的风格) =====
black . # 格式化当前目录所有 Python 文件
black --check . # 只检查,不改(适合 CI)
black --line-length 100 . # 自定义行宽(默认 88)
# 格式化前:
x = { 'a':1,'b': 2,}
result=some_function(arg1,arg2,arg3, arg4)
# 格式化后:
x = {"a": 1, "b": 2}
result = some_function(arg1, arg2, arg3, arg4)
# ===== isort:import 排序 =====
isort . # 自动排序所有 import
# 排序前:
from my_project.models import User
import os
import requests
from datetime import datetime
# 排序后:
import os
from datetime import datetime
import requests
from my_project.models import User
# ===== flake8:代码检查(发现未使用的 import、未定义的变量等)=====
flake8 . # 检查但不修改
# ===== 一条命令格式化全部 =====
black . && isort . && flake8 .
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
🔑 pre-commit:Git 提交前自动检查——最佳实践:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/psf/black
rev: 24.4.0
hooks:
- id: black
language_version: python3.12
- repo: https://github.com/PyCQA/isort
rev: 5.13.2
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pip install pre-commit
pre-commit install # 安装到当前仓库的 .git/hooks/
# 以后每次 git commit 都会自动跑 black + isort + flake8——
# 不规范就拒绝提交
2
3
4
工具链速查:
| 工具 | 作用 | 是否修改代码 |
|---|---|---|
black | 自动格式化——统一风格 | ✅ 修改 |
isort | import 排序分组 | ✅ 修改 |
flake8 | 检查 PEP 8 违规 | ❌ 只报错 |
mypy | 静态类型检查 | ❌ 只报错 |
pre-commit | Git 提交钩子——串联以上工具 | ❌ 拒绝提交 |
# 8.3 新手陷阱
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 全局 pip install | pip install 不加虚拟环境——污染全局 Python,不同项目的依赖互相冲突 |
| 2 | 忘记激活虚拟环境 | pip install 后发现装到了全局——每次新开终端先 source .venv/bin/activate |
| 3 | pip freeze 导出所有包 | 连间接依赖都锁死——跨平台时包不兼容。用 pip-compile 或只维护顶层依赖 |
| 4 | 命名用 l / O / I | l = 1 看起来像 l = l——永远避免单字符变量使用易混淆字母 |
| 5 | 类型注解只在参数写 list | def f(items: list) 没有元素类型——IDE 不知道 items[0] 是什么。写 list[str] |
陷阱 3 详解——跨平台 freeze 的坑:
# macOS 上 pip freeze 导出
# torch==2.1.0 ← macOS 版
# 到 Linux 服务器 pip install -r requirements.txt
# ERROR: No matching distribution found for torch==2.1.0
# ✅ 解决方案:只写 torch>=2.0(语义化版本),不加平台后缀
2
3
4
5
6
# 8.4 综合思考题
poetry vs pip + venv 的团队规模边界:一个人的项目用
pip + venv够用,5 个人的团队呢?20 个人呢?在多大团队规模下,poetry 的pyproject.toml + lock机制成为必要?类型注解的"假安全":Python 的
list[int]在运行时完全不检查——func([1, "hello"])不会报错。那类型注解的价值到底在哪?为什么 Python 不学 TypeScript 那样强制运行时检查?black 的"不可协商"哲学:black 只暴露了
--line-length一个配置——所有其他风格都不可配置。这引来了很多争论——如果你不喜欢 88 字符的行宽怎么办?这种"独裁式"设计对团队有什么好处?PEP 8 的 trade-off:PEP 8 规定行宽 ≤ 79 字符——这个数字来自 1980 年代的 80 列终端。现在超宽显示器普及,为什么社区仍然建议 79 字符?这和"并排比较两个文件"有什么关系?
依赖锁定 vs 依赖范围:
requests==2.31.0(精确锁定)和requests>=2.31,<3.0(范围声明)——前者保证部署一致性,后者允许安全补丁自动升级。二者各适用于什么场景?在requirements.in+requirements.txt双文件方案中,如何实现"开发锁精确版本,部署留升级余地"?