从 JS/TS 到 Python:前端开发者的无缝迁移指南
这是一篇专门写给 已经熟悉 JS/TS/Node.js,但几乎没有 Python 经验 的开发者的迁移指南。
你不会在这里看到大量脱离上下文的“语法背诵”。相反,我会始终站在前端工程师的视角,把 Python 和你已经熟悉的 JS/TS 世界放在一起比较:
- 这个概念在 JS/TS 里是什么
- Python 里对应的写法是什么
- 两者的设计差异在哪里
- 哪些地方最容易带着 JS 思维误用 Python
如果你已经能熟练写 Node.js、JavaScript、TypeScript 类型,本文的目标不是让你“重新学编程”,而是让你 快速建立 Python 心智模型,尽可能低成本地把已有经验迁移过去。
这篇文章会覆盖什么
本文会从基础一路讲到进阶,覆盖这些核心主题:
- 基础
- 函数
- 作用域
- 模块
- 面向对象
- 错误与调试
- IO 编程(本篇跳过,留到 Node.js vs Python 专题展开)
- 进程、线程与协程
- 常用方法与内置模块
- 常用官方/第三方模块
你可以把它理解成一份 Python 版的“前端开发者迁移手册”。
参考资料
文章主要以js/ts为视角,但如果你想继续深挖,还是无法避开实操和啃官方文档:
- Python 官方文档:https://docs.python.org/3/
- Python Tutorial:https://docs.python.org/3/tutorial/
- Python 标准库:https://docs.python.org/3/library/
- Real Python:https://realpython.com/
下面正式进入第一部分:基础。
基础
数据类型和变量
如果你来自 JS/TS,那么 Python 的基础类型并不陌生。真正需要适应的,不是“有没有这些类型”,而是:
- Python 的内置类型更鲜明,语义更稳定
- 很多操作是“返回新值”而不是“原地修改”
- 某些看起来像 JS 的东西,行为细节其实完全不同
先看一个整体对照。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 数字 | number、bigint |
int、float、complex |
Python 的 int 没有 JS number 的 53 位整数精度限制 |
| 字符串 | string、模板字符串 |
str、f-string |
Python 字符串不可变,格式化更常用 f"{name}" |
| 数组 | Array |
list |
Python list 很像 JS 数组,但没有 map/filter/reduce 那么中心化 |
| 只读数组/常量数组 | readonly T[]、as const |
tuple |
tuple 是不可变序列,但不等于 TS 的类型级只读数组 |
| 对象/映射 | Object、Map |
dict |
Python dict 更接近“语言级 Map”,而不是普通对象字面量 |
| 集合 | Set |
set |
两边都用于去重和集合运算,但 Python 集合运算更原生 |
| 布尔值 | boolean |
bool |
Python 的 True/False 首字母大写 |
| 空值 | null、undefined |
None |
Python 只有一个“空值语义中心”:None |
| 变量声明 | let / const / var |
直接赋值 | Python 没有 const,约定大写常量只是约定,不是强约束 |
标准数据类型
Python 常用内置类型可以粗分为这几类:
- 数字:
int、float - 字符串:
str - 序列:
list、tuple - 映射:
dict - 集合:
set - 布尔和空值:
bool、NoneType
你可以先把它们理解为 JS/TS 世界里这些角色:
int / float~=numberstr~=stringlist~=Arraytuple~= “不可变数组”dict~=Object + Map的合体,但更偏Mapset~=Set
数字
JS 里大部分数字默认都是 number,底层是双精度浮点数。这导致一个典型问题:大整数精度。
const a = 9007199254740991;
const b = a + 1;
const c = a + 2;
console.log(b === c); // true,已经超过安全整数范围
a = 9007199254740991
b = a + 1
c = a + 2
print(b == c) # False,Python int 默认支持任意精度
这件事对前端开发者非常重要。你在 JS 里对“大整数”天然警惕,但在 Python 里,int 的体验通常更自然。
避坑警告:不要把 Python 的
int想成 JS 的number。
在 Python 里,整数和浮点数的边界更清晰;很多本来在 JS 里需要BigInt或额外处理的场景,在 Python 里直接就是普通整数运算。
再看除法差异:
console.log(5 / 2); // 2.5
console.log(Math.floor(5 / 2)); // 2
print(5 / 2) # 2.5,普通除法
print(5 // 2) # 2,整除
Python 的 // 是高频运算符,JS 开发者要尽快熟悉。
字符串
两边字符串都支持拼接、切片、插值,但 Python 的字符串操作更“脚本语言化”一些。
const name = "Alice";
const age = 18;
const message = `name=${name}, age=${age}`;
console.log(message);
console.log(name.toUpperCase());
console.log(name.slice(1, 3));
name = "Alice"
age = 18
message = f"name={name}, age={age}"
print(message)
print(name.upper())
print(name[1:3])
几个你需要立刻建立的映射:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 字符串插值 | 模板字符串 `hello ${name}` |
f"hello {name}" |
Python 最常用的是 f-string |
| 长度 | str.length |
len(str) |
Python 倾向函数式风格,不是实例属性 |
| 大写 | str.toUpperCase() |
str.upper() |
方法名风格不同 |
| 切片 | slice(start, end) |
s[start:end] |
Python 切片语法更核心、更通用 |
避坑警告:Python 没有
str.length。
看到“长度”就优先想到len(x),不只是字符串,列表、元组、字典、集合都适用。
列表:对应 JS 数组
如果只从“能装一组值”来看,Python 的 list 很像 JS Array。
const arr = [1, 2, 3];
arr.push(4);
console.log(arr[0]); // 1
console.log(arr.length); // 4
console.log(arr.slice(1)); // [2, 3, 4]
arr = [1, 2, 3]
arr.append(4)
print(arr[0]) # 1
print(len(arr)) # 4
print(arr[1:]) # [2, 3, 4]
但 Python list 和 JS Array 的使用习惯并不完全一样。JS 开发者常常会下意识去找:
mapfilterreducefind
Python 当然也能做这些事,但更常见的写法往往是:
for循环- 列表推导式
- 内置函数如
sum()、any()、all()
例如做一个“所有元素乘 2”的新数组:
const nums = [1, 2, 3, 4];
const doubled = nums.map((n) => n * 2);
console.log(doubled); // [2, 4, 6, 8]
nums = [1, 2, 3, 4]
doubled = []
for n in nums:
doubled.append(n * 2)
print(doubled) # [2, 4, 6, 8]
上面这段是最容易从 JS 迁移过来的写法。如果你把它看懂了,再看 Python 里更常见的日常写法:
nums = [1, 2, 3, 4]
doubled = [n * 2 for n in nums]
print(doubled) # [2, 4, 6, 8]
这不是“缩写”或者“黑魔法”,而是 Python 里非常正常、非常常见的 列表推导式。
你可以先把它机械地翻译成一句话:
从
nums里依次取出每个n,把n * 2的结果放进一个新列表里。
如果硬要类比 JS,它最接近:
const doubled = nums.map((n) => n * 2);
只是 Python 没有把这种场景完全押注在 map() 上,而是提供了一个更原生、更高频的语法形式。
元组:对应“不可变数组”
元组 tuple 可以理解成“不可变的序列”。
const point = [10, 20] as const;
// point[0] = 30; // TypeScript 会报错
point = (10, 20)
# point[0] = 30 # 会直接抛异常:tuple 不支持修改
JS/TS 里“只读数组”很多时候是类型层面的限制;Python 的 tuple 则是 运行时不可变。
这带来两个很实用的结论:
- 你想表达“这组数据不应该被改”,可以考虑
tuple - 你想把一组值当作字典 key,通常需要它是不可变的
字典:对应 Object / Map
如果你把 Python 的 dict 想成 JS 对象,会先上手;但如果你把它理解成“语言内建的 Map”,会更接近真实使用方式。
const user = {
name: "Alice",
age: 18,
};
console.log(user.name);
console.log(user["age"]);
user = {
"name": "Alice",
"age": 18,
}
print(user["name"])
print(user["age"])
表面上很像,但有几个关键差异:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 对象字面量键 | 可省略引号:{ name: "a" } |
键通常显式写字符串:{"name": "a"} |
Python 不支持 JS 这种对象属性名简写语法 |
| 属性访问 | obj.name |
dict["name"] |
Python 字典主要不是通过点语法取值 |
| 是否更像 Map | Object 不完全是 Map |
dict 天然就是映射结构 |
Python 中 dict 是极核心的数据结构 |
| 取默认值 | obj.x ?? defaultValue |
dict.get("x", default) |
Python 常用 get() 防止 KeyError |
const user = { name: "Alice" };
console.log(user.city ?? "Unknown");
user = {"name": "Alice"}
print(user.get("city", "Unknown"))
避坑警告:
dict不是 JS 普通对象。
最常见误区是写出user.name这种思维定式。除非你用的是自定义类对象,否则字典取值应优先使用user["name"]或user.get("name")。
集合:对应 JS Set
集合 set 用于去重、成员判断、交并差集运算。
const ids = new Set([1, 2, 2, 3]);
console.log(ids.has(2)); // true
console.log([...ids]); // [1, 2, 3]
ids = {1, 2, 2, 3}
print(2 in ids) # True
print(ids) # {1, 2, 3}
Python 在集合运算上更直接:
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);
const intersection = [...a].filter((x) => b.has(x));
console.log(intersection); // [3]
a = {1, 2, 3}
b = {3, 4, 5}
print(a & b) # 交集: {3}
print(a | b) # 并集: {1, 2, 3, 4, 5}
print(a - b) # 差集: {1, 2}
如果你经常在前端里做“去重”“权限集合判断”“标签集合交集”,Python 的 set 会非常顺手。
布尔值、空值 None
JS/TS 的空值世界比较复杂:
nullundefined- falsy
false/0/""/''/null/undefined/NaN/ truthy"0"/"false"/[]/{}/function(){}/new Boolean(false) ==和===
Python 则更统一一些:
- 布尔值是
True和False - 空值核心是
None - 判断空值通常用
is None
let value: string | null | undefined = undefined;
if (value == null) {
console.log("empty");
}
value = None
if value is None:
print("empty")
这里最重要的是 is None。
is 不是“值相等”,而是“是否为同一个对象”。对 None 的判断,Python 社区的标准写法就是:
if value is None:
...
而不是:
if value == None:
...
避坑警告:判断
None时优先用is None,不要写== None。
这是 Python 社区几乎默认的代码习惯,也是可读性最好的写法。
再看 truthy / falsy:
console.log(Boolean("")); // false
console.log(Boolean(0)); // false
console.log(Boolean([])); // true
console.log(Boolean({})); // true
print(bool("")) # False
print(bool(0)) # False
print(bool([])) # False
print(bool({})) # False
这里和 JS 差异很大:Python 里空列表、空字典、空集合通常都为 False。
变量声明
JS/TS 有:
letconstvar
Python 没有这些关键字,直接赋值就是定义变量。
let age = 18;
const name = "Alice";
age = 18
name = "Alice"
那 Python 有没有常量?语言级别上没有真正不可改的 const,但有社区约定:
MAX_RETRY = 3
API_BASE_URL = "https://api.example.com"
全大写表示“请把它当常量使用”,但这只是约定,不是强制限制。
避坑警告:Python 的“大写常量”不是
const。
它不会阻止你修改,只是告诉团队成员“这里不应该改”。
数据类型转换
JS 开发者对“隐式类型转换”通常又爱又恨。Python 在这方面更保守,很多转换都要求你显式写出来。
console.log("1" + 2); // "12"
console.log("5" - 2); // 3
console.log(Number("12")); // 12
print("1" + str(2)) # "12"
# print("5" - 2) # 直接报错,Python 不做这种隐式转换
print(int("12")) # 12
常见转换函数:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 转字符串 | String(x) |
str(x) |
两边都常用 |
| 转整数 | Number(x) / parseInt(x, 10) |
int(x) |
Python int("12") 很直接 |
| 转浮点数 | Number(x) / parseFloat(x) |
float(x) |
Python 区分更明确 |
| 转布尔值 | Boolean(x) |
bool(x) |
truthy/falsy 规则细节不同 |
| 转数组/列表 | Array.from(x) |
list(x) |
Python 常把可迭代对象转成列表 |
const value = "123";
const num = Number(value);
console.log(num + 1); // 124
value = "123"
num = int(value)
print(num + 1) # 124
避坑警告:Python 不鼓励 JS 那种“顺手就发生”的隐式转换。
一旦你发现“这两个类型为什么不能直接算”,优先想的是“我是不是应该显式int()/str()/float()一下”。
运算符
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 算术运算符 | + - * / % ** |
+ - * / % ** // |
Python 多一个整除 // |
| 比较运算符 | > < >= <= == === !== |
> < >= <= == != |
Python 没有 === |
| 逻辑运算符 | `&& | !` | |
| 赋值运算符 | = += -= *= |
= += -= *= |
基本一致 |
| 成员判断 | arr.includes(x)、set.has(x) |
x in y |
Python 的 in 非常核心 |
| 身份判断 | 很少显式使用 | is / is not |
常用于 None 判断 |
算术运算符
console.log(2 ** 3); // 8
console.log(7 % 3); // 1
print(2 ** 3) # 8
print(7 % 3) # 1
print(7 // 3) # 2
比较运算符
console.log(1 == "1"); // true
console.log(1 === "1"); // false
print(1 == "1") # False
Python 没有 ===,因为它本身不鼓励 JS 那样复杂的宽松相等规则。多数情况下,== 就已经足够直观。
逻辑运算符
const isReady = true;
const hasAuth = false;
console.log(isReady && hasAuth);
console.log(!hasAuth);
is_ready = True
has_auth = False
print(is_ready and has_auth)
print(not has_auth)
成员运算符和身份运算符
Python 有两个对 JS 开发者来说非常值得重点适应的东西:in 和 is。
const arr = [1, 2, 3];
console.log(arr.includes(2)); // true
arr = [1, 2, 3]
print(2 in arr) # True
in 不只用于列表,也常用于:
- 字符串子串判断
- 字典 key 判断
- 集合成员判断
print("py" in "python") # True
print("name" in {"name": "A"}) # True
再看 is:
a = None
print(a is None) # True
它表达的是“身份相同”,不是“值相等”。
运算符优先级
你不需要死记整张优先级表,但需要记住几个高频规则:
**优先级很高not、and、or是关键字,不是符号- 复杂表达式优先加括号,不要赌阅读者理解成本
const result = (a + b) * c;
result = (a + b) * c
避坑警告:Python 代码的“可读性优先级”比“省字符优先级”高得多。
只要表达式稍微复杂,就直接加括号,不要为了看起来简洁而牺牲可读性。
本节小结
如果只看语法表面,Python 的数据类型并不难;真正需要你调整的是默认思维模式:
- 用
list、dict、set去理解 Python 的核心数据结构 - 用
None替代你对null/undefined的双重心智负担 - 用显式类型转换替代 JS 式隐式转换
- 尽快熟悉
in、is、//、and/or/not
其他
这一节看起来不像“知识点”,更像“代码习惯”。但对 JS/TS 开发者来说,这部分反而非常关键,因为它直接决定你写出来的 Python 像不像 Python。
先看一个总览:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 代码块边界 | {} |
缩进 | Python 的缩进是语法,不是格式化偏好 |
| 语句结束 | 常见 ;,也可省略 |
通常换行结束 | Python 不靠分号组织主流程 |
| 注释 | //、/* */ |
#、三引号字符串常用于文档 |
Python 没有真正对应 JS 的块注释语法 |
| 字符串引号 | '、"、模板字符串 |
'、"、'''、"""、f-string |
Python 三引号常用于多行字符串和文档字符串 |
| 命名风格 | 变量常用 camelCase |
变量/函数常用 snake_case |
Python 社区非常强调风格一致性 |
| 常量风格 | UPPER_SNAKE_CASE |
UPPER_SNAKE_CASE |
这是两边少数高度一致的地方🤣 |
| 类名风格 | PascalCase |
PascalCase |
两边基本一致 |
| 关键字 | if、return、class 等 |
if、return、class、lambda、with 等 |
Python 有一些 JS 没有的语言级关键字 |
行和缩进
JS/TS 里,缩进主要是为了可读性;就算你把缩进打乱,只要花括号还在,代码通常仍然能运行。
if (isReady) {
console.log("ready");
if (hasAuth) {
console.log("allowed");
}
}
Python 不是这样。Python 用缩进本身来定义代码块,这也是它语法最鲜明的特征之一。官方文档可以直接看这里:Compound statements。
if is_ready:
print("ready")
if has_auth:
print("allowed")
这意味着:
- 同一代码块必须保持一致缩进
- 子代码块必须比父级多一级缩进
- 缩进错了,不是“风格不好”,而是语法错误
来看一个最典型的对比:
function greet(name: string) {
console.log("hello", name);
}
上面这段 JS/TS 虽然难看,但多数情况下仍然能运行。
def greet(name):
print("hello", name)
上面这段 Python 则会直接报错,因为函数体没有正确缩进。
避坑警告:在 Python 里,缩进不是 Prettier 会帮你修的“小问题”,而是语法的一部分。
刚开始写 Python 时,不要一边混用 Tab 和空格,一边指望编辑器替你兜底。
实践上你只要记住一个规则就够了:
- 统一使用 4 个空格缩进
这不是强制法律,但它是 Python 社区最稳定的默认约定。对应风格规范见 PEP 8。
引号和注释
单行注释
JS/TS 里最常见的是 //:
// fetch user profile
const userId = 123;
Python 用 #:
# fetch user profile
user_id = 123
多行注释与三引号
JS/TS 有真正的块注释:
/*
fetch user profile
then render page
*/
Python 没有一个完全等价于 /* ... */ 的注释语法。你会看到很多代码里用三引号:
"""
fetch user profile
then render page
"""
但要注意,这在语法上首先是 多行字符串,不是注释关键字。它经常用于:
- 多行字符串
- 文档字符串
- 某些场景下临时说明文本
所以更稳妥的理解是:
- 真正的行注释:
# - 三引号:本质是字符串,只是经常承担“说明文字”的角色
字符串引号
JS/TS:
const a = 'hello';
const b = "world";
const c = `name=${userName}`;
Python:
a = 'hello'
b = "world"
c = f"name={user_name}"
对比可以先记这几件事:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 单引号/双引号 | 都常用 | 都常用 | Python 社区通常要求项目内保持一致 |
| 插值字符串 | 模板字符串 | f-string | Python 没有反引号模板字符串 |
| 多行字符串 | 模板字符串或换行拼接 | 三引号字符串 | Python 三引号非常常见 |
避坑警告:Python 里反引号不是模板字符串语法。
需要插值时,优先想到f"...{value}...",不要用 JS 的模板字符串直觉去找反引号。
语句分割
JS/TS 里分号是“半强制、半约定”的存在。很多项目靠 ASI 自动插入分号,但团队通常仍然会统一代码风格。
const a = 1;
const b = 2;
const sum = a + b;
Python 默认一行就是一条语句:
a = 1
b = 2
sum = a + b
你当然也可以写分号:
a = 1; b = 2; sum = a + b
但这在 Python 里通常不被鼓励,除非你在 REPL 或极少数非常短的场景里临时使用。
如果一行太长,Python 更常见的写法是借助括号自然换行:
const result =
firstValue +
secondValue +
thirdValue;
result = (
first_value
+ second_value
+ third_value
)
避坑警告:不要把 Python 写成“没有花括号的 JS”。
看到多条语句时,优先想到“换行”,而不是“分号”。
命名规范差异
如果你长期写前端,已经对这套风格很熟:
- 变量/函数:
camelCase - 类:
PascalCase - 常量:
UPPER_SNAKE_CASE
Python 不完全一样。Python 社区对命名风格有非常清晰的共识,仍然参考 PEP 8。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 变量名 | userName |
user_name |
Python 变量和函数主流使用 snake_case |
| 函数名 | getUserInfo() |
get_user_info() |
这是最需要改掉的前端习惯之一 |
| 类名 | UserProfile |
UserProfile |
两边基本一致 |
| 常量名 | MAX_RETRY |
MAX_RETRY |
两边一致 |
| 私有约定 | #private 或 TS 修饰符 |
_internal_name |
Python 更依赖约定而不是强封装 |
function getUserInfo(userId: string) {
return { userId };
}
def get_user_info(user_id):
return {"user_id": user_id}
对于 JS/TS 开发者来说,最难受的往往不是语法,而是“眼睛不习惯”。但你最好尽快接受这一点:
- 在 Python 里坚持写
camelCase,技术上能跑 - 但它会明显破坏代码的一致性
- 一旦进团队协作,会立刻显得“不像 Python”
避坑警告:从 JS/TS 切到 Python,最值得最先改掉的习惯之一,就是变量和函数命名的
camelCase。
在 Python 里,优先使用snake_case;把camelCase留给前端代码。
保留字 / 关键字
关键字可以理解成“语言保留给自己用的词”,你不能拿来当变量名。完整列表见官方文档 Keywords。
两边有很多重叠项:
ifelseforreturnclass
但 Python 里还有一些你在 JS/TS 里不太会以同样方式遇到的关键字:
defelifpasswithlambdayieldnonlocal
例如:
function add(a: number, b: number) {
return a + b;
}
def add(a, b):
return a + b
这里的 def 就是 Python 的函数定义关键字。再比如:
if score >= 90:
print("A")
elif score >= 60:
print("B")
else:
print("C")
其中 elif 就相当于 JS/TS 的 else if,只是 Python 把它做成了一个单独关键字。
还有一个现在先认识、后面会频繁见到的关键字:pass
if is_ready:
pass
它的意思不是“跳过当前循环”,也不是“什么都不做然后继续下一项”,而是:
- 这里语法上需要一个语句
- 但我暂时不想做任何事
这个关键字在写占位代码、类定义、接口式骨架时很常见。
避坑警告:
pass不是 JS 的continue,也不是空代码块的自然替代物。
它的作用是“占一个合法语句位置”,后面讲循环时我们会再和break、continue放在一起对照。
本节小结
这一节的重点不在于“记忆语法”,而在于尽快建立 Python 的书写习惯:
- 用缩进定义代码块,而不是靠花括号
- 用
#写注释,用三引号理解多行字符串和文档字符串 - 用换行组织语句,而不是依赖分号
- 用
snake_case命名变量和函数 - 对
def、elif、pass、with这些 Python 关键字建立基本语感
如果说上一节是在适应“语言气质”,那么接下来就开始进入真正的控制流。
条件语句(if/else)
如果你已经熟悉 JS/TS,那么条件语句本身不会陌生。你已经知道:
- 根据条件决定是否执行一段代码
- 多个分支里只会命中其中一个
- 可以嵌套,也可以写三元表达式
Python 在这件事上的核心差异主要有三点:
- 不用圆括号包条件
- 不用花括号包代码块,改用缩进
else if在 Python 里写成elif
先看整体对照。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 基础条件语句 | if (...) {} |
if ...: |
Python 用冒号加缩进,不用花括号 |
| 多分支 | else if |
elif |
Python 把它做成单独关键字 |
| 条件括号 | 通常必须写 () |
不需要 () |
只有复杂表达式时才建议加括号辅助阅读 |
| 条件结果 | truthy / falsy | truthy / falsy | 两边类似,但空数组/空对象在 Python 中是假 |
| 三元表达式 | cond ? a : b |
a if cond else b |
语序和 JS 正好反过来 |
基础写法
先看最基础的一段。
const score = 82;
if (score >= 60) {
console.log("pass");
} else {
console.log("fail");
}
score = 82
if score >= 60:
print("pass")
else:
print("fail")
如果你把它翻译成一句话,可以这样理解:
if 条件:表示“如果满足这个条件”- 缩进块表示“满足后执行的代码”
else:表示“否则执行的代码”
这类代码对 JS 开发者来说最大的不适应点通常不是逻辑,而是外形:
- 没有
() - 没有
{} - 每个分支后面有
:
避坑警告:Python 的
:不是装饰符,也不是“可有可无”的符号。
在if、elif、else、for、while、def、class这类语句后面,冒号是语法的一部分。
多分支:elif
JS/TS 里你会这样写:
const score = 82;
if (score >= 90) {
console.log("A");
} else if (score >= 60) {
console.log("B");
} else {
console.log("C");
}
Python 对应写法:
score = 82
if score >= 90:
print("A")
elif score >= 60:
print("B")
else:
print("C")
elif 可以理解成 Python 专门为“否则如果”准备的关键字。官方文档见 if 语句。
这里最值得记住的不是语法,而是阅读顺序:
- 先检查
if - 不满足再检查每个
elif - 全都不满足才进入
else
条件表达式里可以放什么
JS/TS 中,if (...) 里的内容最终会被转成布尔值再判断。
const userName = "alice";
if (userName) {
console.log("has value");
}
Python 也是类似思路:
user_name = "alice"
if user_name:
print("has value")
也就是说,if 后面并不要求你必须手动写出 True 或 False,只要这个值可以参与真假判断即可。
常见可直接放进条件里的内容包括:
- 比较表达式:
age >= 18 - 变量本身:
if name: - 容器对象:
if items: - 函数返回值:
if fetch_result:
truthy / falsy:和 JS 最容易混淆的地方
表面看两边都支持“真假值”,但细节不能混着用。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 空字符串 | falsy | falsy | 两边一致 |
数字 0 |
falsy | falsy | 两边一致 |
null / undefined / None |
falsy | None 为 falsy |
Python 只有 None 这一种核心空值 |
| 空数组 / 空列表 | [] 是 truthy |
[] 是 falsy |
这是高频差异 |
| 空对象 / 空字典 | {} 是 truthy |
{} 是 falsy |
这是另一个高频差异 |
看一组对照:
if ([]) {
console.log("JS 里会进入这里");
}
if ({}) {
console.log("JS 里这里也会执行");
}
if []:
print("不会执行")
if {}:
print("也不会执行")
这件事非常重要,因为它会直接影响你写“空数据判断”的方式。
在 JS/TS 里,你可能会写:
if (list.length === 0) {
console.log("empty");
}
在 Python 里,经常直接写:
if not items:
print("empty")
因为:
- 空列表
[]为假 - 空字典
{}为假 - 空集合
set()为假 - 空字符串
""为假
避坑警告:不要把 JS 的“空数组和空对象都是真”直接带到 Python。
在 Python 里,if items:往往就已经表达了“这个容器非空”。
if x 和 if x is not None 不是一回事
这是 JS/TS 开发者迁移 Python 时另一个很常见的误区。
如果你只是想判断“这个值是不是存在”,可能会写:
if value:
print("has value")
但这和判断“它是不是 None”不是一回事。
value = 0
if value:
print("会执行吗?") # 不会
if value is not None:
print("这里会执行") # 会
原因很简单:
0是 falsy""是 falsy[]是 falsy- 但它们并不是
None
所以你需要先想清楚自己要判断的到底是什么:
- “有内容吗?” ->
if value: - “是不是没有传值?” ->
if value is None:/if value is not None:
const count = 0;
if (count) {
console.log("truthy");
}
if (count !== null && count !== undefined) {
console.log("not nullish");
}
count = 0
if count:
print("truthy")
if count is not None:
print("not None")
避坑警告:
if value:更像是在判断 truthy/falsy,if value is not None:才是在判断“这个值是否为 None”。这两个语义不要混用。
逻辑组合条件
JS/TS:
const age = 20;
const hasAuth = true;
if (age >= 18 && hasAuth) {
console.log("allowed");
}
Python:
age = 20
has_auth = True
if age >= 18 and has_auth:
print("allowed")
这里唯一需要立即适应的是操作符写法:
- JS/TS 用
&&、||、! - Python 用
and、or、not
如果条件稍微复杂一点,建议主动加括号:
if (age >= 18 and has_auth) or is_admin:
print("allowed")
虽然很多时候括号不是必须的,但它能明显降低阅读成本。
嵌套条件
嵌套条件两边都支持,但 Python 因为依赖缩进,所以层级感会更明显。
if (user) {
if (user.isAdmin) {
console.log("admin");
}
}
if user:
if user.is_admin:
print("admin")
但从可读性角度说,你仍然应该尽量避免过深嵌套。很多时候可以改成更平的写法:
if not user:
return
if user.is_admin:
print("admin")
这类“先提前返回,再处理主逻辑”的写法,和你在 Node.js、React 组件、服务端控制器里做早退出是一个思路。
三元表达式
这是另一个很容易因为“长得像但顺序不同”而写错的地方。
JS/TS:
const status = isReady ? "ready" : "pending";
Python:
status = "ready" if is_ready else "pending"
对照一下:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 三元表达式 | cond ? a : b |
a if cond else b |
Python 把“结果”写在前面,条件放中间 |
第一次看 Python 三元表达式时,你可以强行按这个顺序读:
- 如果
is_ready成立,值是"ready" - 否则值是
"pending"
const label = score >= 60 ? "pass" : "fail";
label = "pass" if score >= 60 else "fail"
避坑警告:不要把 JS 的
cond ? a : b顺序机械搬到 Python。
Python 的三元表达式语序是a if cond else b,刚开始最容易写反。
实践建议也很简单:
- 简短表达式可以用三元表达式
- 条件复杂、分支较多时,回到普通
if / elif / else
没有 switch 怎么办
JS/TS 开发者经常会问:Python 没有传统意义上的 switch,那多分支怎么写?
最常见的答案有三个:
- 简单场景直接
if / elif / else - 映射关系用
dict - 更复杂的结构在新版本 Python 中可用
match,但它已经超出当前基础阶段
先看最简单的 if / elif / else:
switch (role) {
case "admin":
console.log("all access");
break;
case "editor":
console.log("edit access");
break;
default:
console.log("read only");
}
if role == "admin":
print("all access")
elif role == "editor":
print("edit access")
else:
print("read only")
如果只是“值到结果”的映射,字典常常更自然:
role_label_map = {
"admin": "all access",
"editor": "edit access",
}
print(role_label_map.get(role, "read only"))
这里你先不需要深究 match。等后面把函数、模块、异常这些基本功打稳,再看更高级的控制流会更顺。
本节小结
这一节最重要的不是学会写 if,而是修正 JS/TS 带来的默认直觉:
- Python 用
:+ 缩进组织条件分支 else if在 Python 里是elif- 空列表、空字典、空集合在 Python 中都为假
if value:和if value is not None:语义完全不同- 三元表达式的语序是
a if cond else b
如果你对这几件事已经有感觉了,接下来最值得建立的新直觉就是:Python 的 for,本质上不是“计数器循环”,而是“遍历可迭代对象”。
循环语句(while/for)
JS/TS 开发者通常已经很熟这些写法:
for (let i = 0; i < n; i++)for...offorEachwhile
Python 也有循环,但思维重心不完全一样:
while和你熟悉的含义差不多for更接近 JS 的for...of- 很多“按次数循环”的场景,用的是
range() - Python 非常强调“遍历对象本身”,而不是手动维护索引
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 条件循环 | while (...) {} |
while ...: |
两边语义接近,Python 仍然依赖缩进 |
| 计数循环 | for (let i = 0; i < n; i++) |
for i in range(n): |
Python 没有 JS 这种三段式 for |
| 遍历数组 | for...of / forEach |
for item in items: |
Python for 默认就是遍历值 |
| 跳出循环 | break |
break |
基本一致 |
| 跳过本次 | continue |
continue |
基本一致 |
| 占位语句 | 很少需要 | pass |
常用于临时占位,不是跳过循环项 |
while:和 JS 基本同类
先看最熟悉的一种循环。
let count = 0;
while (count < 3) {
console.log(count);
count += 1;
}
count = 0
while count < 3:
print(count)
count += 1
这部分迁移成本很低。你只需要记住:
- Python 不需要
() - Python 用
:开启代码块 - 循环体靠缩进定义
Python 的 for:更像 for...of
这是本节最重要的迁移点。
JS/TS 里你可能最常写的是三段式 for:
const nums = [10, 20, 30];
for (let i = 0; i < nums.length; i++) {
console.log(nums[i]);
}
Python 没有这种 C 风格三段式 for。Python 更鼓励你直接遍历元素本身:
nums = [10, 20, 30]
for num in nums:
print(num)
如果你用 JS 来类比,它更接近:
const nums = [10, 20, 30];
for (const num of nums) {
console.log(num);
}
这就是为什么前面讲列表推导式时,会让你尽快适应 for n in nums 这种表达。它在 Python 里极其核心。
什么是“可迭代对象”
你可以先用一个非常实用的、工程化的理解:
- 凡是“可以一个一个取出元素来遍历”的东西,通常就是可迭代对象
在 Python 里,常见可迭代对象包括:
listtuplestrdictsetrange()
例如:
for ch in "python":
print(ch)
for item in (1, 2, 3):
print(item)
这一点和 JS 的 iterable 概念其实很接近,只是 Python 在日常代码里更频繁、更自然地使用它。
range():Python 的高频基础工具
JS 开发者最容易问的一个问题是:Python 没有三段式 for,那“循环 5 次”怎么写?
答案通常就是 range()。
for (let i = 0; i < 5; i++) {
console.log(i);
}
for i in range(5):
print(i)
range(5) 生成的是一个按顺序可遍历的范围,结果是:
01234
也就是说,它和 JS 里 i < 5 的边界感觉一致:左闭右开,不包含结束值。
常见写法有三种:
range(5) # 0, 1, 2, 3, 4
range(1, 5) # 1, 2, 3, 4
range(1, 10, 2) # 1, 3, 5, 7, 9
对照 JS 来看:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 循环 5 次 | for (let i = 0; i < 5; i++) |
for i in range(5): |
Python 用范围对象代替三段式 for |
| 指定起止 | 手动写初始化和条件 | range(start, stop) |
Python 更集中、更统一 |
| 指定步长 | i += 2 |
range(start, stop, step) |
步长直接成为参数 |
for (let i = 1; i < 10; i += 2) {
console.log(i);
}
for i in range(1, 10, 2):
print(i)
避坑警告:
range(5)不是数组,也不是立即展开后的列表。
它更像一个“可遍历的范围对象”。多数时候你直接拿来for就够了,不要先入为主地把它想成 JS 里的真实数组。
如果你真想看它展开后的结果,可以显式转成列表:
print(list(range(5))) # [0, 1, 2, 3, 4]
什么时候用值遍历,什么时候用索引遍历
对 JS/TS 开发者来说,一个很典型的习惯是:只要循环数组,就先写索引。
const users = ["a", "b", "c"];
for (let i = 0; i < users.length; i++) {
console.log(users[i]);
}
但在 Python 里,如果你只是要用元素本身,优先直接遍历值:
users = ["a", "b", "c"]
for user in users:
print(user)
只有在你 既要索引,又要值 时,才更推荐用 enumerate():
users = ["a", "b", "c"]
for index, user in enumerate(users):
print(index, user)
如果类比 JS,它接近:
const users = ["a", "b", "c"];
users.forEach((user, index) => {
console.log(index, user);
});
但 Python 的 enumerate(users) 更直接,也更 Pythonic。
避坑警告:不要下意识把每个 Python 循环都写成“索引 + 下标访问”。
在 Python 里,只要你不需要索引,就直接for item in items。
遍历字典
字典是 Python 里非常常见的数据结构,所以它的遍历方式也必须尽快习惯。
const user = { name: "Alice", age: 18 };
for (const key in user) {
console.log(key, user[key]);
}
user = {"name": "Alice", "age": 18}
for key in user:
print(key, user[key])
但更常见、也更清晰的写法是用 dict.items():
user = {"name": "Alice", "age": 18}
for key, value in user.items():
print(key, value)
这个写法在 Python 里非常高频,因为它避免了手动再取一次 user[key]。
你也可以只遍历值:
for value in user.values():
print(value)
或者只遍历键:
for key in user.keys():
print(key)
break:提前结束整个循环
这个和 JS 的语义几乎一致。
for (const num of [1, 2, 3, 4]) {
if (num === 3) {
break;
}
console.log(num);
}
for num in [1, 2, 3, 4]:
if num == 3:
break
print(num)
执行到 3 时,整个循环直接结束。
continue:跳过当前这一轮
这个也和 JS 很接近。
for (const num of [1, 2, 3, 4]) {
if (num === 3) {
continue;
}
console.log(num);
}
for num in [1, 2, 3, 4]:
if num == 3:
continue
print(num)
执行结果会跳过 3,但循环继续往下跑。
pass:不是跳过一轮,也不是结束循环
这是前面已经预告过、但现在必须彻底分清楚的东西。
for num in [1, 2, 3]:
if num == 2:
pass
print(num)
执行结果仍然会打印:
1
2
3
因为 pass 的作用不是“跳过当前项”,而只是:
- 这里需要有一条合法语句
- 但我暂时什么都不做
对照一下最容易混淆的三者:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 结束整个循环 | break |
break |
两边一致 |
| 跳过本轮 | continue |
continue |
两边一致 |
| 占位不做事 | 无常用等价物 | pass |
pass 不会跳过本轮,也不会结束循环 |
for num in [1, 2, 3]:
if num == 1:
print("break before print")
break
print(num)
for num in [1, 2, 3]:
if num == 1:
print("continue before print")
continue
print(num)
for num in [1, 2, 3]:
if num == 1:
print("pass before print")
pass
print(num)
这三段代码执行效果完全不同。对前端开发者来说,pass 最容易被误读成“空操作版 continue”,但它不是。
避坑警告:
pass只是占位,不改变循环流程。
如果你真正想“跳过当前这一轮”,请写continue。
嵌套循环
嵌套循环两边都支持,核心仍然是“外层循环的每一项,触发一次内层完整循环”。
const matrix = [
[1, 2],
[3, 4],
];
for (const row of matrix) {
for (const cell of row) {
console.log(cell);
}
}
matrix = [
[1, 2],
[3, 4],
]
for row in matrix:
for cell in row:
print(cell)
Python 因为缩进层级更明显,所以深层嵌套往往更容易一眼看出复杂度。写多了你会更自然地想办法拍平结构。
forEach 和 Python 的关系
JS/TS 开发者经常会下意识找一个“Python 版 forEach”。但 Python 的主流思路不是这样。
JS:
nums.forEach((num) => {
console.log(num);
});
Python 更直接:
for num in nums:
print(num)
换句话说:
- JS 经常把遍历写成“集合调用回调”
- Python 更常把遍历写成语言级语法
for ... in ...
这会带来两个实际体验差异:
- Python 代码通常更直接
- 你不需要为了简单遍历,额外制造一个回调函数
循环里的常见工程建议
这一部分不只是语法,而是写 Python 时很快就会形成的习惯:
- 只要能直接遍历值,就不要先遍历索引
- 只要要索引和值,就优先
enumerate() - 只要是次数循环,就优先想
range() - 只要要遍历字典键值对,就优先
items() - 只要流程控制需要“跳过本轮”,就写
continue,不是pass
本节小结
这一节最值得你带走的是几个核心转换:
- Python 的
for更像 JS 的for...of - Python 没有三段式
for,按次数循环通常用range() - 遍历列表时优先直接拿值,而不是先拿索引
- 需要索引和值时用
enumerate() - 遍历字典键值对时用
items() break、continue和pass的语义必须彻底分清
到这里,基础 这一大章里最核心的语法骨架基本已经搭起来了。
下一章开始进入真正的重点:函数。
函数(重点)
如果你已经有扎实的 JS/TS 基础,那你会很自然地把 Python 函数理解成:
- 能封装逻辑
- 能接收参数
- 能返回结果
- 能作为抽象边界组织代码
这个方向没有错,但 Python 的函数系统和 JS/TS 相比,有两个明显特点:
- 语法更收敛,写法更统一
- 参数模型更强,函数签名更有表达力
也就是说,Python 函数表面上看起来“朴素”,但实际非常强大。很多 Python 代码之所以读起来舒服,很大程度上就是因为函数定义、参数设计和文档约定都比较稳定。
函数定义与调用
这一节先只处理四件事:
def定义 vs JSfunction/ 箭头函数- 函数调用
return- 文档字符串
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 函数定义 | function、箭头函数 |
def |
Python 主流函数定义方式非常统一 |
| 匿名函数 | 箭头函数很常见 | lambda 能力更收敛 |
Python 不鼓励把复杂逻辑塞进匿名函数 |
| 返回值 | return |
return |
两边一致,但 Python 默认返回 None |
| 文档注释 | JSDoc/TSDoc | 文档字符串 | Python 更常把说明直接写进函数体第一行 |
| 类型标注 | TS 是核心能力 | Python type hints 可选但常用 | Python 类型标注不是本节重点,后面会展开 |
def:Python 的主流函数定义方式
JS/TS 里你可能会在项目里混用多种函数写法:
function foo() {}const foo = function () {}const foo = () => {}
Python 没有这么多主流分支。最常见、最标准的函数定义方式就是 def。
function add(a: number, b: number): number {
return a + b;
}
def add(a, b):
return a + b
第一次看 Python 函数定义,你只需要抓住这几个点:
- 用
def开头 - 函数名后面跟参数列表
- 行尾有
: - 函数体靠缩进表示
这意味着 Python 函数定义的外形非常稳定。你不会像在 JS/TS 里那样,频繁在“声明式函数、函数表达式、箭头函数”之间切换。
function 和箭头函数,在 Python 里怎么类比
先给一个直白结论:
- Python 的普通
def,大致对应 JS/TS 里“最常规的函数定义” - Python 也有匿名函数,但能力和存在感都远低于 JS 箭头函数
例如这段 JS:
const add = (a: number, b: number) => {
return a + b;
};
在 Python 里,日常主流写法依然是:
def add(a, b):
return a + b
也就是说,不要带着“我是不是该优先写箭头函数”的习惯来找 Python 对应物。Python 的默认答案就是 def。
避坑警告:在 Python 里,
def不是“比较基础的函数写法”,而是最主流、最正式、最工程化的函数写法。
不要用 JS/TS 那种“能不能都写成箭头函数”的思维来找替代品。
调用方式:和 JS 基本同类
调用函数这件事本身,两边差异不大:
function greet(name: string) {
console.log(`hello, ${name}`);
}
greet("Alice");
def greet(name):
print(f"hello, {name}")
greet("Alice")
也就是:
- 定义函数
- 传入参数
- 执行函数体
这一层没有什么迁移成本。
没写 return 会怎样
JS/TS 里,如果函数没有显式 return,默认返回 undefined。
function logMessage(message: string) {
console.log(message);
}
const result = logMessage("hello");
console.log(result); // undefined
Python 对应的是:默认返回 None。
def log_message(message):
print(message)
result = log_message("hello")
print(result) # None
这件事你最好尽早建立稳定映射:
- JS/TS 默认“没返回值” ->
undefined - Python 默认“没返回值” ->
None
return:和 JS 一样重要
真正写工程代码时,函数最核心的价值之一就是返回结果,而不是只做副作用。
function square(x: number): number {
return x * x;
}
const result = square(5);
console.log(result); // 25
def square(x):
return x * x
result = square(5)
print(result) # 25
这一点表面非常像,但 Python 里你会更经常看到“短小、纯粹、返回明确”的函数风格。
再看一个提前返回的例子:
function getUserLabel(user?: { name: string }) {
if (!user) {
return "guest";
}
return user.name;
}
def get_user_label(user):
if not user:
return "guest"
return user["name"]
这里的“提前返回”思路和 JS/TS 完全一致,都是为了降低嵌套层级,让主逻辑更平。
一个函数可以返回多个值吗
这是很多 JS/TS 开发者第一次接触 Python 时会注意到的点。
JS 里你通常会返回对象或数组:
function getUserSummary() {
return {
name: "Alice",
age: 18,
};
}
const summary = getUserSummary();
console.log(summary.name, summary.age);
Python 里当然也可以返回字典:
def get_user_summary():
return {
"name": "Alice",
"age": 18,
}
summary = get_user_summary()
print(summary["name"], summary["age"])
但 Python 还有一个很常见的写法:一次返回多个值。
def get_user_summary():
return "Alice", 18
name, age = get_user_summary()
print(name, age)
这背后本质上是返回了一个元组。你现在先记住现象就够了:
- Python 看起来像“返回多个值”
- 实际上常常是“返回一个元组,再在接收端拆包”
这个能力在后面讲元组、函数参数、异常处理时会越来越常见。
避坑警告:Python 的“返回多个值”不是魔法。
大多数场景下,它只是把多个值打包成元组返回,然后在左侧做解包。
文档字符串:Python 很有代表性的函数习惯
如果你来自 TS,你很熟悉给函数写 JSDoc / TSDoc:
/**
* Add two numbers.
* @param a first number
* @param b second number
* @returns sum result
*/
function add(a: number, b: number): number {
return a + b;
}
Python 的主流习惯之一,是把说明直接写进函数体第一行,形成 文档字符串:
def add(a, b):
"""Return the sum of a and b."""
return a + b
这里的三引号字符串,不是普通摆设。它是函数对象的正式文档内容,可以被很多工具读取。
例如:
def add(a, b):
"""Return the sum of a and b."""
return a + b
print(add.__doc__)
这也是为什么 Python 社区很重视 docstring。它不是注释的“附属品”,而是函数说明的一部分。
你可以先把两边关系理解成:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 函数说明 | JSDoc / TSDoc | 文档字符串 | Python 把说明直接放进函数体第一句 |
| 工具读取 | 编辑器、TS 工具链、文档生成器 | help()、IDE、文档工具 |
Python 对 docstring 的语言级支持更直接 |
再看一个更贴近日常开发的例子:
def fetch_user(user_id):
"""Fetch a user by id and return a dict result."""
return {"id": user_id, "name": "Alice"}
如果你在 REPL 或脚本里调用 help(),这个说明就会直接显示出来。
help(fetch_user)
什么时候该写 docstring
不是每个函数都要写成长篇说明,但有几类函数非常值得写:
- 对外暴露的公共函数
- 参数或返回值语义不够直观的函数
- 有副作用的函数
- 业务含义强、但实现看起来比较短的函数
反过来说,如果一个函数非常短、名字非常清楚、上下文也很明确,那不一定非要补一大段文档字符串。
工程上更重要的是:
- 说明要和实现保持一致
- 不要复制函数名当废话
- 不要写一大段过时说明
例如下面这种就价值不高:
def add(a, b):
"""Add."""
return a + b
它并没有比函数名本身提供更多信息。
Python 函数和 JS 函数的“气质差异”
到这里,你应该已经能感受到一点差异了。
JS/TS 的函数世界通常更“开放”:
- 函数声明、函数表达式、箭头函数并存
- 回调函数大量出现
- 函数常常和对象、类、闭包、异步逻辑高度混合
Python 的函数世界则更“收敛”:
- 大多数正常函数用
def - 小部分简单表达式才用
lambda - 文档字符串、参数签名、返回值表达都更强调可读性
这也是为什么很多 Python 代码一眼看上去就比较整齐。不是因为它更简单,而是因为它故意减少了同一件事的表达分叉。
本节小结
这一节最值得你建立的迁移认知是:
- Python 的主流函数定义方式是
def - Python 没有把“箭头函数式写法”作为主流
- 不写
return时,函数默认返回None - Python 可以很自然地返回多个值,本质上通常是返回元组
- 文档字符串是 Python 函数设计里非常重要的一部分
下一节继续进入函数真正的重头戏:参数传递。
参数传递
如果说 def 只是让你“能定义函数”,那参数系统才是真正决定 Python 函数是否强大的地方。
JS/TS 开发者对参数并不陌生。你已经习惯了:
- 位置参数
- 默认参数
- 剩余参数
...args - 对象参数解构
Python 也有这些能力的对应物,但组合方式更完整、表达力更强。官方文档建议从这里看起:More on Defining Functions。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 位置参数 | fn(a, b) |
fn(a, b) |
两边最基础形态一致 |
| 默认参数 | function fn(x = 1) |
def fn(x=1) |
Python 默认参数在函数定义时就求值 |
| 关键字参数 | 通常靠对象参数模拟 | fn(name="A") |
Python 可直接按参数名传值 |
| 剩余参数 | ...args |
*args |
两边都表示接收任意多个位置参数 |
| 对象扩展参数 | ...rest / 对象参数 |
**kwargs |
Python 接收任意多个具名参数 |
| 解包调用 | fn(...arr)、fn({...obj}) |
fn(*items)、fn(**data) |
两边概念非常接近 |
位置参数
这是最基础的一类参数:按顺序传值,顺序必须对上。
function introduce(name: string, age: number) {
console.log(`${name} is ${age} years old`);
}
introduce("Alice", 18);
def introduce(name, age):
print(f"{name} is {age} years old")
introduce("Alice", 18)
这一层和 JS/TS 差异不大。你定义两个参数,调用时就按顺序传两个值。
但 Python 对“参数数量是否匹配”通常更严格、报错也更直接:
def introduce(name, age):
print(f"{name} is {age} years old")
# introduce("Alice") # TypeError: missing required positional argument
这种报错信息会明确告诉你少了哪个参数。
默认参数
默认参数在两边都很常见:
function greet(name: string = "Guest") {
console.log(`hello, ${name}`);
}
greet();
greet("Alice");
def greet(name="Guest"):
print(f"hello, {name}")
greet()
greet("Alice")
乍一看和 JS 完全一样,但这里藏着 Python 最经典的一个坑:默认参数的求值时机。
JS 中,默认参数通常可以理解为“每次调用时,如果没传,就使用这个默认值逻辑”。
function appendItem(item: string, list: string[] = []) {
list.push(item);
return list;
}
console.log(appendItem("a")); // ["a"]
console.log(appendItem("b")); // ["b"]
Python 里如果你照着写:
def append_item(item, items=[]):
items.append(item)
return items
print(append_item("a")) # ['a']
print(append_item("b")) # ['a', 'b']
第二次结果会让很多 JS 开发者当场愣住。
原因是:
- Python 的默认参数值,在函数定义时求值
- 不是每次调用时都重新创建
也就是说,上面的 [] 会被复用。
这里顺手澄清一个很容易混淆的点:这不是典型的作用域问题。
它真正涉及的是:
- 默认参数在函数定义时求值
- 可变对象被复用
如果用python的伪代码理解:
_shared_list = []
def append_item(item, items=_shared_list):
items.append(item)
return items
所以连续调用时,改的是同一个列表.
正确写法通常是:
def append_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(append_item("a")) # ['a']
print(append_item("b")) # ['b']
避坑警告:不要把可变对象直接写成 Python 默认参数。
像[]、{}、set()这种可变值,默认参数里通常应改成None,然后在函数体里初始化。
这个坑非常重要,值得你直接形成条件反射:
- 默认值是数字、字符串、布尔值,通常没问题
- 默认值是列表、字典、集合,先警惕
关键字参数
这是 Python 参数系统里非常有辨识度的一部分。
在 JS/TS 里,如果你想“按名字传参数”,通常会改成对象参数:
function createUser(options: { name: string; age: number; city?: string }) {
console.log(options.name, options.age, options.city);
}
createUser({ name: "Alice", age: 18, city: "Beijing" });
Python 除了能传位置参数,还可以直接按参数名传值:
def create_user(name, age, city=None):
print(name, age, city)
create_user(name="Alice", age=18, city="Beijing")
这就是关键字参数。
它的价值很大:
- 调用点更可读
- 参数较多时更不容易传错顺序
- 可选参数会更自然
对照一下:
create_user("Alice", 18, "Beijing")
create_user(name="Alice", age=18, city="Beijing")
第二种通常更清楚,尤其当参数变多时差距会更明显。
你也可以混用,但规则是:
- 位置参数放前面
- 关键字参数放后面
def create_user(name, age, city=None):
print(name, age, city)
create_user("Alice", age=18, city="Beijing")
但反过来不行:
# create_user(name="Alice", 18, city="Beijing") # 语法错误
避坑警告:Python 允许关键字参数,不代表你可以任意乱序混写。
记住一个简单规则:位置参数在前,关键字参数在后。
为什么关键字参数对工程代码很有价值
这是 Python 函数签名经常比 JS 函数调用更“自解释”的原因之一。
比如下面这段 JS:
setCache("user:list", 300, true);
如果你不看定义,光看调用点,很难立刻知道:
300是什么true又表示什么
Python 写成关键字参数后会清楚很多:
set_cache("user:list", ttl=300, refresh=True)
调用点本身就带上了语义。
这也是为什么很多 Python API 会设计成:
- 少量核心参数走位置传递
- 其他配置项走关键字参数
*args:接收任意多个位置参数
JS/TS 里你很熟悉剩余参数:
function sum(...nums: number[]) {
return nums.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3));
Python 对应的是 *args:
def sum_all(*nums):
total = 0
for n in nums:
total += n
return total
print(sum_all(1, 2, 3))
你可以先把它理解成:
- 调用时传入任意多个位置参数
- 函数内部把它们收集成一个元组
def show_args(*args):
print(args)
show_args(1, "a", True) # (1, 'a', True)
如果类比 JS:
- JS
...args收到的是数组 - Python
*args收到的是元组
这是一个细节差异,但值得记住。
**kwargs:接收任意多个具名参数
JS/TS 里你常通过对象参数吸收一批配置:
function createTask(options: Record<string, unknown>) {
console.log(options);
}
createTask({ title: "write post", done: false });
Python 很常见的对应能力是 **kwargs:
def create_task(**kwargs):
print(kwargs)
create_task(title="write post", done=False)
这里的 kwargs 本质上会收到一个字典:
def create_task(**kwargs):
print(type(kwargs))
print(kwargs)
create_task(title="write post", done=False)
可以把它理解成:
*args-> 吸收多余的位置参数**kwargs-> 吸收多余的具名参数
对照表:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 剩余位置参数 | ...args |
*args |
Python 收到的是元组 |
| 具名配置参数 | 常用对象参数 | **kwargs |
Python 收到的是字典 |
同时使用普通参数、默认参数、*args、**kwargs
Python 函数签名可以非常有表达力。例如:
def send_event(event_name, retry=3, *args, **kwargs):
print("event_name =", event_name)
print("retry =", retry)
print("args =", args)
print("kwargs =", kwargs)
调用:
send_event(
"user_login",
5,
"mobile",
"wechat",
user_id=1001,
trace_id="abc123",
)
输出思路大致是:
event_name接第 1 个位置参数retry接第 2 个位置参数- 剩余位置参数进
args - 具名参数进
kwargs
这就是为什么 Python 很适合写框架、库、工具函数。函数签名本身就能承载很多设计意图。
调用时解包:*items 和 **data
这一点和 JS 的扩展运算符非常像。
JS:
const nums = [1, 2, 3];
console.log(Math.max(...nums));
const options = { name: "Alice", age: 18 };
createUser({ ...options });
Python:
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
print(add(*nums))
def create_user(name, age):
print(name, age)
data = {"name": "Alice", "age": 18}
create_user(**data)
这里可以直接建立映射:
*items:把一个可迭代对象拆成位置参数**data:把一个字典拆成关键字参数
这和 JS 的 ... 很像,但要注意 Python 分成了两种符号层级:
- 一个星号
* - 两个星号
**
参数系统带来的设计优势
当你开始写 Python API 时,会很快发现参数设计本身就是表达力。
一个函数签名如果设计得好,调用点往往会非常清楚:
def request(url, method="GET", timeout=3, retry=1, headers=None):
...
这种写法的好处是:
- 核心参数显眼
- 常见可选参数有默认值
- 调用方可以用关键字参数清晰覆盖配置
调用时:
request("/users", timeout=10, retry=2)
你几乎不用再猜每个位置参数的含义。
这和很多 JS 函数习惯把所有东西都塞进一个 options 对象,是两种不同的设计文化:
- JS/TS 常靠对象参数承载配置
- Python 很多时候直接靠函数签名本身承载配置
两种都能用,但 Python 这一套往往更直接。
本节小结
这一节最核心的迁移认知是:
- Python 同样有位置参数和默认参数
- Python 的默认参数在函数定义时求值,这是一个高频坑
- Python 可以直接使用关键字参数,让调用点更清晰
*args对应任意多个位置参数**kwargs对应任意多个具名参数*items和**data则是在调用时做参数解包
如果你把这一节吃透,就会开始真正理解:为什么很多 Python API 看起来“参数很多”,但读起来反而更清楚。
下一节继续进入函数章节的两个特殊主题:装饰器 和 匿名函数。
装饰器
如果你第一次接触 Python 装饰器,很容易产生两种反应:
- “这是不是某种特殊语法糖?”
- “这东西是不是很高级,平时用不到?”
其实都不太准确。
更适合 JS/TS 开发者的理解方式是:
- 装饰器本质上就是“接收一个函数,返回一个新函数”的包装器
如果你熟悉这些概念:
- 高阶函数
- middleware
- AOP 式横切逻辑
- 对函数做埋点、鉴权、日志包装
那你已经掌握了理解装饰器最重要的前置知识。
Python 官方对这类能力的基础说明可以结合看 defining functions 和 functools。
先不要看 @,先看“函数包装”本身。
先从 JS 高阶函数类比开始
JS/TS 里你完全可以手写一个“包装函数”:
function withLog(fn: (...args: any[]) => any) {
return function (...args: any[]) {
console.log("before");
const result = fn(...args);
console.log("after");
return result;
};
}
function add(a: number, b: number) {
return a + b;
}
const loggedAdd = withLog(add);
console.log(loggedAdd(1, 2));
Python 同样可以这么做:
def with_log(fn):
def wrapper(*args, **kwargs):
print("before")
result = fn(*args, **kwargs)
print("after")
return result
return wrapper
def add(a, b):
return a + b
logged_add = with_log(add)
print(logged_add(1, 2))
看到这里,其实装饰器的核心已经出现了:
with_log接收一个函数fn- 内部定义了一个新的
wrapper wrapper调用了原来的fn- 最后返回这个新函数
这和你在 JS 里写高阶函数包装逻辑,本质是一回事。
@decorator 只是更简洁的写法
上面那段 Python,如果换成装饰器语法:
def with_log(fn):
def wrapper(*args, **kwargs):
print("before")
result = fn(*args, **kwargs)
print("after")
return result
return wrapper
@with_log
def add(a, b):
return a + b
print(add(1, 2))
这里:
@with_log
def add(a, b):
return a + b
本质上约等于:
def add(a, b):
return a + b
add = with_log(add)
这就是理解装饰器最重要的一句话:
@decorator不是神秘语法,本质上就是把函数交给另一个函数处理,然后把返回值重新绑定回来。
装饰器最常见的用途
如果你从 Node.js / 前端工程的视角看,装饰器特别适合承载“和业务主逻辑无关,但经常重复出现的横切逻辑”。
例如:
- 日志
- 计时
- 鉴权
- 缓存
- 重试
- 参数校验
比如做一个简单计时:
function withTiming(fn: (...args: any[]) => any) {
return function (...args: any[]) {
const start = Date.now();
const result = fn(...args);
console.log(`cost=${Date.now() - start}ms`);
return result;
};
}
import time
def with_timing(fn):
def wrapper(*args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
print(f"cost={(time.time() - start):.6f}s")
return result
return wrapper
加到函数上:
@with_timing
def slow_add(a, b):
time.sleep(0.1)
return a + b
这时你可以把它理解成:
- 业务函数只关心“怎么算”
- 装饰器负责“执行前后要做什么附加逻辑”
这个思路和 Express/Koa 中间件、React 高阶组件时代的包装思路,都很接近。
为什么装饰器里常写 *args, **kwargs
因为装饰器通常希望尽可能通用。
如果你把 wrapper 写死成:
def wrapper(a, b):
...
那它只能包装固定签名的函数。
更常见的写法是:
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
这也是为什么前一节讲参数系统后,再来看装饰器会顺很多。
一个更贴近实际的例子:权限检查
JS/TS 思路:
function withAuth(fn: (...args: any[]) => any) {
return function (user: { isAdmin: boolean }, ...args: any[]) {
if (!user.isAdmin) {
throw new Error("forbidden");
}
return fn(user, ...args);
};
}
Python:
def require_admin(fn):
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("forbidden")
return fn(user, *args, **kwargs)
return wrapper
@require_admin
def delete_post(user, post_id):
print(f"delete post {post_id}")
这个例子里,装饰器的价值就非常直观:
- 权限逻辑被集中处理
- 业务函数不用重复写相同判断
- 调用关系依然清晰
装饰器会“隐藏”原函数信息
这又是一个很容易踩的工程细节。
如果你直接返回 wrapper,很多时候原函数的元信息会丢掉,比如:
- 函数名
- 文档字符串
看一个简化例子:
def with_log(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
这时候被装饰后的函数,可能看起来名字变成了 wrapper。
所以 Python 里经常会配合 functools.wraps:
from functools import wraps
def with_log(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print("before")
result = fn(*args, **kwargs)
print("after")
return result
return wrapper
这相当于告诉工具链和运行时:
- 尽量保留原函数的名字、文档等元信息
避坑警告:写装饰器时,如果这是正式工程代码,通常要优先考虑
functools.wraps。
否则调试、文档、日志、IDE 提示里看到的可能都是wrapper,不是原函数名。
带参数的装饰器
有时候你希望“装饰器本身也能接配置”,比如:
- 重试次数
- 日志前缀
- 权限角色
这时装饰器会再多包一层。
JS/TS 可以这样想:
function withPrefix(prefix: string) {
return function (fn: (...args: any[]) => any) {
return function (...args: any[]) {
console.log(prefix);
return fn(...args);
};
};
}
Python:
from functools import wraps
def with_prefix(prefix):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(prefix)
return fn(*args, **kwargs)
return wrapper
return decorator
@with_prefix("[LOG]")
def greet(name):
print(f"hello, {name}")
第一次看会觉得套了很多层,但本质还是同一个模式:
- 最外层接收配置
- 中间层接收函数
- 最内层执行包装逻辑
本节小结
这一节最值得你带走的是:
- 装饰器本质上是高阶函数包装
@decorator约等于fn = decorator(fn)- 它适合承载日志、鉴权、缓存、计时等横切逻辑
*args, **kwargs让装饰器更通用- 工程中通常要配合
functools.wraps
匿名函数
JS/TS 开发者看到“匿名函数”这个标题,第一反应通常是:
function () {}() => {}
而在 Python 里,对应物主要是 lambda。
但这里一定要先下一个很重要的判断:
- Python 的
lambda远没有 JS 箭头函数那么核心
也就是说:
- JS/TS 里,箭头函数几乎无处不在
- Python 里,
lambda只适合非常短、非常轻的表达式
最基础的类比
JS:
const add = (a: number, b: number) => a + b;
console.log(add(1, 2));
Python:
add = lambda a, b: a + b
print(add(1, 2))
表面看很像,但你不能据此得出“那我以后都写 lambda 就好了”这个结论。
为什么 Python 不鼓励把 lambda 当箭头函数替身
因为 Python 的 lambda 有一个非常关键的限制:
- 它只能写表达式,不能写多条语句
比如下面这种在 Python 里就不行:
# bad = lambda x: print(x); return x # 不合法
它不像 JS 箭头函数那样,既能写简洁表达式,也能写完整函数体。
所以:
- 复杂逻辑 ->
def - 极短表达式 ->
lambda
这才是 Python 的主流心智模型。
避坑警告:不要把
lambda当成 Python 版箭头函数全面替代def。
在 Python 里,只要逻辑稍微复杂、需要注释、需要文档字符串、需要多行处理,就直接回到def。
lambda 最常见的使用场景
最常见场景不是“定义一个重要函数”,而是:
- 临时传一个很短的函数给别的 API
例如排序:
JS:
const users = [
{ name: "Bob", age: 20 },
{ name: "Alice", age: 18 },
];
users.sort((a, b) => a.age - b.age);
Python:
users = [
{"name": "Bob", "age": 20},
{"name": "Alice", "age": 18},
]
users.sort(key=lambda user: user["age"])
print(users)
这里的 lambda user: user["age"] 其实就是在说:
- 排序时,用每个用户的
age作为比较依据
lambda 和列表推导式怎么选
这是一个很典型的 Python 风格问题。
如果只是对列表做简单变换:
doubled = [n * 2 for n in nums]
往往比:
doubled = list(map(lambda n: n * 2, nums))
更符合 Python 社区主流阅读习惯。
但如果某个 API 就是需要一个临时函数,比如:
sort(key=...)- 某些回调式工具函数
那 lambda 仍然很自然。
本节小结
- Python 装饰器本质是函数包装
@decorator只是包装语法的简化写法- 装饰器非常适合放横切逻辑
- Python 的
lambda存在,但地位远低于 JS 箭头函数 - 复杂逻辑优先
def - 简单临时表达式才适合
lambda
到这里,函数这一章最核心的部分已经成型了。
下一章继续进入:作用域。
作用域
如果你来自 JS/TS,看到“作用域”第一反应大概率是:
varletconst- 块级作用域
- 闭包
Python 当然也有作用域问题,但它的思考入口不太一样。
JS/TS 更常从“这个变量是怎么声明的”来理解作用域。
Python 更常从“这个名字会去哪里查找”来理解作用域。
也就是说,Python 里最重要的不是先问:
- 这是
let还是const
而是先问:
- 当代码写下这个名字时,解释器会先去哪里找
- 找不到时会继续往哪里找
这正是 Python 作用域最核心的心智模型。
先看一个总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 变量声明关键字 | var / let / const |
无 | Python 不靠声明关键字区分作用域 |
| 块级作用域 | let / const 有 |
if / for / while 不创建新作用域 |
Python 的函数作用域比块作用域更关键 |
| 函数作用域 | 有 | 有 | 两边都重要 |
| 闭包 | 有 | 有 | 两边都支持,但写法和修改规则不同 |
| 修改外层变量 | 依赖作用域链和声明方式 | global / nonlocal |
Python 明确要求声明意图 |
名字查找:先建立 Python 的基本心智模型
Python 里最常见的一套解释是 LEGB:
L= Local,本地作用域E= Enclosing,外层嵌套函数作用域G= Global,模块全局作用域B= Built-in,内置作用域
你先不需要死记缩写,但要记住这个查找顺序:
- 先找当前函数内部
- 再找外层函数
- 再找当前模块的全局变量
- 最后找 Python 内置名字
先看一个很简单的例子:
name = "global"
def show_name():
name = "local"
print(name)
show_name() # local
print(name) # global
这里发生的事情并不复杂:
- 函数内部有一个局部
name print(name)优先命中局部变量- 函数外部的全局
name没被改掉
如果你用 JS 对照:
const name = "global";
function showName() {
const name = "local";
console.log(name);
}
showName(); // local
console.log(name); // global
这部分直觉是相通的。
变量作用域:Python 不靠 let / const
这是和 JS/TS 最显眼的差异之一。
JS/TS:
let count = 1;
const name = "Alice";
Python:
count = 1
name = "Alice"
你看不到任何“声明关键字”。Python 默认的思路是:
- 名字通过赋值产生绑定
- 作用域由代码所在位置和名字查找规则决定
所以在 Python 里,下面这些问题比“有没有 let”更关键:
- 这个赋值发生在函数内部还是外部
- 当前代码是在函数体里,还是模块顶层
- 这个名字是不是在外层函数里已经存在
局部变量和全局变量
先看最基础的区分:
site_name = "Hexo Blog"
def print_site():
message = "welcome"
print(site_name)
print(message)
print_site()
这里:
site_name是全局变量message是局部变量
函数内部可以读取全局变量,这没问题:
site_name = "Hexo Blog"
def print_site():
print(site_name)
但函数外部不能直接读取函数内部的局部变量:
def print_site():
message = "welcome"
print(message)
# print(message) # NameError
如果你类比 JS:
const siteName = "Hexo Blog";
function printSite() {
const message = "welcome";
console.log(siteName);
console.log(message);
}
// console.log(message); // ReferenceError
这一层迁移成本不高。
Python 没有你想象中的“块级作用域”
这一点非常值得单独拿出来讲,因为它和 JS let/const 差异很大。
JS/TS:
if (true) {
const x = 1;
}
// console.log(x); // 报错
在 JS 里,if 代码块本身就能形成块级作用域。
但 Python 里,if、for、while 这类代码块不会像 JS 那样创建新的块级作用域。
if True:
x = 1
print(x) # 1
再看循环:
for i in range(3):
pass
print(i) # 2
这对 JS 开发者通常有点反直觉,因为你会下意识觉得:
- 这个变量不是在循环里定义的吗
- 为什么出了循环还能访问
原因就是:Python 更强调函数作用域,而不是你熟悉的那种块级作用域。
避坑警告:不要把 JS 的
let/const块级作用域直觉直接带进 Python。
在 Python 里,if、for、while代码块不会自动制造一个新的局部作用域。
在函数里给名字赋值,会发生什么
这又是 Python 作用域最容易让人误判的地方之一。
看一个例子:
count = 10
def update_count():
count = 20
print(count)
update_count() # 20
print(count) # 10
很多刚转 Python 的开发者第一眼会觉得:
- 函数里给
count赋值 - 那是不是把外面的全局
count改掉了
并没有。
因为在 Python 里,只要你在函数内部对某个名字赋值,解释器通常会把它当作当前函数的局部变量。
这就是为什么上面的 count = 20 只影响函数内部。
为什么会出现 UnboundLocalError
再看一个更有代表性的坑:
count = 10
def update_count():
print(count)
count = count + 1
# update_count() # UnboundLocalError
很多 JS 开发者第一次看到这个错误会很疑惑:
- 外面不是明明有
count吗 - 为什么前面的
print(count)还会报错
根因在于:
- 解释器看到函数内部有
count = ... - 就先认定
count是这个函数里的局部变量 - 于是前面的
print(count)也会尝试读取“尚未赋值的局部变量”
这个错误非常典型,值得你直接建立固定理解:
- 在函数里一旦给某个名字赋值,它默认就会被视为局部变量
如果类比 JS,这种行为不像 let 或 const 的直观体验,所以更容易踩坑。
global:明确告诉 Python,我要改全局变量
如果你真的想在函数内部修改全局变量,就要显式使用 global。
count = 10
def update_count():
global count
count = count + 1
update_count()
print(count) # 11
这里的 global count 等于在告诉解释器:
- 这个名字不要当局部变量处理
- 我要操作模块级那个全局
count
你可以把它理解成一种“显式声明意图”的机制。
如果用 JS/TS 对照,虽然没有完全等价物,但它有点像你在刻意说明:
- 我不是要定义局部变量
- 我要改外面的那一个
避坑警告:
global不是“推荐默认使用”的能力。
它能解决问题,但如果一个函数频繁依赖和修改全局状态,通常也意味着代码结构可能需要调整。
nonlocal:修改外层函数变量,而不是全局变量
这又是 Python 很有代表性的一个点,也是和闭包强相关的能力。
先看一个例子:
def outer():
count = 0
def inner():
nonlocal count
count += 1
print(count)
inner()
inner()
outer()
这里:
count不是全局变量- 它定义在外层函数
outer()里 inner()想修改它
如果没有 nonlocal count,Python 会把 count += 1 理解成:
- 你在
inner()里定义了一个新的局部count
于是就会出错。
而 nonlocal 的意思是:
- 这个名字不要当当前局部变量
- 去外层函数作用域里找同名变量
它对应的是 LEGB 里的 E,也就是 Enclosing。
官方文档里可以配合前面的作用域说明一起看:Python Scopes and Namespaces。
如果这里有多层嵌套函数,规则也一样,但要多记一句:
nonlocal会绑定到“最近的一层、已经存在该名字的外层函数作用域”
看一个例子:
def outer():
x = "outer"
def middle():
x = "middle"
def inner():
nonlocal x
x = "changed by inner"
print("inner:", x)
inner()
print("middle:", x)
middle()
print("outer:", x)
outer()
输出结果是:
inner: changed by inner
middle: changed by inner
outer: outer
这里 inner() 里的 nonlocal x 改到的是 middle() 这一层的 x,不是 outer() 的 x。原因很简单:
- 先往外找最近一层函数作用域
- 发现
middle()里已经有x - 那就绑定到它
如果中间那层没有这个名字,Python 才会继续往更外层找。
def outer():
x = "outer"
def middle():
def inner():
nonlocal x
x = "changed by inner"
inner()
middle()
print(x)
outer() # changed by inner
你可以把这条规则记成一句话:
nonlocal只会改“最近的、已存在该名字的外层函数变量”
它不能显式指定“我要改第 2 层还是第 3 层”。如果你在多层嵌套里很在意“到底改哪一层”,最稳妥的做法通常是:
- 不要在多层里复用同名变量
- 或者干脆重构成对象状态,而不是继续堆嵌套函数
global 和 nonlocal 的区别
这是非常值得单独对照的一组概念:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 修改模块全局变量 | 无直接对应关键字 | global |
明确指向模块级变量 |
| 修改外层函数变量 | 闭包中可捕获,但修改规则不同 | nonlocal |
明确指向最近的外层函数变量 |
你可以先把它记成:
global-> 改全局nonlocal-> 改外层函数
不要混。
闭包:两边都有,但 Python 的修改规则更显式
JS/TS 开发者对闭包通常并不陌生。
JS:
function createCounter() {
let count = 0;
return function () {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
Python 里也可以写出类似模式:
def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = create_counter()
print(counter()) # 1
print(counter()) # 2
这说明两边都支持:
- 外层函数返回内层函数
- 内层函数记住外层作用域里的变量
区别在于:
- JS 中你对闭包变量的修改直觉通常更自然
- Python 中如果你要重新绑定外层变量,常常需要显式写
nonlocal
“读取外层变量”和“修改外层变量”不是一回事
这是理解 Python 闭包和作用域最关键的分界线之一。
只读取时,通常没问题:
def outer():
name = "Alice"
def inner():
print(name)
inner()
outer()
但一旦你想修改:
def outer():
count = 0
def inner():
count += 1
return count
这时就需要 nonlocal。
所以你最好把它拆成两种情况记忆:
- 只读外层名字:通常直接能用
- 要改外层名字绑定:通常要
nonlocal
一个容易和 JS 混淆的点:循环变量和闭包
这个话题比较细,但对有前端经验的人很值得提早建立感觉。
JS 里你可能已经熟悉这个经典差异:
var在循环里有坑let会为每次循环提供更符合直觉的绑定
Python 没有 let / const 这套方案,因此在“循环 + 闭包”场景里也要格外小心。
例如下面这种代码,很多人会误以为每个函数都记住了当时的 i:
funcs = []
for i in range(3):
funcs.append(lambda: i)
print(funcs[0]()) # 2
print(funcs[1]()) # 2
print(funcs[2]()) # 2
为什么都是 2?
因为这些 lambda 并不是在创建时把值“拍快照”,而是在执行时去查找 i。
循环结束后,i 的最终值就是 2。
这个现象和前面说的“名字查找”其实是一脉相承的。
如果你想在定义时固定当前值,常见写法是借助默认参数:
funcs = []
for i in range(3):
funcs.append(lambda i=i: i)
print(funcs[0]()) # 0
print(funcs[1]()) # 1
print(funcs[2]()) # 2
这里的 i=i 就是在函数定义时把当前值绑定进去。
这个例子你现在先知道“有这么个坑”就够了,后面再看闭包时会越来越顺。
避坑警告:闭包里引用循环变量时,不要想当然地以为它会自动记住“当时那个值”。
Python 很多时候记住的是“名字”,不是你想象中的值快照。
给 JS/TS 开发者的作用域迁移建议
如果你想尽快减少作用域相关 bug,可以先遵守这些规则:
- 把 Python 作用域先理解成“名字查找规则”,而不是“声明关键字规则”
- 默认认为函数内赋值会创建局部变量
- 需要改全局变量时显式用
global - 需要改外层函数变量时显式用
nonlocal - 不要把
if/for/while当成 JS 式块级作用域 - 对“循环里的闭包”保持警惕
本节小结
这一节最值得你带走的是:
- Python 作用域的理解入口是名字查找,而不是
let/const LEGB是 Python 名字解析的核心模型- Python 没有 JS 那种同等地位的块级作用域心智
- 函数里一旦赋值,名字通常会被视为局部变量
- 修改全局变量用
global - 修改外层函数变量用
nonlocal - 只读外层变量和修改外层变量,是两件不同的事
下一章继续进入:模块。
模块
如果你来自 JS/TS,那么“模块”这个词你一定不陌生。你已经很熟这些东西:
importexportrequire- 文件拆分
- 目录组织
- 包管理
Python 也有完整的模块系统,但它的设计和前端世界并不是一一对应。
最适合前端开发者的第一层理解是:
- Python 里,一个
.py文件天然就是一个模块
这句话非常关键,因为它直接决定了你怎么看待:
- 文件边界
- 导入方式
- 项目结构
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 文件模块 | a.js / a.ts 是模块 |
a.py 是模块 |
两边都以文件为基本组织单位 |
| 导入语法 | import / require |
import / from ... import ... |
Python 没有 export 这个对应关键字 |
| 导出机制 | export / module.exports |
顶层名字默认可被导入 | Python 更像“模块命名空间暴露” |
| 目录级模块组织 | 包、别名、路径映射 | package 包 | Python 更强调包目录结构 |
| 入口判断 | require.main === module 等 |
if __name__ == "__main__": |
这是非常有代表性的 Python 习惯 |
一个文件就是一个模块
假设你有一个文件 math_tools.py:
def add(a, b):
return a + b
PI = 3.14159
那么这个文件本身就是模块 math_tools。
在另一个文件里你可以这样导入:
import math_tools
print(math_tools.add(1, 2))
print(math_tools.PI)
如果类比 JS/TS,你可以把它先想成:
import * as mathTools from "./mathTools";
console.log(mathTools.add(1, 2));
但 Python 有一个关键差异:
- Python 不需要显式写
export
只要某个名字定义在模块顶层,它通常就属于这个模块命名空间的一部分。
import xxx:导入整个模块命名空间
这是最基础也最稳的一种写法:
import math_tools
print(math_tools.add(1, 2))
它的好处是非常清晰:
- 你一眼就知道
add来自math_tools - 不容易和当前文件里的名字混在一起
JS 对应感受最接近:
import * as mathTools from "./mathTools";
对于刚开始写 Python 的人,这种写法通常比“把一堆名字直接导进当前作用域”更不容易乱。
from ... import ...:导入模块里的指定名字
Python 也允许你只导入其中几个名字:
from math_tools import add, PI
print(add(1, 2))
print(PI)
这有点像 JS:
import { add, PI } from "./mathTools";
优点是写起来更短,缺点也很明显:
- 当前文件里名字来源没那么显眼
- 更容易和本地变量重名
所以工程上通常可以先建立这个简单倾向:
- 模块较大、来源想更明确 ->
import module - 只取少量高频函数、语义很清楚 ->
from module import name
Python 没有 export,那“导出”到底是怎么回事
这是 JS/TS 开发者第一次接触 Python 模块时很常问的问题。
JS:
export function add(a: number, b: number) {
return a + b;
}
export const PI = 3.14159;
Python:
def add(a, b):
return a + b
PI = 3.14159
也就是说,Python 更像是:
- 这个模块里顶层定义了哪些名字
- 别的文件就可以按模块名把这些名字拿进来
你可以把模块先理解成一个命名空间对象。
import math_tools
print(dir(math_tools))
dir() 会列出模块里可见的名字。官方文档见 dir()。
避坑警告:不要在 Python 里下意识寻找
export default、named export这一套完全等价物。
Python 的模块暴露方式更接近“模块顶层命名空间”,不是 JS 那种显式导出语法。
包(package):目录级组织单位
如果说:
.py文件是模块
那么:
- 包含多个模块的目录结构,通常会形成包
你可以先看一个简单结构:
blog_tools/
__init__.py
formatters.py
validators.py
这里 blog_tools 可以看作一个包,里面有两个模块:
blog_tools.formattersblog_tools.validators
导入时:
from blog_tools.formatters import format_date
如果类比 JS/TS,你可以把包先理解成:
- 一个目录级的模块命名空间
但 Python 的包结构通常比前端目录“更语义化”,因为导入路径会直接体现目录层次。
__init__.py 是什么
对很多前端开发者来说,这是 Python 包里最陌生的文件之一。
你可以先把它理解成:
- 告诉 Python:这个目录要作为包来参与导入
同时它也常承担这些职责:
- 包初始化逻辑
- 对外统一暴露一些常用名字
例如:
from .formatters import format_date
from .validators import validate_slug
这样你就可以写:
from blog_tools import format_date, validate_slug
而不一定非要深入到具体子模块。
如果类比前端,它有点像目录层的“对外出口整理文件”,但不要机械对标成 JS 的 index.ts,因为语义并不完全相同。
import 发生时到底做了什么
你可以先抓住一个对工程非常有用的事实:
- 模块第一次被导入时,会执行模块顶层代码
例如:
print("module loaded")
def add(a, b):
return a + b
别的文件一旦 import 它,顶层这句 print("module loaded") 就会执行。
这和很多 JS 模块的加载直觉其实挺像:
- 模块加载时,顶层代码会运行
因此工程上一个非常重要的习惯是:
- 不要在模块顶层随手写副作用很重的逻辑
比如:
- 大量网络请求
- 数据库写入
- 启动主任务
这些逻辑最好放到函数里,或者放进入口判断中。
if __name__ == "__main__":
这是 Python 模块系统里最具代表性的习惯之一。
先看例子:
def main():
print("run app")
if __name__ == "__main__":
main()
你可以先把它理解成:
- 如果这个文件是“直接运行”的入口,就执行
main() - 如果它只是被别的文件
import,就不要执行这段入口逻辑
这和 Node.js 里下面这种思路很接近:
if (require.main === module) {
main();
}
为什么 Python 需要这一层?
因为前面说过:
- 导入模块时,模块顶层代码会执行
所以你通常不希望“导入一个工具模块,就顺便把它的测试逻辑也跑了”。
这时 if __name__ == "__main__": 就很有用。
官方文档可以结合模块说明和脚本入口习惯一起理解,这也是 Python 日常脚本和工具代码里非常高频的模式。
避坑警告:不要把测试代码、演示代码、命令行入口逻辑随手写在模块顶层。
如果这个文件既可能被导入,也可能被直接运行,就优先考虑if __name__ == "__main__":。
Python 模块和 ESM / CommonJS 的对照理解
从前端开发者视角看,可以先这样建立粗略映射:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
import * as x |
import * as utils from "./utils" |
import utils |
两边都保留模块命名空间 |
| 命名导入 | import { foo } from "./utils" |
from utils import foo |
语义相近 |
| 默认导出 | export default |
无完全等价默认机制 | Python 不鼓励默认导出那套心智 |
| 模块入口判断 | require.main === module |
__name__ == "__main__" |
Python 有明确习惯写法 |
别名导入
和 JS 一样,Python 也支持别名:
import math_tools as mt
from math_tools import add as add_numbers
这在几个场景里特别常见:
- 名字太长
- 防止冲突
- 遵循社区习惯
最典型例子之一:
import numpy as np
import pandas as pd
这类写法在数据生态里几乎已经变成约定俗成。
不推荐一上来就用 from x import *
这是一个非常值得前端开发者提早建立的习惯。
from math_tools import *
这种写法的坏处很明显:
- 当前作用域突然进来一堆名字
- 很难判断某个名字来自哪里
- 容易和本地定义冲突
这点和 JS/TS 工程里反对“命名来源不清晰”的做法是同一个思路。
所以通常建议:
- 优先
import module - 或者
from module import explicit_name - 少用
import *
相对导入和绝对导入
这一部分你现在不需要学得太深,但至少要先认识这两种写法。
假设包结构如下:
blog_tools/
__init__.py
formatters.py
validators.py
在 validators.py 里,你可以写绝对导入:
from blog_tools.formatters import format_date
也可以写相对导入:
from .formatters import format_date
你可以先这样理解:
- 绝对导入:从项目/包根路径说全名
- 相对导入:从当前包位置出发
这和前端里:
@/utils/date../utils/date
这种心智有点接近。
本节小结
这一节最值得你带走的是:
.py文件天然就是模块- Python 通过
import和from ... import ...使用模块内容 - Python 没有 JS 那种显式
export关键字体系 - 多个模块组成目录结构后,通常形成包
__init__.py经常承担包初始化和对外整理入口的职责if __name__ == "__main__":是区分“直接运行”和“被导入”的关键模式
下一章继续进入:面向对象。
面向对象
如果你已经写过 TS class、React class 组件时代代码、Node.js 服务类、ORM Model 或 NestJS Service,那你对面向对象不会陌生。
你已经习惯这些概念:
- 类
class - 实例
instance - 构造函数
- 实例属性
- 实例方法
- 继承
super
Python 同样支持完整的面向对象能力,但它的“语言气质”和 TS/JS 仍然有明显差异:
- 写法更统一
- “对象模型”更直接暴露在语法层
- 很多你在 TS 里依赖类型系统表达的约束,在 Python 里更依赖约定和运行时行为
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 类定义 | class User {} |
class User: |
Python 类定义依赖冒号和缩进 |
| 构造函数 | constructor() |
__init__() |
Python 初始化逻辑写在 __init__ |
| 当前实例 | this |
self |
self 需要显式写在方法参数里 |
| 实例属性 | this.name = ... |
self.name = ... |
语义相近,但写法不同 |
| 类属性 | static 字段 |
类体顶层属性 | Python 类属性不需要 static 关键字 |
| 方法重写 | override / 同名方法覆盖 | 同名方法覆盖 | Python 运行时决定行为,类型提示是辅助 |
| 继承 | extends |
class Admin(User): |
Python 用括号声明父类 |
类 Class
先从最熟悉的类定义开始。
JS/TS:
class User {
name: string;
constructor(name: string) {
this.name = name;
}
greet() {
console.log(`hello, ${this.name}`);
}
}
const user = new User("Alice");
user.greet();
Python:
class User:
def __init__(self, name):
self.name = name
def greet(self):
print(f"hello, {self.name}")
user = User("Alice")
user.greet()
这段代码已经把 Python 类最核心的几个特点暴露出来了:
- 用
class User:定义类 - 构造初始化逻辑写在
__init__ - 实例方法第一个参数通常显式写
self - 实例化时直接
User("Alice"),不需要new
避坑警告:Python 实例化对象时不用写
new。
这是 JS/TS 开发者切过来最容易顺手多敲出来的关键字之一。
self:不要把它理解成关键字
JS/TS 开发者看到 Python 方法时,第一眼通常会被这个参数吸引:
def greet(self):
...
为什么要手动写 self?
因为 Python 没有 JS 那种隐式的 this 绑定表现形式。你可以先把它理解成:
self就是“当前实例对象”的引用
JS:
class User {
constructor(name: string) {
this.name = name;
}
greet() {
console.log(this.name);
}
}
Python:
class User:
def __init__(self, name):
self.name = name
def greet(self):
print(self.name)
需要特别记住的是:
self不是关键字- 它只是社区约定的名字
理论上你可以写成:
class User:
def greet(current_instance):
print("hello")
但几乎没有人会这么写,因为 self 已经是 Python 社区的事实标准。
避坑警告:虽然
self不是语法关键字,但你不应该自作聪明换名字。
对团队协作来说,self就是默认标准写法。
__init__ 不是“构造函数本体”
这又是一个从 JS/TS 切到 Python 时值得单独说明的点。
在 TS 里你通常会说:
constructor是构造函数
Python 里最常看到的是 __init__,很多人会直接把它等同成构造函数。这样理解能帮助入门,但不够精确。
先在工程层面记住就够了:
__init__是对象创建后用于初始化实例状态的方法
class User:
def __init__(self, name):
self.name = name
你现在不需要深挖 __new__ 之类更底层的话题,但至少不要把 __init__ 理解成“Python 版 constructor 的逐字替换”。它更准确的角色是“初始化”。
数据成员
这一节你可以先把“数据成员”理解成类或实例身上承载的数据。
在 Python 里,最常见的就是:
- 实例属性
- 类属性
实例变量 / 实例属性
实例属性属于每个对象自己。
JS/TS:
class User {
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
const u1 = new User("Alice", 18);
const u2 = new User("Bob", 20);
Python:
class User:
def __init__(self, name, age):
self.name = name
self.age = age
u1 = User("Alice", 18)
u2 = User("Bob", 20)
这里:
u1.name和u2.name是各自实例自己的值- 一个实例改了自己的属性,不会影响另一个实例
u1.name = "Alice Updated"
print(u1.name) # Alice Updated
print(u2.name) # Bob
这和 JS/TS 的实例字段直觉基本一致。
类属性
类属性则定义在类本身上,而不是单个实例上。
JS/TS 里你会想到 static:
class User {
static platform = "web";
}
console.log(User.platform);
Python 没有这个完全等价的字段关键字写法。最常见的方式是直接写在类体顶层:
class User:
platform = "web"
print(User.platform)
实例通常也能访问到它:
user = User()
print(user.platform) # web
但它和实例属性不是一回事。
对照理解:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 实例属性 | this.name |
self.name |
每个实例自己持有 |
| 类属性 | static platform |
platform = "web" 写在类体里 |
Python 不需要 static 关键字来定义类属性 |
看一个容易混淆的例子:
class User:
platform = "web"
u1 = User()
u2 = User()
u1.platform = "mobile"
print(u1.platform) # mobile
print(u2.platform) # web
print(User.platform) # web
为什么 u1.platform 改了,但 User.platform 没变?
因为这里发生的是:
- 你不是在修改类属性本身
- 而是在
u1这个实例上新建了一个同名实例属性
这对 JS/TS 开发者来说很值得提早知道,因为它不像 static 关键字那样“看起来就明确”。
避坑警告:通过实例给类属性同名赋值时,很多时候你并不是在改类属性,而是在实例上遮蔽了它。
一旦涉及共享状态,最好明确区分“这是类级数据”还是“实例级数据”。
属性可以动态添加
这也是 Python 和 JS/TS 很接近、但又更“自由”的地方。
class User:
pass
user = User()
user.name = "Alice"
print(user.name)
从运行时行为上看,这有点像你在 JS 里给对象动态挂属性:
const user: any = {};
user.name = "Alice";
但从工程角度说,Python 虽然允许你这么做,不代表你应该滥用。正常业务代码里,仍然更推荐:
- 在
__init__里明确初始化实例属性
这样类的结构会更清晰。
方法重写
方法重写就是:
- 子类提供和父类同名的方法
- 从而替换或扩展父类行为
JS/TS:
class Animal {
speak() {
console.log("...");
}
}
class Dog extends Animal {
speak() {
console.log("wang");
}
}
Python:
class Animal:
def speak(self):
print("...")
class Dog(Animal):
def speak(self):
print("wang")
使用时:
dog = Dog()
dog.speak() # wang
这就是最基本的重写。
如果你来自 TS,需要注意一件事:
- Python 不需要写
override关键字也能重写成功
它主要依赖运行时方法解析,而不是编译期强约束。
这意味着:
- 灵活
- 但也更依赖你自己保持命名和签名清晰
继承
Python 继承的写法也很直观。
JS/TS:
class Animal {}
class Dog extends Animal {}
Python:
class Animal:
pass
class Dog(Animal):
pass
也就是说:
- JS/TS 用
extends - Python 用类名后面的括号列出父类
你可以把:
class Dog(Animal):
...
理解成:
Dog继承自Animal
这部分迁移难度不高。
super()
当子类想复用父类逻辑时,两边都会用到 super。
JS/TS:
class Animal {
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
}
Python:
class Animal:
def __init__(self, name):
self.name = name
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name)
self.breed = breed
这里最重要的是建立一个顺手直觉:
- 父类初始化逻辑要复用时,用
super().__init__(...)
再看重写方法时调用父类实现:
class Animal:
def speak(self):
print("...")
class Dog(Animal):
def speak(self):
super().speak()
print("wang")
这和你在 TS 里“先调父类,再补子类行为”的思路非常相近。
避坑警告:子类重写
__init__后,如果父类本来负责初始化关键属性,而你又忘了super().__init__(...),很容易留下半初始化对象。
这类 bug 在 Python 里不会总是第一时间报得很漂亮,所以要主动养成习惯。
局部变量 / 实例变量
这一组概念在 Python 类方法里很容易和前面作用域章节串起来。
看一个例子:
class User:
def __init__(self, name):
self.name = name
def greet(self):
prefix = "hello"
print(prefix, self.name)
这里:
self.name是实例变量 / 实例属性prefix是方法内部的局部变量
对照 JS:
class User {
constructor(name: string) {
this.name = name;
}
greet() {
const prefix = "hello";
console.log(prefix, this.name);
}
}
所以你可以这样区分:
- 只在某个方法执行期间临时存在的 -> 局部变量
- 需要挂在对象身上、供多个方法共享的 -> 实例变量
这是写 Python OOP 时非常基础、但特别重要的判断。
一个高频误区:忘了写 self.
这个错误和前端开发者第一次写 Python 类几乎是绑定出现的。
错误示例:
class User:
def __init__(self, name):
name = name
上面这段不会把参数保存到实例上,它只是把局部变量 name 赋值给局部变量 name,毫无意义。
正确写法:
class User:
def __init__(self, name):
self.name = name
避坑警告:在类方法里,想把值保存到对象身上,必须显式写
self.xxx = ...。
不写self.,多数时候你操作的只是一个普通局部变量。
实例化
实例化就是根据类创建对象。
JS/TS:
const user = new User("Alice");
Python:
user = User("Alice")
你可以把它理解成:
- 调用类对象
- 得到一个实例
实例创建后,就可以访问属性和方法:
user = User("Alice")
print(user.name)
user.greet()
和 JS/TS 最大的表面差异只有一个:
- Python 不写
new
但真正更重要的差异是:
- Python 更依赖运行时行为
- 没有 TS 那种同强度的编译期类字段保护
所以在 Python 里写类时,更需要你自己保持结构清晰。
什么时候该用类,什么时候不该硬上 OOP
这部分虽然超出语法本身,但对前端开发者特别重要。
因为很多从 TS 迁移过来的人,容易下意识把所有东西都包成类。
在 Python 里,类当然常见,但并不是所有逻辑都必须 OOP。
这些场景通常更适合用类:
- 需要维护对象状态
- 多个方法围绕同一份状态协作
- 需要继承或多态
- 你在表达一个明确的领域实体
这些场景则未必要上类:
- 只是做几个纯函数转换
- 只是组织少量工具方法
- 没有明显状态,也没有对象边界
这和现代前端逐渐减少“为类而类”的趋势其实有点像。
本节小结
这一节最值得你带走的是:
- Python 支持完整的类、实例、继承和重写
__init__负责初始化实例状态self对应当前实例,但需要显式写出来self.name是实例属性,方法内普通变量只是局部变量- 类属性和实例属性要明确区分
- 子类重写后,常常要考虑是否调用
super() - Python 可以写 OOP,但也不需要为了像 TS 一样“到处 class”而滥用类
下一章继续进入:错误与调试。
错误与调试
如果你来自 JS/TS,那么你已经很熟这种问题:
- 语法写错,代码根本跑不起来
- 运行时抛异常
- 用
try...catch捕获错误 - 用
throw new Error(...)主动抛错
Python 也有完整的错误处理体系,而且在很多脚本、后端、自动化任务里,异常处理甚至比前端应用代码更常见。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 语法错误 | SyntaxError |
SyntaxError |
两边都有,代码在执行前就会失败 |
| 运行时错误 | TypeError、ReferenceError 等 |
TypeError、NameError、ValueError 等 |
Python 的异常类型划分很细 |
| 捕获错误 | try...catch |
try...except |
Python 用 except,不是 catch |
| 兜底清理 | finally |
finally |
两边语义相近 |
| 主动抛错 | throw new Error(...) |
raise ValueError(...) 等 |
Python 常直接抛具体异常类型 |
| 自定义错误 | class MyError extends Error |
class MyError(Exception) |
两边都支持自定义异常类 |
语法错误
语法错误的意思很简单:
- 解释器 / 引擎连代码结构都看不懂
- 所以程序还没真正执行,就已经失败了
JS:
if (true {
console.log("hello");
}
这会直接报语法错误,因为括号不完整。
Python:
if True
print("hello")
这也会直接报 SyntaxError,因为:
if True后面缺少:- 代码结构不合法
异常
语法错误之外,程序在运行过程中也可能出错,这类错误在 Python 里通常称为 异常(exception)。官方文档见 Built-in Exceptions。
先看几个最常见的对照。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 类型不匹配 | TypeError |
TypeError |
两边都有 |
| 名字/变量不存在 | ReferenceError |
NameError |
Python 更常叫名字未定义 |
| 非法值 | 常自定义 Error 或抛 RangeError |
ValueError |
Python 很常用 ValueError 表达“值不合法” |
| 下标越界 | 常得到 undefined 或后续出错 |
IndexError |
Python 列表越界会直接报错 |
| 字典/对象键缺失 | undefined |
KeyError |
Python 对 dict 缺键更严格 |
看几个例子。
NameError
console.log(userName); // ReferenceError
print(user_name) # NameError
JS 会更常说“引用错误”;Python 更强调“这个名字不存在”。
TypeError
console.log((123 as any).toUpperCase()); // TypeError
print(123.upper()) # AttributeError
这里要注意一个细节:
- JS 很多“对象没有这个方法”的情况最终是
TypeError - Python 常常会更精细地区分成
AttributeError
所以不要期待两边异常类型一一对应。
IndexError
const arr = [1, 2];
console.log(arr[10]); // undefined
arr = [1, 2]
print(arr[10]) # IndexError
这是一个非常值得前端开发者尽快适应的差异:
- JS 里很多越界访问会得到
undefined - Python 里很多越界访问会直接抛错
KeyError
const user = { name: "Alice" };
console.log(user.age); // undefined
user = {"name": "Alice"}
print(user["age"]) # KeyError
这也是为什么前面讲字典时,会推荐你尽快习惯:
user.get("age")
而不是在不确定 key 是否存在时直接硬取。
避坑警告:不要把 JS 那种“取不到属性通常只是
undefined”的直觉带进 Python。
Python 很多容器在你访问非法下标或不存在 key 时,会明确抛异常。
异常的处理
JS/TS 最熟悉的写法是:
try {
JSON.parse("bad json");
} catch (err) {
console.error("parse failed", err);
} finally {
console.log("done");
}
Python 对应写法:
try:
import json
json.loads("bad json")
except json.JSONDecodeError as err:
print("parse failed", err)
finally:
print("done")
对照理解:
try-> 可能出错的代码except-> 捕获并处理异常finally-> 不管成不成功都会执行
最基础的 try / except
try:
num = int("abc")
except ValueError:
print("invalid number")
这和 JS 里的 try...catch 没有本质门槛差异。
但 Python 社区非常强调一件事:
- 尽量捕获具体异常,而不是一把梭全吞
例如,不太推荐:
try:
num = int("abc")
except Exception:
print("something went wrong")
更推荐:
try:
num = int("abc")
except ValueError:
print("invalid number")
原因很简单:
- 具体异常更容易定位问题
- 不会把原本不该吞掉的错误也一起吞掉
避坑警告:不要一上来就
except Exception:当万能兜底。
它虽然省事,但很容易把真正的 bug 一起吃掉,后面调试会非常痛苦。
捕获多个异常
JS 里你通常在 catch 后再自己判断错误类型。
Python 可以直接在语法层表达多个异常类型:
try:
value = int(data)
except (TypeError, ValueError):
print("bad input")
这种写法很适合“多种错误都需要同一套处理逻辑”的场景。
获取异常对象
JS:
try {
throw new Error("boom");
} catch (err) {
console.error(err.message);
}
Python:
try:
raise ValueError("boom")
except ValueError as err:
print(err)
这里的 as err 就相当于把异常对象绑定到一个变量上,方便你读取信息或继续记录日志。
else:只有没异常时才执行
这是 Python 异常处理里一个 JS 开发者不一定熟悉的点。官方文档见 Errors and Exceptions。
try:
num = int("123")
except ValueError:
print("invalid")
else:
print("success:", num)
else 的语义是:
try块没有抛异常- 才执行这里
你可以把它理解成一种“成功分支”。
这在“失败走异常处理,成功走正常逻辑”时会让结构更清晰。
finally:不管怎样都执行
这个和 JS 的理解几乎一致。
最常见用途就是清理资源:
file = open("demo.txt", "r", encoding="utf-8")
try:
content = file.read()
print(content)
finally:
file.close()
但 Python 更推荐你在资源管理场景里优先使用 with,这一点到 IO 章节还会展开。
主动抛出异常
JS:
function divide(a: number, b: number) {
if (b === 0) {
throw new Error("b cannot be 0");
}
return a / b;
}
Python:
def divide(a, b):
if b == 0:
raise ValueError("b cannot be 0")
return a / b
这里的 raise 就对应 JS 的 throw。
但 Python 一个很明显的工程习惯是:
- 优先抛“语义更具体”的异常类型
比如:
- 参数值不合法 ->
ValueError - 类型不对 ->
TypeError - key 不存在 ->
KeyError - 权限不够 ->
PermissionError
而不是所有情况都自己 raise Exception("something bad")
避坑警告:在 Python 里,能抛具体异常类型时,就不要偷懒都抛
Exception。
具体异常类型本身就是 API 语义的一部分。
重新抛出异常
有时候你只是想记录一下日志,但不想吞掉错误:
try:
do_something()
except ValueError as err:
print("log:", err)
raise
这里单独写一个 raise 的意思是:
- 保留当前异常
- 原样继续向上抛出
这在很多服务端代码、任务调度代码里非常常见。
用户自定义异常
当内置异常类型已经不足以表达你的业务语义时,就可以自定义异常。
JS/TS:
class AuthError extends Error {
constructor(message: string) {
super(message);
this.name = "AuthError";
}
}
Python:
class AuthError(Exception):
pass
然后你就可以这样抛出:
class AuthError(Exception):
pass
def access_admin_panel(user):
if not user.get("is_admin"):
raise AuthError("admin only")
print("welcome")
捕获时:
try:
access_admin_panel({"is_admin": False})
except AuthError as err:
print("auth failed:", err)
对照理解:
- JS 里常继承
Error - Python 里常继承
Exception
如果你只需要一个有语义的错误类型,最简版本就是:
class BizError(Exception):
pass
自定义异常适合什么场景
这些场景通常很适合:
- 你想让业务错误和系统错误明确分层
- 你想在调用方只捕获某一类业务错误
- 你在写库、SDK、框架,希望调用方能精确处理错误
例如:
AuthErrorPaymentErrorConfigErrorValidationError
这会比单纯抛一个没有分类的 Exception 更利于维护。
调试思路
严格来说,“调试”不只是会不会写 try / except,更重要的是你如何定位问题。
对于 JS/TS 开发者来说,迁移到 Python 后先建立这几个习惯最有用:
- 先看异常类型
- 再看异常信息
- 再看出错行和调用栈
- 不要一上来就用“大而全”的异常吞掉问题
例如:
data = {"name": "Alice"}
print(data["age"])
如果报错是 KeyError: 'age',你就应该立刻联想到:
- 这是字典 key 不存在
- 不是“对象属性是 undefined 继续往下跑”
也就是说,Python 的异常信息很多时候已经给了你很具体的排查方向。
打印不是低级手段
很多前端开发者会把“打印调试”看作临时办法,但在 Python 脚本、后端任务、命令行工具里,它依然非常实用。
print("user =", user)
print("items =", items)
print("step 2 reached")
只要你打印得有针对性,它就能非常快地帮助你确认:
- 值是不是你以为的那个值
- 逻辑有没有走到某一段
- 异常前最后一步发生了什么
当然,正式工程里更推荐用日志系统,但“先快速定位问题”这一步,打印从来不丢人。
本节小结
这一节最值得你带走的是:
- 语法错误和运行时异常是两类不同问题
- Python 异常类型很多,而且语义通常比 JS 更细
try / except / else / finally是 Python 的核心错误处理结构- 用
raise主动抛异常,对应 JS 的throw - 能抛具体异常时,就不要偷懒只抛
Exception - 自定义异常类能让业务错误更清晰
- 调试时先看异常类型、异常信息和调用栈,不要急着把错误吞掉
本篇先跳过 IO 编程,这部分更适合放到“Node.js 与 Python 对照”的专题里单独展开。
下一章直接进入:进程、线程与协程。
进程和线程
如果你来自前端 / Node.js 世界,那么这章一定会有一种“既熟悉又陌生”的感觉。
熟悉在于你已经知道这些关键词:
- 进程
- 线程
- 事件循环
async/await- 并发
陌生在于 Python 这套并发模型和 Node.js 并不是同一套默认心智:
- Node.js 默认更偏单进程、单线程事件循环
- Python 同时提供进程、线程、协程多条路线
- Python 还有一个必须绕不过去的概念:
GIL
这一章最重要的不是记 API,而是先建立选型直觉:
- 什么时候该用进程
- 什么时候该用线程
- 什么时候该用协程
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 主流默认模型 | Node.js 事件循环 + 异步 IO | 同时有同步、线程、进程、协程 | Python 路线更多,选型也更多 |
| 进程 | child_process、cluster |
multiprocessing |
Python 常用多进程绕开 CPU 密集型瓶颈 |
| 线程 | worker_threads |
threading |
Python 线程常用于 IO 密集任务 |
| 协程 | async/await + Promise |
async/await + asyncio |
语法相似,但运行模型和生态不同 |
| GIL | 无完全对应核心机制 | 有 | 这是 Python 并发选型里必须考虑的因素 |
先建立最小心智模型
你可以先把三者粗暴但实用地理解成:
- 进程:多个彼此独立的 Python 解释器 / 运行实例
- 线程:同一进程里的多个执行流,共享内存
- 协程:单线程内通过事件循环协作切换的任务
如果要用前端开发者容易理解的话说:
- 进程像多个独立 Node 进程
- 线程像一个进程内部开多个 worker 执行单元
- 协程像“语言内建、语法级支持的异步任务调度”
但这只是第一层。真正影响选型的,是它们各自擅长的任务类型。
进程
进程最大的特点是:
- 相互独立
- 默认不共享内存
- 一个进程崩了,不一定拖死另一个
如果你类比 Node.js,最接近的是:
child_processcluster
Python 里最常见的进程工具是 multiprocessing。
先看一个非常基础的例子:
from multiprocessing import Process
def run_task(name):
print(f"task {name} is running")
if __name__ == "__main__":
p1 = Process(target=run_task, args=("A",))
p2 = Process(target=run_task, args=("B",))
p1.start()
p2.start()
p1.join()
p2.join()
对照 Node.js,你可以先把它想成:
// 只是概念类比,不是完全等价代码
import { fork } from "node:child_process";
fork("./worker-a.js");
fork("./worker-b.js");
什么时候优先考虑进程
Python 里,进程最典型的适用场景是:
- CPU 密集型任务
比如:
- 大量图像处理
- 视频转码
- 数值计算
- 大规模数据压缩
- 复杂批处理
原因后面讲 GIL 时会更清楚,但你现在先记住一句够用了:
- CPU 很忙时,Python 往往更倾向多进程,而不是多线程
进程的代价
它并不是“更强就无脑用”。
进程也有明显成本:
- 创建成本更高
- 进程之间通信更重
- 内存占用更大
所以如果任务主要不是吃 CPU,而是等网络、等文件、等数据库,那直接上多进程通常不划算。
线程
线程可以理解成:
- 同一个进程里的多个执行单元
- 共享同一份进程内存空间
如果类比 Node.js,它有点像 worker_threads,但不要机械等同。
Python 里最常见的是 threading。
基础例子:
import threading
import time
def fetch_data(name):
print(f"start {name}")
time.sleep(1)
print(f"done {name}")
t1 = threading.Thread(target=fetch_data, args=("A",))
t2 = threading.Thread(target=fetch_data, args=("B",))
t1.start()
t2.start()
t1.join()
t2.join()
这段代码的理解方式很简单:
- 开两个线程
- 它们分别执行
fetch_data - 主线程等待它们完成
什么时候线程比较合适
Python 里线程更常见的场景通常是:
- IO 密集型任务
比如:
- 同时请求多个外部接口
- 批量读写文件
- 等数据库返回
- 等第三方服务响应
为什么?
因为在这些场景里,程序大部分时间不是在疯狂算 CPU,而是在“等待”。
这时线程就能把等待时间重叠起来,提高整体吞吐。
线程的问题:共享状态会带来复杂度
这点和任何语言都一样,只要共享内存,就会有这些风险:
- 竞态条件
- 状态不一致
- 调试困难
例如:
import threading
count = 0
def add_once():
global count
count += 1
这类代码在多线程环境下就可能不是你想象中那么“理所当然”。
所以一旦进入多线程,除了会用 API,更重要的是要对“共享状态”保持警惕。
避坑警告:线程不是“更快的普通函数调用”。
只要多个线程读写同一份状态,你就要开始考虑同步、锁、竞态这些工程问题。
GIL 说明
这是 Python 并发里最绕不过去的话题之一。
GIL 全名是:
- Global Interpreter Lock
先给前端开发者一个务实版本解释:
- 在 CPython 里,同一时刻通常只有一个线程真正执行 Python 字节码
这句话直接影响一个常见误解:
- “我开了很多线程,所以 CPU 密集任务一定能线性加速”
不一定。
为什么 GIL 很重要
因为它会导致一个结果:
- 多线程对 IO 密集型任务常常有帮助
- 但对 CPU 密集型 Python 代码,不一定能带来你期待中的并行收益
这就是为什么 Python 世界里经常会有这条经验法则:
- IO 密集 -> 线程 / 协程
- CPU 密集 -> 进程
这不是死规定,但它确实是很高频、很实用的默认判断。
用 Node.js 视角怎么理解 GIL
可以这样粗略类比:
- Node.js 默认就不是靠多线程跑 JS 计算任务
- Python 虽然有线程,但也不是“开线程就等于 CPU 并行”
两边都在提醒你一件事:
- 并发模型要根据任务类型选
只是 Python 把这个差异暴露得更明显,因为 threading API 就摆在那里,而 GIL 会让很多初学者误以为“API 有了,性能一定就线性起飞”。
避坑警告:不要把“能创建很多线程”和“CPU 计算一定并行”画等号。
在 Python 里,GIL是你做并发选型时必须考虑的现实约束。
协程 async/await
如果你是前端 / Node.js 开发者,这一节会是整章里最容易产生“终于熟了”的部分。
因为 Python 也有:
asyncawait
而且它同样是用来表达异步任务协作的核心语法。官方文档主要看 asyncio。
先看最小对照。
JS:
async function fetchUser() {
const res = await fetch("/api/user");
return res.json();
}
Python:
import asyncio
async def fetch_user():
await asyncio.sleep(1)
return {"name": "Alice"}
表面上确实很像:
- JS 用
async function - Python 用
async def
Python 协程最该怎么类比 JS
你可以先把它理解成:
- Python 协程 ~= JS Promise 风格异步流程,在语法层的另一种实现
但要注意,它们不是完全同一套运行时模型。
JS/Node.js 里你脑子里通常会自然联想到:
- Promise
- event loop
- microtask
- I/O callback
Python 里更常见的关键词是:
- coroutine
- event loop
- task
asyncio
也就是说,语法长得像,不代表底层细节和生态使用方式完全一样。
一个更完整的 asyncio 例子
import asyncio
async def fetch_data(name, delay):
print(f"start {name}")
await asyncio.sleep(delay)
print(f"done {name}")
return name
async def main():
results = await asyncio.gather(
fetch_data("A", 1),
fetch_data("B", 1),
)
print(results)
asyncio.run(main())
这段代码对前端开发者来说,最接近:
async function fetchData(name: string, delay: number) {
console.log(`start ${name}`);
await new Promise((resolve) => setTimeout(resolve, delay * 1000));
console.log(`done ${name}`);
return name;
}
async function main() {
const results = await Promise.all([
fetchData("A", 1000),
fetchData("B", 1000),
]);
console.log(results);
}
其中:
asyncio.gather(...)很像Promise.all(...)asyncio.run(main())可以先理解成“启动这个异步入口”
什么时候优先协程
协程特别适合:
- 高并发 IO 场景
- 网络请求很多
- 任务切换频繁
- 希望用单线程事件循环管理大量等待中的任务
例如:
- 爬虫
- 批量 API 调用
- Web 服务中的异步处理
- 大量 socket / 网络连接
这和 Node.js 开发者对 async/await 的使用直觉就非常接近了。
Python async/await 和 JS async/await 的关键差异
表面相似,但有几件事必须尽快建立认知:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 异步函数定义 | async function |
async def |
语法不同但语义接近 |
| 并发等待 | Promise.all() |
asyncio.gather() |
很适合类比理解 |
| 休眠 | setTimeout / Promise 封装 |
await asyncio.sleep() |
Python 不直接用浏览器/Node 定时器模型 |
| 运行入口 | 环境自动管理事件循环较多 | 常显式 asyncio.run() |
Python 对事件循环入口更显式 |
最重要的一条是:
- Python 的
async/await并不会自动让所有同步代码 magically 变快
如果你在异步函数里做的是阻塞式同步操作,比如:
- 大量纯 CPU 计算
- 阻塞式文件操作
- 阻塞式网络库调用
那它依然会卡住事件循环。
这点和 JS 一样:
async不是性能魔法- 它只是让你更优雅地组织异步流程
无论在 Node.js 还是 Python,真正决定效果的,仍然是你等待的任务类型,以及底层调用是否真的是非阻塞的。
进程、线程、协程怎么选
如果你只想先拿到一个足够实用的经验表,可以先记这个:
| 场景 | 更常见选择 | 原因 |
|---|---|---|
| 大量计算、吃 CPU | 进程 | 更容易利用多核,也更绕开 GIL 限制 |
| 大量阻塞 IO | 线程 | 心智简单,很多老库直接可用 |
| 大量高并发网络 IO | 协程 | 单线程下可管理大量等待任务 |
本节小结
这一章最值得你带走的是:
- Python 同时提供进程、线程、协程三条常见并发路线
- 进程更适合 CPU 密集型任务
- 线程更常用于 IO 密集型任务
GIL是 Python 并发选型里必须理解的现实约束- Python
async/await和 JS 很像,但运行时心智不完全相同 asyncio.gather()很适合类比Promise.all()async不是性能魔法,底层是否非阻塞才决定效果
下一章继续进入:常用方法(内置模块)。
常用方法(内置模块)
这一章的目标不是把 Python 标准库背成电话簿,而是帮你快速建立一套“日常够用”的工具感。
如果你来自 JS/TS,你已经习惯了:
string上一堆方法Array.prototype上一堆方法Object.keys()/Object.entries()MathDateprocesspath/fs/os
Python 也有对应能力,但风格差异很明显:
- 有些能力挂在对象方法上
- 有些能力更偏内置函数风格
- 标准库里很多模块都非常实用,而且“即开即用”
这一章我先聚焦最常用的几类:
- 字符串常用方法
- 列表常用方法
- 字典常用方法
mathrandomdatetimeossys
先建立一个总体直觉
对前端开发者来说,最先适应的往往不是“有没有这些 API”,而是“它们摆放的位置不一样”。
看一个非常典型的差异:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 长度 | arr.length / str.length |
len(arr) / len(str) |
Python 大量基础能力偏内置函数风格 |
| 最大值 | Math.max(...nums) |
max(nums) 或 max(a, b) |
Python 常直接用内置函数 |
| 求和 | arr.reduce(...) |
sum(nums) |
Python 内置函数很强 |
| 成员判断 | arr.includes(x) |
x in arr |
Python 更喜欢语法级表达 |
| 键值对遍历 | Object.entries(obj) |
dict.items() |
Python dict 自带映射能力 |
也就是说:
- JS/TS 经常把能力挂在对象或全局对象上
- Python 更常在“对象方法 + 内置函数 + 标准库模块”三者之间分配职责
这就是为什么学 Python 常用方法时,不能完全照搬 JS 的“对象原型方法表”心智。
字符串常用方法
Python 字符串是 str,很多场景和 JS 字符串类似,但也有一批特别高频的方法。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 长度 | str.length |
len(s) |
Python 用内置函数 |
| 大小写转换 | toUpperCase() / toLowerCase() |
upper() / lower() |
方法名不同 |
| 去首尾空格 | trim() |
strip() |
Python 有 strip/lstrip/rstrip |
| 查找子串 | includes() / indexOf() |
in / find() / index() |
Python in 很常用 |
| 替换 | replace() |
replace() |
两边都有 |
| 分割 | split() |
split() |
两边都有 |
| 拼接数组 | arr.join("-") |
"-".join(items) |
Python 是“分隔符调用 join” |
大小写、去空格、替换
JS:
const text = " Hello World ";
console.log(text.trim());
console.log(text.toUpperCase());
console.log(text.replace("World", "Python"));
Python:
text = " Hello World "
print(text.strip())
print(text.upper())
print(text.replace("World", "Python"))
这里有几个特别值得建立的映射:
trim()->strip()toUpperCase()->upper()toLowerCase()->lower()
如果你只想去左边或右边空格:
print(text.lstrip())
print(text.rstrip())
查找子串
JS:
const text = "hello python";
console.log(text.includes("python")); // true
console.log(text.indexOf("python")); // 6
Python:
text = "hello python"
print("python" in text) # True
print(text.find("python")) # 6
这里的迁移建议非常简单:
- 判断“有没有” -> 优先
in - 需要位置 -> 用
find()或index()
find() 和 index() 的区别:
find()找不到返回-1index()找不到会抛异常
text = "hello"
print(text.find("x")) # -1
# print(text.index("x")) # ValueError
分割和拼接
这组能力在脚本、日志处理、配置解析里特别高频。
JS:
const csv = "a,b,c";
const items = csv.split(",");
console.log(items.join("-"));
Python:
csv = "a,b,c"
items = csv.split(",")
print("-".join(items))
这里有一个 JS 开发者非常容易写反的点:
- JS 是数组调用
join - Python 也是序列参与拼接,但写法是“分隔符字符串去调用
join”
也就是:
"-".join(items)
而不是:
# items.join("-") # 不对
避坑警告:Python 的
join是字符串的方法,不是列表的方法。
你可以把它理解成:“用这个分隔符,把一组字符串连起来”。
格式化字符串
前面已经介绍过 f-string,这里再强调一次:它是 Python 日常字符串拼接里最推荐的方式之一。
JS:
const name = "Alice";
const age = 18;
console.log(`name=${name}, age=${age}`);
Python:
name = "Alice"
age = 18
print(f"name={name}, age={age}")
除了 f-string,你也会看到:
print("name={}, age={}".format(name, age))
format() 仍然常见,但现在多数日常场景优先 f-string。
列表常用方法
Python 的 list 很像 JS 数组,但“常用套路”不完全一样。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 追加尾部 | push() |
append() |
最常用 |
| 扩展多个元素 | push(...arr) / concat() |
extend() |
Python 直接扩展序列 |
| 插入指定位置 | splice() |
insert() |
语义接近但返回值不同 |
| 删除尾部 | pop() |
pop() |
两边都有 |
| 删除指定值 | filter() / splice() |
remove() |
Python remove() 删除第一个匹配值 |
| 排序 | sort() |
sort() |
Python 支持 key= 很强 |
| 反转 | reverse() |
reverse() |
两边都有 |
| 新列表变换 | map() / filter() |
列表推导式更常见 | Python 风格差异很大 |
append() / extend() / insert()
const nums = [1, 2];
nums.push(3);
nums.push(...[4, 5]);
console.log(nums);
nums = [1, 2]
nums.append(3)
nums.extend([4, 5])
nums.insert(0, 0)
print(nums)
三者区别很值得快速记住:
append(x):把一个元素追加到末尾extend(items):把一组元素逐个追加进去insert(index, x):插到指定位置
例如:
nums = [1, 2]
nums.append([3, 4])
print(nums) # [1, 2, [3, 4]]
这里不是展开,而是把整个列表当成一个元素追加进去。
避坑警告:
append()和extend()不一样。append()是“加一个元素”,extend()才更像“把一个列表展开后逐个并进去”。
pop() / remove() / clear()
items = ["a", "b", "c", "b"]
last = items.pop()
print(last) # c
print(items) # ['a', 'b', 'b']
items.remove("b")
print(items) # ['a', 'b']
items.clear()
print(items) # []
对照理解:
pop():按位置弹出并返回,默认最后一个remove(x):按值删除第一个匹配项clear():清空整个列表
JS 里往往需要手写更多逻辑;Python 在这类日常操作上直接内建得很完整。
排序
排序是列表方法里非常值得前端开发者重点适应的一部分。
JS:
const users = [
{ name: "Bob", age: 20 },
{ name: "Alice", age: 18 },
];
users.sort((a, b) => a.age - b.age);
Python:
users = [
{"name": "Bob", "age": 20},
{"name": "Alice", "age": 18},
]
users.sort(key=lambda user: user["age"])
print(users)
Python 排序一个特别核心的心智是:
- JS 常写比较函数
(a, b) => ... - Python 更常写
key=...
也就是说,Python 更像是在说:
- “请告诉我每个元素按什么值来排序”
而不是:
- “请你自己实现两两比较逻辑”
如果不想原地改,可以用 sorted():
nums = [3, 1, 2]
new_nums = sorted(nums)
print(nums) # [3, 1, 2]
print(new_nums) # [1, 2, 3]
这个和 list.sort() 的区别也很重要:
list.sort():原地排序sorted(iterable):返回新列表
列表推导式仍然是列表处理核心
前面提过一次,这里再明确一下:对于 JS/TS 开发者,很多 map() / filter() 场景,Python 主流写法其实是列表推导式。
JS:
const nums = [1, 2, 3, 4];
const evenDoubled = nums.filter((n) => n % 2 === 0).map((n) => n * 2);
Python:
nums = [1, 2, 3, 4]
even_doubled = [n * 2 for n in nums if n % 2 == 0]
这个写法你后面会越看越多,尽快熟悉它非常值。
字典常用方法
Python 的 dict 是最核心的数据结构之一,它既像 JS 对象,又更像语言级 Map。
先看总览。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 取值 | obj.x / obj["x"] |
d["x"] / d.get("x") |
Python 更常用方括号或 get() |
| 取所有 key | Object.keys(obj) |
d.keys() |
Python 返回视图对象 |
| 取所有 value | Object.values(obj) |
d.values() |
常配合循环 |
| 取键值对 | Object.entries(obj) |
d.items() |
Python 非常高频 |
| 合并 | { ...a, ...b } |
d.update() / ` |
` |
| 删除 | delete obj.x |
pop() / del |
Python 两种方式都常见 |
get():非常值得养成习惯
user = {"name": "Alice"}
print(user["name"]) # Alice
print(user.get("age")) # None
print(user.get("age", 18)) # 18
这里最重要的是:
user["age"]取不到会抛KeyErroruser.get("age")取不到返回None或你指定的默认值
它在工程代码里非常高频,尤其适合:
- 可选字段
- 配置读取
- JSON 数据提取
keys() / values() / items()
JS:
const user = { name: "Alice", age: 18 };
console.log(Object.keys(user));
console.log(Object.values(user));
console.log(Object.entries(user));
Python:
user = {"name": "Alice", "age": 18}
print(user.keys())
print(user.values())
print(user.items())
最常见的日常场景是遍历:
for key, value in user.items():
print(key, value)
这在 Python 里和前面讲循环时一样,是非常高频、非常自然的写法。
update() / pop() / setdefault()
user = {"name": "Alice"}
user.update({"age": 18})
print(user) # {'name': 'Alice', 'age': 18}
age = user.pop("age")
print(age) # 18
city = user.setdefault("city", "Beijing")
print(city) # Beijing
print(user) # {'name': 'Alice', 'city': 'Beijing'}
其中:
update():更新合并字典pop(key):删除并返回对应值setdefault(key, default):没有就设默认值,有就返回现有值
setdefault() 对 JS 开发者可能稍微陌生,但在“按 key 分组”“初始化容器”一类场景会挺方便。
字典合并
JS:
const a = { x: 1 };
const b = { y: 2 };
const c = { ...a, ...b };
Python:
a = {"x": 1}
b = {"y": 2}
c = a | b
print(c)
或者:
a = {"x": 1}
b = {"y": 2}
a.update(b)
print(a)
这里差异要注意:
a | b:生成新字典a.update(b):原地修改a
一些非常高频的内置函数
严格说它们不属于某个模块,但实际使用频率极高,值得顺手一起建立心智。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 长度 | .length |
len() |
Python 到处都用 |
| 最大/最小 | Math.max() / Math.min() |
max() / min() |
Python 常直接传序列 |
| 求和 | reduce() |
sum() |
Python 内置得更直接 |
| 任一满足 | some() |
any() |
语义接近 |
| 全部满足 | every() |
all() |
语义接近 |
| 枚举索引 | arr.entries() / forEach index |
enumerate() |
Python 非常高频 |
| 排序后新数组 | arr.slice().sort() |
sorted() |
Python 内置很顺手 |
例子:
nums = [1, 2, 3, 4]
print(len(nums)) # 4
print(sum(nums)) # 10
print(max(nums)) # 4
print(min(nums)) # 1
print(any(n > 3 for n in nums)) # True
print(all(n > 0 for n in nums)) # True
for index, value in enumerate(nums):
print(index, value)
如果你来自 JS/TS,这一组内置函数会明显帮你减少:
- 手写循环
- 手写累加器
- 手写布尔判断
math
math 可以理解成 Python 版 Math,但它是模块,不是全局对象。
JS:
console.log(Math.floor(3.9));
console.log(Math.ceil(3.1));
console.log(Math.sqrt(16));
console.log(Math.PI);
Python:
import math
print(math.floor(3.9))
print(math.ceil(3.1))
print(math.sqrt(16))
print(math.pi)
高频成员可以先记这些:
math.floor()math.ceil()math.sqrt()math.pimath.sin()/math.cos()math.log()
对于前端开发者来说,math 几乎没有理解门槛,主要就是适应:
- 它需要
import - 它不是全局
Math
random
random 用来生成随机数、随机选择元素、打乱顺序。
JS:
const n = Math.random();
const idx = Math.floor(Math.random() * 10);
Python:
import random
print(random.random()) # 0~1 之间浮点数
print(random.randint(1, 10)) # 1~10 之间整数
一些很常用的方法:
import random
items = ["a", "b", "c"]
print(random.choice(items)) # 随机取一个
print(random.sample(items, 2)) # 随机取多个,不重复
random.shuffle(items) # 原地打乱
print(items)
可以先这样记:
random():0 到 1 的随机浮点数randint(a, b):闭区间整数choice(items):随机一个sample(items, k):随机多个shuffle(items):原地打乱
避坑警告:
shuffle()会原地修改列表,不返回新列表。
如果你带着 JS 那种“很多数组方法会返回新数组”的直觉,很容易误判。
datetime
datetime 是前端开发者第一次接触时常觉得“比 JS Date 更分明,但也更碎”的模块。
先看最常见场景:获取当前时间、格式化输出。
JS:
const now = new Date();
console.log(now.toISOString());
Python:
from datetime import datetime
now = datetime.now()
print(now.isoformat())
格式化:
from datetime import datetime
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
解析字符串:
from datetime import datetime
dt = datetime.strptime("2026-06-21 10:30:00", "%Y-%m-%d %H:%M:%S")
print(dt)
你可以先记这三个高频动作:
- 现在时间:
datetime.now() - 格式化:
strftime(...) - 解析:
strptime(...)
如果类比 JS:
Date把很多东西揉在一个对象里- Python 更倾向通过
datetime/date/time/timedelta等类型拆开表达
再看时间差:
from datetime import datetime, timedelta
now = datetime.now()
tomorrow = now + timedelta(days=1)
print(tomorrow)
这个 timedelta 在 Python 时间计算里非常高频。
os
os 很适合先理解成:
- “和操作系统打交道的一组基础工具”
如果你来自 Node.js,它有点像:
processos- 部分文件系统 / 环境相关能力
先看几个高频能力。
环境变量
JS:
console.log(process.env.NODE_ENV);
Python:
import os
print(os.environ.get("NODE_ENV"))
这里最常用的是:
os.environos.environ.get("KEY")
当前工作目录
import os
print(os.getcwd())
这和你在 Node.js 里看 process.cwd() 的心智很接近。
路径拼接
import os
path = os.path.join("data", "users", "a.json")
print(path)
这点对跨平台尤其重要,因为不要自己硬拼 / 和 \。
如果你熟悉 Node.js:
os.path.join(...)的感觉很像path.join(...)
创建目录
import os
os.makedirs("output/logs", exist_ok=True)
这里:
makedirs():递归创建多层目录exist_ok=True:目录已存在时不报错
sys
sys 可以先理解成:
- “解释器运行时相关信息和参数接口”
对 Node.js 开发者来说,它的一部分感觉接近:
process.argvprocess.exit
命令行参数
import sys
print(sys.argv)
如果你直接运行:
python demo.py hello world
那 sys.argv 大致会是:
["demo.py", "hello", "world"]
这和 Node.js 读取命令行参数的思路非常接近。
退出程序
import sys
if not config_ok:
sys.exit("config invalid")
这个可以类比:
process.exit(1);
不过 Python 常常还能顺手带上一段退出信息。
解释器路径和版本
import sys
print(sys.version)
print(sys.executable)
这些在排查环境问题、脚本运行问题时很有帮助。
给 JS/TS 开发者的常用方法迁移建议
如果你想尽快把这些工具用顺,可以先遵守这些简单规则:
- 看到“长度”,先想
len() - 看到“成员判断”,先想
in - 看到“列表变换”,先想列表推导式
- 看到“字典取可选字段”,先想
get() - 看到“排序”,先想
key=... - 看到“时间处理”,先想
datetime - 看到“系统环境 / 路径 / 环境变量”,先想
os和sys
本节小结
这一章最值得你带走的是:
- Python 的常用能力分布在对象方法、内置函数和标准库模块之间
- 字符串、列表、字典各自都有一批非常稳定的高频方法
len()、sum()、max()、min()、any()、all()这类内置函数非常值得尽快用熟math、random、datetime、os、sys是最值得优先掌握的一批标准库模块- 很多 JS/TS 里的常见需求,Python 标准库通常已经给了很直接的答案
下一章继续进入:常用模块(官方/第三方)。
常用模块(官方/第三方)
到这一章,重点已经不再是“语法会不会写”,而是:
- 你开始实际做事时,应该先认识哪些模块
- 哪些是标准库里就够用的
- 哪些是第三方生态里最值得优先掌握的
如果你来自 JS/TS,这个问题你一定很熟:
- 做 HTTP 请求先想到什么库
- 写后端先选什么框架
- 做数据处理先用什么工具
- 写自动化脚本该装什么包
Python 的生态也很大,但对前端 / Node.js 开发者来说,最重要的不是“知道所有库”,而是先建立一个清晰的优先级地图。
先建立一个总览地图
你可以先把 Python 模块世界粗分成两层:
- 官方标准库:安装 Python 就自带
- 第三方模块:需要用
pip安装
先看一张对照表。
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 标准库 | Node.js 内置模块如 fs、path、http |
json、pathlib、re、collections 等 |
Python 标准库覆盖面很强 |
| 第三方包管理 | npm / pnpm / yarn |
pip |
Python 官方生态更围绕 pip |
| 环境隔离 | node_modules + lockfile + nvm 等 |
venv + pip |
Python 更强调虚拟环境隔离 |
| Web 框架 | Express、NestJS、Koa、Fastify | Flask、Django、FastAPI | Python Web 生态选择也很多 |
| HTTP 客户端 | fetch、axios |
requests、httpx |
两边都有非常成熟的主流方案 |
如果你想先建立一个最务实的策略,可以先记住:
- 先尽量用标准库
- 标准库明显不顺手或能力不够,再上第三方
这和很多 Node.js 场景不太一样,因为 Python 标准库本身就已经很厚实。
官方模块:哪些最值得优先认识
严格来说,前一章已经覆盖了一部分最常用的内置模块,比如:
mathrandomdatetimeossys
这一章我再补一批你很快就会高频遇到的官方模块。
json
json 几乎是前端开发者最没有理解门槛的 Python 标准库之一。
JS:
const text = '{"name":"Alice","age":18}';
const data = JSON.parse(text);
const output = JSON.stringify(data);
Python:
import json
text = '{"name":"Alice","age":18}'
data = json.loads(text)
output = json.dumps(data)
print(data)
print(output)
映射非常直接:
JSON.parse->json.loadsJSON.stringify->json.dumps
如果是文件场景,你后面还会经常看到:
json.load(file)json.dump(data, file)
pathlib
pathlib 很值得前端开发者尽快认识,因为它比老式 os.path 更现代、更直观。
如果你熟悉 Node.js 的 path 模块,会觉得它很好理解。
from pathlib import Path
base = Path("data")
file_path = base / "users" / "a.json"
print(file_path)
print(file_path.name)
print(file_path.suffix)
print(file_path.parent)
这里的 / 在 Path 对象上被重载成了路径拼接操作,读起来会比字符串拼接舒服很多。
对照理解:
| 特性/概念 | JS/TS | Python | 核心差异与避坑说明 |
|---|---|---|---|
| 路径拼接 | path.join(a, b) |
Path(a) / b 或 os.path.join() |
pathlib 更现代 |
| 文件名 | path.basename() |
path.name |
Python Path 对象风格更强 |
| 后缀名 | path.extname() |
path.suffix |
读取更自然 |
如果你后面真的要写大量文件操作,pathlib 会很值得优先使用。
re
re 是正则表达式模块。
JS:
const text = "user_123";
console.log(/\d+/.test(text));
console.log(text.match(/\d+/));
Python:
import re
text = "user_123"
print(bool(re.search(r"\d+", text)))
print(re.findall(r"\d+", text))
这里先只要建立几个最常用动作:
re.search():查找第一个匹配re.findall():找出所有匹配re.sub():替换
import re
text = "user_123"
print(re.sub(r"\d+", "456", text)) # user_456
如果你在前端已经写过足够多正则,这里几乎没概念门槛,主要是适应函数接口。
collections
collections 是一个非常实用的标准库,适合在“普通 dict/list 不够顺手”时补上。
最值得前端开发者先认识的有两个:
Counterdefaultdict
Counter
如果你想做频次统计,它会非常顺手。
from collections import Counter
items = ["a", "b", "a", "c", "a"]
counter = Counter(items)
print(counter)
print(counter["a"]) # 3
对 JS/TS 来说,这有点像你手写一个频次对象,但更现成:
const items = ["a", "b", "a", "c", "a"];
const counter = items.reduce<Record<string, number>>((acc, item) => {
acc[item] = (acc[item] ?? 0) + 1;
return acc;
}, {});
defaultdict
它特别适合做“按 key 分组”。
from collections import defaultdict
grouped = defaultdict(list)
for item in [
{"type": "fruit", "name": "apple"},
{"type": "fruit", "name": "banana"},
{"type": "drink", "name": "tea"},
]:
grouped[item["type"]].append(item["name"])
print(dict(grouped))
如果你做过前端里的列表分组、聚合、数据归类,就会很快体会它的方便。
itertools
itertools 可以理解成“迭代器工具箱”,它对数据处理、组合生成、懒计算很有帮助。
前端开发者不一定需要一开始就深挖,但至少先知道有这些东西:
chain():串联多个可迭代对象product():笛卡尔积zip_longest():更灵活的并行迭代
例如:
from itertools import chain
items = list(chain([1, 2], [3, 4], [5]))
print(items) # [1, 2, 3, 4, 5]
这类模块你可以先“认识它存在”,等后面真的处理复杂数据流时再深入。
functools
functools 前面讲装饰器时已经露过脸了。
对前端开发者来说,最值得先认识的通常是:
wrapspartial
partial 的感觉有点像“预先绑定部分参数”:
from functools import partial
def multiply(a, b):
return a * b
double = partial(multiply, 2)
print(double(5)) # 10
这和 JS 里手写一个包一层的小函数、或 bind 的某些使用心智有点接近。
第三方模块:按使用方向看
Python 第三方生态很大,如果没有地图,很容易一上来就乱装一堆库。
更实用的方式是按方向理解:
- HTTP 请求 / 网络
- Web 开发
- 数据处理
- 自动化 / 爬虫
- 测试
HTTP 请求
对 Node.js / 前端开发者来说,这通常是最先关心的一类。
requests
requests 是 Python 世界里最经典的 HTTP 客户端之一。
JS:
const res = await fetch("https://api.example.com/users");
const data = await res.json();
Python:
import requests
res = requests.get("https://api.example.com/users")
data = res.json()
print(data)
为什么它经典:
- API 直观
- 学习成本低
- 生态成熟
如果你是从前端转过来,它很像“Python 里非常顺手的 axios / fetch 替代”。
httpx
httpx 可以理解成更现代、同步/异步都兼顾的 HTTP 客户端。
如果你已经在关注 Python async/await,那它会很值得认识。
import httpx
async def fetch_users():
async with httpx.AsyncClient() as client:
res = await client.get("https://api.example.com/users")
return res.json()
你可以先这样选:
- 只想快速发同步请求 ->
requests - 想兼顾异步生态 ->
httpx
Web 开发
这部分如果用前端视角来理解,最重要的是先知道三条主路线。
Flask
Flask 可以理解成:
- 轻量
- 灵活
- 像 Express 那样容易快速起步
from flask import Flask
app = Flask(__name__)
@app.get("/")
def home():
return {"message": "hello"}
如果你以前写 Express,会觉得它很亲切。
Django
Django 更像:
- “大而全”的后端框架
- 自带 ORM、管理后台、表单系统、认证系统
如果类比前端/Node 生态,它不像 Express 这种轻骨架,更像一套“框架级解决方案”。
适合:
- 后台系统
- CMS
- 传统 Web 项目
- 需要大量内建能力的业务
FastAPI
FastAPI 是前端 / TypeScript 开发者通常最容易产生好感的 Python Web 框架之一。
为什么?
- API 定义清晰
- 类型提示友好
- 自动生成文档
- 非常适合构建现代 API 服务
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
如果你喜欢:
- 清晰的接口定义
- 自动文档
- 类似 TS 那种“签名即文档”的体验
那 FastAPI 很值得优先看。
Web 框架怎么选
可以先用这个粗粒度判断:
| 场景 | 更常见选择 | 原因 |
|---|---|---|
| 快速小服务 / 轻量接口 | Flask | 简单直接,上手快 |
| 大而全业务系统 | Django | 自带能力丰富 |
| 现代 API 服务 | FastAPI | 类型友好、文档好、异步生态强 |
数据处理
这是 Python 相比纯前端生态优势特别明显的一块。
pandas
pandas 是数据分析和表格处理里的超级常客。
如果你曾经在前端里做过:
- CSV 清洗
- 表格统计
- 数据聚合
- Excel 导入导出前处理
你会很快感受到它的威力。
import pandas as pd
df = pd.DataFrame([
{"name": "Alice", "score": 90},
{"name": "Bob", "score": 80},
])
print(df["score"].mean())
它不像 JS 里你经常要自己 map/filter/reduce 半天才能凑出分析逻辑,很多数据操作在 pandas 里都已经抽象好了。
numpy
numpy 是数值计算基础设施,很多科学计算、机器学习库都建立在它之上。
如果你只是 Web 开发,不一定要第一时间深学;但只要你开始碰:
- 数值计算
- 向量 / 矩阵
- 科学计算
- AI / 机器学习
那它几乎绕不过去。
自动化 / 爬虫 / 浏览器控制
这块对前端开发者也很实用,因为很多“日常脏活累活”都能很快自动化。
beautifulsoup4
Beautiful Soup 适合做 HTML 解析。
如果你只是想:
- 抓页面
- 提取 DOM 内容
- 做简单爬虫
它很顺手。
selenium
Selenium 适合做浏览器自动化。
如果你熟悉前端测试或浏览器脚本,就很好理解:
- 打开页面
- 点击按钮
- 填表单
- 自动化操作浏览器
playwright
Playwright Python 对前端开发者往往更亲切,因为很多人已经在 JS/TS 世界里用过它。
这意味着:
- API 心智迁移成本低
- 特别适合浏览器自动化、E2E 测试、页面抓取
如果你已经熟悉 Playwright 的 TS 版本,那 Python 版几乎是天然延续。
测试
对写过前端工程的人来说,测试框架通常也是第一批想确认的东西。
pytest
pytest 是 Python 世界里非常主流、非常值得优先掌握的测试框架。
如果你熟悉:
- Jest
- Vitest
那你会很快理解它的价值:
- 写法简洁
- 断言自然
- 插件生态强
最小例子:
def add(a, b):
return a + b
def test_add():
assert add(1, 2) == 3
这种直白感,是很多前端开发者第一次用 pytest 时会喜欢上的地方。
还有一些值得知道、但不用一口气全学的模块
这一部分更像“留个地标”:
logging:正式日志系统subprocess:执行外部命令argparse:命令行参数解析typing:类型提示dataclasses:更轻量的数据类表达urllib.parse:URL 处理
这些模块你现在不一定马上深挖,但后面写脚本、工具、服务时会不断遇到。
给 JS/TS 开发者的模块学习路径
如果你不想一下子被一堆库淹没,可以先按这条顺序学:
- 标准库先打底
- HTTP 客户端先补
requests/httpx - Web 开发按目标选一个框架
- 测试优先学
pytest - 数据处理再看
pandas/numpy - 自动化需求出现时再补
playwright/selenium
再翻译成更实用的话:
- 写脚本 -> 标准库 +
requests - 写 API ->
FastAPI很值得优先看 - 写传统后台 -> 看
Django - 写轻服务 -> 看
Flask - 写测试 ->
pytest - 做数据分析 ->
pandas
模块选型的一个现实建议
从前端迁移过来时,很容易带着一种习惯:
- “遇到需求先搜第三方库”
但在 Python 里,很多需求其实标准库已经够好了。
所以更稳的顺序是:
- 先看标准库有没有
- 再看第三方是不是明显更省事
- 再考虑生态成熟度、维护状态、文档质量
这会帮你减少很多不必要的依赖。
避坑警告:不要因为 Python 生态丰富,就一上来装一堆库。
很多需求标准库已经够用,而真正值得长期依赖的第三方库,通常也就那么一批头部选择。
全文小结
如果你能一路读到这里,说明你已经不再是在“看 Python 长什么样”,而是在建立一套真正可迁移的 Python 心智模型。
从 JS/TS 迁移到 Python,最重要的从来不是死记语法,而是完成这几个认知转变:
- 从“声明关键字驱动”转向“名字查找和对象模型驱动”
- 从“原型方法心智”转向“内置函数 + 对象方法 + 标准库并存”
- 从“默认事件循环直觉”转向“进程 / 线程 / 协程按任务类型选”
- 从“对象参数包一切”转向“充分利用 Python 参数系统表达函数签名”
- 从“把 Python 当另一套 JS”转向“接受 Python 自己的语言气质”
如果你已经有扎实的 JS/TS 基础,那么学 Python 最大的优势不是你能更快记住语法,而是你已经知道:
- 怎么组织代码
- 怎么抽象函数
- 怎么管理模块
- 怎么理解并发
- 怎么做工程取舍
你现在要做的,只是把这些经验迁移到 Python 的表达方式里。
下一步建议
读完这篇后,你最值得继续做的不是“再看十篇语法文章”,而是立刻做一点真实练习。
建议按这个顺序:
- 写几个 Python 小脚本
- 用函数和模块把脚本拆开
- 处理几类常见异常
- 试着写一个简单类
- 再选一个方向深入:
- Web API
- 自动化脚本
- 数据处理
- 异步并发