Python 对象模型内部机制

PythonPythonBeginner
立即练习

This tutorial is from open-source community. Access the source code

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

本节将详细介绍 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.append(999)
>>> a
[1,2,3,999]
>>> b
[1,2,3,999]
>>> c
[[1,2,3,999], [1,2,3,999]]
>>>

注意原始列表的更改是如何在其他所有地方都体现出来的(哎呀!)。这是因为从未进行过复制操作。所有引用都指向同一个对象。

重新赋值

重新赋值一个值 绝不会 覆盖前一个值所使用的内存。

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

print(a)      ## [4, 5, 6]
print(b)      ## [1, 2, 3]    保留原始值

记住:变量是名称,而不是内存位置。

一些风险

如果你不了解这种共享机制,迟早会给自己带来麻烦。常见的情况是,你以为自己在修改一份私有数据副本,但实际上却意外地破坏了程序其他部分的数据。

注释:这就是原始数据类型(如 int、float、string)是不可变(只读)类型的原因之一。

身份标识与引用

使用 is 运算符来检查两个值是否为同一个对象。

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

is 比较的是对象的身份标识(一个整数)。可以使用 id() 函数获取对象的身份标识。

>>> id(a)
3588944
>>> id(b)
3588944
>>>

注意:检查对象时,几乎总是使用 == 更好。is 的行为常常出人意料:

>>> a = [1,2,3]
>>> b = a
>>> c = [1,2,3]
>>> a is b
True
>>> a is c
False
>>> a == c
True
>>>

浅拷贝

列表和字典有用于复制的方法。

>>> a = [2,3,[100,101],4]
>>> b = list(a) ## 进行复制
>>> a is b
False

这是一个新的列表,但列表中的元素是共享的。

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

例如,内部列表 [100, 101, 102] 是共享的。这就是所谓的浅拷贝。下面是一张示意图。

浅拷贝

深拷贝

有时你需要复制一个对象以及它所包含的所有对象。你可以使用 copy 模块来实现这一点:

>>> a = [2,3,[100,101],4]
>>> import copy
>>> b = copy.deepcopy(a)
>>> a[2].append(102)
>>> b[2]
[100,101]
>>> a[2] is b[2]
False
>>>

名称、值、类型

变量名没有 类型,它仅仅是一个名称。然而,值 确实 有底层类型。

>>> a = 42
>>> b = 'Hello World'
>>> type(a)
<type 'int'>
>>> type(b)
<type 'str'>

type() 函数会告诉你值的类型。类型名通常用作一个函数,用于创建值或将值转换为该类型。

类型检查

如何判断一个对象是否为特定类型。

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

检查对象是否属于多种可能类型之一。

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

*注意:不要过度进行类型检查。这可能会导致代码复杂度大幅增加。通常,只有在这样做能够避免使用你代码的人犯常见错误时,你才应该进行类型检查。

一切皆对象

数字、字符串、列表、函数、异常、类、实例等都是对象。这意味着所有可以被命名的对象都能作为数据传递、放入容器等,没有任何限制。不存在 特殊 类型的对象。有时人们会说所有对象都是“一等公民(first-class)”。

一个简单的例子:

>>> import math
>>> items = [abs, math, ValueError ]
>>> items
[<built-in function abs>,
  <module 'math' (builtin)>,
  <type 'exceptions.ValueError'>]
>>> items[0](-45)
45
>>> items[1].sqrt(2)
1.4142135623730951
>>> try:
        x = int('not a number')
    except items[2]:
        print('Failed!')
Failed!
>>>

在这里,items 是一个包含函数、模块和异常的列表。你可以直接使用列表中的元素来替代原来的名称:

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

能力越大,责任越大。仅仅因为你能这么做,并不意味着你就应该这么做。

在这组练习中,我们将探讨一等公民对象所带来的一些强大功能。

练习 2.24:一等公民数据

在文件 portfolio.csv 中,我们读取的数据按列组织,如下所示:

name,shares,price
"AA",100,32.20
"IBM",50,91.10
...

在之前的代码中,我们使用 csv 模块读取文件,但仍需手动进行类型转换。例如:

