【问题标题】:Python - Why not all immutable objects are always cached?Python - 为什么不总是缓存所有不可变对象?
【发布时间】:2019-04-27 06:17:53
【问题描述】:

我不确定下面代码的 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

【问题讨论】:

  • id 返回对象的内存位置。循环中可能会回收一些内存并重用地址,因此您会得到一个较小的数字。你想达到什么目的?
  • 我有太多待修改的内容,但我可以建议将此问题的标题更改为其他人更有可能找到的内容吗?也许,“Python - 比对象少唯一的 ID”,或类似的东西。
  • 关于你的编辑:所以最后你问为什么不总是缓存所有不可变对象?
  • @timgeb 是的。我的意思是缓存短比特而不是大比特背后的思考过程。
  • 这只是一个猜测。我添加了python-internals标签,也许它吸引了可以权威回答的人。

标签: python string caching python-internals


【解决方案1】:

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

rows[0]
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}

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

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

字符串的规则更复杂 - 正如@timgeb 指出的那样,它们是被实习的。有a greate article about interning,即使它是关于 Python2.7 的——但从那时起并没有太大变化。简而言之,最重要的规则是:

  1. 所有长度为01 的字符串都被保留。
  2. 具有多个字符的字符串如果由可在标识符中使用的字符组成并且在编译时直接或通过peephole optimization/constant folding 创建(但在第二种情况下仅当结果为不超过 20 个字符 (4096 since Python 3.7)。

以上所有内容都是实现细节,但考虑到它们,我们会为上面的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-字符数),这可能不会支付大字符串关闭。

因此,Python 进行了权衡,这在大多数情况下都能很好地工作 - 但在某些特殊情况下并不完美。但是对于那些特殊情况,您可以使用sys.intern() 进行单手优化。


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

【讨论】:

  • 另外,我假设散列以查明字符串是否已被缓存会带来一些性能成本。
【解决方案2】:

rows 中有 736461 个元素。

因此,您将id(row['route']) 添加到集合route_ids 736461 次。

由于id 返回的任何东西都保证在同时存在的对象中是唯一的,我们预计route_ids 最终会得到 736461 个项目减去足够小的字符串数量为rows 中两行的两个'route' 键缓存。

事实证明,在您的具体情况下,该数字是 736461 - 690072 == 46389。

缓存小的不可变对象(字符串、整数)是您不应该依赖的实现细节 - 但这里有一个演示:

>>> s1 = 'test' # small string
>>> s2 = 'test'
>>> 
>>> s1 is s2 # id(s1) == id(s2)
True
>>> s1 = 'test'*100 # 'large' string
>>> s2 = 'test'*100
>>> 
>>> s1 is s2
False

最后,您的程序中可能存在语义错误。你想用 Python 对象的唯一 ids 做什么?

【讨论】:

  • 感谢您的回答。我只是想了解 python 如何在后台缓存内存。我已经相应地更新了问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-06
  • 2015-12-28
  • 1970-01-01
  • 2016-04-27
  • 2012-09-18
  • 1970-01-01
相关资源
最近更新 更多