Python 笔记:工程实践与常见陷阱
- 1Python 笔记:给前端开发者的语法对照
- 2Python 笔记:工程实践与常见陷阱本文
上一篇只对照了语法。这一篇记工程里真正会用到的东西:项目怎么建、依赖怎么管、模块怎么导入、类型注解能做什么、类怎么写、异常怎么处理,以及一批标准库的速查写法。默认你已经会写 JS,也看过上一篇。
文中命令与输出都在 uv 0.11.7 + Python 3.14 上实际跑过。
uv:把 npm 那一套换过来
uv 是目前最快的 Python 包管理器,一个工具同时干了 npm、nvm、npx 的活。
创建项目
uv init my_project # 应用:生成 main.py,可直接运行uv init --package my_pkg # 库:生成 src/ 布局 + 构建配置uv init --bare my_project # 只生成 pyproject.tomluv init --vcs none my_app # 不初始化 gituv 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 到本地目录”,这一点后面导入系统会细说。
命名规范:目录/包名用 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 在哪就行。
常用命令速查
| 场景 | uv | npm / pnpm 对应 |
|---|---|---|
| 初始化 | uv init | npm init |
| 安装依赖 | uv add requests | pnpm add axios |
| 安装开发依赖 | uv add --dev pytest | pnpm add -D vitest |
| 删除依赖 | uv remove requests | pnpm remove axios |
| 按 lock 安装 | uv sync | pnpm install |
| 运行脚本 | uv run python app.py | node app.js |
| 运行命令 | uv run pytest | pnpm test |
| 查看依赖树 | uv tree | pnpm why |
| 一次性运行工具 | uvx ruff check | npx eslint |
| 更新 lock | uv lock | pnpm update |
| 构建产物 | uv build | pnpm build |
uv run:不用再手动激活环境
uv run python main.pyuv run pytest -quv run 会自动:找项目 → 检查 .venv(不存在就创建)→ 按 uv.lock 同步依赖 → 执行命令。
也就是说,大多数时候不需要 source .venv/bin/activate。激活只在你要在同一个 shell 里连续敲很多次 python 时才方便:
source .venv/bin/activate # macOS / Linux.venv\Scripts\Activate.ps1 # Windows PowerShellpyproject.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 内联依赖
不想为一个小脚本建整个项目时,用脚本模式:
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__)临时试一个包也可以,不落盘:
uv run --with requests python -c "import requests; print(requests.__version__)"国内网络:换镜像源
[[tool.uv.index]]name = "tuna"url = "https://pypi.tuna.tsinghua.edu.cn/simple"default = true或者临时用环境变量:
export UV_DEFAULT_INDEX=https://pypi.tuna.tsinghua.edu.cn/simpleuv add requests依赖相关最容易踩的坑
-
uv.lock要提交,.venv不要提交。.gitignore里uv init已经帮你写好。 -
不要混用
pip install。 混用会把包装到.venv之外的全局环境,uv run里根本 import 不到。要么全用uv add,要么用uv pip install(明确作用于当前.venv)。 -
CI 上用
uv sync --locked。 它要求uv.lock与pyproject.toml完全一致,不一致直接报错,避免 CI 里悄悄改了依赖版本:The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.本地改完依赖记得先
uv lock再提交。 -
uv sync默认会删掉“不在 lock 里”的包。 想保留手动装进去的东西,加--inexact。 -
Python 版本由
.python-version决定。 换版本用uv python pin 3.12,uv python list查看本机已安装版本。 -
uv init的--package和--app不一样。 应用不需要构建后端,库才需要;选错了会多出一堆build-system配置。
导入系统:前端最容易栽的地方
JS 里 import x from "./x.js" 是路径,永远能找到。Python 的 import 是模块搜索,能不能找到取决于两件事:包结构,以及你从哪个目录启动。
模块与包
project/└── app/ ├── __init__.py # 有这个文件就是“包” ├── main.py # 模块 └── utils.pyfrom app.utils import hello # 绝对导入(推荐)from .utils import hello # 相对导入,只能在包内用__init__.py 的作用:标记目录是包、可以写包的初始化代码。Python 3.3 之后没有它也能当“命名空间包”用,但建议始终加上,行为最可预测。
运行方式决定能不能 import
同一个文件,换个启动方式结果完全不同:
# 从项目根目录,以模块方式运行: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 packagefrom .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 默认就是这个结构。
导入相关的坑
-
文件名不要和标准库重名。 项目里放一个
json.py、types.py、random.py,import 时就会优先命中你自己的文件,报一堆莫名其妙的错。遇到AttributeError: module 'json' has no attribute 'loads',先查文件名。 -
循环导入。
a.py导入b.py,b.py又导入a.py,轻则ImportError,重则拿到半初始化的模块。解法:把公共部分抽到第三个模块,或者把 import 挪进函数内部。 -
src/布局要“安装”才能 import。 库项目在src/my_pkg/下,直接python跑根目录脚本是找不到的,需要uv sync把项目本身以 editable 模式装进环境(uv init --package默认就是这么配的)。 -
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+ 直接用 | 表示联合类型,不需要 Optionaldef find(name: str | None = None) -> str | None: ...
# 内置泛型,不需要 typing.List / typing.Dictids: 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 NonePython 3.12+ 可以更接近 TS 的写法:
class Box[T]: def __init__(self, item: T): self.item = itemProtocol:结构化类型
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
@dataclassclass 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()) # DBCAprint([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.0t.fahrenheit = 212print(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 1print(Color(1)) # Color.RED比用字符串常量安全,拼错了直接 AttributeError。
常用魔法方法
| 方法 | 触发时机 | JS 对应 |
|---|---|---|
__init__ | 构造 | constructor |
__repr__ | print() / 调试 | toString |
__eq__ | == | 自定义相等 |
__len__ | len(x) | length |
__iter__ / __next__ | for ... in | Symbol.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 errfrom err 会保留原始异常,traceback 里能看到完整因果链。裸 raise 则用于原样重抛。
两个容易忽略的细节
# finally 里的 return 会覆盖 try 里的 returndef 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, tempfilefrom 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
@timeddef add(a, b): return a + b
print(add(1, 2), add.__name__) # before add / 3 addfunctools.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
from app.utils import hello
def test_hello(): assert hello() == "hello world"uv add --dev pytestuv 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
uv add --dev ruffuv run ruff check . # 检查uv run ruff check --fix . # 自动修uv run ruff format . # 格式化[tool.ruff]line-length = 100target-version = "py314"
[tool.ruff.lint]select = ["E", "F", "I"] # pycodestyle / pyflakes / isortruff 用 Rust 写的,速度极快,配置比 eslint 简单得多。
调试
breakpoint() # 进入 pdb,比 console.log 好用常用命令:n 下一行、s 进入函数、c 继续、p 变量 打印、q 退出。
踩坑清单
- 可变默认参数在调用间共享 —— 用
None占位。 is不是===—— 它比较对象身份,只用于is None/is True;值比较用==。- 空容器是假值 ——
bool([])、bool({})、bool("")、bool(0)全是False,而 JS 里[]和{}是真值。 - 字典缺键抛
KeyError—— 不确定就用.get(key, default);注意键存在但值是None时,get返回None,默认值不生效。 - 字符串不能和数字相加 ——
"x" + 1报TypeError,要f"x{1}"。 //是向下取整不是截断 ——-7 // 2 == -4,int(-7 / 2) == -3。- 浮点数比较别用
==——0.1 + 0.2 == 0.3是False,用math.isclose。 round是银行家舍入 ——round(0.5) == 0、round(2.5) == 2。- 赋值不是复制 ——
b = a之后改b会改a;浅拷贝用a.copy(),嵌套结构用copy.deepcopy。 a += [x]原地修改,a = a + [x]创建新列表 —— 函数传参时结果完全不同。- 遍历列表时删元素会跳过元素 —— 要过滤就新建列表或用推导式。
- 遍历字典时改字典直接抛
RuntimeError—— 先list(d.items())再遍历。 - 类变量是所有实例共享的 —— 可变类变量当默认值,会在实例之间串数据。
- 子类构造不自动调父类 —— 必须手动
super().__init__(...)。 - 闭包捕获的是变量不是值 —— 循环里创建函数要用默认参数固定当前值。
sorted()返回新列表,list.sort()原地修改并返回None—— 写nums = nums.sort()会得到None。- 迭代器/生成器只能消费一次 —— 第二次
list(gen)是空列表。 - 中文标点是语法错误 ——
["a","b"]报SyntaxError: invalid character ',' (U+FF0C),只能从编辑器里揪出来。 - 文件读写不写
encoding会乱码 —— 尤其是 Windows。 subprocess.run不加check=True会静默失败 —— 命令挂了你也以为成功。assert在-O下会被移除 —— 不能用于校验外部输入。- 裸
except:会吞掉KeyboardInterrupt—— 至少写except Exception:。 finally里的return会覆盖try的返回值 —— 不要在finally里return。json.dumps默认转义中文 —— 加ensure_ascii=False。- naive 与 aware 时间不能相减 —— 统一用带时区的
datetime。
JS → Python 速查补充
上一篇没覆盖到的工程部分:
| 场景 | JavaScript | Python |
|---|---|---|
| 项目配置 | package.json | pyproject.toml |
| 锁文件 | pnpm-lock.yaml | uv.lock |
| 装依赖 | pnpm add axios | uv add requests |
| 跑脚本 | node app.js | uv run python app.py |
| 跑命令 | pnpm test | uv run pytest |
| 一次性工具 | npx eslint | uvx ruff |
| 路径拼接 | path.join(a, b) | Path(a) / b |
| 读文件 | fs.readFileSync(p, "utf8") | Path(p).read_text(encoding="utf-8") |
| 解析 JSON | JSON.parse(text) | json.loads(text) |
| 序列化 JSON | JSON.stringify(obj) | json.dumps(obj, ensure_ascii=False) |
| 正则 | /re/g | re.compile(r"re") |
| 频率统计 | 手写 reduce | Counter(items) |
| 日志 | console.log | logging.info |
| 测试 | vitest | pytest |
| Lint + 格式化 | eslint + prettier | ruff |
| 抽象类 | abstract class | class X(ABC) + @abstractmethod |
| 结构化类型 | interface | Protocol |
| 泛型 | <T> | TypeVar / class X[T] |
| 私有字段 | #field | _field(约定) |
| 只读属性 | get x() | @property |
| 静态方法 | static m() | @staticmethod |
| 工厂构造 | static from() | @classmethod |
| 模块入口 | if (require.main === module) | if __name__ == "__main__" |
| 并行等待 | Promise.all | asyncio.gather |
写到这里,日常写 Python 项目需要查的东西基本齐了。剩下的就是选一个方向——脚本自动化、Web 后端(FastAPI / Django)还是数据处理——在真实项目里把语法磨熟。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!






























































































































































