Python - 为什么不总是缓存所有不可变对象?

Lok*_*wal 2 python string caching python-internals

我不确定下面代码的 Python 对象模型在幕后发生了什么。

您可以从此链接下载 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
Run Code Online (Sandbox Code Playgroud)

当我称它为print(len(route_ids))prints 时"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
Run Code Online (Sandbox Code Playgroud)

ead*_*ead 5

文件中的典型记录如下所示:

rows[0]
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
Run Code Online (Sandbox Code Playgroud)

这意味着大多数不可变对象都是字符串,只有'rides'-value 是整数。

对于小整数 ( -5...255),Python3 保留了一个整数池——所以这些小整数感觉就像被缓存了一样(只要PyLong_FromLong使用 和 Co.)。

字符串的规则更复杂 - 正如@timgeb 所指出的那样,它们是实习的。有一篇关于实习的伟大文章,即使它是关于 Python2.7 - 但从那时起并没有太大变化。简而言之,最重要的规则是:

  1. 所有长度0和长度的字符串1都被实习。
  2. 如果字符串由可用于标识符的字符组成并在编译时直接或通过窥孔优化/常量折叠创建(但在第二种情况下仅当结果不超过 20 个字符时) (自 Python 3.7 以来为 4096)。

以上都是实现细节,但考虑到它们,我们得到以下内容row[0]

  1. 'route', 'date', 'daytype', 'rides'都是实习的,因为它们是在函数的编译时创建的,read_as_dicts并且没有“奇怪”的字符。
  2. '3'并被'W'拘留,因为它们的长度仅为1.
  3. 01/01/2001没有被实习,因为它比 长1,在运行时创建并且无论如何都不符合条件,因为它里面有字符/
  4. 7354不是来自小整数池,因为太大了。但其他条目可能来自此池。

这是对当前行为的解释,只有一些对象被“缓存”。

但是为什么 Python 不缓存所有创建的字符串/整数?

让我们从整数开始。如果已经创建了整数(比 快得多O(n)),为了能够快速查找,必须保留一个额外的查找数据结构,这需要额外的内存。但是,由于整数太多,再次命中一个已经存在的整数的概率不是很高,因此在大多数情况下不会偿还查找数据结构的内存开销。

因为字符串需要更多内存,查找数据结构的相对(内存)成本并不高。但是实习一个 1000 个字符的字符串没有任何意义,因为随机创建的字符串具有相同字符的概率几乎是0

另一方面,例如,如果使用散列字典作为查找结构,则散列的计算将花费O(n)( n-number of characters),这可能不会为大字符串带来回报。

因此,Python 进行了权衡,在大多数情况下都可以很好地工作 - 但在某些特殊情况下它不可能是完美的。然而,对于那些特殊场景,您可以使用sys.intern().


注意:如果两个对象的生存时间不重叠,则具有相同的 id 并不意味着是同一个对象 - 所以你在问题中的推理并不是完全防水 - 但这在这个特殊的情况下无关紧要案件。