跳到主要内容

对象、名称、赋值与基本类型

本节目标

从对象模型与名称绑定出发,查询 Python 基本内置类型、可变性以及相等与身份边界。

Python 程序处理对象,源码中的名称负责引用它们。赋值并非把值装入固定盒子,而是建立名称到对象的绑定;重新绑定名称与修改对象也因此是两种不同操作。本章以这套模型解释数字、文本和字节等基本类型。尚未运行过脚本时,可先从入门导览、解释器与第一个程序开始。

对象的身份、类型与值

身份(identity)、类型(type)和值(value)是观察对象的三个维度。身份回答两个引用是否指向同一对象;类型决定对象支持的操作及其含义;值则承载程序关心的内容或状态。对象创建后,身份和类型保持不变,值能否变化则由类型的可变性决定。

type(obj) 查询类型;id(obj) 返回一个在该对象生命周期内唯一且不变的整数,可用于观察身份,却不适合作为业务数据。CPython 可能以对象的内存地址实现 id(),也会用引用计数参与生命周期管理。这些均属实现细节,Python 语言并不要求其他实现采取相同做法,可移植逻辑不能建立在它们之上。

对象可以直接保存数据,也可以包含对其他对象的引用。例如列表的值由一组对象引用组成。修改列表中的某个元素会改变列表的值,但不会把列表变成另一个类型,也不会改变它的身份。

名称绑定、赋值与删除

执行 number = 314 时,Python 先得到整数对象,再让名称 number 引用它。随后的 number = 315 重新建立绑定:名称改指另一个对象,原来的 314 对象并未在内部变成 315。同一对象可以同时被多个名称引用;名称本身也没有永久固定的类型,类型属于它当前引用的对象。

del number 只删除当前作用域中的名称绑定,不保证对象立即销毁;对象不再可达后,实现才可以回收它。链式赋值 left = right = [] 让两个名称共享同一个新列表。交换赋值 left, right = right, left 则先计算右侧对象,再建立两项新绑定。

代表脚本把共享对象、重新绑定和文本到 UTF-8 字节的转换放在一次运行中。输出只报告可移植的关系和值,不显示地址,也不依赖对象缓存或字符串驻留。

object_binding_report.py
number = 314
original = ["Python"]
shared = original
shared.append("3.14")
rebound_name = shared
rebound_name = ["local"]
text = "编程"
encoded = text.encode("utf-8")

print(f"number-type={type(number).__name__}")
print(f"same-list={original is shared}")
print(f"shared-values={shared}")
print(f"rebound-name={rebound_name}")
print(f"original-values={original}")
print(f"text-codepoints={len(text)}")
print(f"bytes-length={len(encoded)}")
number-type=int
same-list=True
shared-values=['Python', '3.14']
rebound-name=['local']
original-values=['Python', '3.14']
text-codepoints=2
bytes-length=6

shared = original 没有复制列表;两个名称起初引用同一个列表。rebound_name = ["local"] 只改变 rebound_name 的绑定,所以 originalshared 仍能看到之前追加的内容。

可变对象与不可变对象

可变对象能在身份不变的前提下改变值。常见例子包括 listbytearrayappend() 修改原列表,bytearray 的元素也可直接替换。两个名称若引用同一可变对象,从任一名称发起的修改都能通过另一名称观察到。这是别名共享的结果,并非两个变量之间存在自动同步。

intfloatcomplexboolstrbytes 以及 None 所属类型都是不可变的:对象创建后不能改变自身的值。text += "!" 这类看似修改的操作会产生结果对象,再让名称重新绑定。这里的限制落在对象自身,既不阻止名称改指其他对象,也不递归保证该对象涉及的其他对象一律不可变。

判断代码效果时,可以依次问:操作针对名称还是对象?对象类型是否提供原地修改?还有哪些名称引用这个对象?容器复制、嵌套共享等规则将在下一章进一步展开。

整数、浮点数与复数

int 表示整数,例如 0-7314。Python 整数的精度不由固定的 32 位或 64 位范围限制,实际大小受可用内存等资源约束。// 是向下取整除法,% 求余;普通 / 对整数操作数也产生浮点结果。

float 表示浮点数,例如 3.141e-3。许多十进制小数无法用二进制浮点精确表示,计算结果因此可能带有微小舍入误差。屏幕显示的短表示不能证明每一步计算都精确;比较近似测量值时,应按问题尺度选择容差。

