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

时间:2018-11-25 11:23:27

标签: python string caching python-internals

对于下面代码的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

2 个答案:

答案 0 :(得分:4)

rows中有736461个元素。

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

由于保证id返回的结果在同时存在的对象中都是唯一的,因此我们希望route_ids最终得到736461个项目,减去所包含的字符串数量足够小,可以缓存'route'中两行的两个rows键。

事实证明,在您的特定情况下,该数字为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对象的唯一id做些什么?

答案 1 :(得分:3)

文件中的典型记录如下:

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

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

对于小整数(-5...255,Python3保留an integer pool-因此,这些小整数就像被缓存一样(只要使用PyLong_FromLong和Co。)。

对于字符串而言,规则更为复杂-如@timgeb所指出的那样,它们是受约束的。有a greate article about interning,即使它是关于Python2.7的,但此后没有太大变化。简而言之,最重要的规则是:

  1. 所有长度为01的字符串都被屏蔽。
  2. 如果具有多个字符的
  3. sting包含可以在标识符中使用的字符,并且可以直接或通过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并不意味着是同一对象-因此,您在问题中的推理并非十分可靠-但这无关紧要在这种特殊情况下。