从 JS/TS 到 Python:前端开发者的无缝迁移指南

这是一篇专门写给 已经熟悉 JS/TS/Node.js,但几乎没有 Python 经验 的开发者的迁移指南。

你不会在这里看到大量脱离上下文的“语法背诵”。相反,我会始终站在前端工程师的视角,把 Python 和你已经熟悉的 JS/TS 世界放在一起比较:

  • 这个概念在 JS/TS 里是什么
  • Python 里对应的写法是什么
  • 两者的设计差异在哪里
  • 哪些地方最容易带着 JS 思维误用 Python

如果你已经能熟练写 Node.js、JavaScript、TypeScript 类型,本文的目标不是让你“重新学编程”,而是让你 快速建立 Python 心智模型,尽可能低成本地把已有经验迁移过去。

这篇文章会覆盖什么

本文会从基础一路讲到进阶,覆盖这些核心主题:

  1. 基础
  2. 函数
  3. 作用域
  4. 模块
  5. 面向对象
  6. 错误与调试
  7. IO 编程(本篇跳过,留到 Node.js vs Python 专题展开)
  8. 进程、线程与协程
  9. 常用方法与内置模块
  10. 常用官方/第三方模块

你可以把它理解成一份 Python 版的“前端开发者迁移手册”。

参考资料

文章主要以js/ts为视角,但如果你想继续深挖,还是无法避开实操和啃官方文档:


下面正式进入第一部分:基础

基础

数据类型和变量

如果你来自 JS/TS,那么 Python 的基础类型并不陌生。真正需要适应的,不是“有没有这些类型”,而是:

  • Python 的内置类型更鲜明,语义更稳定
  • 很多操作是“返回新值”而不是“原地修改”
  • 某些看起来像 JS 的东西,行为细节其实完全不同

先看一个整体对照。

特性/概念 JS/TS Python 核心差异与避坑说明
数字 numberbigint intfloatcomplex 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 的类型级只读数组
对象/映射 ObjectMap dict Python dict 更接近“语言级 Map”,而不是普通对象字面量
集合 Set set 两边都用于去重和集合运算,但 Python 集合运算更原生
布尔值 boolean bool Python 的 True/False 首字母大写
空值 nullundefined None Python 只有一个“空值语义中心”:None
变量声明 let / const / var 直接赋值 Python 没有 const,约定大写常量只是约定,不是强约束

标准数据类型

Python 常用内置类型可以粗分为这几类:

  • 数字:intfloat
  • 字符串:str
  • 序列:listtuple
  • 映射:dict
  • 集合:set
  • 布尔和空值:boolNoneType

你可以先把它们理解为 JS/TS 世界里这些角色:

  • int / float ~= number
  • str ~= string
  • list ~= Array
  • tuple ~= “不可变数组”
  • dict ~= Object + Map 的合体,但更偏 Map
  • set ~= 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 开发者常常会下意识去找:

  • map
  • filter
  • reduce
  • find

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 的空值世界比较复杂:

  • null
  • undefined
  • falsy false/0/""/''/null/undefined/NaN / truthy"0"/"false"/[]/{}/function(){}/new Boolean(false)
  • =====

Python 则更统一一些:

  • 布尔值是 TrueFalse
  • 空值核心是 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 有:

  • let
  • const
  • var

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 开发者来说非常值得重点适应的东西:inis

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

它表达的是“身份相同”,不是“值相等”。

运算符优先级

你不需要死记整张优先级表,但需要记住几个高频规则:

  • ** 优先级很高
  • notandor 是关键字,不是符号
  • 复杂表达式优先加括号,不要赌阅读者理解成本
const result = (a + b) * c;
result = (a + b) * c

避坑警告:Python 代码的“可读性优先级”比“省字符优先级”高得多。
只要表达式稍微复杂,就直接加括号,不要为了看起来简洁而牺牲可读性。

本节小结

