python基本语法
这份笔记整理的是 Python 的基础语法:三元表达式、and / or / not、列表推导式、lambda、tuple、yield 生成器、try / except、with ... as ...、类型标注、@dataclass、Literal。
这些都是和js不一样的,所以放在这里边讲一下。
1. 三元表达式
Python 的三元表达式,写法是把”取哪个值”写在前面,条件夹在中间:
1 | 值1 if 条件 else 值2 |
1 | args = json.loads(json_accum) if json_accum else {} |
读法就是英语语序——“用 A,如果条件成立;否则用 B”。Python 强调可读性,官方认为 A if 条件 else B 这种写法更接近自然语言。
1 | age = 20 |
嵌套也能写,但别写:
1 | result = "A" if x > 0 else "B" if x < 0 else "C" |
这行能跑,但三个分支压在一行里,读的人得来回数 if 和 else 才配得上对。超过两个分支就换回 if / elif / else。
2. and / or / not 和默认值写法
三个逻辑运算符:
| 运算符 | 含义 | 例子 |
|---|---|---|
and |
并且(两边都为真才为真) | A and B |
or |
或者(有一个为真就为真) | A or B |
not |
取反 | not A |
or 最常见的一个用法是设置默认值:
1 | timeout = options.get("timeout") or 30 |
它取的是”第一个为真的值”。所以要判断这个写法合不合适,得先知道 Python 里哪些值算假:
| 值 | 真 / 假 |
|---|---|
False |
假 |
None |
假 |
0、0.0 |
假 |
"" |
假 |
[]、()、{}、set() |
假 |
其它(包括 float("nan")) |
真 |
空容器一律是假值,这一点要记住。所以上面那句里,传进来的 0、""、[] 都会被换成 30。
如果 0 是一个合法的取值,就不能这么写,只能显式判断:
1 | timeout = options.get("timeout") |
一段实际的兜底代码
1 | if not input_tokens and delta_input_tokens: |
逐词拆开:
| 部分 | 含义 |
|---|---|
not input_tokens |
input_tokens 是假值(0、None、空等) |
and |
并且 |
delta_input_tokens |
delta_input_tokens 有真实值 |
| 整体 | 如果原来的是空的,并且备用值有内容,就用备用值替换 |
翻译过来就是:如果 input_tokens 没有值,并且 delta_input_tokens 有值,就把 delta_input_tokens 赋给 input_tokens。
为什么需要这层兜底?因为有些兼容 Claude 的 provider(MiniMax 这类)在 message_start 里把 input_tokens 报成 0,真正的用量藏在后面的 message_delta 里。所以逻辑是:
- 优先用
message_start里的 usage - 如果它是 0(没拿到),就降级使用之前从
message_delta里抓到的delta_xxx值
这就是一种兼容兜底写法。
3. 列表推导式
Python 的列表推导式(List Comprehension),语法本身就支持”一边过滤一边转换”:
1 | parts = [s.content.strip() for s in self._sections if s.content.strip()] |
等价于下面这段普通写法:
1 | parts = [] |
结构读法:[对每个元素做什么 for 元素 in 集合 if 条件]。for 前面的表达式是转换,if 是过滤,一前一后,别搞混。
上面那句 strip() 被算了两遍(判断一次、取值一次)。要省掉重复计算,用海象运算符在条件里赋值:
1 | parts = [t for s in self._sections if (t := s.content.strip())] |
还有一个容易搞错的地方:推导式有自己的作用域,循环变量不会漏到外面。
1 | [x for x in range(3)] |
除了列表,字典和集合也能这么写:
1 | {name: len(name) for name in names} # 字典推导式 |
4. lambda:写匿名函数
lambda 是 Python 里用来写匿名函数的语法(没有名字的函数)。
基本语法:
1 | lambda 参数: 表达式 |
它等价于一个很简单的函数,但不用写 def。对比一下,用 def 写:
1 | def get_length(tr): |
用 lambda 写,效果完全一样:
1 | lambda tr: len(tr.content) |
在排序里的用法
1 | ranked = sorted(remaining, key=lambda tr: len(tr.content)) |
这里的 lambda tr: len(tr.content) 就是告诉 sorted:请按照每个元素的 content 长度来排序。
如果不写 lambda,就得先定义一个函数再传进去,会麻烦一些:
1 | def get_length(tr): |
关于 sorted 本身,两点值得记:
key对每个元素只算一次,不是每次比较都算。所以把耗时的事放进key函数,代价是n次,不是n log n次。sorted()返回新列表,list.sort()原地改。
最重要的一条限制:lambda 只能是一个表达式
1 | lambda 参数: 表达式 |
没有 {} 代码块,不能写语句,不能有 return(表达式的结果就是返回值),也不能加类型标注。所以它只够用在”给高阶函数塞一个一次性参数”这一种场合:
1 | sorted(items, key=lambda x: x.score) |
需要中间变量、需要循环、需要提前返回的,老实写 def:
1 | def first_line(tr): |
把 first_line 这种逻辑硬塞进 lambda 是不可能的——它要一个循环加一个提前返回,都是语句。判断标准很简单:一个表达式写得下就用 lambda,写不下就 def。
5. tuple:有序、不可变
Tuple(元组)是 Python 中一种有序、不可变的数据结构。
| 特点 | 说明 |
|---|---|
| 有序 | 元素有固定顺序,可以通过索引访问 |
| 不可变 | 创建后不能修改、添加或删除元素 |
| 可包含任意类型 | 数字、字符串、列表、甚至另一个元组都可以 |
| 用圆括号定义 | 也可以不加括号(逗号才是关键) |
1 | t1 = (1, 2, 3) |
解包很常用:
1 | name, age, city = ("张三", 25, "北京") |
“不可变”听着像个限制,但它换来两件实际有用的东西。
一、能当字典的 key。 一个坐标点可以直接拿去查表:
1 | cache = {} |
换成列表当 key,直接 TypeError: unhashable type: 'list'。原因就是元组不会变,哈希值稳定——不可变不是随便加的约束,它是”能当 key”这个能力的前提。
二、多返回值其实就是元组。 Python 的 return a, b 返回的是一个元组,所以可以原地拆开:
1 | def min_max(nums): |
6. yield:算一个、交一个,下次接着算
yield 是 Python 里用来创建**生成器(generator)**的关键字。
普通函数用 return 会立刻结束并返回结果。用了 yield 的函数会变成生成器函数,调用它不会立刻执行完,而是返回一个生成器对象。每次用 next() 或 for 循环去取下一个值时,函数才会从上次暂停的地方继续执行,直到再次遇到 yield。
所以 yield 和 return 的分工是:
return:一次把结果全算完,交出去,函数结束yield:交出一个值,函数在这里暂停;下次再来要,从暂停的地方接着往下跑
1 | def count_up_to(n): |
把四次取值拆开看,就是函数在”跑 → 停 → 接着跑”之间来回:
1 | next(gen) 第 1 次 -> 跑到 yield i,交出 1,暂停在这里 |
平时一般不会手写 next(),直接 for 就行——for 会自动一次次取,取到 StopIteration 就结束,这个异常在循环里根本看不见:
1 | for num in count_up_to(3): |
为什么要用它:不把数据一次性装进内存
这是 yield 最实在的理由。读一个几百万行的日志:
1 | def read_lines(path): |
换成返回列表,写法只差一点,内存差很多:
1 | def read_lines(path): |
用 yield 的版本,处理到第 500 万行时,内存里只有那一行。这就是**”边走边给”和”先攒齐再给”**的区别。
判断标准:这份结果你是打算遍历一遍就扔,还是后面要反复用? 遍历一遍就扔,用 yield;要反复用、要取长度、要按下标访问,那就返回列表。
两个要记住的点
- 生成器只能遍历一次。 取完就空了,再
for什么都不出来;要重新调一次函数,拿一个新的生成器。 yield from用来把另一个可迭代对象逐个交出去,等于省掉一层for:
1 | def all_lines(*paths): |
7. try / except
这是 Python 的异常处理语法,用来捕获和处理代码运行时可能出现的错误,防止程序直接崩溃。
基本结构:
1 | try: |
最简单的例子:
1 | try: |
输出:
1 | 转换失败,请输入数字 |
如果没有 try / except,程序会直接报错崩溃。
完整写法
1 | try: |
| 子句 | 什么时候执行 |
|---|---|
except |
出了对应的错才执行 |
else |
没出错才执行,用得少 |
finally |
无论有没有出错都执行,用来收尾 |
except 后面可以接具体类型,也可以接一个元组:
1 | try: |
except Exception as e 是最宽的那档”能捕获的都捕获”。注意它接的不是 BaseException,所以 KeyboardInterrupt(Ctrl+C)和 SystemExit 不会被吞掉——这是故意的。别写裸的 except:,它连 Ctrl+C 都拦。
raise 不带参数就是原样重抛当前这个异常:
1 | try: |
还有一条和前几节一脉相承的规则:能 raise 的只有异常对象,必须是 BaseException 的子类实例,raise "字符串" 这种写法不合法。
8. with ... as ...:进入时拿资源,离开时自动还
with ... as ... 是 Python 的上下文管理器语法,主要用于:在进入代码块时自动获取资源,在离开代码块时自动释放资源。
1 | with httpx.Client(timeout=30.0) as client: |
这一行拆成三部分:
| 部分 | 含义 |
|---|---|
httpx.Client(timeout=30.0) |
创建一个 HTTP 客户端,并设置默认超时时间为 30 秒 |
as client |
把这个客户端对象绑定到变量名 client |
with |
进入一个受管理的代码块。代码块结束后,Python 会自动关闭客户端,即使中间发生异常,也会尽量完成清理 |
所以它大致相当于”创建 + 记得关闭”两件事合起来写,好处是你不容易忘记写 close()。
为什么 HTTP 客户端需要关闭
httpx.Client 内部可能管理:
- TCP 连接
- HTTPS 连接
- 连接池
- Socket
- 系统文件描述符
如果创建了客户端却一直不关闭,可能导致资源泄漏——短期看不出来,跑久了是句柄耗尽或者连接堆积。用 with 的话,代码块结束时等同于自动调用:
1 | client.close() |
return 也不会跳过清理
这里有一个很重要的点:
1 | with httpx.Client(timeout=30.0) as client: |
上面有两条 return 路径,两条都一样:即使代码块中执行了 return,客户端也会先关闭,然后函数才真正返回。
清理的执行时机是”控制流离开这个块”,return 只是离开的一种方式,绕不过去。
自己写一个:@contextmanager + yield
如果前面 yield 那节看懂了,这里正好接上——写一个上下文管理器最简单的方式就是用生成器:
1 | import time |
yield 之前的代码是”进入时”,yield 之后的代码是”离开时”。 外面包 try / finally 是为了块里抛异常时也能走到打印那句。
两个坑:
- 手写类的时候,
__exit__返回真值会把异常吞掉。with块里抛出的异常会静默消失,排错时非常难受。@contextmanager装饰出来的版本不会这样(除非你自己用except把它接住),所以能用装饰器就别手写类。 - 多个资源从前往后依次进入、从后往前依次退出。 所以下面这种写法里
fout先关、fin后关,比嵌套两层with干净:
1 | with open("in.txt") as fin, open("out.txt", "w") as fout: |
9. 类型标注:list[str]、dict[str, Any]、type[X]
1 | names: list[str] = ["张三", "李四"] |
意思是:names 是一个列表,列表里的元素应该是字符串 str。
冒号后面是标注,等号后面才是值。标注不影响运行——Python 不会因为你标了 list[str] 就往里做检查,它只给 IDE 和 mypy 这类静态检查器看。
list[str] 这种小写泛型要 Python 3.9+(PEP 585)。老代码里写成 List[str],那是从 typing import 的,现在没必要了。
最常用的其实是嵌套的字典列表,AI / Agent 代码里到处都是:
1 | from typing import Any |
dict[str, Any] 读作”键是字符串、值是任何类型”。Any 表示 value 可以是任何类型,比较宽松;想更严格就把 Any 换成具体类型,比如 dict[str, int]。
列表里放对象也一样,写类名就行:
1 | class User: |
这里 list[User] 表示:users 是一个列表,列表中的每个元素都应该是 User 类型的对象。
type[X]:传的是类,不是实例
有一个标注容易读反:
1 | def make_new_user(user_class: type[User]) -> User: |
type[X] 表示的是”X 的类对象本身“,也就是 X 这个类,或者它的子类。它不是表示”X 的实例”,而是表示”能创建 X 实例的那个类”。
1 | make_new_user(User) # ✅ 传的是类 |
10. @dataclass:让类型标注替你写 __init__
@dataclass 是 Python 标准库 dataclasses 模块提供的装饰器(Python 3.7+ 内置)。它的作用是:自动帮你生成一堆常用方法,让你不用手写大量样板代码。
1 | from dataclasses import dataclass |
加上 @dataclass 之后,上面这段会自动长出:
__init__—— 构造函数。字段声明顺序就是参数顺序,带默认值的必须排在后面__repr__—— 打印出来是StreamEnd(stop_reason='end_turn', input_tokens=12, ...),不再是<object at 0x7f...>__eq__—— 按字段逐个比,==能直接用__hash__——frozen=True时才生成
等价于自己手写:
1 | class StreamEnd: |
一个必须知道的坑:可变类型不能直接当默认值。
1 | from dataclasses import dataclass, field |
原因和 Python 默认参数那个经典坑是同一个:默认值在定义时求值一次,所有实例会共享同一个列表,改一个全都变。default_factory 是让”每次新建实例时调一次这个函数”。
11. Literal 比 Enum 轻在哪
要表达”这个字段只能取三个值之一”,传统写法是用 Enum:
1 | from enum import Enum |
缺点:
- 必须额外定义一个类
- 运行时是
Enum对象,不是普通字符串 - 序列化成 JSON 时比较麻烦(需要自己处理
.value,否则json.dumps抛TypeError) - 代码稍微啰嗦一点
现在更推荐用 Literal:
1 | from typing import Literal |
优点:
| 优点 | 说明 |
|---|---|
| 更轻量 | 不需要单独定义一个类 |
| 运行时就是普通字符串 | category 的值直接是 "read",不是对象 |
| 序列化零成本 | 直接能转 JSON,不用额外处理 |
| 类型检查依然有效 | 静态检查器能拦住非法值 |
| IDE 提示好 | 自动补全只会出现 "read" / "write" / "command" |
代价也要说清楚:Literal 只给类型检查器看,运行时不做任何校验。 从 JSON 里读进来一个字符串,标了 Literal 也拦不住非法值——要运行时校验还得自己写判断,或者用 Enum / pydantic。它管的是”写代码时 IDE 会拦”,不管”跑起来时数据是脏的”。
12. 小结
真正要记住的只有三件事:
一、判断和推导都是”先说结果、后说条件”。 A if 条件 else B、[对每个元素做什么 for 元素 in 集合 if 条件],条件和过滤都放在后面。
二、空容器是假值。 0、""、[]、{}、() 全都算假,所以 or 当默认值用会把它们一起兜掉。0 是有效值的时候,只能写显式判断。
三、类型标注不参与运行。 list[str]、type[X]、Literal 管的是写代码时的提示和补全,管不了运行时的数据。
参考
- 内置类型 / 真值测试:https://docs.python.org/zh-cn/3/library/stdtypes.html#truth-value-testing
dataclasses:https://docs.python.org/zh-cn/3/library/dataclasses.htmltyping(Any/Literal):https://docs.python.org/zh-cn/3/library/typing.html- PEP 585(
list[str]这种内置泛型):https://peps.python.org/pep-0585/