从这里开始
模型页讲「一个 token 怎么走完网络」,数学页讲「那些符号是什么意思」。这一页讲第三件事:你坐在电脑前,怎么把环境、代码、数据和接口串成一条能跑的线。
真正要带走的是服务器上天天用的那一套:目录怎么长、命令怎么拼、Python 怎么装数据、MySQL 怎么查、FastAPI 怎么对外、Docker 怎么把「我电脑能跑」变成「哪都能跑」。
先认路。Linux 里没有盘符,只有一棵从 / 长出来的树。
Linux:一切皆文件
Windows 用 C:、D: 把世界切开。Linux 用一棵树:根叫 /,往下全是路径。硬盘、进程、网卡,最后都能在某个文件名下面找到。
真实服务器你摸不到机箱,日常都是 SSH 连上去。连上之后面对的就是这棵树:没有 C: D:,只有从 / 长出来的路径。
点开一棵目录树
每个目录只记一件事:它住的是「程序」「配置」还是「会变的数据」。
/usr 是已经装好的家具,/opt 是你后来搬进来的大家电,/etc 是墙上的开关面板,/var 是会不断写的流水账,/home 才是你的书桌。模型权重、conda 环境,优先放 /opt 或自己家目录,别塞进 /usr。
软件从哪来
Ubuntu 用 APT。先 sudo apt update 刷新目录,再 sudo apt install 包名。国内把源换成清华 / 阿里,下载才不会卡死。这和后面 conda、Docker 换镜像源是同一件事:默认源在海外,先换近的。
sudo apt update
sudo apt install -y ssh net-tools vim
命令与权限:和机器说话
命令不是魔法单词,是 /usr/bin 里的小程序。权限则是「谁能碰这个文件」。把这两件事摸熟,后面的脚本、Docker、SSH 都只是在复用它们。
| 命令 | 干什么 | 记得的那一招 |
|---|---|---|
| pwd / ls / cd | 我在哪、有什么、去哪 | cd - 回到上一处,ls -alh 看隐藏文件和权限 |
| mkdir / touch / cp / mv / rm | 增、改名、删 | rm -rf 没有回收站。复制目录要 cp -r |
| cat / tail / less | 看内容 | 日志用 tail -f,大文件别 cat |
| > >> | | 重定向、管道 | > 覆盖,>> 追加,| 把左边的输出变成右边的输入 |
| find / grep | 找文件、找字 | ps -ef | grep python 是日常动作 |
| tar / gzip | 打包压缩 | tar -zcvf a.tar.gz dir 打包,-zxvf 解开 |
| ps / top / kill | 进程 | kill -9 PID 是强迫停止,先试试不带 -9 |
| chmod / chown | 权限、主人 | 目录要有 x 才能 cd 进去,这和文件的 x 不是一回事 |
chmod 的九个开关
从左到右三组:属主、属组、其他人。每组 r=4、w=2、x=1,加起来就是 7、5、4 这些数字。
属主 u
属组 g
其他人 o
ps -ef | grep uvicorn | awk '{print $2}' | xargs kill 读作:列出进程 → 滤出 uvicorn → 取出 PID → 交给 kill。每个程序只做一件小事,用 | 接起来。Shell 章节会把这件事写成脚本。
命令敲多了就要写成文件。那就是 Shell。
Shell:把命令变成程序
Ubuntu 默认的解析器是 bash。脚本第一行 #!/bin/bash 是在指定「用谁来读我」。变量默认全是字符串,等号两边不能有空格。
两种跑法
bash script.sh或sh script.sh:解析器帮你跑,脚本本身不用 +x./script.sh:脚本自己作为程序启动,必须chmod +x- 子脚本里要看见某个变量,父 Shell 得先
export
特殊变量,面试爱问
$0 $1 $2脚本名和参数,$#参数个数$*把所有参数看成一串;$@把每个参数分开。加了双引号区别才明显$?上一条命令的退出码,0 是成功
引号决定「展开还是原样」
单引号平权、双引号展开变量、不打引号还会把 * 当成通配符。
判断和循环
条件写成 [ $a -gt 10 ],方括号内侧必须有空格。-eq -ne -lt -le -gt -ge 比整数;-e -f -d -r -w -x 问文件。if / case / for / while 的关键字都要收尾:fi、esac、done。
#!/bin/bash
if [ $# -eq 0 ]; then
echo '请携带年龄'
elif [ $1 -lt 18 ]; then
echo '未成年人'
else
echo '成年人'
fi
S=$[(2+3)*4] # 算术用 $[] 或 $(())
sum=0
for ((i=1;i<=100;i++)); do sum=$[$sum+$i]; done
cut -d: -f1 按分隔符切列;awk -F: '{print $1}' 能算、能过滤,NR 是行号、NF 是列数;正则里 ^ 行首、$ 行尾、. 任意一字、* 前一字重复。日志和 /etc/passwd 都靠它们拆。
机器听懂命令了。下面用 Python 描述数据。
Python:先分清「盒子」
语法规则(缩进、注释、标识符)一天就能混个眼熟。真正决定你会不会写的,是四只盒子:list、tuple、dict、set,以及它们可不可变。
四只盒子
有序还是无序、能不能改、用下标还是用键。大模型课后面的 tokenizer、batch、配置文件,全是这四只盒子的组合。
可变 / 不可变
- 不可变:
int float str tuple frozenset。看起来像「改了」,其实是绑到新对象上 - 可变:
list dict set。函数里如果改了传入的 list,外面能看见 - 面试只爱问一件事:list 可变、不能当字典的键;tuple 不可变、能被哈希。里面嵌了 list,那个 list 仍可变
切片、enumerate、zip
- 切片
a[1:5:2],倒序a[::-1]。切片是新对象,但里面的可变元素仍是同一份 enumerate同时拿下标,zip并行遍历is问是不是同一个对象,==问值相不相等。小整数和短字符串有 intern,别被is骗到
三个推导式,外加一个圆括号
Python 原生只认四种写法。方括号立刻造出整份 list;花括号看有没有冒号,决定是 set 还是 dict;圆括号不造容器——它返回生成器,next() 一次才算一个。面试常把后两个问成一对:列表推导式吃内存,生成器表达式吃时间。
同一句话,四个括号
数据源都是 range(5),表达式都是 x * 2。换括号,得到的东西完全不是一类。
匿名函数:就一行
lambda 参数: 表达式。没有名字、不能写语句,复杂逻辑别塞。真正出场的地方是「函数当参数」:sorted(..., key=)、filter、map。能写成推导式的,优先推导式——更像人话。
和 def 差在哪
def有名字、有语句块、能return多次lambda是表达式,只能有一个返回值,常被随手塞进参数里- 两者都是函数对象,都可以当闭包用。工厂函数返回
lambda n: n * k,捕获的还是那个k
跟谁走
- 排序:
sorted(rows, key=lambda r: r['age']) - 过滤 / 映射:能写推导式就别用
filter+map - 面试更常问闭包和装饰器。lambda 只是闭包的一种瘦写法
students = [{'name': '张三', 'age': 36}, {'name': '李四', 'age': 14}]
sorted(students, key=lambda s: s['age'])
[s['name'] for s in students if s['age'] > 18] # 比 filter+map 更清楚
流程控制就这些
顺序、分支、循环。分支用 if / elif / else,3.10 以后还有 match-case。循环里 continue 跳过本轮,break 离开,for-else / while-else 的 else 在「没被 break」时才走。输出优先 f-string:f'{name} 的 loss={loss:.4f}'。
闭包、三器、三程
高频面试把 Python 进阶压成三叠:闭包把状态揣走;三器分别解决遍历、内存和监控;三程决定 CPU 密集还是 I/O 密集该派谁上场。
参数怎么传
位置参数、默认值、关键字、*args 收成元组、**kwargs 收成字典。默认值只求值一次,所以不要写 def f(xs=[])。不可变对象当参数,函数里重新赋值不影响外面;可变对象当参数,.append 会影响外面。
def train(model, data, lr=1e-4, *hooks, **opt):
...
train(net, ds, 3e-4, log, wandb, device='cuda')
闭包:外层走了,变量还在
内部函数引用了外部函数的局部变量,并且外部函数把内部函数 return 出去——这三件齐了就是闭包。外层结束本该销毁的变量,被内层「捕获」留在内存里。装饰器、函数工厂、藏一点私有状态,全靠它。
make_mul(n) 把 n 揣走
外层函数已经返回,按理说 n 该销毁。点下面的倍数,看内层函数还拿得到它。
三条缺一不可
- 有函数嵌套:内层定义在外层里面
- 内层引用了外层的变量(不是全局变量)
- 外层把内层的引用 return 出去
改它、扔它
- 内层默认只能读外层变量。要改,得写
nonlocal n,否则 Python 会当成「正在定义局部变量」报错 - 闭包会把大对象一直握在手里,不用了把引用设成
None - 作用域查找顺着 LEGB:Local → Enclosing → Global → Built-in
三器:迭代器、生成器、装饰器
一句话带走:迭代器解决遍历,生成器解决内存,装饰器解决监控。循环的本质是先 iter() 拿到迭代器,再不断 next(),取完抛 StopIteration 结束。
next() 一次吐一个 token
列表本身不是迭代器,iter(tokens) 才是。惰性、单向、取完不能自动重来——和模型一次吐一个 token 同一个形状。
可迭代 vs 迭代器
- 可迭代:实现
__iter__。list、str、dict、文件对象都是 - 迭代器:再实现
__next__。一次性、只能往前、取完作废 - 超大文件、百万行日志,用迭代器逐条读,避免一次性装进内存
生成器是特殊的迭代器
- 函数体里有
yield,调用它不执行,只返回生成器 - 每次
next(),代码才跑到下一个yield - LangGraph 的
stream()、SSE 边生成边推 token,都是生成器在干活
def batched(xs, n):
buf = []
for x in xs:
buf.append(x)
if len(buf) == n:
yield buf
buf = []
if buf: yield buf
装饰器:不改原函数,外包一层
@log 就是 chat = log(chat)。点开关,看出栈顺序:先进后出。计时、日志、重试、鉴权都是这一招。
装饰器本质是闭包:外层收下原函数,内层在调用前后加活,再把原函数的返回值递出去。functools.wraps 用来把原函数的名字抄回来,不然堆栈里全是 wrapper。
类只要三件事
封装 · 继承 · 多态
_name是约定别碰,__name会被改写成_类名__name- 多继承靠 MRO(C3),
super()按这张表走,不要手搓父类名 - Python 的多态常常是鸭子类型:有
.predict()就能当模型用
方法三种
- 实例方法:第一个参数
self - 类方法:
@classmethod,第一个参数cls,工厂函数常用 - 静态方法:
@staticmethod,和类相关但不用到实例
三程:进程、线程、协程
同步:点完菜站在柜台等,期间什么也不干。异步:拿号回去刷手机,叫到再取。进程是操作系统分资源的独立房间;线程是房间里的几双手,共享家具;协程是自己排队,遇到 await 就让位,不劳操作系统。
三间工位
CPU 密集看谁能真并行;I/O 密集看谁切换更便宜。GIL 是那把只能递来递去的钥匙。
| 适合 | 不适合 | 典型任务 | |
|---|---|---|---|
| 线程 | I/O 密集、要共享内存、几十到几百个任务 | CPU 密集(GIL)、成千上万连接 | 读文件、调接口、后台轮询 |
| 协程 | 高并发 I/O、连接数多、单任务计算量小 | CPU 密集(会堵住事件循环)、大量同步库 | HTTP、WebSocket、SSE、模型流式输出 |
| 多进程 | CPU 密集、要绕开 GIL、要强隔离 | 纯等 I/O、要频繁共享数据(IPC 贵) | 图像处理、批量特征、模型并行 |
async 定义协程,await 让出执行权,asyncio.gather 把检索、调模型、调工具叠在一起等。
Python 会写了。模型要的不是列表,是一块整齐的数字。
NumPy 与 Pandas:张量和表格
list 能装东西,但装一百万个数就慢,还不能广播。NumPy 的 ndarray 是「同一类型、矩形、连续内存」的多维数组。Pandas 在它外面加了标签,变成 Series 和 DataFrame。
ndarray 记住这些
shape dtype ndim:形状、元素类型、维数- 切片是视图(改切片会改原数组),花式索引 / 布尔索引是副本
*和np.multiply是逐元素乘;矩阵乘用@或np.dot- 造数组:
array zeros ones full arange linspace randn
和模型页的关系
- 词向量是
(vocab, d)的矩阵,一批 token 是(B, T, d) - 注意力分数
Q @ K.T就是这里的矩阵乘 - 方差、标准差是归一化的原料,数学页 P2 有公式
广播:形状不够,就用 1 去补
两个数组要做逐元素运算,从右边对齐。某一维是 1 就「拉长」。两边都不是 1 且不相等,就报错。
Series 和 DataFrame
Series 是带索引的一列;DataFrame 是带行索引、列标签的一张表。loc 按标签取,iloc 按下标取。两列相加按索引对齐,对不上的格子填 NaN——和广播是亲戚,只不过对齐的是标签不是位置。
df.loc['aa':'cc', ['id', 'name']] # 标签,含末尾
df.iloc[0:3, 2] # 位置,不含末尾
df.groupby('dept')['salary'].mean()
pd.merge(left, right, on='id', how='left')
pd.concat([a, b], axis=0) # 0 往下接,1 往右接
n(总体),Pandas 默认除以 n-1(样本)。数据越散,方差越大,后面 LayerNorm 要除掉的就是这个尺度。
| 格式 | 特点 | 什么时候用 |
|---|---|---|
| CSV / TSV | 纯文本、通用 | 交换、小表 |
| JSON | 嵌套、跨语言 | 接口、配置 |
| Parquet / Feather | 列式、快 | 稍大的分析数据 |
| PKL | Python 对象 | 临时缓存,别当交换格式 |
| XLSX | 给人看 | 汇报,不进训练流水线 |
表格能算了。要长期存、多人查,就进数据库。
MySQL:把表问清楚
库是文件夹,表是带类型的 Excel,行是一条记录,列是字段。SQL 不是编程语言,是「描述你想要什么」的语言。分类记五个字母就够:DDL 建结构,DML 改数据,DQL 查询,DCL 管权限,TCL 管事务。
SELECT 真正的执行顺序
手写顺序是 SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT。引擎心里的顺序是:先 FROM / JOIN 拿表,WHERE 滤行,GROUP BY 分组,HAVING 滤组,SELECT 算列,ORDER BY 排序,LIMIT 切开。所以 HAVING 里可以用聚合,WHERE 里不行;别名在 WHERE 里也还看不见。
JOIN:两张表怎么对上
员工表有一条没有部门的王五,部门表有一个没有员工的财务。三种 JOIN 对这两个人的态度完全不同。
NULL 是特殊公民
- 任何和 NULL 的算术、比较,结果都是 NULL
- 判断用
IS NULL/IS NOT NULL,不要写= NULL - 实发工资这类计算,先
IFNULL(bonus, 0)
约束和事务
- 主键、唯一、非空、外键、检查:把「脏数据」挡在门外
- 事务 ACID:要么一起成功,要么一起回滚
START TRANSACTION; … COMMIT;失败就ROLLBACK
SELECT d.dname, AVG(e.salary) AS avg_sal
FROM t_employee e
JOIN t_department d ON e.did = d.id
WHERE e.salary IS NOT NULL
GROUP BY d.dname
HAVING AVG(e.salary) > 10000
ORDER BY avg_sal DESC
LIMIT 5;
FastAPI:把函数变成接口
浏览器打过来的是 HTTP。FastAPI 做的事:按路径找到函数,把参数从 URL 或 JSON 里拆出来,校验类型,再把返回值变成 JSON。底下用 Uvicorn 跑,调度靠协程。
进程、线程、协程的对比在 05 三程。FastAPI 选协程,因为 Web 几乎全是等网络。下面只看 await 让不让路。
await 让不让路
任务 A 睡 1 秒,任务 B 睡 2 秒。串行写成两个 await 上下排;并发要先 create_task 或 gather。
路径、查询、请求体
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
price: float
@app.get('/items/{item_id}')
async def read_item(item_id: int, q: str | None = None):
return {'id': item_id, 'q': q}
@app.post('/items')
async def create_item(item: Item):
return item
/items/{item_id} 里的是路径参数,?q= 是查询参数,POST JSON 用 Pydantic 模型当请求体。类型写在函数签名里,FastAPI 负责转换和 422。模块一多,用 APIRouter 拆到 routers/user.py 再 include_router。
select,容易 N+1,列表接口改 selectin。
Docker:把环境装进箱子
没有容器的时候,开发机和服务器永远差那么一点:Python 版本、系统库、环境变量。「在我电脑上能跑」就是这样来的。Docker 的目标是一次打包,到处跑。
虚拟机
- 连客操作系统一起虚拟化
- 隔离更彻底,启动以分钟计
- 整台操作系统一起虚拟化
容器
- 共享宿主机内核,只装箱应用和依赖
- 启动以秒计,密度高
- 隔离没虚拟机那么硬,但日常够用
镜像、容器、数据卷
点每一层。镜像是安装包,容器是跑起来的进程,卷是专门存数据的盘。
③ 数据卷 Volume
mysql-data → /var/lib/mysql
② 容器 Container
可写层 + 正在跑的 mysqld
① 镜像 Image
mysql:8.0 只读模板
两份文件就够上线
# Dockerfile 自定义镜像的菜谱
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml 多容器一起起
services:
db:
image: mysql:8.0
environment: { MYSQL_ROOT_PASSWORD: secret }
volumes: [mysql-data:/var/lib/mysql]
api:
build: .
ports: ["8000:8000"]
depends_on: [db]
volumes:
mysql-data:
docker compose up -d 会先起 MySQL,再起 API。镜像源换成国内的,pull 才不会超时。Windows 上 Docker Desktop 依赖 WSL2,需要先打开 CPU 虚拟化。
回头看一遍,从头到尾只有一条线。
回头看:一套环境的一以贯之
先有一台 Linux。目录和权限让你在这台机器上走动。Shell 把走动写成脚本。Python 描述逻辑,NumPy / Pandas 描述数字和表,MySQL 把表存住,FastAPI 把函数暴露出去,Docker 把这一切封进箱子。
三个能带走的心智模型
- 一切皆文件。进程、权限、配置、日志,最后都是路径。找不到东西就
ls、find、cat。 - 对齐再运算。NumPy 按形状广播,Pandas 按索引对齐,SQL 按 JOIN 键对齐。对不齐的格子不是 0,是「空」。
- I/O 让出,CPU 独占。等网络用协程;算矩阵用 NumPy / GPU / 多进程。GIL 不是 bug,是在提醒你别用纯 Python 线程砸计算。
面试里常被问到的
- list / tuple,闭包三条件,迭代器 / 生成器 / 装饰器,进程线程协程和 GIL
loc和iloc,广播规则,INNER 和 LEFT JOIN,事务 ACID- 镜像和容器的区别,为什么容器删了数据会丢,为什么要数据卷