Объекты, память и управляющие конструкции в Python

В этой главе мы углубимся в то, как Python работает с памятью, что такое изменяемые и неизменяемые объекты, и как эффективно использовать условные конструкции и циклы. Мы также затронем некоторые неочевидные аспекты языка, которые помогут вам писать более качественный код.

Объекты в памяти

В Python всё является объектом. Каждый объект имеет:

  • Тип (например, int, str, list),
  • Значение (например, 42, "hello"),
  • Уникальный идентификатор в памяти, который можно получить с помощью функции id().

Пример:

s = "hello"
print(id(s))  # Выведет что-то вроде 4390213040

Ссылки, а не копирования

Когда вы присваиваете переменную другой переменной, вы создаёте ссылку на тот же объект, а не копируете его:

a = [1, 2, 3]
b = a
b.append(4)
print(a)  # [1, 2, 3, 4] – изменился и a!

Сравнение: is vs ==

  • is проверяет, ссылаются ли две переменные на один и тот же объект.
  • == проверяет, равны ли значения объектов.

Пример:

x = [1, 2]
y = [1, 2]
print(x == y)  # True
print(x is y)  # False

Неочевидное поведение: кэширование целых чисел

Python кэширует небольшие целые числа (от -5 до 256). Поэтому:

a = 256
b = 256
print(a is b)  # True

c = 257
d = 257
print(c is d)  # False (вне диапазона кэширования)

💡 Золотое правило: всегда используйте == для сравнения значений, а is — только для проверки на None.

Изменяемые и неизменяемые объекты

Неизменяемые (immutable)

  • int, float, str, tuple, bytes, bool.
  • Нельзя изменить после создания. Любая операция создаёт новый объект.

Пример:

s = "hello"
print(id(s))
s += "!"
print(id(s))  # ID изменился – это новый объект!

Изменяемые (mutable)

  • list, dict, set.
  • Можно изменять без создания нового объекта.

Пример:

lst = [1, 2]
print(id(lst))
lst.append(3)
print(id(lst))  # ID остался прежним

Неочевидный нюанс: кортежи с изменяемыми элементами

Кортеж неизменяем, но если он содержит изменяемые объекты, их можно менять:

t = ([1, 2], [3, 4])
t[0].append(3)
print(t)  # ([1, 2, 3], [3, 4]) – это допустимо!

Строки и байты

Строки (str)

  • Предназначены для текста (Unicode).
  • Неизменяемы.

Байты (bytes)

  • "Сырые" данные (числа от 0 до 255).
  • Тоже неизменяемы.

Пример преобразования:

text = "привет"
encoded = text.encode('utf-8')  # str -> bytes
decoded = encoded.decode('utf-8')  # bytes -> str

Условные конструкции

Базовый синтаксис

temperature = 15
if temperature > 20:
    print("Наденьте футболку")
else:
    print("Лучше взять куртку")

Элегантные проверки

Используйте "истинность" объектов:

name = input("Введите имя: ")
if name:  # False, если строка пустая
    print(f"Привет, {name}!")
else:
    print("Привет, незнакомец!")

Современный match (Python 3.10+)

Аналог switch из других языков:

http_status = 404
match http_status:
    case 200 | 201:
        print("Успех")
    case 401 | 403 | 404:
        print("Ошибка клиента")
    case 500 | 503:
        print("Ошибка сервера")
    case _:
        print("Неизвестный статус")

💡 Совет: используйте match, когда сравниваете одну переменную с несколькими значениями. Для сложных условий оставайтесь с if/elif/else.

Циклы

while

i = 1
while i < 1000:
    print(i)
    i *= 2

for и range

for i in range(5):      # 0, 1, 2, 3, 4
    print(i)

for i in range(1, 10, 2):  # 1, 3, 5, 7, 9
    print(i)

enumerate для получения индекса

for idx, char in enumerate("abc"):
    print(f"Индекс: {idx}, Символ: {char}")

Неочевидный трюк: else в циклах

Блок else выполняется, если цикл завершился естественно (без break):

for i in range(5):
    if i == 10:
        break
else:
    print("Цикл завершился без break")

Производительность при работе со строками

Помните задачу сборки строки по частям?

Неэффективный способ:

result = ""
for _ in range(100000):
    result += "a"  # Создаёт новый объект на каждой итерации!

Эффективный способ:

parts = []
for _ in range(100000):
    parts.append("a")
result = "".join(parts)  # Быстрое объединение

💡 Совет: для частых операций конкатенации используйте list + join() для строк или bytearray для байтов.

Сборка мусора

Python использует двухуровневую систему управления памятью:

  1. Подсчёт ссылок:

    • Быстрый и предсказуемый.
    • Не справляется с циклическими ссылками.
  2. Циклический сборщик мусора (Generational GC):

    • Находит и удаляет "острова" объектов с циклическими ссылками.
    • Работает периодически.

Пример циклической ссылки:

a = []
b = [a]
a.append(b)  # Теперь a и b ссылаются друг на друга
del a, b     # Объекты недостижимы, но ссылки остались

Сборщик мусора найдёт и удалит такие объекты.

Неочевидные особенности Python

1. Изменяемые аргументы по умолчанию

Опасно использовать изменяемые объекты как значения по умолчанию:

def append_to(element, target=[]):  # Не делайте так!
    target.append(element)
    return target

print(append_to(1))  # [1]
print(append_to(2))  # [1, 2] – тот же список!

Правильный подход:

def append_to(element, target=None):
    if target is None:
        target = []
    target.append(element)
    return target

2. Ленивые логические операторы

and и or вычисляются лениво:

def expensive_call():
    print("Вызов выполнен!")
    return True

# expensive_call() не будет вызвана, т.к. первое условие False
if False and expensive_call():
    pass

3. Атрибуты функций

Функции в Python — это тоже объекты, и им можно добавлять атрибуты:

def my_func():
    pass

my_func.custom_attr = 42
print(my_func.custom_attr)  # 42

Заключение

Понимание работы с памятью и объектами в Python — ключ к написанию эффективного и надёжного кода. Помните:

  • Используйте is только для сравнения с None
  • Для частой конкатенации строк применяйте join()
  • Избегайте изменяемых аргументов по умолчанию
  • Знайте разницу между изменяемыми и неизменяемыми объектами

Полезные материалы:

Эта глава даёт прочную основу для понимания того, как Python работает "под капотом", что необходимо для написания эффективных и надёжных программ.