Python 笔记:工程实践与常见陷阱

5179 字
26 分钟
Python 笔记:工程实践与常见陷阱
  1. 1Python 笔记:给前端开发者的语法对照
  2. 2Python 笔记:工程实践与常见陷阱本文

上一篇只对照了语法。这一篇记工程里真正会用到的东西:项目怎么建、依赖怎么管、模块怎么导入、类型注解能做什么、类怎么写、异常怎么处理,以及一批标准库的速查写法。默认你已经会写 JS,也看过上一篇。

文中命令与输出都在 uv 0.11.7 + Python 3.14 上实际跑过。

uv:把 npm 那一套换过来#

uv 是目前最快的 Python 包管理器,一个工具同时干了 npm、nvm、npx 的活。

创建项目#

Terminal window
uv init my_project # 应用:生成 main.py,可直接运行
uv init --package my_pkg # 库:生成 src/ 布局 + 构建配置
uv init --bare my_project # 只生成 pyproject.toml
uv init --vcs none my_app # 不初始化 git

uv init 默认会 git init,并且生成 .python-version 把 Python 版本钉住,这一点比前端项目更省心。

两种布局的差别:

# uv init my_project(应用)
my_project/
├── .python-version
├── main.py
├── pyproject.toml
└── uv.lock
# uv init --package my_pkg(库,可被 import / 发布)
my_pkg/
├── pyproject.toml
├── uv.lock
└── src/
└── my_pkg/
└── __init__.py

库项目用 src/ 布局,是为了防止“在项目根目录跑脚本时恰好 import 到本地目录”,这一点后面导入系统会细说。

Note

命名规范:目录/包名用 snake_case(my_project),发行名用 kebab-case(my-project),uv 会自动转换。不要用小驼峰,PEP 8 不认。

目录结构与 package.json 对照#

文件 / 目录作用前端对应
pyproject.toml项目元信息 + 依赖声明package.json
uv.lock精确锁定版本,必须提交pnpm-lock.yaml
.python-version钉住解释器版本.nvmrc
.venv/虚拟环境(依赖装在这里),不要提交node_modules/
src/项目源码src/

.venv 里到底有什么:

.venv/
├── pyvenv.cfg # 记录 Python 路径与版本
├── Scripts/ # Windows:可执行文件(python.exe、activate)
└── Lib/site-packages/ # 装好的第三方包

macOS / Linux 上是 bin/ + lib/python3.x/site-packages/。你基本不需要进去看,知道 activate 在哪就行。

常用命令速查#

场景uvnpm / pnpm 对应
初始化uv initnpm init
安装依赖uv add requestspnpm add axios
安装开发依赖uv add --dev pytestpnpm add -D vitest
删除依赖uv remove requestspnpm remove axios
按 lock 安装uv syncpnpm install
运行脚本uv run python app.pynode app.js
运行命令uv run pytestpnpm test
查看依赖树uv treepnpm why
一次性运行工具uvx ruff checknpx eslint
更新 lockuv lockpnpm update
构建产物uv buildpnpm build

uv run:不用再手动激活环境#

Terminal window
uv run python main.py
uv run pytest -q

uv run 会自动:找项目 → 检查 .venv(不存在就创建)→ 按 uv.lock 同步依赖 → 执行命令。

也就是说,大多数时候不需要 source .venv/bin/activate。激活只在你要在同一个 shell 里连续敲很多次 python 时才方便:

Terminal window
source .venv/bin/activate # macOS / Linux
.venv\Scripts\Activate.ps1 # Windows PowerShell

pyproject.toml 长什么样#

[project]
name = "my-project"
version = "0.1.0"
description = "Add your description here"
requires-python = ">=3.14"
dependencies = [
"requests>=2.34.2",
]
[dependency-groups]
dev = [
"pytest>=9.2.0",
"ruff>=0.17.4",
]
[project.scripts]
my-project = "my_project:main" # 装了之后可以直接 my-project 运行

对应关系:dependencies ≈ dependencies,[dependency-groups].dev ≈ devDependencies,[project.scripts] ≈ package.json 的 bin。

