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

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

Ответы [ 2 ]

0 голосов
/ 27 ноября 2018

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

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

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

Для маленьких целых чисел (-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 не кэширует все созданные строки / целые числа?

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

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

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

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


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

0 голосов
/ 25 ноября 2018

В 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

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

...