简介
本节将详细介绍 Python 的内部对象模型,并讨论一些与内存管理、复制和类型检查相关的问题。
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
能力越大,责任越大。仅仅因为你能这么做,并不意味着你就应该这么做。
在这组练习中,我们将探讨一等公民对象所带来的一些强大功能。
在文件 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
变量是类型转换函数之一(例如,str
、int
等),val
变量是像 'AA'
、'100'
这样的值。表达式 func(val)
正在转换一个值(有点像类型转换)。
上述代码可以压缩成一个单行的列表推导式。
>>> converted = [func(val) for func, val in zip(types, row)]
>>> converted
['AA', 100, 32.2]
>>>
还记得如果有键名和值的序列,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}
>>>
运用本练习中的技巧,你可以编写语句,轻松地将几乎任何按列组织的数据文件中的字段转换为 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 中练习更多实验来提升你的技能。