单文件脚本:PEP 723 内联依赖#

不想为一个小脚本建整个项目时,用脚本模式:

Terminal window
uv add --script script.py requests

它会在文件头写一段元数据,uv run script.py 时会自动准备依赖:

# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests>=2.34.2",
# ]
# ///
import requests
print(requests.__version__)

临时试一个包也可以,不落盘:

Terminal window
uv run --with requests python -c "import requests; print(requests.__version__)"

国内网络:换镜像源#

pyproject.toml
[[tool.uv.index]]
name = "tuna"
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

或者临时用环境变量:

Terminal window
export UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple
uv add requests

依赖相关最容易踩的坑#

  1. uv.lock 要提交,.venv 不要提交。 .gitignore 里 uv init 已经帮你写好。

  2. 不要混用 pip install。 混用会把包装到 .venv 之外的全局环境,uv run 里根本 import 不到。要么全用 uv add,要么用 uv pip install(明确作用于当前 .venv)。

  3. CI 上用 uv sync --locked。 它要求 uv.lock 与 pyproject.toml 完全一致,不一致直接报错,避免 CI 里悄悄改了依赖版本:

    The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

    本地改完依赖记得先 uv lock 再提交。

  4. uv sync 默认会删掉“不在 lock 里”的包。 想保留手动装进去的东西,加 --inexact。

  5. Python 版本由 .python-version 决定。 换版本用 uv python pin 3.12,uv python list 查看本机已安装版本。

  6. uv init 的 --package 和 --app 不一样。 应用不需要构建后端,库才需要;选错了会多出一堆 build-system 配置。

导入系统:前端最容易栽的地方#

JS 里 import x from "./x.js" 是路径,永远能找到。Python 的 import 是模块搜索,能不能找到取决于两件事:包结构,以及你从哪个目录启动。

模块与包#

project/
└── app/
├── __init__.py # 有这个文件就是“包”
├── main.py # 模块
└── utils.py
app/main.py
from app.utils import hello # 绝对导入(推荐)
from .utils import hello # 相对导入,只能在包内用

__init__.py 的作用:标记目录是包、可以写包的初始化代码。Python 3.3 之后没有它也能当“命名空间包”用,但建议始终加上,行为最可预测。

运行方式决定能不能 import#

同一个文件,换个启动方式结果完全不同:

Terminal window
# 从项目根目录,以模块方式运行:sys.path 起点是当前目录
uv run python -m app.main # ✅ from app.utils import hello 可以
# 直接运行文件:sys.path 起点是这个文件所在目录(app/)
uv run python app/main.py # ❌ ModuleNotFoundError: No module named 'app'

直接运行时,app/ 目录被加进 sys.path,但项目根目录不在里面,所以找不到 app 这个包。改成 from utils import hello 又能跑了——但这只是因为恰好和文件同目录,换到子目录里再 import 就崩。

结论:项目代码统一用 python -m 包.模块 启动,导入统一写绝对路径。

相对导入的报错也很典型:

ImportError: attempted relative import with no known parent package

from .utils import hello 只有在文件被当作包的一部分加载时才成立。直接 python app/rel.py 必然失败,python -m app.rel 就正常。

if __name__ == "__main__"#

def main() -> None:
print("Hello from my-project")
if __name__ == "__main__":
main()

__name__ 在“被直接运行”时是 "__main__",被 import 时是模块名。这段判断的作用等同于“这个文件是入口才执行”,防止被 import 时产生副作用。uv init 生成的 main.py 默认就是这个结构。

导入相关的坑#

  1. 文件名不要和标准库重名。 项目里放一个 json.py、types.py、random.py,import 时就会优先命中你自己的文件,报一堆莫名其妙的错。遇到 AttributeError: module 'json' has no attribute 'loads',先查文件名。

  2. 循环导入。 a.py 导入 b.py,b.py 又导入 a.py,轻则 ImportError,重则拿到半初始化的模块。解法:把公共部分抽到第三个模块,或者把 import 挪进函数内部。

  3. src/ 布局要“安装”才能 import。 库项目在 src/my_pkg/ 下,直接 python 跑根目录脚本是找不到的,需要 uv sync 把项目本身以 editable 模式装进环境(uv init --package 默认就是这么配的)。

  4. sys.path 不是 PYTHONPATH。 想临时加搜索路径用环境变量 PYTHONPATH,不要靠 sys.path.append 硬塞,那会让依赖关系变得不可预测。