如果只看语法表面,Python 的数据类型并不难;真正需要你调整的是默认思维模式:

  • listdictset 去理解 Python 的核心数据结构
  • None 替代你对 null/undefined 的双重心智负担
  • 用显式类型转换替代 JS 式隐式转换
  • 尽快熟悉 inis//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 两边基本一致
关键字 ifreturnclass ifreturnclasslambdawith 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

两边有很多重叠项:

  • if
  • else
  • for
  • return
  • class

但 Python 里还有一些你在 JS/TS 里不太会以同样方式遇到的关键字:

  • def
  • elif
  • pass
  • with
  • lambda
  • yield
  • nonlocal

例如:

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,也不是空代码块的自然替代物。
它的作用是“占一个合法语句位置”,后面讲循环时我们会再和 breakcontinue 放在一起对照。

本节小结

这一节的重点不在于“记忆语法”,而在于尽快建立 Python 的书写习惯:

  • 用缩进定义代码块,而不是靠花括号
  • # 写注释,用三引号理解多行字符串和文档字符串
  • 用换行组织语句,而不是依赖分号
  • snake_case 命名变量和函数
  • defelifpasswith 这些 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 的 : 不是装饰符,也不是“可有可无”的符号。
ifelifelseforwhiledefclass 这类语句后面,冒号是语法的一部分。

多分支: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 后面并不要求你必须手动写出 TrueFalse,只要这个值可以参与真假判断即可。

常见可直接放进条件里的内容包括:

  • 比较表达式: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 xif 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 用 andornot

如果条件稍微复杂一点,建议主动加括号:

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...of
  • forEach
  • while

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 里,常见可迭代对象包括:

  • list
  • tuple
  • str
  • dict
  • set
  • range()

例如:

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) 生成的是一个按顺序可遍历的范围,结果是:

  • 0
  • 1
  • 2
  • 3
  • 4

也就是说,它和 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()
  • breakcontinuepass 的语义必须彻底分清

到这里,基础 这一大章里最核心的语法骨架基本已经搭起来了。

下一章开始进入真正的重点:函数

函数(重点)

如果你已经有扎实的 JS/TS 基础,那你会很自然地把 Python 函数理解成:

  • 能封装逻辑
  • 能接收参数
  • 能返回结果
  • 能作为抽象边界组织代码

这个方向没有错,但 Python 的函数系统和 JS/TS 相比,有两个明显特点:

  • 语法更收敛,写法更统一
  • 参数模型更强,函数签名更有表达力

也就是说,Python 函数表面上看起来“朴素”,但实际非常强大。很多 Python 代码之所以读起来舒服,很大程度上就是因为函数定义、参数设计和文档约定都比较稳定。

函数定义与调用

这一节先只处理四件事:

  • def 定义 vs JS function / 箭头函数
  • 函数调用
  • 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 functionsfunctools

先不要看 @,先看“函数包装”本身。

先从 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,看到“作用域”第一反应大概率是:

  • var
  • let
  • const
  • 块级作用域
  • 闭包

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,内置作用域

你先不需要死记缩写,但要记住这个查找顺序:

  1. 先找当前函数内部
  2. 再找外层函数
  3. 再找当前模块的全局变量
  4. 最后找 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 里,ifforwhile 这类代码块不会像 JS 那样创建新的块级作用域

if True:
    x = 1

print(x)  # 1

再看循环:

for i in range(3):
    pass

print(i)  # 2

这对 JS 开发者通常有点反直觉,因为你会下意识觉得:

  • 这个变量不是在循环里定义的吗
  • 为什么出了循环还能访问

原因就是:Python 更强调函数作用域,而不是你熟悉的那种块级作用域。

避坑警告:不要把 JS 的 let/const 块级作用域直觉直接带进 Python。
在 Python 里,ifforwhile 代码块不会自动制造一个新的局部作用域。

在函数里给名字赋值,会发生什么