complex 表示复数,字面量的虚部使用 j,例如 2 + 3j。实部和虚部可通过 .real.imag 查询;复数支持算术和相等比较,但没有自然的大小顺序,因此不能用 < 排序两个复数。混合数值运算会按规则转换到能容纳操作数的共同数值类型,但显式写出意图通常更容易维护。

布尔值、None 与真值

bool 只有 TrueFalse 两个值。它是 int 的子类型,因此 True + True 的结果是 2,但业务代码应优先表达明确的布尔意图,不要把真假值当作普通计数技巧使用。比较和成员判断等操作通常产生布尔值,bool(value) 可按真值规则得到 TrueFalse

条件判断会读取对象的真值,不要求对象本身属于 boolNoneFalse、各种数值零、空字符串和空容器通常为假,其余多数对象为真;自定义类型还可定义自己的真值行为。“为空”“为零”和“缺失”虽都可能使条件失败,业务含义却未必相同,此时需要明确比较。

None 表示“没有值”或“尚未提供”等约定含义,它是单例。检查这个哨兵应写 value is Nonevalue is not None,因为这里要判断的正是同一个单例对象;不要用 is 代替普通值的相等比较。

字符串、Unicode 与常用操作

str 保存 Unicode 文本,而不是按某种固定编码得到的字节数组。单引号和双引号创建的字面量类型相同,三引号允许跨行;反斜杠引入转义序列,前缀 r 能减少许多转义处理,但得到的仍是 str

字符串是不可变序列,支持索引、切片、拼接、成员测试和迭代。len(text) 计算字符串中的 Unicode 码位数,不保证等于用户眼中的字符数,也不等于编码后的字节数。脚本中的 "编程" 含两个码位,所以长度是 2;它编码为 UTF-8 后占六个字节。

text.lower()text.replace(old, new)separator.join(parts) 等方法返回字符串,不会原地修改原字符串。组合大量片段时通常收集片段后使用 join(),并在输入边界明确文本采用的编码。

bytes、bytearray 与文本边界

二进制数据可分别用不可变的 bytes 或可变的 bytearray 保存,两者的元素都是 0255 的整数。文件内容、网络数据和协议字段常使用这些类型;缺少编码约定时,字节序列不能被当作文本解释。

text.encode("utf-8") 按 UTF-8 把 str 转换为 bytesdata.decode("utf-8") 做反向转换。UTF-8 是这次边界转换选择的编码,不是每个 Python 字符串的内部表示。编码和实际数据不匹配时会出现错误或错误文本,因此编码名称属于输入输出协议的一部分。

需要就地编辑字节时可用 bytearray(data),完成后用 bytes(buffer) 得到不可变结果。字节序列的索引返回整数,而切片返回同类字节序列;这与字符串索引返回长度为一的 str 不同。

相等比较与身份比较

left == right 比较相等性,含义由对象类型定义,通常用于判断两个对象表示的值是否相等。left is right 比较身份,只在两个表达式引用同一个对象时为真。两个内容相等的独立列表可以满足 ==,但不满足 is;两个名称引用同一列表时两者都可为真。

数字、字符串、字节和容器通常按值使用 ==!= 比较;value is None 这类单例哨兵检查才是身份比较的典型用途。一次运行中的 is 结果不能证明值语义,id() 的整数也不能替代对象本身充当长期标识。

基本对象与类型的边界检查

  • 把名称想成装值的固定盒子:赋值建立或改变绑定;只有可变对象上的修改操作才改变对象本身。
  • 把别名当副本:second = first 不复制可变对象。通过任一别名修改它,其他别名都会观察到变化。
  • is 比较数字或文本:一些实现会缓存小整数或驻留某些字符串,但是否共享对象不是值语义保证。值相等用 ==,单例 None 才用 is None
  • 假设浮点十进制运算精确:先识别二进制浮点的表示误差,再按业务尺度选择合理的比较策略。
  • 混淆字符数与字节数:len(str)len(str.encode(...)) 回答不同问题;编码只发生在明确的文本/字节边界。
  • 误以为不可变对象会被原地更新:字符串和数字操作产生结果对象,必须保存返回值或重新绑定名称。
  • 用真值判断混在一起的状态:若空字符串、零和 None 的业务含义不同,就分别明确判断,不要只写一个含糊的条件。

继续阅读内置容器、切片、解包与复制语义,把本章的身份、可变性与别名模型应用到列表、元组、字典和集合。