类型注解:只给工具看,不约束运行时#

TS 里类型错了编译就过不去;Python 的注解在运行时完全不检查:

def f(n: int) -> int:
return n
print(f("abc")) # abc —— 不报错

注解的价值在于编辑器补全、mypy / pyright 静态检查、以及给读代码的人看。真正要运行时校验,得用 pydantic 之类的库。

常用写法#

from typing import Any, Callable, Iterable, Literal, Protocol, TypedDict
# 3.10+ 直接用 | 表示联合类型,不需要 Optional
def find(name: str | None = None) -> str | None: ...
# 内置泛型,不需要 typing.List / typing.Dict
ids: list[int] = []
mapping: dict[str, int] = {}
pair: tuple[int, str] = (1, "a")
nums: set[int] = set()
# 字面量类型
Mode = Literal["r", "w"]
def open_file(mode: Mode) -> str: ...
# 可调用对象
def apply(fn: Callable[[int], int], x: int) -> int:
return fn(x)

泛型#

from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T | None:
return items[0] if items else None

Python 3.12+ 可以更接近 TS 的写法:

class Box[T]:
def __init__(self, item: T):
self.item = item

Protocol:结构化类型#

TS 的 interface 是“长得像就行”。Python 的 Protocol 就是干这个的,不需要显式继承:

from typing import Protocol
class Reader(Protocol):
def read(self) -> str: ...
class FileSource:
def read(self) -> str:
return "data"
def load(source: Reader) -> str:
return source.read()
load(FileSource()) # 类型检查通过

对比一下:ABC 是“必须继承”,Protocol 是“形状匹配”。写库对外接口时 Protocol 更灵活。

TypedDict 与 dataclass#

字典当对象用时,TypedDict 能给键名和值类型提示:

from typing import TypedDict
class User(TypedDict):
name: str
age: int
user: User = {"name": "Mia", "age": 20}

需要真实对象、有默认值和方法时用 dataclass:

from dataclasses import dataclass, field
@dataclass
class User:
name: str
age: int = 0
tags: list[str] = field(default_factory=list)
u = User("Mia")
u.tags.append("admin")
print(u) # User(name='Mia', age=0, tags=['admin'])

dataclass 会自动生成 __init__、__repr__、__eq__,省掉大量样板代码。列表、字典这类可变默认值必须用 field(default_factory=...),原因和函数默认参数是同一个坑。

加 frozen=True 变成不可变对象,加 slots=True 禁止动态添加属性(更省内存):

@dataclass(frozen=True, slots=True)
class Point:
x: int
y: int

面向对象:和 JS class 的差别#

基础写法上一篇已经对照过,这里只记工程里真正会用到的部分。

构造、继承与 super#

class Animal:
def __init__(self, name: str):
self.name = name
def speak(self) -> str:
return "..."
def __repr__(self) -> str:
return f"<{type(self).__name__} {self.name}>"
class Dog(Animal):
def __init__(self, name: str, breed: str):
super().__init__(name) # 必须手动调用父类构造
self.breed = breed
def speak(self) -> str:
return "woof"

和 JS 不同,子类 __init__ 不会自动调用父类 __init__,忘了写 super().__init__() 就会缺属性。

MRO:多继承的查找顺序#

Python 支持多继承,方法按 MRO(C3 线性化)查找:

class A:
def hi(self): return "A"
class B(A):
def hi(self): return "B" + super().hi()
class C(A):
def hi(self): return "C" + super().hi()
class D(B, C):
def hi(self): return "D" + super().hi()
print(D().hi()) # DBCA
print([c.__name__ for c in D.__mro__]) # ['D', 'B', 'C', 'A', 'object']

