Я не уверен, что происходит под капотом в отношении объектной модели 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
Типичная запись из файла выглядит следующим образом:
rows[0]
{'route': '3', 'date': '01/01/2001', 'daytype': 'U', 'rides': 7354}
Это означает, что большинство ваших неизменных объектов являются строками, и только 'rides'
-value являются целыми числами.
Для маленьких целых чисел (-5...255
) Python3 хранит целочисленный пул - поэтому эти маленькие целые числа кажутся кэшированными (при условии, что используются PyLong_FromLong
и Co.).
Правила более сложны для строк - они, как указывает @timgeb, интернированы. Существует большая статья об интернировании, даже если речь идет о Python2.7 - но с тех пор мало что изменилось. Короче говоря, самые важные правила:
0
и 1
интернированы. Все вышеперечисленное является деталями реализации, но, принимая их во внимание, мы получаем следующее для row[0]
выше:
'route', 'date', 'daytype', 'rides'
- все они интернированы, потому что они созданы во время компиляции функции read_as_dicts
и не имеют "странных" символов.'3'
и 'W'
интернированы, потому что их длина составляет всего 1
.01/01/2001
не интернирован, потому что он длиннее 1
, создан во время выполнения и не может быть квалифицирован в любом случае, потому что в нем есть символ /
.7354
не из маленького целочисленного пула, потому что слишком большой. Но другие записи могут быть из этого пула.Это было объяснением текущего поведения, только некоторые объекты были "кэшированы".
Но почему Python не кэширует все созданные строки/целые числа?
Давайте начнем с целых чисел. Чтобы иметь возможность быстрого поиска, если целое число -number уже создано (намного быстрее, чем O(n)
), необходимо сохранить дополнительную структуру данных для поиска, которая требует дополнительной памяти. Однако целых чисел так много, что вероятность повторного попадания в одно уже существующее целое число не очень велика, поэтому накладные расходы на память для структуры данных looku-up-data в большинстве случаев не возмещаются.
Поскольку строкам требуется больше памяти, относительная (оперативная) стоимость структуры данных поиска не так высока. Но нет смысла интернировать 1000-символьную строку, потому что вероятность того, что случайно созданная строка будет иметь те же символы, почти равна 0
!
С другой стороны, если, например, хеш-словарь используется в качестве структуры поиска, вычисление хеша займет O(n)
(n
-number символов), что, вероятно, не окупится для больших строки.
Таким образом, Python делает компромисс, который работает довольно хорошо в большинстве сценариев - но он не может быть идеальным в некоторых особых случаях. Тем не менее, для этих особых сценариев вы можете оптимизировать для каждой руки, используя sys.intern()
.
Примечание: Наличие одного и того же идентификатора не означает, что он является одним и тем же объектом, если время жизни двух объектов не перекрывается, - поэтому ваши рассуждения в этом вопросе не являются влагозащищенными - но это не имеет значения в этом специальном дело.
Есть 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?