这又是 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,这种行为不像 letconst 的直观体验,所以更容易踩坑。

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 层”。如果你在多层嵌套里很在意“到底改哪一层”,最稳妥的做法通常是:

  • 不要在多层里复用同名变量
  • 或者干脆重构成对象状态,而不是继续堆嵌套函数

globalnonlocal 的区别

这是非常值得单独对照的一组概念:

特性/概念 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,那么“模块”这个词你一定不陌生。你已经很熟这些东西:

  • import
  • export
  • require
  • 文件拆分
  • 目录组织
  • 包管理

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 defaultnamed export 这一套完全等价物。
Python 的模块暴露方式更接近“模块顶层命名空间”,不是 JS 那种显式导出语法。

包(package):目录级组织单位

如果说:

  • .py 文件是模块

那么:

  • 包含多个模块的目录结构,通常会形成包

你可以先看一个简单结构:

blog_tools/
  __init__.py
  formatters.py
  validators.py

这里 blog_tools 可以看作一个包,里面有两个模块:

  • blog_tools.formatters
  • blog_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 通过 importfrom ... 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.nameu2.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 两边都有,代码在执行前就会失败
运行时错误 TypeErrorReferenceError TypeErrorNameErrorValueError 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、框架,希望调用方能精确处理错误

例如:

  • AuthError
  • PaymentError
  • ConfigError
  • ValidationError

这会比单纯抛一个没有分类的 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_process
  • cluster

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 也有:

  • async
  • await

而且它同样是用来表达异步任务协作的核心语法。官方文档主要看 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()
  • Math
  • Date
  • process
  • path / fs / os

Python 也有对应能力,但风格差异很明显:

  • 有些能力挂在对象方法上
  • 有些能力更偏内置函数风格
  • 标准库里很多模块都非常实用,而且“即开即用”

这一章我先聚焦最常用的几类:

  • 字符串常用方法
  • 列表常用方法
  • 字典常用方法
  • math
  • random
  • datetime
  • os
  • sys

先建立一个总体直觉

对前端开发者来说,最先适应的往往不是“有没有这些 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() 找不到返回 -1
  • index() 找不到会抛异常
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"] 取不到会抛 KeyError
  • user.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.pi
  • math.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,它有点像:

  • process
  • os
  • 部分文件系统 / 环境相关能力

先看几个高频能力。

环境变量

JS:

console.log(process.env.NODE_ENV);

Python:

import os

print(os.environ.get("NODE_ENV"))

这里最常用的是:

  • os.environ
  • os.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.argv
  • process.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
  • 看到“系统环境 / 路径 / 环境变量”,先想 ossys

本节小结

这一章最值得你带走的是:

  • Python 的常用能力分布在对象方法、内置函数和标准库模块之间
  • 字符串、列表、字典各自都有一批非常稳定的高频方法
  • len()sum()max()min()any()all() 这类内置函数非常值得尽快用熟
  • mathrandomdatetimeossys 是最值得优先掌握的一批标准库模块
  • 很多 JS/TS 里的常见需求,Python 标准库通常已经给了很直接的答案

下一章继续进入:常用模块(官方/第三方)

常用模块(官方/第三方)

到这一章,重点已经不再是“语法会不会写”,而是:

  • 你开始实际做事时,应该先认识哪些模块
  • 哪些是标准库里就够用的
  • 哪些是第三方生态里最值得优先掌握的

如果你来自 JS/TS,这个问题你一定很熟:

  • 做 HTTP 请求先想到什么库
  • 写后端先选什么框架
  • 做数据处理先用什么工具
  • 写自动化脚本该装什么包

Python 的生态也很大,但对前端 / Node.js 开发者来说,最重要的不是“知道所有库”,而是先建立一个清晰的优先级地图

先建立一个总览地图

你可以先把 Python 模块世界粗分成两层:

  • 官方标准库:安装 Python 就自带
  • 第三方模块:需要用 pip 安装

先看一张对照表。