for row in rows:
    name   = row[0]
    shares = int(row[1])
    price  = float(row[2])

这种转换也可以使用一些列表基本操作以更巧妙的方式完成。

创建一个 Python 列表,其中包含用于将每列转换为适当类型的转换函数名称:

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

你之所以能够创建这个列表,是因为 Python 中的一切都是 一等公民。所以,如果你想创建一个函数列表,没问题。你创建的列表中的元素是用于将值 x 转换为给定类型的函数(例如,str(x)int(x)float(x))。

现在,从上述文件中读取一行数据:

>>> import csv
>>> f = open('portfolio.csv')
>>> rows = csv.reader(f)
>>> headers = next(rows)
>>> row = next(rows)
>>> row
['AA', '100', '32.20']
>>>

如前所述,这一行数据无法用于计算,因为类型不对。例如:

>>> row[1] * row[2]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't multiply sequence by non-int of type 'str'
>>>

不过,也许可以将数据与你在 types 中指定的类型进行配对。例如:

>>> types[1]
<type 'int'>
>>> row[1]
'100'
>>>

尝试转换其中一个值:

>>> types[1](row[1])     ## 等同于 int(row[1])
100
>>>

尝试转换另一个值:

>>> types[2](row[2])     ## 等同于 float(row[2])
32.2
>>>

尝试使用转换后的值进行计算:

>>> types[1](row[1])*types[2](row[2])
3220.0000000000005
>>>

将列类型与字段进行 zip 操作并查看结果:

>>> r = list(zip(types, row))
>>> r
[(<type 'str'>, 'AA'), (<type 'int'>, '100'), (<type 'float'>,'32.20')]
>>>

你会注意到,这将类型转换与值进行了配对。例如,int 与值 '100' 配对。

如果你想依次对所有值进行转换,这个 zip 后的列表会很有用。试试这个:

>>> converted = []
>>> for func, val in zip(types, row):
          converted.append(func(val))
...
>>> converted
['AA', 100, 32.2]
>>> converted[1] * converted[2]
3220.0000000000005
>>>

确保你理解上述代码中发生了什么。在循环中,func 变量是类型转换函数之一(例如,strint 等),val 变量是像 'AA''100' 这样的值。表达式 func(val) 正在转换一个值(有点像类型转换)。

上述代码可以压缩成一个单行的列表推导式。

>>> converted = [func(val) for func, val in zip(types, row)]
>>> converted
['AA', 100, 32.2]
>>>

练习 2.25:创建字典

还记得如果有键名和值的序列,dict() 函数可以轻松创建字典吗?让我们根据列标题来创建一个字典:

>>> headers
['name', 'shares', 'price']
>>> converted
['AA', 100, 32.2]
>>> dict(zip(headers, converted))
{'price': 32.2, 'name': 'AA', 'shares': 100}
>>>

当然,如果你精通列表推导式,你可以使用字典推导式一步完成整个转换:

>>> { name: func(val) for name, func, val in zip(headers, types, row) }
{'price': 32.2, 'name': 'AA', 'shares': 100}
>>>

练习 2.26:整体概览

运用本练习中的技巧,你可以编写语句,轻松地将几乎任何按列组织的数据文件中的字段转换为 Python 字典。

为了说明这一点,假设你从另一个数据文件中读取数据,如下所示:

>>> f = open('dowstocks.csv')
>>> rows = csv.reader(f)
>>> headers = next(rows)
>>> row = next(rows)
>>> 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
{'volume': 181800, 'name': 'AA', 'price': 39.48, 'high': 39.69,
'low': 39.45, 'time': '9:36am', 'date': '6/11/2007', 'open': 39.67,
'change': -0.18}
>>> record['name']
'AA'
>>> record['price']
39.48
>>>

额外挑战:你将如何修改这个示例,以额外地将 date 条目解析为类似 (6, 11, 2007) 这样的元组?

花些时间思考你在这个练习中所做的事情。我们稍后会再次探讨这些概念。

总结

恭喜你!你已经完成了“对象”实验。你可以在 LabEx 中练习更多实验来提升你的技能。