Заранее создаваемые объекты - целые числа в Питоне
И снова здравствуйте! Здесь мы проверяем "руками" разные штуки в Питоне.
Наверняка все что-то слышали о том, что часть объектов - целых чисел в Питоне заводится заранее, чтобы сэкономить на создании объектов. В Питоне каждая сущность, даже такая как целые числа - это полноценный объект со всеми положенными объекту прибамбасами. Создавать полноценные объекты - дорого. Поэтому в Питоне, да и в других языках, насколько я помню, кажется в Java, например, часть целых чисел, которые считаются часто используемыми, создаётся заранее и потом используется всё время жизни программы. Т.е. когда вы используете какое-то большое целое число, например, n = 10_000
, то под такое число создаётся новый объект каждый раз, а если используете маленькое, например, n = 10
, то новый объект не создаётся, а делается ссылка на один и тот же, заранее созданный объект.
Но давайте сами проверим: действительно ли есть такие числа и каков их диапазон. Будем проделывать с числом простейшие манипуляции - сначала увеличивать на 1, потом уменьшать на 1, чтобы получилось тоже самое число. И потом проверим, поменялся ли id
(адрес в памяти) у этого числа. Конечно, тут многое будет зависеть от конкретной версии интерпретатора. Какой-то интерпретатор и код k = n - 1 + 1
не будет оптимизировать, а какой-то и в приведённом ниже коде догадается, что все операции можно сделать как одну операцию, посокращает все добавления-вычитания и мы ничего не сможем определить. И тогда нас спасёт только какой-нибудь eval
с вычислениями в виде строки. Но обычно интерпретаторы Питона не настолько хитрые и приведённый ниже код вполне работает в Google Colab.
def check_if_int_cached(n):
k = n + 1
k -= 1
return id(k) == id(n)
checks = [(i, check_if_int_cached(i)) for i in range(-10000, 10000)]
for (x, a), (y, b) in zip(checks, checks[1:]):
if a != b:
print((x, y)[b])
В этом коде мы:
Проверяем, сохраняется ли id
у числа после некоторых математических манипуляций, которые в итоге дают тоже самое число
Создаём последовательность из чисел диапазона [-10000, 9999]
и результатов нашей проверки [(число, результат_проверки), ...]
Сцепляем попарно текущий и следующий элемент нашей последовательности, чтобы легче было проверять смену результата проверки, т.е. найти границу диапазона, где проверка даёт уже другой результат
Если результат поменялся - выводим либо текущий элемент, либо следующий, пользуясь тем трюком питона, что True
- это 1
. а False
- это 0
, и таким образом можно легко выбрать из двух чисел либо первое либо второе не через тернарный оператор, а через индексацию [num1, num2][условие]
Запустим наш код. Вывод:
-5
256
Итак, мы определили, что, действительно, целые числа в диапазоне [-5, 256]
заранее создаются Питоном и какие бы ни были вычисления в программе, если в их результате получается число из этого диапазона, то под него не создаётся новый объект, а переиспользуется старый.
Давайте ещё проверим - а действительно ли эта оптимизация Питона даёт какой-то выигрыш. Попробуем создавать список из чисел диапазонов [0, 200]
и [1000, 1200]
и проделаем это миллион раз для солидности стабильности результата.
import time
n = 1_000_000
k = (0, 1000)
m = 200
for i in k:
t1 = time.perf_counter()
for _ in range(n):
lst = list(range(i, i+m))
t2 = time.perf_counter()
print(t2-t1)
1.732138877000125
3.547805026000333
Выигрыш по времени получился практически ровно в 2 раза! Но это если ничего не делать, а только создавать список из объектов-чисел. Если там будут ещё какие-то действия и вычисления, возможно, выигрыш будет вообще не заметен.
В предыдущем посте я писал о встроенной оптимизации добавления символов в строку в Питоне. Далее будут и другие посты об интересных мелочах в Питоне, которые быстро и просто могут быть проверены своими руками (за что мне и нравится Питон) . Спасибо за чтение.