写 Python 时,你会不断写出这样的代码:price * count、name in users、text.strip()、score if passed else 0。它们长得不一样,却都在做同一件事:交给 Python 一段可以求值的代码,再拿回一个结果。
刚入门时,表达式最容易制造一种错觉:符号都认识,结果就应该一眼能看出来。可真正让人摔跟头的,往往不是某个运算符本身,而是 Python 先算谁、后算谁,哪些部分根本不会算,以及算出的到底是布尔值还是某个原始对象。比如 name or "匿名用户" 返回的通常是字符串,不是 True;0.1 + 0.2 == 0.3 是 False;两个内容相同的列表用 is 比较,也很可能是 False。
这一章会把这些容易混在一起的规则拆开。我们先跟着解释器走一遍求值过程,再处理算术、比较、布尔、成员和身份运算,然后进入字符串、序列、切片、函数调用、条件表达式与赋值表达式。学完之后,你不仅能算出结果,还能看出一段表达式为什么容易出错、应该怎样改得更清楚。

表达式会按优先级逐层求值:括号先算,接着乘除,最后加减。
最小的表达式可以只是一个字面量,例如 42、3.14、"你好"、True;也可以只是一个名字,例如 price。只要 Python 能对它求值并得到一个对象,它就是表达式。把多个小表达式用运算符、索引、属性访问或函数调用组合起来,就得到更大的表达式。
price = 39.8
count = 3
discount = 0.9
total = price * count * discount
message = f"应付金额:{total:.2f} 元"
print(message)这里真正被求值的部分很多:39.8 是表达式,price 是表达式,price * count 是表达式,整个 price * count * discount 也是表达式。total = ... 则是一条赋值语句。等号右边先得到值,随后这个值才绑定到左边的名字 total。
这个区别很实用。表达式可以放进函数参数、列表元素、条件、f 字符串花括号等需要“一个值”的位置;普通赋值语句不能随意塞进去。
numbers = [1 + 2, len("Python"), max(4, 9)]
print(numbers) # [3, 6, 9]列表里的三个部分会分别求值,得到的三个结果再组成列表。函数调用也是表达式,所以 len("Python") 和 max(4, 9) 都能直接放在列表中。
“表达式能求值”不等于“任何时候都一定成功”。名字必须已经绑定,运算符必须支持当前类型,索引必须有效,函数也可能主动抛出异常。
price = 20
print(price * 3) # 60
# print(prcie * 3) # NameError:名字拼错了
# print(price + "3") # TypeError:整数和字符串不能直接相加Python 会先计算已经走到的部分,直到遇到错误才停下。如果表达式前半段调用了会修改状态的函数,后半段才报错,前面的修改不会自动撤销。下面的 pop() 先移除了最后一项,随后除零才失败:
numbers = [10, 20, 30]
try:
result = numbers.pop() / 0
except ZeroDivisionError:
print(numbers) # [10, 20]这就是为什么不建议把许多有副作用的动作塞进一条长表达式。把“取数据”“验证”“计算”拆成几步,异常发生时更容易知道程序已经做了什么。
表达式的结果也未必是新对象。单独求值一个名字,得到的是它当前引用的对象;切片、拼接和某些函数会创建新对象;列表方法则可能直接修改原对象。后面遇到共享引用的坑时,我们会再次看到这个区别。
这两个概念经常被说成一件事,其实它们回答的是不同问题:
2 + 3 * 4 会被理解成 2 + (3 * 4)。看一个会留下痕迹的例子:
def mark(label, value):
print(f"正在计算:{label}")
return value
result = mark("左边", 10) + mark("右边", 2) * mark("最后", 3)
print(result)输出顺序是“左边、右边、最后”,因为三个调用从左向右发生;但乘法仍然比加法先组合,所以结果是 10 + (2 * 3),也就是 16。不要把“乘法优先”误解成最右边两个调用一定先执行。
阅读表达式时可以分两遍:第一遍按优先级加上心里的括号,确认它如何分组;第二遍再从左向右追踪真正会发生的求值。遇到 and、or、条件表达式时,还要进一步判断哪些分支会被跳过。
下面的交互页可以把一条复合表达式拆成求值步骤。先选一个例子,再观察“分组”和“执行先后”为什么不能混为一谈。
Python 的常用算术运算符包括 +、-、*、/、//、% 和 **。前四个很像普通数学,但后三个经常让初学者判断失误。
a = 17
b = 5
print(a + b) # 22
print(a - b) # 12
print(a * b) # 85
print(a / b) # 3.4
print(a // b) # 3
print(a % b) # 2
print(a ** b) # 1419857/ 是真除法。即使刚好整除,结果通常也是浮点数:8 / 4 得到 2.0。// 是向下取整除法,它得到不大于真实商的最大整数方向结果。对正数来说很像“去掉小数”,碰到负数就能看出差别。
print(7 // 3) # 2
print(-7 // 3) # -3
print(-7 % 3) # 2-7 / 3 大约是 -2.333,向下走到整数是 -3,不是朝零靠近的 -2。取余 % 会与整除配合,使下面的关系成立:
a = -7
b = 3
print((a // b) * b + (a % b)) # -7做循环编号、按页分组和时钟计算时,// 与 % 经常一起出现。先根据问题说清楚“商”和“余数”分别代表什么,比背结果更稳。
如果商和余数都要,可以用一次 divmod(a, b) 调用同时得到它们:
seconds = 367
minutes, remaining_seconds = divmod(seconds, 60)
print(minutes) # 6
print(remaining_seconds) # 7divmod() 返回一个二元组,这里又通过解包分别绑定给两个名字。它表达的意图比连续写 seconds // 60 和 seconds % 60 更集中。
一元 + 和一元 - 只需要一个操作数。-amount 表示取相反数,+amount 通常保留数值本身。连续符号虽然能运行,例如 --3 得到 3,却容易让人误读;业务代码里直接写清目标值更好。
还要区分“运算符长得一样”和“类型支持同一种行为”。数字的 + 是加法,序列的 + 是拼接;集合支持 - 来做差集,但字符串不支持减法。Python 不会仅凭符号替你猜转换方式,类型不匹配时会直接报错。
幂运算 ** 是少数从右向左结合的运算符,所以 2 ** 3 ** 2 等同于 2 ** (3 ** 2),结果是 512。如果你想先算 2 ** 3,必须写成 (2 ** 3) ** 2。
一元负号与幂运算放在一起也要小心:
print(-3 ** 2) # -9,相当于 -(3 ** 2)
print((-3) ** 2) # 9
print(2 ** -2) # 0.25看到负数参与幂运算时,主动写括号最省心。你真正想平方的是 -3,就写 (-3) ** 2,不要要求读代码的人替你猜。
整数可以任意大,只要机器内存允许;浮点数通常是有限精度的二进制近似值。整数和浮点数混合运算时,结果通常会进入浮点数范围。
print(4 + 2.5) # 6.5
print(type(4 + 2.5)) # <class 'float'>
print(8 / 4) # 2.0除数为零时,Python 会抛出 ZeroDivisionError。普通 Python 浮点运算也不会把 1.0 / 0 自动变成无穷大,所以不要照搬某些计算器或数值库的行为。
denominator = 0
if denominator != 0:
result = 10 / denominator
else:
result = None
浮点数保存的是二进制近似值,看似简单的小数运算也可能留下极小误差。
你很快会遇到这个结果:
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False原因不是 Python 不会算加法,而是大多数十进制小数不能用有限位二进制小数精确表示。机器保存的是离目标值很近的数,连续运算后,微小差距可能在输出或比较时露出来。
如果你比较的是测量值、计算结果或容许误差的数值,使用接近比较:
import math
actual = 0.1 + 0.2
expected = 0.3
print(math.isclose(actual, expected)) # True如果是金额等要求十进制规则准确的场景,可以从字符串构造 Decimal:
from decimal import Decimal
price = Decimal("0.10")
tax = Decimal("0.20")
print(price + tax == Decimal("0.30")) # True注意这里用了字符串。若先写成二进制浮点数再交给 Decimal,原先的近似已经发生了。显示时保留两位小数只改变显示形式,也不能反过来让存储值变得精确。
不要把 round() 当成所有浮点比较的修复按钮。显示金额时按业务规则舍入没有问题;判断两个计算结果是否足够接近时,应该明确容许误差;要求精确十进制运算时,应选用适合的数值类型。
计数、索引、分页数量通常适合整数;传感器读数、几何计算通常接受浮点近似;货币、税率结算若要求确定的十进制舍入规则,更适合 Decimal。选择类型不是表达式写完后的装饰,它会直接决定运算和比较的行为。
例如“平均每箱多少件”与“至少需要几箱”不是一个问题:
items = 25
capacity = 6
average = items / capacity
boxes_needed = (items + capacity - 1) // capacity
print(average) # 4.166666666666667
print(boxes_needed) # 5第一条表达式保留真实商,第二条需要向上取整得到容器数量。不要看到 // 就机械使用;先从业务问题判断结果应该是连续数值、向下分组还是向上补齐。
==、!=、<、<=、>、>= 用来比较值。比较结果通常是 True 或 False。
score = 86
print(score >= 60) # True
print(score == 100) # False
print(score != 0) # TruePython 不会为所有类型胡乱规定大小顺序。3 < "4" 会抛出 TypeError,复数之间也不能用普通大小运算符排序。接收外部输入时,先把数据转换成预期类型再比较。
raw_age = "18"
age = int(raw_age)
print(age >= 18) # True区间判断可以写成 18 <= age < 65。它表达的是“年龄不小于 18,并且小于 65”,而且中间的 age 只求值一次。
age = 25
eligible = 18 <= age < 65
print(eligible) # True链式比较也会短路。如果第一段已经是 False,后面的部分不会继续求值。它和下面的逻辑意义接近:
eligible = 18 <= age and age < 65但不要把链式比较当成任意条件的压缩器。a < b == c 是合法的,含义是 a < b and b == c;读者若需要停下来猜意图,就应该拆开或改用带名字的中间结果。
in 问的是“是否属于”,而且容器不同,规则也不同成员运算符是 in 和 not in:
roles = ["编辑", "审核", "管理员"]
text = "今天开始学习 Python 表达式"
settings = {"theme": "dark", "language": "zh-CN"}
print("审核" in roles) # True
print("Python" in text) # True,字符串检查子串
print("theme" in settings) # True,字典默认检查键
print
空字符串是任何字符串的子串,所以 "" in "Python" 是 True。如果你拿用户输入直接做子串检查,又不希望空输入匹配成功,应先检查输入是否非空。
keyword = ""
text = "Python 表达式"
matched = bool(keyword) and keyword in text
print(matched) # False== 比内容,is 比对象身份这是最常见的表达式坑之一。两个列表内容一样,== 可以是 True;它们分别创建时,通常不是同一个对象,所以 is 是 False。
left = [1, 2, 3]
right = [1, 2, 3]
alias = left
print(left == right) # True
print(left is right) # False
print(left is alias) # True判断字符串、数字、列表等值是否相等,使用 ==。不要因为某次在交互环境中看到小整数或短字符串碰巧 is 为真,就把身份比较当成值比较。解释器可能复用某些对象,但这种实现细节不是你的业务规则。
is 最稳定、最常见的用途是与单例对象比较,尤其是 None:
result = None
if result is None:
print("还没有结果")这里不能随意改成 if not result,因为 0、空字符串、空列表也是假值,但它们可能是完全有效的结果。
字符串比较会从左到右寻找第一处不同的字符,并按字符顺序决定大小;列表和元组也会逐项比较。不是“谁更长谁更大”。
print("apple" < "banana") # True
print("Zoo" < "apple") # True,大小写字符顺序不同
print([1, 9] < [2, 0]) # True,第一项已经决定结果
print([1, 2] < [1, 2, 0]) # True,共同前缀相同,较短序列在前这类比较适合遵循 Python 自身顺序的排序,不一定符合人类语言的字典顺序,也不自动忽略大小写。做用户名或标签的大小写无关比较时,可以先用统一规则规范化两边:
left = "Python"
right = "PYTHON"
same = left.casefold() == right.casefold()
print(same) # True容器比较还要求对应元素之间能比较。如果第一项相同,第二项却是无法排序的不同类型,比较会在走到那里时抛出 TypeError。这再次说明,比较表达式不是一次把整个容器“打分”,而是按需要逐步求值。
NaN 会打破“值应该等于自身”的直觉浮点非数 nan 用来表示某些未定义或缺失的数值结果。它有一个特别的性质:连自己都不等于自己。
import math
value = float("nan")
print(value == value) # False
print(math.isnan(value)) # True判断非数应该使用 math.isnan(),不要写 value == float("nan")。这也提醒我们,比较规则最终由对象类型定义,不能把普通整数的直觉无条件套到所有对象上。

值相等、指向同一对象和属于某个集合,是三种含义完全不同的判断。
if、while、and、or 和 not 不要求操作数必须先写成 True 或 False。它们会先检测对象的真值。
常见假值包括:None、False、各种数值零,以及空字符串、空列表、空元组、空字典、空集合、空 range。大多数其他对象都被当成真值。
print(bool(0)) # False
print(bool("")) # False
print(bool([])) # False
print(bool("0")) # True,字符串里有一个字符
print(bool([0])) # True,列表里有一个元素因此,检查容器有没有内容时,直接写 if items: 通常比 if len(items) > 0: 更自然。
items = ["铅笔", "橡皮"]
if items:
print("购物车里有商品")但“是假值”和“业务上无效”不是同一个概念。数量 0 可能是合法输入;空字符串可能表示用户确实选择了清空昵称。写条件之前,先说清楚你要排除的究竟是 None,还是所有假值。
自定义对象也能决定自己的真值。类可以定义 __bool__(),或者在没有它时通过 __len__() 的结果参与真值检测。你暂时不必马上编写这样的类,但要知道 if object: 背后可能触发对象自己的逻辑,并不总是一次免费的常量判断。
class TaskList:
def __init__(self, tasks):
self.tasks = tasks
def __len__(self):
return len(self.tasks)
pending = TaskList([])
print(bool(pending)) # False自定义的 __bool__() 也可能抛出异常,这个异常会继续向外传播。碰到这类对象时,不要以为再包一层 bool() 就能绕过去;应该查看该类型公开的判断方式,明确检查你真正关心的状态。
and 和 or 返回操作数,不保证返回布尔值这是布尔表达式里最值得单独记住的规则:
x and y:如果 x 是假值,返回 x;否则求值并返回 y。x or y:如果 x 是真值,返回 x;否则求值并返回 y。not x:返回真正的布尔值,表示对 x 真值的取反。print("" or "匿名用户") # 匿名用户
print("小林" or "匿名用户") # 小林
print([] and "继续") # []
print([1] and "继续") # 继续
print(not []) # True利用 or 提供默认值很方便,但它会把所有假值都当成“缺少”。下面这段代码会把合法的 0 替换成 10:
user_limit = 0
limit = user_limit or 10
print(limit) # 10如果只有 None 表示未提供,应该明确判断:
user_limit = 0
limit = 10 if user_limit is None else user_limit
print(limit) # 0and 左边为假时,结果已经确定,右边不求值;or 左边为真时,右边不求值。这叫短路求值。
records = []
first_is_admin = bool(records) and records[0]["role"] == "admin"
print(first_is_admin) # False因为 records 是空列表,and 直接返回左边的假值,危险的 records[0] 没有执行,因而不会出现 IndexError。
短路也常用于先验证条件,再做代价更高的工作:
def check_permission():
print("正在检查权限")
return True
logged_in = False
allowed = logged_in and check_permission()
print(allowed) # False,函数没有被调用这里要意识到,右边的函数调用可能带有副作用。若程序正确性依赖这个函数一定执行,就不该把它藏在可能短路的表达式里。
not 的优先级高于 and,and 又高于 or。因此:
result = active or verified and not blocked会按 active or (verified and (not blocked)) 理解。如果你的真实规则是“账号已激活或已验证,并且没有被封禁”,应写成:
result = (active or verified) and not blocked两段代码对某些输入给出不同结果。假设 active=True、blocked=True,第一段会因为 active 为真直接通过,第二段则会被封禁条件拦下。权限、付款、删除确认这类条件不适合依赖读者背优先级,括号就是规则的一部分。
not 总是返回布尔值,但它的优先级低于比较运算,所以 not age >= 18 会理解成 not (age >= 18)。更自然的写法往往是换成相反比较 age < 18,或者保留括号明确表达。

左侧结果足以决定整体结果时,短路判断会跳过右侧表达式。
在下面的实验里切换不同输入,注意观察“返回哪个操作数”和“是否执行右侧”这两件事。
不要用位运算符 & 和 | 代替普通条件中的 and 和 or。它们的含义、优先级和求值方式都不同,也不会提供同样的短路保护。处理布尔条件时,优先使用 and、or、not。
对字符串、列表、元组等序列来说,+ 通常表示拼接,* 表示重复。运算符的写法与数字相同,含义由操作数类型决定。
title = "Python" + " 表达式"
line = "-" * 12
numbers = [1, 2] + [3, 4]
pattern = ("红", "蓝") * 2
print(title) # Python 表达式
print(line) # ------------
print(numbers) # [1, 2, 3, 4]
print拼接通常要求两边是兼容的序列类型。"年龄:" + 18 会报错,因为字符串不能直接和整数拼接。可以显式转换,或者更自然地使用 f 字符串。
age = 18
message = f"年龄:{age}"f 字符串花括号内部可以放表达式:
name = "小禾"
scores = [88, 92, 85]
report = f"{name}的平均分是 {sum(scores) / len(scores):.1f}"
print(report)表达式可以放进去,不代表越长越好。花括号里如果嵌套了复杂条件、多个函数调用和索引访问,先在外面计算并命名,报错时更容易定位。
f 字符串还把“求值”和“显示格式”分成两层。冒号前面是要计算的表达式,冒号后面是格式说明:
price = 12345.6
ratio = 0.378
print(f"金额:{price:,.2f}") # 金额:12,345.60
print(f"完成率:{ratio:.1%}") # 完成率:37.8%格式说明改变的是最终字符串,不会修改原变量。price 仍然是原来的浮点数,ratio 也没有乘上 100。调试数值问题时,要把“存储的值”“计算结果”和“显示出来的文本”分开看。
这个坑很像“看起来创建了三份,实际共享了一份”:
rows = [[]] * 3
rows[0].append("数据")
print(rows) # [['数据'], ['数据'], ['数据']]* 3 重复的是对同一个内部列表的引用。三个位置都指向那一个列表,所以改动任何一处,其他位置也能看到。
需要三个彼此独立的列表时,应分别创建:
rows = [[] for _ in range(3)]
rows[0].append("数据")
print(rows) # [['数据'], [], []]对于字符串、数字、元组这类不可变对象,共享通常不会产生同样的原地修改问题;一旦序列内部装的是可变对象,就要对“重复引用”保持警惕。
索引从 0 开始,负索引从末尾往回数:
letters = ["A", "B", "C", "D", "E"]
print(letters[0]) # A
print(letters[-1]) # E索引越界会抛出 IndexError。切片通常更宽容,超过边界会被截到有效范围。
print(letters[1:4]) # ['B', 'C', 'D']
print(letters[:3]) # ['A', 'B', 'C']
print(letters[3:99]) # ['D', 'E']
print(letters[99:]) # []切片的基本形式是 sequence[start:stop:step]。可以把它理解成从 start 出发,每次走 step,在碰到 stop 之前停下,永远不包含 stop 对应的位置。
numbers = [0, 1, 2, 3, 4, 5, 6, 7]
print(numbers[1:6:2]) # [1, 3, 5]
print(numbers[::2]) # [0, 2, 4, 6]
print(numbers[::-1]) # [7, 6, 5, 4, 3, 2, 1, 0]步长不能为 0。步长为负数时,默认起点和终点方向也会反过来,因此 [::-1] 能生成逆序结果。初学阶段不要硬背所有负步长组合;先画出索引,标记起点、停止边界和行进方向,会更可靠。
切片三个位置本身也可以是表达式,而不必写死数字:
data = list(range(20))
page = 2
page_size = 5
start = (page - 1) * page_size
stop = start + page_size
current_page = data[start:stop]
print(current_page) # [5, 6, 7, 8, 9]分页时先命名 start 和 stop,比把两个算式全塞进方括号更容易核对。切片边界超出范围通常不会报错,这对取页很方便,也可能掩盖上游计算错误。若业务要求页码必须有效,应该在切片前单独验证,而不是把“得到空列表”一律当成正常结果。
索引和切片的错误处理也不同:索引必须指向实际存在的单项,越界就失败;切片描述的是一个范围,范围与序列没有交集时得到空的同类序列。这种宽容不是“自动补齐不存在的数据”,它只是返回当前范围内能取到的部分。
列表切片会创建一个新列表,但它是浅层复制。新旧列表里的可变元素仍可能指向同一对象:
original = [[1], [2]]
copied = original[:]
copied[0].append(99)
print(original) # [[1, 99], [2]]
print(copied) # [[1, 99], [2]]字符串和元组本身不可变,切片会得到相应类型的新值;列表可变,切片还可以出现在赋值语句左边,用来替换一段内容。这里先把重点放在切片表达式如何产生结果。
items[a:b] 会构造切片结果,left + right 会构造拼接结果,text * n 也会生成重复后的序列。数据很小时不用过早担心,但如果一段循环反复拼接巨大列表或字符串,表达式每次都可能复制已有内容。
处理许多字符串片段时,通常先收集到列表,再用 "".join(parts) 一次连接;处理列表时可以根据“要新列表”还是“要修改原列表”选择 +、切片、extend() 等不同方式。性能建议的前提仍是语义正确:先明确是否允许修改原对象,再决定表达式和方法。

切片从起点出发,在终点之前停下,并按照指定步长挑选元素。
下面可以拖动起点、终点和步长,亲手验证“左闭右开”以及负步长的行进方向。
看到 name(...) 时,可以先按三步理解:找到可调用对象,求值各个参数,执行调用并拿回返回值。
numbers = [4, 1, 9, 2]
average = sum(numbers) / len(numbers)
print(round(average, 2)) # 4.0sum(numbers) 和 len(numbers) 各自返回结果,除法再组合这两个值;外层 round(...) 返回舍入后的值;最后 print(...) 把它输出。嵌套调用从内到外看比较容易理解,但实际参数仍按从左到右的顺序求值。
def value(label, number):
print(label)
return number
result = max(value("先算左参数", 3), value("再算右参数", 7))
print(result)两个 value(...) 调用会依次发生,然后 max(...) 才能运行。参数里如果放了写文件、修改列表、发请求之类的副作用,顺序就会影响程序状态。更稳妥的做法是把这些动作拆成清楚的语句。
调用开始前,Python 还要先找到圆括号前面的可调用对象。如果你不小心用普通值覆盖了函数名,错误会在调用位置出现:
len = 10
# print(len([1, 2, 3])) # TypeError:整数不可调用这里不是列表有问题,而是名字 len 已经绑定成整数,遮住了原来的内置函数。新手看到“对象不可调用”时,可以先检查圆括号前面的名字现在是什么类型。避免用 list、str、sum、max、len 等内置名称当普通变量名,会少掉一类很隐蔽的错误。
参数求值成功后,还要与函数参数正确匹配。位置参数靠顺序,关键字参数靠名字;缺少必需参数、重复传同一个参数或使用未知关键字,都会让调用失败。
def total_price(price, count, discount=1.0):
return price * count * discount
print(total_price(20, 3)) # 60.0
print(total_price(20, 3, discount=0.9)) # 54.0关键字参数能让布尔开关和多个相近数值更容易辨认。调用时应让表达式说明含义,不要为了展示语法而混用过多拆包和嵌套。
return 的函数仍然有返回值Python 函数如果没有显式返回其他值,会返回 None。许多原地修改容器的方法也返回 None,用来提醒你“对象已经改了,不要把调用结果当成修改后的对象”。
numbers = [3, 1, 2]
result = numbers.sort()
print(numbers) # [1, 2, 3]
print(result) # None常见错误是这样写:
numbers = [3, 1, 2]
numbers = numbers.sort()
print(numbers) # None如果你需要一个新排序结果并保留原列表,使用返回新列表的 sorted(numbers):
numbers = [3, 1, 2]
ordered = sorted(numbers)
print(numbers) # [3, 1, 2]
print(ordered) # [1, 2, 3]方法链也有同样边界。字符串方法通常返回新字符串,所以 text.strip().lower() 可以继续链下去;list.append() 返回 None,因此 items.append(x).sort() 会失败。
函数调用支持位置参数、关键字参数,以及用 * 和 ** 拆包。表达式都能很灵活地塞进参数位置,但不要把一次调用写成谜题。
def create_order(product, count, *, urgent=False):
return {"product": product, "count": count, "urgent": urgent}
product = "键盘"
count = 2
order = create_order(product, count, urgent=count >= 2)这里 urgent=count >= 2 很短,放在参数里能直接说明规则。如果参数来自长链式调用或复杂条件,先计算中间变量会更清楚,也更方便打印检查。
* 在调用中会把可迭代对象展开成位置参数,** 会把映射展开成关键字参数:
def describe(name, age, city="未知"):
return f"{name},{age} 岁,来自{city}"
basic = ["小岚", 20]
extra = {"city": "杭州"}
print(describe(*basic, **extra))展开前的对象本身也要先求值。字典键必须是字符串,并且必须能匹配参数名;若展开内容与显式参数重复,Python 会报告重复传值。比起在一行调用里合并许多动态字典,先构造并检查参数往往更可靠。
调用是否成功不能只看函数名,还要看返回值约定。原地修改方法常返回 None;查询、转换和创建新对象的函数通常返回可继续使用的值。拿不准时,先单独调用并打印 repr(result),不要盲目接着链式调用。
当你需要根据一个条件在两个值之间选择,可以使用:
value_when_true if condition else value_when_false它的阅读顺序和普通 if 语句不完全一样。Python 会先计算中间的条件,条件为真才计算左边,为假才计算右边。
score = 76
status = "通过" if score >= 60 else "未通过"
print(status) # 通过只有被选中的分支会求值,所以可以安全地写出这样的表达式:
numbers = []
first = numbers[0] if numbers else None
print(first) # None空列表使条件为假,numbers[0] 不会执行。反过来写就会改变含义:
# 错误方向:列表为空时反而尝试访问第一个元素
first = None if numbers else numbers[0]“只计算一边”还能避免不必要的工作,但不要把它当异常处理:
cached = "已经算好的结果"
def build_report():
print("正在生成报告")
return "新报告"
report = cached if cached is not None else build_report()
print(report)这里 cached 已有值,build_report() 不会执行。如果条件写错、缓存内容类型不对或函数本身会失败,条件表达式不会替你修复这些问题。它只负责根据一个条件选择一条求值路径。
条件表达式得到的结果类型也可以因分支不同而不同,例如一边返回整数、一边返回 None。Python 允许这样做,但使用结果的后续代码必须处理两种可能。若统一类型更利于调用方理解,可以让两个分支都返回同类值,或者在变量名和类型标注中明确“可能为空”。
条件表达式适合“一眼能看完”的二选一,例如显示文本、提供默认值、选择一个简单参数。层层嵌套虽然合法,却很难读:
# 能运行,但读起来费劲
level = "优秀" if score >= 90 else "良好" if score >= 75 else "继续努力"超过两个分支时,普通 if / elif / else 往往更清楚:
if score >= 90:
level = "优秀"
elif score >= 75:
level = "良好"
else:
level = "继续努力"条件表达式的优先级低于 or。若多个运算符混在一起,使用括号表达意图,不要拿优先级表考同事。
nickname = ""
label = (nickname or "匿名用户") if nickname is not None else "未设置"普通赋值 = 是语句,只负责把值绑定到名字。赋值表达式 := 在绑定名字的同时,还会把这个值作为整个表达式的结果返回。
最容易看懂的场景,是某个值需要先判断、随后立刻复用:
text = " Python "
if cleaned := text.strip():
print(f"有效内容:{cleaned}")text.strip() 只调用一次,结果绑定给 cleaned,同一个结果又被 if 检测真值。若结果是空字符串,条件为假;若非空,代码块里可以直接使用 cleaned。
循环读取数据也是典型用法:
queue = ["任务一", "任务二", ""]
def read_item():
return queue.pop(0) if queue else ""
while item := read_item():
print(f"正在处理:{item}")每轮只调用一次 read_item()。取到非空字符串就进入循环,取到空字符串就结束。
:= 不是“到处都能用的等号”赋值表达式的目标必须是一个名字,不能直接是属性、下标或解包目标。
# 合法
(count := len("Python"))
# 以下写法都不合法
# (user.name := "小林")
# (items[0] := "新值")
# ((x, y) := (1, 2))它在不少位置需要括号,单独作为表达式语句时也要写括号:
(length := len("Python"))
print(length)在 if 和 while 的顶层条件中通常可以省略括号,但当它与其他表达式组合时,加括号往往更清楚:
text = "Python"
if (length := len(text)) > 5:
print(f"长度是 {length}")f 字符串里的 : 本来就负责格式说明,因此赋值表达式放进花括号时必须格外谨慎并加括号。教学和业务代码中,更建议先在外面赋值,再格式化输出,避免让读者误判。
赋值表达式放在推导式里还有一个容易意外影响外部名字的边界。它绑定的目标可能留在包含这条推导式的作用域中:
numbers = [1, 2, 3]
doubled = [(latest := number * 2) for number in numbers]
print(doubled) # [2, 4, 6]
print(latest) # 6这种写法合法,却把推导式内部的计算痕迹带到了外面。更麻烦的是,目标名字不能与同一推导式的 for 目标冲突。对刚入门的代码,宁可写普通循环或只做简单映射,也不要在推导式里用 := 累积状态。
doubled = []
for number in numbers:
latest = number * 2
doubled.append(latest)多几行换来了清楚的执行顺序,也让 latest 为什么留下来一目了然。
下面的普通写法已经很清楚:
length = len(text)
if length > 5:
print(length)把它强行改成一行,并不会自动变得更好。:= 真正有用的判断标准是:计算结果需要立即参与条件,而且后续确实要复用;合并后仍然能一眼看出绑定了哪个名字、判断了什么条件。如果一条表达式里出现多个赋值、嵌套条件或复杂调用,就该拆开。

条件表达式先判断条件,再从两个候选值中选择一个作为结果。
普通赋值负责把步骤说清楚,赋值表达式负责在少数场景减少重复求值。不要以“少一行”为唯一目标。对新手和维护者来说,一个恰当的中间变量通常比紧凑写法更省时间。
完整优先级表很长,日常先掌握下面这条从高到低的骨架就够用:
**,然后是一元 +、-、~。*、/、//、%,然后是 +、-。not,然后 and,最后 or。x if condition else y。lambda 与赋值表达式 := 位于更低的位置。位运算和移位运算也有自己的层级,但如果你正在写普通业务条件,不要把它们和布尔运算挤在一起。需要时查表并加括号,比凭印象可靠。
result = 2 + 3 * 4 > 10 and not False可以按下面的方式逐层理解:
先按算术优先级计算 3 * 4,得到 12。
再计算 2 + 12,得到 14。
比较 14 > 10,得到 True;同时 not False 得到 True。
最后计算 True and True,整个表达式得到 。
Python 不需要下面这些括号也能正确运行,但人会读得更快:
can_enter = is_member and (age >= 18 or has_guardian)
total = subtotal + (shipping_fee if needs_shipping else 0)
matched = keyword and (keyword in title or keyword in description)括号应该表达业务分组,而不是无规律地包住每一个操作数。再复杂一点,就别继续加括号了,直接命名中间结果:
is_adult = age >= 18
has_support = has_guardian or has_permission
can_enter = is_member and (is_adult or has_support)这种写法还有一个好处:你可以单独打印 is_adult 和 has_support,马上看出错误发生在哪一步。
表达式里的圆括号常用来分组,但 (3) 仍然只是整数 3。单元素元组需要逗号:
value = (3)
single = (3,)
print(type(value)) # <class 'int'>
print(type(single)) # <class 'tuple'>这条规则在函数返回值和解包附近尤其容易让人误会。return x, y 返回的是一个二元组;x, y = point 是解包赋值。看到逗号时,要考虑它是否正在构造或拆解一组值,而不只是把注意力放在括号上。
赋值表达式与逗号组合时也很容易产生错误分组。如果真的需要把一个元组绑定给名字,应写 (point := (x, y)),明确让右边整个元组成为赋值表达式的值。日常代码若必须靠细抠这种优先级才能读懂,通常已经到了该拆分的程度。
# 容易误读:not 的作用范围到底是什么?
if not name in blocked_names:
...
# 更自然
if name not in blocked_names:
...# and 先于 or
allowed = is_admin or is_member and paid
# 若业务规则是“管理员或会员,并且已付款”,必须明确写出
allowed = (is_admin or is_member) and paid第一段原表达式会被解释为 is_admin or (is_member and paid)。管理员即使未付款也能通过。这个结果也许正确,也许是权限漏洞,关键在于你的代码没有把业务分组说清楚。
表达式越长,不代表程序越专业。真正好维护的代码,会让每一步的输入、判断和结果都容易观察。
假设我们要计算订单实付金额:会员打九折,满 100 元免运费,输入数量必须大于零。把所有规则塞成一行当然能写:
final = price * count * (0.9 if is_member else 1) + (0 if price * count >= 100 else 8)这段代码藏着一个问题:免运费门槛按折扣前还是折扣后计算?同一个 price * count 重复出现,也增加了以后改规则时漏改一处的风险。
分步后,业务含义就清楚了:
if count <= 0:
raise ValueError("商品数量必须大于零")
subtotal = price * count
discount_rate = 0.9 if is_member else 1.0
discounted_total = subtotal * discount_rate
shipping_fee = 0 if discounted_total >= 100 else 8
final_total = discounted_total + shipping_fee现在你能明确看到:免运费门槛按折扣后金额判断。若产品规则改变,只要调整对应步骤。
先确认每个名字实际绑定的类型和值。用户输入通常是字符串,"10" + "2" 得到 "102",不会自动变成数字加法。
给表达式补上表示业务分组的括号,尤其检查 and 与 or、幂运算与负号、条件表达式与其他运算的组合。
检查短路与条件分支,确认本来应该执行的调用有没有被跳过,本来不该执行的危险索引有没有落在错误分支。
把函数调用和中间计算拆成变量,分别打印 repr(value) 与 。 能让空格、换行和空字符串更容易被看见。
is 当成 ==,拿对象身份比较普通数值或字符串内容。and、or 一定返回 True 或 False。value or default 时意外覆盖合法的 0、False 或空容器。/ 整除时返回整数,或认为负数 // 只是去掉小数。== 直接比较经过计算的浮点数。0。[[]] * n 创建嵌套列表,导致多行共享同一个内部列表。list.sort()、list.append() 的返回值当成修改后的列表。做表达式题时,不要只报最后答案。试着说明分组、求值顺序、短路位置和结果类型。你能把过程说清楚,换一组数据时才不容易靠猜。
阅读下面的代码,先不运行,写出四行输出:
values = []
print(values or [0])
print(bool(values or [0]))
print(values and values[0])
print("" in "Python")下面的程序允许用户把重试次数设为 0,表示不重试。为什么最后却变成了 3?应该怎样改?
user_retries = 0
retries = user_retries or 3给定列表 data = list(range(10)),表达式 data[8:2:-2] 得到什么?
补全下面两处,让代码分别判断“结果尚未提供”和“两个列表内容相同”:
result = None
expected = [1, 2]
actual = [1, 2]
missing = result # 请补全右侧表达式,判断结果尚未提供
same_content = actual # 请补全右侧表达式,判断列表内容相同最后给自己一个可操作的检查动作:下次遇到看不懂的表达式,先不要急着整行运行。给它补上分组括号,再把有副作用的调用拆出来,用 repr() 和 type() 逐步观察。表达式的难点并不在符号多,而在隐藏的求值过程;一旦过程可见,大多数坑都能在真正影响程序之前被发现。
Truetype(value)repr()最后再检查浮点误差、对象共享和原地方法返回 None 这类不容易从表面发现的问题。