我不确定下面代码的 Python 对象模型背后发生了什么。
您可以从此link 下载ctabus.csv 文件的数据。
import csv
def read_as_dicts(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows)
for row in rows:
route = row[0]
date = row[1]
daytype = row[2]
rides = int(row[3])
records.append({
'route': route,
'date': date,
'daytype': daytype,
'rides': rides})
return records
# read data from csv
rows = read_as_dicts('ctabus.csv')
print(len(rows)) #736461
# record route ids (object ids)
route_ids = set()
for row in rows:
route_ids.add(id(row['route']))
print(len(route_ids)) #690072
# unique_routes
unique_routes = set()
for row in rows:
unique_routes.add(row['route'])
print(len(unique_routes)) #185
当我调用 print(len(route_ids)) 时,它会打印 "690072"。为什么 Python 最终要创建这么多对象?
我希望此计数为 185 或 736461。185 因为当我计算集合中的唯一路由时,该集合的长度为 185。736461 因为这是 csv 文件中的记录总数。
这个奇怪的数字“690072”是什么?
我想了解为什么要使用这种部分缓存?为什么 python 无法执行如下所示的完整缓存。
import csv
route_cache = {}
#some hack to cache
def cached_route(routename):
if routename not in route_cache:
route_cache[routename] = routename
return route_cache[routename]
def read_as_dicts(filename):
records = []
with open(filename) as f:
rows = csv.reader(f)
headers = next(rows)
for row in rows:
row[0] = cached_route(row[0]) #cache trick
route = row[0]
date = row[1]
daytype = row[2]
rides = int(row[3])
records.append({
'route': route,
'date': date,
'daytype': daytype,
'rides': rides})
return records
# read data from csv
rows = read_as_dicts('ctabus.csv')
print(len(rows)) #736461
# unique_routes
unique_routes = set()
for row in rows:
unique_routes.add(row['route'])
print(len(unique_routes)) #185
# record route ids (object ids)
route_ids = set()
for row in rows:
route_ids.add(id(row['route']))
print(len(route_ids)) #185
请您参考如下方法:
文件中的典型记录如下所示:
rows[0]
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
这意味着您的大部分不可变对象(immutable对象)都是字符串,只有 'rides' 值是整数。
对于小整数 (-5...255),Python3 保留 an integer pool - 所以这些小整数感觉像是被缓存了(只要使用 PyLong_FromLong 和 Co.)。
字符串的规则更为复杂——正如@timgeb 所指出的那样,它们是 interned 的。有 a greate article about interning ,即使它是关于 Python2.7 的——但从那以后变化不大。简而言之,最重要的规则是:
- 所有长度为
0和1的字符串都被保留。 - 具有多个字符的字符串如果由可用于标识符的字符组成并在编译时直接或通过peephole optimization 创建,则将被保留。/constant folding (但在第二种情况下,只有结果不超过 20 个字符 ( 4096 since Python 3.7 )。
以上所有都是实现细节,但考虑到它们,我们得到上面的 row[0] 的以下内容:
'route'、'date'、'daytype'、'rides'都被保留了,因为它们是在函数read_as_dicts的编译时创建的,并且没有“奇怪”的字符。'3'和'W'被驻留,因为它们的长度仅为1。01/01/2001未被驻留,因为它比1长,在运行时创建并且无论如何都不符合条件,因为它具有字符/在里面。7354不是来自小整数池,因为太大了。但其他条目可能来自此池。
这是对当前行为的解释,只有一些对象被“缓存”。
但是为什么 Python 不缓存所有创建的字符串/整数?
让我们从整数开始。如果已经创建了一个整数,为了能够快速查找(比 O(n) 快得多),必须保留一个额外的查找数据结构,这需要额外的内存。然而,整数太多,再次命中一个已经存在的整数的概率不是很高,因此查找数据结构的内存开销在大多数情况下不会被偿还。
因为字符串需要更多内存,所以查找数据结构的相对(内存)成本并没有那么高。但是实习一个 1000 个字符的字符串没有任何意义,因为随机创建的字符串具有完全相同字符的概率几乎为 0!
另一方面,如果例如使用哈希字典作为查找结构,则哈希的计算将花费 O(n) (n-字符数),这可能不会为大字符串带来返回。
因此,Python 做了一个权衡,它在大多数情况下工作得很好——但在某些特殊情况下它并不完美。然而,对于那些特殊情况,您可以使用 sys.intern() 优化每手牌.
注意:拥有相同的 id 并不意味着是同一个对象,如果两个对象的生存时间不重叠,那么你在这个问题上的推理并不是完全防水的 - 但这无关紧要在这种特殊情况下。
