Files

353 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
doc_type: short
full_text: sources/07_Objects.md
---
# 07_Objects 总结
本文介绍 Python 的内部对象模型,重点说明赋值、引用、对象身份、浅拷贝与深拷贝、类型检查,以及“一切皆对象”的含义。核心思想是:Python 变量只是名字,值才是真正的对象;赋值不会复制对象,只会复制引用。相关主题可连接到 [[concepts/Python-对象模型]]、[[concepts/可变性与引用]]、[[concepts/Python-拷贝语义]]、一等对象。
## 赋值不是复制
Python 中许多操作本质上都是“赋值”或“存储引用”:
```python
a = value
s[n] = value
s.append(value)
d['key'] = value
```
这些操作都不会复制被赋的值,而只是复制对象引用。也就是说,多个变量或容器元素可能指向同一个底层对象。
例如:
```python
a = [1, 2, 3]
b = a
c = [a, b]
```
这里实际上只有一个列表对象 `[1, 2, 3]`,但有多个引用指向它:`a`、`b`、`c[0]`、`c[1]`。如果修改列表:
```python
a.append(999)
```
那么通过 `a`、`b`、`c` 看到的内容都会变化。这是 Python 中 [[concepts/可变性与引用]] 的典型陷阱。
## 重新赋值不会覆盖旧对象
重新赋值不会修改旧对象本身,而是让变量名绑定到另一个对象:
```python
a = [1, 2, 3]
b = a
a = [4, 5, 6]
```
此时:
```python
print(a) # [4, 5, 6]
print(b) # [1, 2, 3]
```
关键原则:**变量是名字,不是内存位置。**
这也是理解 [[concepts/Python-对象模型]] 的基础。
## 共享可变对象的风险
如果不了解引用共享,程序中很容易出现意外的数据污染:以为自己在修改“私有副本”,实际上却修改了其他代码也在使用的同一个对象。
这也是 Python 中 `int`、`float`、`str` 等基础类型设计为不可变对象的重要原因之一:不可变对象即使被多个引用共享,也不会因为原地修改而互相影响。
## 对象身份与 `is`
`is` 用于判断两个变量是否引用同一个对象:
```python
a = [1, 2, 3]
b = a
a is b # True
```
对象身份可以通过 `id()` 查看:
```python
id(a)
id(b)
```
如果两个变量指向同一个对象,它们的 `id()` 相同。
但通常情况下,比较对象内容应使用 `==`,而不是 `is`:
```python
a = [1, 2, 3]
b = a
c = [1, 2, 3]
a is b # True
a is c # False
a == c # True
```
`is` 比较身份,`==` 比较值。这个区别是 [[concepts/Python-对象模型]] 中的重要概念。
## 浅拷贝
列表和字典可以创建拷贝,例如:
```python
a = [2, 3, [100, 101], 4]
b = list(a)
```
此时 `a` 和 `b` 是两个不同的外层列表:
```python
a is b # False
```
但其中的内部对象仍然被共享:
```python
a[2].append(102)
b[2] # [100, 101, 102]
a[2] is b[2] # True
```
这种只复制外层容器、不递归复制内部对象的行为称为浅拷贝。它属于 [[concepts/Python-拷贝语义]] 的核心内容。
## 深拷贝
如果需要复制对象以及它包含的所有嵌套对象,可以使用 `copy.deepcopy()`:
```python
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 中变量名本身没有类型,类型属于对象值:
```python
a = 42
b = 'Hello World'
type(a) # int
type(b) # str
```
`type()` 可以查看对象类型。类型名通常也可以作为构造或转换函数使用,例如 `int()`、`str()`、`float()`。
## 类型检查
可以使用 `isinstance()` 判断对象是否属于某种类型:
```python
if isinstance(a, list):
print('a is a list')
```
也可以检查是否属于多个类型之一:
```python
if isinstance(a, (list, tuple)):
print('a is a list or tuple')
```
不过,文章提醒不要过度使用类型检查。过多类型判断会增加代码复杂度。通常只有在防止常见误用时才值得加入类型检查。
## 一切皆对象
Python 中数字、字符串、列表、函数、异常、类、实例、模块等都是对象。所有可以被命名的东西,都可以作为数据传递、放入容器、作为参数使用。
这体现了 Python 的 一等对象 特性。
例如:
```python
import math
items = [abs, math, ValueError]
items[0](-45) # abs(-45)
items[1].sqrt(2) # math.sqrt(2)
```
甚至异常类也可以放在列表中并用于 `except`:
```python
try:
x = int('not a number')
except items[2]:
print('Failed!')
```
这种能力很强大,但也需要谨慎使用。能这样写并不代表总应该这样写。
## 练习 2.24:一等数据与类型转换函数
练习展示了如何利用“一切皆对象”的特性,把类型转换函数放入列表:
```python
types = [str, int, float]
```
读取 CSV 行后,原始数据都是字符串:
```python
row = ['AA', '100', '32.20']
```
可以用对应的转换函数处理字段:
```python
types[1](row[1]) # int('100') -> 100
types[2](row[2]) # float('32.20') -> 32.2
```
通过 `zip()` 将转换函数与字段配对:
```python
list(zip(types, row))
```
得到类似:
```python
[(str, 'AA'), (int, '100'), (float, '32.20')]
```
然后可统一转换:
```python
converted = []
for func, val in zip(types, row):
converted.append(func(val))
```
也可以写成列表推导式:
```python
converted = [func(val) for func, val in zip(types, row)]
```
得到:
```python
['AA', 100, 32.2]
```
这一部分连接了 [[concepts/函数作为对象]]、[[concepts/列表推导式]] 与 [[concepts/数据清洗与类型转换]]。
## 练习 2.25:构造字典
将列名与转换后的值配对,可以用 `dict()` 创建字典:
```python
headers = ['name', 'shares', 'price']
converted = ['AA', 100, 32.2]
dict(zip(headers, converted))
```
结果:
```python
{'name': 'AA', 'shares': 100, 'price': 32.2}
```
也可以使用字典推导式一步完成转换和建表:
```python
{name: func(val) for name, func, val in zip(headers, types, row)}
```
这展示了如何把 CSV 行转换为结构化记录,是 [[concepts/数据清洗与类型转换]] 的实用模式。
## 练习 2.26:更大的应用图景
同样技巧可推广到其他面向列的数据文件。例如读取股票数据:
```python
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']
```
定义每列对应的转换函数:
```python
types = [str, float, str, str, float, float, float, float, int]
```
然后转换并构造记录:
```python
converted = [func(val) for func, val in zip(types, row)]
record = dict(zip(headers, converted))
```
得到的 `record` 可以通过字段名访问:
```python
record['name']
record['price']
```
文末还提出扩展问题:如何将 `date` 字段解析为 `(6, 11, 2007)` 这样的元组。这暗示可以自定义转换函数,而不仅限于内置类型函数。
## 核心要点
- 赋值不会复制对象,只会复制引用。
- 变量是名字,不是内存位置。
- 修改可变对象会影响所有共享该对象的引用。
- `is` 比较对象身份,`==` 比较对象值。
- 浅拷贝只复制外层容器,内部对象仍共享。
- 深拷贝会递归复制嵌套对象。
- 类型属于值,不属于变量名。
- `isinstance()` 可用于类型检查,但不应滥用。
- Python 中一切皆对象,函数、模块、异常也能作为普通数据使用。
- 一等对象特性可用于构建通用的数据转换流程。
## 相关概念
- [[concepts/Python-对象模型]]
- [[concepts/可变性与引用]]
- [[concepts/Python-拷贝语义]]
- 一等对象
- [[concepts/列表推导式]]
- [[concepts/数据清洗与类型转换]]
## Related Concepts
- [[concepts/Python-拷贝语义]]
- [[concepts/可变性与引用]]
- [[concepts/列表与序列]]
- [[concepts/Python-运算符与表达式]]
- [[concepts/对象身份与相等性]]
- [[concepts/浅拷贝与深拷贝]]
- [[concepts/Python-对象模型]]
- [[concepts/Python-可变对象]]
- [[concepts/Python-不可变对象]]
- [[concepts/变量与数据类型]]
- [[concepts/函数作为对象]]
- [[concepts/CSV-数据处理]]
- [[concepts/字典与数据建模]]
- [[concepts/Python-容器]]
- [[concepts/元组与解包]]
- [[concepts/鸭子类型]]
- [[concepts/文件读写]]
- [[concepts/模块与-import]]
- [[concepts/浮点数精度]]