特性/概念 JS/TS Python 核心差异与避坑说明
标准库 Node.js 内置模块如 fspathhttp jsonpathlibrecollections 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 requestshttpx 两边都有非常成熟的主流方案

如果你想先建立一个最务实的策略,可以先记住:

  • 先尽量用标准库
  • 标准库明显不顺手或能力不够,再上第三方

这和很多 Node.js 场景不太一样,因为 Python 标准库本身就已经很厚实。

官方模块:哪些最值得优先认识

严格来说,前一章已经覆盖了一部分最常用的内置模块,比如:

  • math
  • random
  • datetime
  • os
  • sys

这一章我再补一批你很快就会高频遇到的官方模块。

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.loads
  • JSON.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) / bos.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 不够顺手”时补上。

最值得前端开发者先认识的有两个:

  • Counter
  • defaultdict
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 前面讲装饰器时已经露过脸了。

对前端开发者来说,最值得先认识的通常是:

  • wraps
  • partial

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 时会喜欢上的地方。

还有一些值得知道、但不用一口气全学的模块

这一部分更像“留个地标”:

这些模块你现在不一定马上深挖,但后面写脚本、工具、服务时会不断遇到。

给 JS/TS 开发者的模块学习路径

如果你不想一下子被一堆库淹没,可以先按这条顺序学:

  1. 标准库先打底
  2. HTTP 客户端先补 requests / httpx
  3. Web 开发按目标选一个框架
  4. 测试优先学 pytest
  5. 数据处理再看 pandas / numpy
  6. 自动化需求出现时再补 playwright / selenium

再翻译成更实用的话:

  • 写脚本 -> 标准库 + requests
  • 写 API -> FastAPI 很值得优先看
  • 写传统后台 -> 看 Django
  • 写轻服务 -> 看 Flask
  • 写测试 -> pytest
  • 做数据分析 -> pandas

模块选型的一个现实建议

从前端迁移过来时,很容易带着一种习惯:

  • “遇到需求先搜第三方库”

但在 Python 里,很多需求其实标准库已经够好了。

所以更稳的顺序是:

  1. 先看标准库有没有
  2. 再看第三方是不是明显更省事
  3. 再考虑生态成熟度、维护状态、文档质量

这会帮你减少很多不必要的依赖。

避坑警告:不要因为 Python 生态丰富,就一上来装一堆库。
很多需求标准库已经够用,而真正值得长期依赖的第三方库,通常也就那么一批头部选择。

全文小结

如果你能一路读到这里,说明你已经不再是在“看 Python 长什么样”,而是在建立一套真正可迁移的 Python 心智模型。

从 JS/TS 迁移到 Python,最重要的从来不是死记语法,而是完成这几个认知转变:

  • 从“声明关键字驱动”转向“名字查找和对象模型驱动”
  • 从“原型方法心智”转向“内置函数 + 对象方法 + 标准库并存”
  • 从“默认事件循环直觉”转向“进程 / 线程 / 协程按任务类型选”
  • 从“对象参数包一切”转向“充分利用 Python 参数系统表达函数签名”
  • 从“把 Python 当另一套 JS”转向“接受 Python 自己的语言气质”

如果你已经有扎实的 JS/TS 基础,那么学 Python 最大的优势不是你能更快记住语法,而是你已经知道:

  • 怎么组织代码
  • 怎么抽象函数
  • 怎么管理模块
  • 怎么理解并发
  • 怎么做工程取舍

你现在要做的,只是把这些经验迁移到 Python 的表达方式里。

下一步建议

读完这篇后,你最值得继续做的不是“再看十篇语法文章”,而是立刻做一点真实练习。

建议按这个顺序:

  1. 写几个 Python 小脚本
  2. 用函数和模块把脚本拆开
  3. 处理几类常见异常
  4. 试着写一个简单类
  5. 再选一个方向深入:
    • Web API
    • 自动化脚本
    • 数据处理
    • 异步并发