实际项目里优先用组合而不是多继承,只有 mixin 场景才值得用它。

抽象类#

from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float: ...
class Square(Shape):
def __init__(self, side: float):
self.side = side
def area(self) -> float:
return self.side ** 2

直接 Shape() 会抛 TypeError,子类没实现抽象方法也一样。相当于 TS 的 abstract class。

property:把方法写成属性#

class Temperature:
def __init__(self, celsius: float):
self._celsius = celsius
@property
def fahrenheit(self) -> float:
return self._celsius * 9 / 5 + 32
@fahrenheit.setter
def fahrenheit(self, value: float) -> None:
self._celsius = (value - 32) * 5 / 9
t = Temperature(0)
print(t.fahrenheit) # 32.0
t.fahrenheit = 212
print(t._celsius) # 100.0

好处是调用方写 t.fahrenheit 而不是 t.get_fahrenheit(),以后加校验也不破坏接口。

classmethod 与 staticmethod#

class Config:
def __init__(self, data: dict):
self.data = data
@classmethod
def from_file(cls, path: str) -> "Config":
return cls({}) # 工厂方法,cls 是类本身
@staticmethod
def parse(text: str) -> dict:
return {} # 不碰 self / cls,纯工具函数

@classmethod 常用于“另一个构造函数”,@staticmethod 相当于挂在类命名空间下的普通函数。

私有属性只是约定#

class P:
def __init__(self):
self._x = 1 # 单下划线:约定私有,外部仍能访问
self.__y = 2 # 双下划线:名字被改写
print(P().__dict__) # {'_x': 1, '_P__y': 2}

__y 实际存成了 _P__y,这是“防子类误覆盖”,不是安全机制。真要隔离靠模块边界,不靠下划线。

枚举#

from enum import Enum, auto
class Color(Enum):
RED = auto()
GREEN = auto()
print(Color.RED.name, Color.RED.value) # RED 1
print(Color(1)) # Color.RED

比用字符串常量安全,拼错了直接 AttributeError。

常用魔法方法#

方法触发时机JS 对应
__init__构造constructor
__repr__print() / 调试toString
__eq__==自定义相等
__len__len(x)length
__iter__ / __next__for ... inSymbol.iterator
__enter__ / __exit__with无直接对应
__call__实例当函数调用函数对象
__getitem__x[key]Proxy / 索引器

异常处理#

异常层级#

BaseException
├── SystemExit / KeyboardInterrupt
└── Exception
├── ValueError / TypeError / KeyError / IndexError
├── LookupError → KeyError, IndexError
├── OSError → FileNotFoundError, PermissionError
└── RuntimeError ...

捕获异常时永远捕获 Exception 的子类,不要用裸 except:——那会把 Ctrl+C 也吞掉,程序都没法正常退出。

基本写法#

try:
value = int(text)
except (TypeError, ValueError) as err: # 可以一次捕多种
print(type(err).__name__, err)
except KeyError:
print("缺键")
else:
print("没异常才执行", value)
finally:
print("无论如何都执行")

else 分支很少用,但比把代码塞进 try 里更清晰:只有 try 块真的可能抛错。

raise 与异常链#

class ConfigError(Exception):
"""配置错误"""
try:
settings = {}
settings["host"]
except KeyError as err:
raise ConfigError("缺少 host 配置") from err

from err 会保留原始异常,traceback 里能看到完整因果链。裸 raise 则用于原样重抛。

两个容易忽略的细节#

# finally 里的 return 会覆盖 try 里的 return
def f():
try:
return "try"
finally:
return "finally"
print(f()) # finally
# assert 会在 python -O 下被整体移除,不要拿它做参数校验
assert user_id > 0, "user_id 必须为正"

assert 只适合“不可能发生”的内部断言;对外部输入用 if ...: raise ValueError(...)。

自定义异常的组织方式#

class AppError(Exception):
"""应用异常基类"""
class NotFoundError(AppError):
pass
class PermissionDeniedError(AppError):
pass

