Python变量是指向对象的名称,不是带有固定类型的容器;类型属于对象,名称可以重新绑定到另一种对象。
赋值不会复制对象,共享同一个列表或字典的多个名称都会观察到原地修改;0、False、空容器与None也不能随意混为一谈。
明确区分重新绑定与原地修改,在输入边界显式转换并验证类型,并用==比较值、用is检查None等哨兵对象。
是什么,为什么存在
Python中的变量更准确地说是名称(name)。执行赋值时,Python先计算右侧表达式,再把左侧名称绑定到所得对象。名称本身没有固定类型;对象才有类型、值与标识。
这种 名称绑定(name binding) 模型让同一种代码可以处理实现相同操作的不同对象,也让函数、类和模块都能像数据一样被名称引用。代价是你必须分清两个动作:把名称改为指向另一个对象,以及通过名称修改原对象。前者是重新绑定(rebinding),后者是原地修改(mutation)。
数据类型规定对象支持哪些操作,以及对象的值能否改变。int、float、complex、bool、str与NoneType是常见的标量类型;list、tuple、dict和set则组织多个对象。变量声明、函数参数、循环目标与导入名称最终都遵循同一套绑定规则。
你会在读取配置、解析JSON、计算金额、传递列表以及处理可选值时遇到这些规则。很多看似偶发的错误,例如共享列表被意外修改、有效的零被默认值覆盖,根源都是把名称、对象与类型混成了一个概念。
工作原理
名称绑定到对象
每个Python对象都有值、类型与 对象标识(object identity) 。type(value)返回对象的类型,id(value)返回表示其标识的整数,is比较两个引用是否指向同一对象。对象创建后,类型与标识不会改变。
赋值quantity = 3让名称quantity指向一个整数对象。随后执行quantity = "3"是重新绑定:名称开始指向字符串对象,原来的整数没有变成字符串。如果旧对象不再被任何引用使用,它之后可以被回收。
名称并不只由普通赋值引入。函数定义、类定义、导入、函数参数、for循环目标、with ... as ...与异常处理器中的as都会绑定名称。具体绑定位于哪个作用域,由这些语句所在的代码块决定。
图中cart与alias起初引用同一个列表,所以通过任一名称修改列表都能从另一名称看到。重新绑定cart后,alias仍指向旧列表。箭头变了,对象没有凭空复制。
内置类型各管一件事
Python的内置类型不是一组可以任意互换的标签。类型决定有效操作,例如字符串支持拼接,整数支持位运算,字典按键查找值。对不兼容对象执行操作时,Python通常抛出TypeError,而不是猜测程序意图。
| 类型 | 值的含义 | 可变性 | 常见边界 |
|---|---|---|---|
int | 任意精度整数 | 不可变 | bool是其子类 |
float | 通常为机器双精度浮点数 | 不可变 | 十进制小数不一定能精确表示 |
complex | 实部与虚部均为浮点数 | 不可变 | 不支持大小顺序比较 |
bool | True或False | 不可变 | 参与整数运算,但业务语义通常不同 |
str | Unicode文本序列 | 不可变 | 索引返回新字符串,不能原地改字符 |
NoneType | 缺失或无结果的单例值None | 不可变 | 应使用is None检查 |
容器类型保存对其他对象的引用。列表、字典和集合可以原地改变;元组不能替换自身槽位中的引用,但元组指向的列表仍然可以改变。因此,“容器不可变”不等于“从它出发能到达的所有对象都不可变”。
赋值、解包与别名
逗号分隔的目标可以一次绑定多个名称。left, right = right, left会先计算右侧对象,再把结果解包到两个目标,因此不需要临时变量。目标数量与右侧可迭代对象产出的数量不匹配时,会抛出ValueError。
链式赋值primary = backup = []只创建一个列表对象,再把两个名称都绑定到它。它不等于两次独立执行[]。如果需要独立容器,应分别创建,或根据所需复制深度显式复制。
增强赋值的结果取决于对象类型。列表的items += more通常原地扩展现有列表,其他别名也能看到;整数的count += 1创建另一个整数并重新绑定count。相同语法并不保证相同的共享效果。
动态类型不是没有类型
Python使用 动态类型(dynamic typing) :名称不需要永久声明一种类型,操作运行时会检查相关对象。一个名称先后绑定整数与字符串是合法的,但表达式"3" + 2仍会抛出TypeError。动态类型与“所有类型会自动兼容”是两回事。
类型注解可以描述预期契约,例如quantity: int或def total(price: int) -> int。普通Python调用不会因为注解而自动转换或拒绝参数。静态类型检查器、编辑器或框架可以读取注解,但运行时输入仍需由程序验证。
需要允许多种类型时,应先问这些类型是否真的共享同一业务语义。接受int | float适合一般测量值,却不一定适合货币分值;bool又是int的子类,所以单纯的isinstance(value, int)也可能比契约更宽。
真值、缺失与布尔运算
条件语句会对任意对象执行 真值测试(truth value testing) 。None、False、各类数值零和空容器通常为假,其他对象通常为真。自定义类型还可以通过__bool__()或__len__()决定真值。
真值相同不代表业务含义相同。库存数量0可以表示确实无货,None可以表示尚未盘点;空字符串可能是有效输入,也可能表示字段缺失。条件应保留领域需要区分的状态。
and与or会短路,并返回某个操作数,不保证返回bool。configured_port or 8000在端口为0时也返回8000。只有当所有假值都应当使用同一默认值时,这种写法才符合契约。
显式转换与类型检查
int()、float()、str()和bool()构造目标类型的对象,但它们不是通用的数据清洗器。int(3.9)向零截断为3,int("3.9")抛出ValueError,而bool("false")是True,因为任何非空字符串都为真。
混合内置数值运算会执行规定好的数值提升,例如整数与浮点数相加会得到浮点数。这类行为不代表Python会普遍把字符串转换为数字。外部文本应在边界处按明确格式解析,并为失败选择具体的错误路径。
isinstance(value, ExpectedType)会考虑子类,通常适合检查对象是否支持某个类型家族。type(value) is ExpectedType只接受确切类型,适用范围更窄。业务契约要求“整数但不能是布尔值”时,可以组合isinstance(value, int)与not isinstance(value, bool)。
示例
查看绑定对象的类型
第一个示例展示赋值、解包与重新绑定。quantity的类型变化来自名称指向了新对象,不是旧对象改变了类型。
order_id = "A-104"
quantity = 3
unit_price = 19.5
paid = False
missing_note = None
route = ("warehouse", "store")
# 重新绑定名称不会改变旧整数对象。
quantity = "3"
values = {
"order_id": order_id,
"quantity": quantity,
"unit_price": unit_price,
"paid": paid,
"missing_note": missing_note,
"route": route,
}
for label, value in values.items():
print(f"{label}: {type(value).__name__} = {value!r}")order_id: str = 'A-104'
quantity: str = '3'
unit_price: float = 19.5
paid: bool = False
missing_note: NoneType = None
route: tuple = ('warehouse', 'store')type(value).__name__适合演示与诊断,但生产逻辑通常不应根据类型名称字符串分支。真正需要类型检查时,应直接传入类型对象给isinstance()。
字典保存的是对象引用,所以循环取得的value仍是原对象。打印表示形式中的引号来自!r,它让字符串与数字在输出中容易区分。
区分修改与重新绑定
第二个示例让两个名称共享列表,同时保留一份浅复制。观察两次操作后各名称指向的内容与标识关系。
cart = ["keyboard", "mouse"]
# alias 与 cart 指向同一个列表。
alias = cart
# 浅复制创建新的外层列表。
snapshot = cart.copy()
alias.append("cable")
print(f"cart={cart}")
print(f"snapshot={snapshot}")
print(f"alias_is_cart={alias is cart}")
# 此表达式创建新列表,然后只重新绑定 cart。
cart = [*cart, "adapter"]
print(f"alias={alias}")
print(f"cart={cart}")
print(f"alias_is_cart={alias is cart}")cart=['keyboard', 'mouse', 'cable']
snapshot=['keyboard', 'mouse']
alias_is_cart=True
alias=['keyboard', 'mouse', 'cable']
cart=['keyboard', 'mouse', 'cable', 'adapter']
alias_is_cart=Falseappend()修改共享列表,所以cart与alias起初都能看到cable。snapshot是另一个外层列表,不受这次修改影响。
列表展开表达式创建新列表,再把cart绑定过去。alias继续引用旧列表,说明重新绑定一个名称不会追踪并修改其他别名。
在边界转换文本
来自命令行、环境变量或表单的值通常是文本。转换函数应同时定义允许的格式与数值范围,而不是把任何失败都换成一个看似正常的默认值。
def parse_quantity(raw: str) -> int:
try:
quantity = int(raw)
except ValueError as error:
raise ValueError("quantity must be a base-10 integer") from error
if quantity < 0:
raise ValueError("quantity must not be negative")
return quantity
samples = ["12", "3.5", "-1", ""]
for sample in samples:
try:
print(f"{sample!r} -> {parse_quantity(sample)}")
except ValueError as error:
print(f"{sample!r} -> invalid: {error}")'12' -> 12
'3.5' -> invalid: quantity must be a base-10 integer
'-1' -> invalid: quantity must not be negative
'' -> invalid: quantity must be a base-10 integer这里的try只包住可能产生预期转换失败的int(raw)。函数没有捕获所有Exception,因此后续代码中的编程错误不会被伪装成无效输入。
raise ... from error保留原始ValueError作为异常原因,同时向调用方提供稳定的领域消息。调用方根据异常类型处理失败,不应解析Python自带的错误消息。
验证反序列化后的类型
JSON解析成功只证明文本语法有效,不证明字段符合应用契约。这个示例逐层检查容器、布尔字段与整数分值,并特意排除bool。
import json
def active_total(raw: str) -> int:
records = json.loads(raw)
if not isinstance(records, list):
raise ValueError("orders must be a list")
total = 0
for index, record in enumerate(records):
if not isinstance(record, dict):
raise ValueError(f"order {index}: expected an object")
active = record.get("active")
cents = record.get("cents")
if not isinstance(active, bool):
raise ValueError(f"order {index}: active must be boolean")
if not isinstance(cents, int) or isinstance(cents, bool):
raise ValueError(f"order {index}: cents must be an integer")
if cents < 0:
raise ValueError(f"order {index}: cents must not be negative")
if active:
total += cents
return total
payloads = [
'[{"active": true, "cents": 1250}, {"active": false, "cents": 900}]',
'[{"active": true, "cents": true}]',
]
for payload in payloads:
try:
print(active_total(payload))
except ValueError as error:
print(f"invalid: {error}")1250
invalid: order 0: cents must be an integer虽然isinstance(True, int)的结果为真,但这里的分值字段不应接受布尔值。检查顺序先确认类型,再比较数值范围,避免在错误类型上执行<。
函数不保留输入记录,也不修改解析出的列表。每次调用都从局部的total = 0开始,因此不同请求之间没有隐藏的共享状态。
陷阱
名称与对象的细节
赋值顺序
普通赋值先完整计算右侧表达式,再处理左侧目标。链式赋值把同一个结果对象依次绑定到多个目标;解包赋值先取得右侧各项,再按目标结构绑定。右侧表达式只求值一次,这也是交换两个名称能安全写成left, right = right, left的原因。
目标不一定是名称。account.balance = amount会设置属性,items[index] = value会写入下标位置;这些动作由目标对象的类型实现。把所有赋值都理解成“创建变量”会漏掉属性或容器的原地状态变化。
增强赋值会读取左侧目标,尝试相应的原地操作,再把结果写回目标。可变对象可能返回自身,不可变对象通常返回新对象。审查+=、|=等语句时,需要查看操作数类型与别名,不能只看运算符。
相等、标识与类型
==调用类型定义的相等规则,is不可重载,只比较对象标识。两个独立列表可以值相等而标识不同;两个名称也可以既值相等又标识相同,因为它们引用同一列表。这两个问题没有谁比谁“更严格”。
id()返回在对象生命周期内唯一且稳定的整数,但具体数值没有业务含义。对象销毁后,后来的对象可以复用同一标识。CPython当前常把内存地址用作id(),应用代码不应把这一实现细节当成跨进程或持久化标识。
type()返回确切类型对象,isinstance()还会考虑直接、间接或注册的子类。大多数面向接口的代码应使用isinstance()或直接尝试所需操作。只有契约确实排除所有子类时,才使用type(value) is SomeType。
可变性沿引用传播
可变性属于类型,不属于名称。列表方法append()改变列表对象的值;所有引用该对象的名称、容器字段与函数参数之后都能观察到变化。重新绑定某个局部名称不会改变其他引用。
浅复制只新建最外层容器,里面的元素引用仍来自源容器。若两个列表都指向同一个嵌套字典,通过任一列表修改字典都能从另一列表看到。需要复制时,应沿实际修改路径判断哪些节点必须独立。
不可变对象不能原地改变自身的值,但名称仍可以重新绑定。字符串拼接与整数加法会产生新对象。元组的不可变性只固定长度与槽位引用;槽位指向的可变对象仍有自己的状态。
内置类型的边界
数值类型
Python整数具有任意精度,实际大小受可用内存等资源限制。浮点数通常由C的双精度类型实现,具体精度可查看sys.float_info。这两种类型解决不同问题,不能把float当成带小数点的精确整数。
bool是int的子类,因此True + True得到2,True == 1也为真。这是语言的数据模型,不表示任何整数都适合作为业务布尔值。解析API数据时,布尔字段与计数字段通常应分别验证。
不同内置数值类型参与二元算术时,会按数值规则提升较窄的操作数。整数与浮点数运算通常得到浮点数,实数与复数运算得到复数。这种有限的数值提升不适用于字符串与数字之间。
真值协议
对象默认视为真,除非其类型定义返回假的__bool__(),或在没有该方法时由__len__()返回零。真值测试可能执行用户定义代码,也可能抛出异常。对第三方对象写条件判断时,不应假设转换永远无副作用或必然成功。
not总是产生True或False,而and与or返回决定结果的操作数。表达式candidate and normalize(candidate)可能返回原始假值,也可能返回规范化结果,所以它的返回类型往往比表面更宽。需要布尔结果时使用bool(expression)或明确比较。
None是单例对象,常用来表示“没有提供”或“没有结果”。它和数值零、空字符串、空列表都不相等,但真值都为假。选择None前仍要写清楚它在API中的具体含义。
转换边界
转换分为表示解析与数值转换。int("101", 2)按二进制文本得到5;int(3.9)则把数值向零截断。两者都写成int(),却有不同的输入契约与失败方式。
str(value)生成适合阅读的文本表示,但不保证可逆。需要机器交换数据时,应使用JSON等明确格式与模式验证,不要依赖str()再猜测原类型。需要调试表示时,repr()的目的又不同。
浮点数转整数可能丢弃小数部分,超出浮点范围的整数转浮点数还可能抛出OverflowError。转换应发生在信息含义清楚的边界,转换后的值仍需接受范围与业务约束检查。
注解是契约说明
类型注解(type annotation) 为参数、返回值与变量提供机器可读的契约说明。Python运行时不会自动强制函数与变量注解;静态检查器可以在运行前报告不一致,框架也可能选择读取注解并实现自己的行为。
Python 3.14默认延迟求值注解表达式,但这项变化仍不会验证普通函数调用。读取注解的库需要遵循3.14的注解API,而不是假设__annotations__中的每个值都已立即求值。应用的运行时验证逻辑应与注解描述保持一致。
注解可以减少名称所代表值的歧义,却不会改变名称绑定模型。quantity: int = "3"在普通运行时仍会绑定字符串。把静态检查与边界测试放进开发流程,才能让注解中的契约持续可信。
延伸阅读
5个问题 · 2道输出预测题 · 1道找错题