01 · Python 语言基础与 TS 对照
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 给有 TS/Dart 基础的人看的 Python 迁移手册——只讲会坑到你的差异,不讲语法大全 |
| 核心内容 | 三个心智模型差异、语法对照、10 个直觉陷阱、类型注解、asyncio 与 JS 事件循环的区别、环境管理 |
| 为什么写 | AI 生态与后端服务的主力语言;AI 岗 JD 里 Python 的出现率极高 [易变],是前端转 AI 全栈最该先补的一门 |
| 读完能回答 | 我写 Python 时会踩哪些坑?为什么不能照搬 JS 的直觉? |
| 相关板块 | A03 · TypeScript 进阶与 Dart 对照 | 实操 01 · LLM API 实操入门 | G12 · 上下文工程 |
语法大全网上有的是,本篇不重复。这里只写"你带着 TS 直觉写 Python 一定会翻车"的地方。
一、三个心智模型差异(先换脑子)
| # | TS / JS | Python | 后果 |
|---|---|---|---|
| 1 | 块级作用域:{} 划作用域,let/const 活在块里 | 函数作用域:缩进只管层级,不产生作用域 | if 里定义的变量,外面照样能访问 |
| 2 | async 是运行时内置的默认能力,随手 await | asyncio 是库,必须显式起事件循环 | 在 async 函数里调一个同步阻塞函数,整个 loop 卡死 |
| 3 | 单线程 + 事件循环,没有"全局锁"概念 | 有 GIL:同一进程内只有一个线程能执行 Python 字节码 | 多线程跑 CPU 密集任务不会变快 |
第 3 条决定了 Python 的并发写法:CPU 密集用多进程,IO 密集用 asyncio。 用"多线程加速计算"这个直觉在 Python 里是错的。
二、语法对照速查
只列形态差别大的,其余"长得差不多"的不写。
| 场景 | TypeScript | Python |
|---|---|---|
| 变量声明 | const x = 1 / let y = 2 | x = 1(无关键字,赋值即定义) |
| 常量 | const X = 1 | X = 1(靠约定:全大写表示常量,语言不拦你) |
| 空值 | null / undefined | None(只有一个) |
| 布尔 | true / false | True / False(首字母大写) |
| 逻辑运算 | && || ! | and or not(英文单词) |
| 字符串模板 | `hi ${name}` | f"hi {name}" |
| 数组/列表 | [1,2,3] | [1,2,3](同形,但叫 list) |
| 键值对 | Map / {} 对象 | {"a": 1}(叫 dict,键必须是可哈希的) |
| 遍历 | for (const x of arr) | for x in arr: |
| 带索引遍历 | arr.forEach((v,i)=>{}) | for i, v in enumerate(arr): |
| 同时遍历两个 | zip 要自己写或 lodash | for a, b in zip(xs, ys):(内置) |
| 区间 | Array.from({length:n},(_,i)=>i) | range(n) |
| 切片 | arr.slice(1,3) | arr[1:3](步长 arr[::-1] 反转) |
| 推导 | arr.map(f).filter(g) | [f(x) for x in arr if g(x)](列表推导) |
| 解构 | const {a,b} = obj | a, b = tup(元组解包) |
| 展开 | ...args | *args / **kwargs |
| 类 | class A { constructor(){} } | class A: def __init__(self): |
| this / self | this(关键字) | self(约定名,位置参数,必须写) |
| 私有 | private x / #x | _x(约定)/ __x(name mangling,都不是真私有) |
| 接口 | interface | Protocol / ABC(鸭子类型是默认) |
| 模块导入 | import x from 'y' | import y / from y import x |
| 异常处理 | try{}catch(e){}finally{} | try: / except E as e: / finally: |
三、10 个会坑到你的直觉差异
① 空列表是 falsy(与 JS 相反)
if not items: # [] 会进这个分支
...
# JS 里 [] 是 truthy,Python 里 [] 是 falsy这是最高频的 bug 来源。JS 的
if (arr)判断"有没有这个对象", Python 的if items:判断"这个容器是不是非空"。要判断"是不是 None"必须写if items is None:。
Falsy 清单:None、False、0、0.0、''、[]、()、{}、set()。 自定义对象默认 truthy,除非定义 __bool__ 或 __len__。
② 可变默认参数只初始化一次
def add(x, target=[]): # ❌ 这个 [] 在函数定义时创建一次,之后共享
target.append(x)
return target
add(1) # [1]
add(2) # [1, 2] ← 不是你想要的 [2]def add(x, target=None): # ✅ 标准写法
target = target if target is not None else []
target.append(x)
return target③ is 与 == 不是一回事
== 比较值,is 比较是不是同一个对象。
a = [1, 2]; b = [1, 2]
a == b # True 值相等
a is b # False 不是同一个对象
x = 256; y = 256
x is y # True ← 但这是「小整数缓存」的巧合,不是规则 [易变]规则:判断
None用is,判断值相等用==。别用is比较数字或字符串。
④ 闭包是"晚绑定"
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] ← 不是 [0, 1, 2]因为 i 在调用时才去查,那时循环已结束。修正:
funcs = [lambda i=i: i for i in range(3)] # 用默认参数固化⑤ 除法:/ 永远返回 float
5 / 2 # 2.5 (JS 里 5/2 也是 2.5)
5 // 2 # 2 ← 整除
-5 // 2 # -3 ← 向下取整,不是向零取整⑥ 列表乘法共享引用
m = [[0]] * 3
m[0][0] = 1
# [[1], [1], [1]] ← 三行是同一个 list
m = [[0] for _ in range(3)] # ✅⑦ 没有块级作用域
if True:
x = 1
print(x) # 1 ← 能访问!TS 里这会报错
for i in range(3):
pass
print(i) # 2 ← 循环变量也会泄漏出去⑧ import 会执行模块代码
Python 的 import 是真执行,不像 ES module 那样只是绑定。 所以模块顶层不要放有副作用的代码,要写成:
if __name__ == "__main__":
main()这也是循环导入在 Python 里容易出问题的原因。
⑨ dict 的键必须可哈希
d = {}
d[[1, 2]] = "x" # ❌ TypeError: unhashable type: 'list'
d[(1, 2)] = "x" # ✅ 元组可以判断标准:可变对象不能做键(list / dict / set),不可变的可以(int / str / tuple / frozenset)。
⑩ __init__ 不是构造函数
__init__ 是初始化,对象在它之前已由 __new__ 创建。 日常写 __init__ 就够了,但要知道这个名字的含义——它对应的是"初始化"而非"构造"。
四、类型注解:Python 也在"TS 化"
Python 是动态类型,但类型注解已成为工程标配——它是给工具(mypy / IDE)看的, 运行时不校验。
from typing import Optional
def greet(name: str, times: int = 1) -> str:
return name * times
def find(ids: list[int]) -> Optional[str]: # 等价于 TS 的 string | null
...| TS | Python(3.10+) |
|---|---|
string | null | Optional[str] 或 str | None |
Array<T> | list[T] |
Record<K,V> | dict[K, V] |
interface User {...} | @dataclass class User: 或 TypedDict |
type Alias = ... | Alias = ...(直接赋值)或 TypeAlias |
⚠️ 注解不校验,是它和 TS 最大的区别。 写了
def f(x: int)然后传字符串,运行时不会报错。必须靠 mypy / pyright 静态检查。
数据类的三种选择(这是 AI/后端场景天天要用的):
| 方式 | 适用 | 特点 |
|---|---|---|
dataclass | 内部数据结构 | 标准库,自动生成 __init__/__eq__/__repr__ |
pydantic.BaseModel | API 边界、配置 | 运行时校验 + 序列化,FastAPI 的基石 |
NamedTuple | 轻量元组 | 内存省,行为像元组 |
判断规则:数据要进出系统边界(HTTP、配置文件、LLM 输出解析)→ 用 pydantic, 因为它会在运行时拦住脏数据;只在内部传递 →
dataclass就够。
五、异步:asyncio 与 JS 事件循环的关键差异
| JS | Python | |
|---|---|---|
| 事件循环 | 运行时内置,自动跑 | 需要你显式启动:asyncio.run(main()) |
await 位置 | 模块顶层(ESM)也能 await | 只能在 async def 里 |
| 并发原语 | Promise.all | asyncio.gather |
| 阻塞的后果 | 阻塞整个线程 | 阻塞整个事件循环(所有协程都停) |
import asyncio
async def fetch(i):
await asyncio.sleep(1)
return i
async def main():
# ✅ 并发:三个一起跑,总共约 1 秒
return await asyncio.gather(*[fetch(i) for i in range(3)])
asyncio.run(main())⚠️ 最常见的性能事故:在 async 函数里调了
requests.get()(同步库) 或time.sleep(),整个事件循环卡住,并发退化成串行。 规则:async 函数里只能用 async 库(aiohttp/httpx.AsyncClient/asyncio.sleep)。
什么时候根本不需要 asyncio:脚本、数据处理、模型训练——同步代码更简单。 asyncio 只在高并发 IO(爬虫、API 网关、大量并发 LLM 请求)时才值得。
六、环境与包管理:与 npm 的对照
这是前端最容易困惑的一块——为什么 Python 要有虚拟环境。
| npm 生态 | Python 生态 | 说明 |
|---|---|---|
package.json | pyproject.toml | 依赖声明 |
package-lock.json | uv.lock / poetry.lock | 锁定版本 |
node_modules/(每个项目一份) | venv/(每个项目一个隔离环境) | 关键差异 ↓ |
npm install | uv sync / pip install | |
npm run dev | uv run main.py / python main.py |
为什么需要 venv:npm 把依赖装在项目目录里,天然隔离; 而
pip install默认装到全局 site-packages,多个项目依赖冲突会互相打架。 venv 就是给每个项目造一个"独立的世界"。
工具选型(2026 现状 [易变]):
| 工具 | 定位 | 建议 |
|---|---|---|
venv + pip | 标准库自带 | 够用,但锁版本要手动 pip freeze |
uv | Rust 写的,极快,统一了安装/锁版本/运行 | 新项目首选 |
poetry | 成熟,依赖解析严谨 | 已有项目继续用 |
conda | 科学计算,能管非 Python 依赖 | 只在需要 CUDA 等场景 |
给前端的一句话建议:把
uv当成pnpm用——uv init/uv add/uv run, 心智负担最低。
七、AI 场景的最小必备
学 Python 多半是为了用 AI/后端,这几个是高频出现的:
# 1. 环境(等价于 package.json + node_modules)
# uv init && uv add openai numpy
# 2. 调 LLM(与 JS SDK 形态几乎一致)
from openai import OpenAI
client = OpenAI()
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hi"}],
)
print(resp.choices[0].message.content)
# 3. FastAPI:类型注解即校验即文档
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.post("/items")
def create(item: Item) -> Item: # 自动校验请求体、自动生成 OpenAPI
return item| 库 | 干什么 | 前端类比 |
|---|---|---|
numpy | 多维数组与向量运算 | 没有直接对应;别用 Python 循环做数值计算 |
pandas | 表格数据处理 | 类似 SQL + Excel |
pydantic | 运行时数据校验 | 类似 zod |
FastAPI | Web 框架 | 类似 Express + zod + Swagger 三合一 |
httpx | HTTP 客户端 | 类似 axios(有同步与异步两种 API) |
numpy 的心智模型:用数组整体运算代替逐元素循环。
arr * 2是对整个数组乘 2,不是把数组重复两遍(这点与 Python list 相反!)。
八、自检清单
- [ ] 能说出 falsy 清单,并解释为什么
if items:和if items is None:不等价 - [ ] 写函数时不用可变对象做默认参数
- [ ] 知道
is只该用于None - [ ] 理解
/与//的区别,且知道//是向下取整 - [ ] 知道 async 函数里不能调同步阻塞库
- [ ] 能解释 GIL,并说出 CPU 密集场景该用什么(多进程)
- [ ] 会建虚拟环境,能说清为什么需要它
- [ ] 知道类型注解运行时不校验
- [ ] 能在
dataclass与pydantic之间做选择 - [ ] 能独立写出一个 FastAPI 接口 + 一次 LLM 调用
九、常见误区
| 误区 | 事实 |
|---|---|
| "Python 简单,看两天就会" | 语法简单,但作用域、可变性、GIL、导入机制各有坑,照搬 JS 直觉必翻车 |
| "有类型注解就等于 TS" | 注解运行时不校验,不跑 mypy 等于没写 |
| "多线程能加速计算" | GIL 挡着,CPU 密集要用多进程 |
| "async 一定更快" | 只在高并发 IO 时成立;脚本用 async 是给自己找麻烦 |
| "Python 慢,不能做生产" | 热点路径常用 C 实现(numpy 等);真正的瓶颈通常在 IO 与架构 |
| "pip install 装哪都行" | 不隔离环境迟早冲突,用 venv / uv |
"self 是关键字" | 只是约定名,换成别的也能跑(但别这么做) |
| "Python 没有私有" 就等于"随便访问" | 下划线是强约定,破坏它后果自负 |
十、动手练习
练习 A:找出 bug
def append_to(item, target=[]):
target.append(item)
return target
print(append_to(1))
print(append_to(2))预期是什么?实际是什么?怎么改?
练习 B:把一段 JS 直觉翻译成 Python
const rows = Array.from({length: 3}, () => [0]);
rows[0][0] = 1;用 Python 写出来。注意别踩第 ⑥ 条。
练习 C:判断该不该用 asyncio
给三个场景(批量调 LLM API 1000 次、训练一个模型、处理一个 10MB 的 CSV), 分别说明用不用 asyncio、为什么。
相关文档
本区
- E · 通用基本功 计划 —— 本域的后续主题
前端区(对照的另一侧)
- A03 · TypeScript 进阶与 Dart 对照 —— TS 侧的类型系统深水区
- 09 · JavaScript 运行时 —— 与 Python GIL 对照着看事件循环
AI 区(学 Python 的主要去处)
- 实操 01 · LLM API 实操入门 —— 第一段 Python AI 代码
- 实操 02 · RAG 实战 —— Python 写的完整链路
- G12 · 上下文工程