调用方可以按粒度选择:except NotFoundError 精确处理,except AppError 兜底。

常用标准库速查#

pathlib 代替 os.path#

from pathlib import Path
p = Path("data") / "config.json" # 用 / 拼路径,跨平台
print(p.exists(), p.suffix, p.stem, p.parent)
print(p.read_text(encoding="utf-8"))
p.write_text("内容", encoding="utf-8")
for f in Path("src").glob("**/*.py"):
print(f)

读写文件永远显式写 encoding="utf-8",否则 Windows 默认 GBK,中文直接乱码或报 UnicodeDecodeError。

json#

import json
text = json.dumps({"name": "小明"}, ensure_ascii=False)
data = json.loads(text)
print(text) # {"name": "小明"}
print(data["name"]) # 小明

ensure_ascii=False 才会输出中文原文,否则是 \uXXXX。和 JS 不同,json.loads 失败抛 json.JSONDecodeError,不是返回 undefined。

datetime 与时区#

from datetime import datetime, timedelta, timezone
now = datetime.now() # naive,没有时区信息
utc = datetime.now(timezone.utc) # aware
print(now.isoformat(timespec="seconds"))
print((now + timedelta(days=1)).date())
print(datetime.fromtimestamp(1700000000, tz=timezone.utc).isoformat())

最大的坑:naive 和 aware 不能相减,会抛 TypeError: can't subtract offset-naive and offset-aware datetimes。项目里统一用 datetime.now(timezone.utc),展示时再转本地时区。

re#

import re
m = re.match(r"(\d+)-(\d+)", "2026-11")
print(m.group(1), m.group(2)) # 2026 11
print(re.findall(r"\d+", "a1b22")) # ['1', '22']
print(re.sub(r"\s+", " ", " a b ")) # a b

正则字符串一律用 raw string r"...",否则 \d、\s 会被 Python 先转义一遍。

collections#

from collections import Counter, defaultdict, deque, namedtuple
dd = defaultdict(list)
dd["k"].append(1) # 不存在的键自动给默认值
print(Counter("hello").most_common(2)) # [('l', 2), ('h', 1)]
q = deque([1, 2])
q.popleft() # 两端操作都是 O(1)
Point = namedtuple("Point", "x y")
print(Point(1, 2)._asdict()) # {'x': 1, 'y': 2}

defaultdict(list) 比 d.setdefault(k, []).append(v) 干净得多;统计频次别手写循环,用 Counter。

itertools#

import itertools
print(list(itertools.chain([1], [2, 3]))) # [1, 2, 3]
print(list(itertools.islice(itertools.count(0, 2), 5))) # [0, 2, 4, 6, 8]
print(list(itertools.product("ab", repeat=2))) # 笛卡尔积

命令行参数#

import argparse
parser = argparse.ArgumentParser()
parser.add_argument("name")
parser.add_argument("--count", type=int, default=1)
args = parser.parse_args()
print(args.name, args.count)

简单场景直接读 sys.argv[1:],需要 -h、类型转换、必填校验就用 argparse。

日志代替 print#

import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logging.info("服务启动")

print 没有级别、没有时间、没法关。库代码里用 logging.getLogger(__name__),应用入口配一次 basicConfig 即可。

其他常用#

import hashlib, os, shutil, subprocess, tempfile
from pathlib import Path
hashlib.md5(b"abc").hexdigest()
os.getenv("MY_KEY", "default") # 读环境变量
shutil.copy("a.txt", "b.txt") # 复制文件
subprocess.run(["git", "status"], check=True) # 执行外部命令
with tempfile.TemporaryDirectory() as tmp:
print(tmp) # 用完自动清理

subprocess.run 记得加 check=True,否则命令失败也不会抛异常——这大概是 Python 里最容易静默出错的一处。

函数进阶#

位置参数与关键字参数#

def f(a, /, b, *, c):
return a, b, c
f(1, 2, c=3) # ✅
f(a=1, b=2, c=3) # ❌ a 是仅位置参数
f(1, 2, 3) # ❌ c 是仅关键字参数

