353 lines
8.4 KiB
Markdown
353 lines
8.4 KiB
Markdown
---
|
||
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/浮点数精度]]
|