Python - Почему не все неизменяемые объекты всегда кэшируются?

1

Я не уверен, что происходит под капотом в отношении объектной модели 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

Когда я вызываю 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
  • 3
    id возвращает ячейку памяти объекта. Некоторая память может быть переработана в вашем цикле, а адреса повторно использованы, поэтому вы получите меньшее число. Чего ты пытаешься достичь?
  • 1
    У меня слишком много ожидающих изменений, но могу ли я предложить изменить название этого вопроса на то, что другие с большей вероятностью найдут? Возможно, «Python - меньше уникальных идентификаторов, чем объекты», или что-то в этом роде.
Показать ещё 7 комментариев
Теги:
string
caching
python-internals

2 ответа

3
Лучший ответ

Типичная запись из файла выглядит следующим образом:

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

Это означает, что большинство ваших неизменных объектов являются строками, и только 'rides' -value являются целыми числами.

Для маленьких целых чисел (-5...255) Python3 хранит целочисленный пул - поэтому эти маленькие целые числа кажутся кэшированными (при условии, что используются PyLong_FromLong и Co.).

Правила более сложны для строк - они, как указывает @timgeb, интернированы. Существует большая статья об интернировании, даже если речь идет о Python2.7 - но с тех пор мало что изменилось. Короче говоря, самые важные правила:

  1. все строки длиной 0 и 1 интернированы.
  2. строки с более чем одним символом интернируются, если они состоят из символов, которые могут использоваться в идентификаторах, и создаются во время компиляции либо напрямую, либо путем оптимизации глазка/свертывания констант (но во втором случае, только если результат не длиннее 20 символов) (4096 начиная с 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 не кэширует все созданные строки/целые числа?

Давайте начнем с целых чисел. Чтобы иметь возможность быстрого поиска, если целое число -number уже создано (намного быстрее, чем O(n)), необходимо сохранить дополнительную структуру данных для поиска, которая требует дополнительной памяти. Однако целых чисел так много, что вероятность повторного попадания в одно уже существующее целое число не очень велика, поэтому накладные расходы на память для структуры данных looku-up-data в большинстве случаев не возмещаются.

Поскольку строкам требуется больше памяти, относительная (оперативная) стоимость структуры данных поиска не так высока. Но нет смысла интернировать 1000-символьную строку, потому что вероятность того, что случайно созданная строка будет иметь те же символы, почти равна 0 !

С другой стороны, если, например, хеш-словарь используется в качестве структуры поиска, вычисление хеша займет O(n) (n -number символов), что, вероятно, не окупится для больших строки.

Таким образом, Python делает компромисс, который работает довольно хорошо в большинстве сценариев - но он не может быть идеальным в некоторых особых случаях. Тем не менее, для этих особых сценариев вы можете оптимизировать для каждой руки, используя sys.intern().


Примечание: Наличие одного и того же идентификатора не означает, что он является одним и тем же объектом, если время жизни двух объектов не перекрывается, - поэтому ваши рассуждения в этом вопросе не являются влагозащищенными - но это не имеет значения в этом специальном дело.

  • 0
    Кроме того, я бы предположил, что хеширование, чтобы выяснить, кэширована ли уже строка, приведет к некоторым затратам производительности.
4

Есть 736461 элементов в rows.

Таким образом, вы добавляете 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

В конце концов, возможно, в вашей программе есть семантическая ошибка. Что вы хотите сделать с уникальным id объектов Python?

  • 0
    Спасибо за ответ. Я просто пытаюсь понять, как Python кеширует память под капотом. Я обновил вопрос соответственно.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню