Files

8.4 KiB
Raw Permalink Blame History

doc_type, full_text
doc_type full_text
short sources/07_Objects.md

07_Objects 总结

本文介绍 Python 的内部对象模型,重点说明赋值、引用、对象身份、浅拷贝与深拷贝、类型检查,以及“一切皆对象”的含义。核心思想是:Python 变量只是名字,值才是真正的对象;赋值不会复制对象,只会复制引用。相关主题可连接到 concepts/Python-对象模型、concepts/可变性与引用、concepts/Python-拷贝语义、一等对象。

赋值不是复制

Python 中许多操作本质上都是“赋值”或“存储引用”:

a = value
s[n] = value
s.append(value)
d['key'] = value

这些操作都不会复制被赋的值,而只是复制对象引用。也就是说,多个变量或容器元素可能指向同一个底层对象。

例如:

a = [1, 2, 3]
b = a
c = [a, b]

这里实际上只有一个列表对象 [1, 2, 3],但有多个引用指向它:a、b、c[0]、c[1]。如果修改列表:

a.append(999)

那么通过 a、b、c 看到的内容都会变化。这是 Python 中 concepts/可变性与引用 的典型陷阱。

重新赋值不会覆盖旧对象

重新赋值不会修改旧对象本身,而是让变量名绑定到另一个对象:

a = [1, 2, 3]
b = a
a = [4, 5, 6]

此时:

print(a)  # [4, 5, 6]
print(b)  # [1, 2, 3]

关键原则:变量是名字,不是内存位置。

这也是理解 concepts/Python-对象模型 的基础。

共享可变对象的风险

如果不了解引用共享,程序中很容易出现意外的数据污染:以为自己在修改“私有副本”,实际上却修改了其他代码也在使用的同一个对象。

这也是 Python 中 int、float、str 等基础类型设计为不可变对象的重要原因之一:不可变对象即使被多个引用共享,也不会因为原地修改而互相影响。

对象身份与 is

is 用于判断两个变量是否引用同一个对象:

a = [1, 2, 3]
b = a
a is b  # True

对象身份可以通过 id() 查看:

id(a)
id(b)

如果两个变量指向同一个对象,它们的 id() 相同。

但通常情况下,比较对象内容应使用 ==,而不是 is:

a = [1, 2, 3]
b = a
c = [1, 2, 3]

 a is b  # True
 a is c  # False
 a == c  # True

is 比较身份,== 比较值。这个区别是 concepts/Python-对象模型 中的重要概念。

浅拷贝

列表和字典可以创建拷贝,例如:

a = [2, 3, [100, 101], 4]
b = list(a)

此时 a 和 b 是两个不同的外层列表:

a is b  # False

但其中的内部对象仍然被共享:

a[2].append(102)
b[2]  # [100, 101, 102]
a[2] is b[2]  # True

这种只复制外层容器、不递归复制内部对象的行为称为浅拷贝。它属于 concepts/Python-拷贝语义 的核心内容。

深拷贝

如果需要复制对象以及它包含的所有嵌套对象,可以使用 copy.deepcopy():

import copy

a = [2, 3, [100, 101], 4]
b = copy.deepcopy(a)

a[2].append(102)
b[2]  # [100, 101]
a[2] is b[2]  # False

深拷贝适用于需要完全隔离嵌套可变结构的场景,但也可能带来额外开销和复杂性。

名字、值与类型

Python 中变量名本身没有类型,类型属于对象值:

a = 42
b = 'Hello World'

type(a)  # int
type(b)  # str

type() 可以查看对象类型。类型名通常也可以作为构造或转换函数使用,例如 int()、str()、float()。

类型检查

可以使用 isinstance() 判断对象是否属于某种类型:

if isinstance(a, list):
    print('a is a list')

也可以检查是否属于多个类型之一:

if isinstance(a, (list, tuple)):
    print('a is a list or tuple')

不过,文章提醒不要过度使用类型检查。过多类型判断会增加代码复杂度。通常只有在防止常见误用时才值得加入类型检查。

一切皆对象

Python 中数字、字符串、列表、函数、异常、类、实例、模块等都是对象。所有可以被命名的东西,都可以作为数据传递、放入容器、作为参数使用。

这体现了 Python 的 一等对象 特性。

例如:

import math
items = [abs, math, ValueError]

items[0](-45)       # abs(-45)
items[1].sqrt(2)    # math.sqrt(2)

甚至异常类也可以放在列表中并用于 except:

try:
    x = int('not a number')
except items[2]:
    print('Failed!')

这种能力很强大,但也需要谨慎使用。能这样写并不代表总应该这样写。

练习 2.24:一等数据与类型转换函数

练习展示了如何利用“一切皆对象”的特性,把类型转换函数放入列表:

types = [str, int, float]

读取 CSV 行后,原始数据都是字符串:

row = ['AA', '100', '32.20']

可以用对应的转换函数处理字段:

types[1](row[1])  # int('100') -> 100
types[2](row[2])  # float('32.20') -> 32.2

通过 zip() 将转换函数与字段配对:

list(zip(types, row))

得到类似:

[(str, 'AA'), (int, '100'), (float, '32.20')]

然后可统一转换:

converted = []
for func, val in zip(types, row):
    converted.append(func(val))

也可以写成列表推导式:

converted = [func(val) for func, val in zip(types, row)]

得到:

['AA', 100, 32.2]

这一部分连接了 concepts/函数作为对象、concepts/列表推导式 与 concepts/数据清洗与类型转换。

练习 2.25:构造字典

将列名与转换后的值配对,可以用 dict() 创建字典:

headers = ['name', 'shares', 'price']
converted = ['AA', 100, 32.2]

dict(zip(headers, converted))

结果:

{'name': 'AA', 'shares': 100, 'price': 32.2}

也可以使用字典推导式一步完成转换和建表:

{name: func(val) for name, func, val in zip(headers, types, row)}

这展示了如何把 CSV 行转换为结构化记录,是 concepts/数据清洗与类型转换 的实用模式。

练习 2.26:更大的应用图景

同样技巧可推广到其他面向列的数据文件。例如读取股票数据:

headers = ['name', 'price', 'date', 'time', 'change', 'open', 'high', 'low', 'volume']
row = ['AA', '39.48', '6/11/2007', '9:36am', '-0.18', '39.67', '39.69', '39.45', '181800']

定义每列对应的转换函数:

types = [str, float, str, str, float, float, float, float, int]

然后转换并构造记录:

converted = [func(val) for func, val in zip(types, row)]
record = dict(zip(headers, converted))

得到的 record 可以通过字段名访问:

record['name']
record['price']

文末还提出扩展问题:如何将 date 字段解析为 (6, 11, 2007) 这样的元组。这暗示可以自定义转换函数,而不仅限于内置类型函数。

核心要点

  • 赋值不会复制对象,只会复制引用。
  • 变量是名字,不是内存位置。
  • 修改可变对象会影响所有共享该对象的引用。
  • is 比较对象身份,== 比较对象值。
  • 浅拷贝只复制外层容器,内部对象仍共享。
  • 深拷贝会递归复制嵌套对象。
  • 类型属于值,不属于变量名。
  • isinstance() 可用于类型检查,但不应滥用。
  • Python 中一切皆对象,函数、模块、异常也能作为普通数据使用。
  • 一等对象特性可用于构建通用的数据转换流程。

相关概念