/ 之前只能按位置传,* 之后只能按关键字传。看到标准库里这种签名不用慌,就是限制调用方式。

默认参数只求值一次#

import time
def stamp(t=time.time()):
return t
print(stamp() == stamp()) # True,两次是同一个时间

默认值在函数定义时计算,不是每次调用。所以默认值必须是不可变对象:

def add(item, items=None):
if items is None:
items = []
items.append(item)
return items

写 items=[] 的话,这个列表会在所有调用之间共享,是 Python 最经典的 bug 之一。

闭包延迟绑定#

fns = [lambda: i for i in range(3)]
print([f() for f in fns]) # [2, 2, 2]
fns = [lambda i=i: i for i in range(3)]
print([f() for f in fns]) # [0, 1, 2]

循环变量在闭包里是引用,等真正调用时循环早结束了。用默认参数把当前值“钉住”。

装饰器#

import functools
def timed(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print("before", fn.__name__)
return fn(*args, **kwargs)
return wrapper
@timed
def add(a, b):
return a + b
print(add(1, 2), add.__name__) # before add / 3 add

functools.wraps 别省,它负责保留原函数的名字和文档,否则调试时所有函数都叫 wrapper。装饰器相当于 JS 里的高阶函数 + 语法糖,Vue 的 @Component 也是同一类思路。

生成器:惰性求值#

def read_lines(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.rstrip()
for line in read_lines("big.log"):
...

生成器一次只产出一个值,处理大文件不会把内存撑爆。注意迭代器只能消费一次:

gen = (x for x in [1, 2])
print(list(gen), list(gen)) # [1, 2] []

异步:对应 async/await#

import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "data"
async def main():
data = await fetch_data()
print(data)
asyncio.run(main())

关键差异:

  • main() 只是创建协程对象,不会执行;必须 asyncio.run() 启动事件循环。
  • 忘记 await 不会报错,只会拿到一个协程对象,然后静默不执行——这是最常见的异步 bug。
  • Promise.all 对应 asyncio.gather:
async def main():
results = await asyncio.gather(fetch_a(), fetch_b())

CPU 密集任务用 multiprocessing,阻塞 IO 用线程池 asyncio.to_thread;async 只解决并发 IO,不解决 CPU 瓶颈。

测试与工具链#

pytest#

tests/test_utils.py
from app.utils import hello
def test_hello():
assert hello() == "hello world"
Terminal window
uv add --dev pytest
uv run pytest -q

测试文件命名 test_*.py,函数命名 test_*,普通 assert 就行,不需要 expect。

导入报错的坑:pytest 默认不会把项目根目录加进 sys.path,from app.xxx import 会 ModuleNotFoundError。在 pyproject.toml 里补一行:

[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]

ruff:一个工具顶 eslint + prettier#

Terminal window
uv add --dev ruff
uv run ruff check . # 检查
uv run ruff check --fix . # 自动修
uv run ruff format . # 格式化
[tool.ruff]
line-length = 100
target-version = "py314"
[tool.ruff.lint]
select = ["E", "F", "I"] # pycodestyle / pyflakes / isort

ruff 用 Rust 写的,速度极快,配置比 eslint 简单得多。

调试#

breakpoint() # 进入 pdb,比 console.log 好用

常用命令:n 下一行、s 进入函数、c 继续、p 变量 打印、q 退出。

踩坑清单#

  1. 可变默认参数在调用间共享 —— 用 None 占位。
  2. is 不是 === —— 它比较对象身份,只用于 is None / is True;值比较用 ==。
  3. 空容器是假值 —— bool([])、bool({})、bool("")、bool(0) 全是 False,而 JS 里 [] 和 {} 是真值。
  4. 字典缺键抛 KeyError —— 不确定就用 .get(key, default);注意键存在但值是 None 时,get 返回 None,默认值不生效。
  5. 字符串不能和数字相加 —— "x" + 1 报 TypeError,要 f"x{1}"。
  6. // 是向下取整不是截断 —— -7 // 2 == -4,int(-7 / 2) == -3。
  7. 浮点数比较别用 == —— 0.1 + 0.2 == 0.3 是 False,用 math.isclose。
  8. round 是银行家舍入 —— round(0.5) == 0、round(2.5) == 2。
  9. 赋值不是复制 —— b = a 之后改 b 会改 a;浅拷贝用 a.copy(),嵌套结构用 copy.deepcopy。
  10. a += [x] 原地修改,a = a + [x] 创建新列表 —— 函数传参时结果完全不同。
  11. 遍历列表时删元素会跳过元素 —— 要过滤就新建列表或用推导式。
  12. 遍历字典时改字典直接抛 RuntimeError —— 先 list(d.items()) 再遍历。
  13. 类变量是所有实例共享的 —— 可变类变量当默认值,会在实例之间串数据。
  14. 子类构造不自动调父类 —— 必须手动 super().__init__(...)。
  15. 闭包捕获的是变量不是值 —— 循环里创建函数要用默认参数固定当前值。
  16. sorted() 返回新列表,list.sort() 原地修改并返回 None —— 写 nums = nums.sort() 会得到 None。
  17. 迭代器/生成器只能消费一次 —— 第二次 list(gen) 是空列表。
  18. 中文标点是语法错误 —— ["a","b"] 报 SyntaxError: invalid character ',' (U+FF0C),只能从编辑器里揪出来。
  19. 文件读写不写 encoding 会乱码 —— 尤其是 Windows。
  20. subprocess.run 不加 check=True 会静默失败 —— 命令挂了你也以为成功。
  21. assert 在 -O 下会被移除 —— 不能用于校验外部输入。
  22. 裸 except: 会吞掉 KeyboardInterrupt —— 至少写 except Exception:。
  23. finally 里的 return 会覆盖 try 的返回值 —— 不要在 finally 里 return。
  24. json.dumps 默认转义中文 —— 加 ensure_ascii=False。
  25. naive 与 aware 时间不能相减 —— 统一用带时区的 datetime。

JS → Python 速查补充#

上一篇没覆盖到的工程部分:

场景JavaScriptPython
项目配置package.jsonpyproject.toml
锁文件pnpm-lock.yamluv.lock
装依赖pnpm add axiosuv add requests
跑脚本node app.jsuv run python app.py
跑命令pnpm testuv run pytest
一次性工具npx eslintuvx ruff
路径拼接path.join(a, b)Path(a) / b
读文件fs.readFileSync(p, "utf8")Path(p).read_text(encoding="utf-8")
解析 JSONJSON.parse(text)json.loads(text)
序列化 JSONJSON.stringify(obj)json.dumps(obj, ensure_ascii=False)
正则/re/gre.compile(r"re")
频率统计手写 reduceCounter(items)
日志console.loglogging.info
测试vitestpytest
Lint + 格式化eslint + prettierruff
抽象类abstract classclass X(ABC) + @abstractmethod
结构化类型interfaceProtocol
泛型<T>TypeVar / class X[T]
私有字段#field_field(约定)
只读属性get x()@property
静态方法static m()@staticmethod
工厂构造static from()@classmethod
模块入口if (require.main === module)if __name__ == "__main__"
并行等待Promise.allasyncio.gather

写到这里,日常写 Python 项目需要查的东西基本齐了。剩下的就是选一个方向——脚本自动化、Web 后端(FastAPI / Django)还是数据处理——在真实项目里把语法磨熟。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Python 笔记:工程实践与常见陷阱
https://blog.ddlyt.top/posts/2026/1109/
作者
兜兜里有糖
发布于
2026-11-09
许可协议
CC BY-NC-ND 4.0

评论区

Profile Image of the Author
兜兜里有糖
越努力,越幸运。
公告
欢迎来到 DBlog,这里记录前端开发、技术学习和一些实用工具。
分类
标签
最新动态
站点统计
文章
21
分类
2
标签
18
总字数
15,966
运行时长
0 天
最后活动
0 天前
访问次数
--
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v2.0.0
文章许可
CC BY-NC-ND 4.0
文章目录