В мире Python существует много мифов о том, как работают переменные. Одни говорят, что "всё передаётся по ссылке", другие утверждают обратное. Правда, как обычно, лежит где-то посередине и гораздо интереснее простых объяснений. В этой статье мы детально разберём механизмы работы с памятью в Python 3.13, изучим различия между mutable и immutable объектами, и поймём, когда Python создаёт новые объекты, а когда переиспользует существующие. Дабы статье пожить подольше - рассмотрю только версию 3.13.

Фундаментальные концепции: всё есть объект

Начнём с самого важного принципа Python: всё является объектом. Когда мы пишем:

pythonx = 42
x = 42

Мы не создаём переменную x, которая содержит значение 42. Вместо этого происходит следующее:

  1. Python создаёт объект типа int со значением 42 в куче (heap)

  2. Создаётся имя x в пространстве имён (namespace)

  3. Имя x связывается с объектом через ссылку

Это принципиальное отличие от языков вроде C, где переменная - это именованная область памяти, содержащая значение.

Исследуем объекты изнутри

Каждый объект в Python имеет три обязательных атрибута:

pythonx = 42
print(f"Значение: {x}")           # 42
print(f"Тип: {type(x)}")          # <class 'int'>
print(f"ID (адрес): {id(x)}")     # Уникальный идентификатор в памяти
print(f"Размер: {x.__sizeof__()}") # Размер в байтах
x = 42
print(f"Значение: {x}")           # 42
print(f"Тип: {type(x)}")          # <class 'int'>
print(f"ID (адрес): {id(x)}")     # Уникальный идентификатор в памяти
print(f"Размер: {x.__sizeof__()}") # Размер в байтах

ID объекта - это его адрес в памяти (в CPython). Этот механизм позволяет понять, когда мы работаем с одним и тем же объектом:

pythona = 1000
b = 1000
print(id(a) == id(b))  # Может быть False!

a = 5
b = 5
print(id(a) == id(b))  # True - интернирование малых чисел
a = 1000
b = 1000
print(id(a) == id(b))  # Может быть False!

a = 5
b = 5
print(id(a) == id(b))  # True - интернирование малых чисел

Архитектура памяти Python: многоуровневая система

Python использует сложную систему управления памятью, состоящую из нескольких уровней:

1. Системный уровень (malloc/free)

На самом низком уровне Python взаимодействует с системными функциями выделения памяти. Однако прямое обращение к malloc/free было бы неэффективно для множества мелких объектов.

2. Менеджер памяти Python (PyMalloc)

Python реализует собственный аллокатор памяти, оптимизированный для работы с объектами размером до 512 байт:

pythonimport sys

# Информация о состоянии менеджера памяти
def memory_info():
    import gc
    print(f"Количество объектов: {len(gc.get_objects())}")
    print(f"Статистика GC: {gc.get_stats()}")
    
    # В Python 3.13 добавлены новые методы мониторинга
    if hasattr(sys, 'getallocatedblocks'):
        print(f"Выделено блоков: {sys.getallocatedblocks()}")
import sys

# Информация о состоянии менеджера памяти
def memory_info():
    import gc
    print(f"Количество объектов: {len(gc.get_objects())}")
    print(f"Статистика GC: {gc.get_stats()}")
    
    # В Python 3.13 добавлены новые методы мониторинга
    if hasattr(sys, 'getallocatedblocks'):
        print(f"Выделено блоков: {sys.getallocatedblocks()}")

3. Объектные аллокаторы

Каждый тип объекта может иметь свой специализированный аллокатор:

  • Integers: кеширование малых чисел (-5 до 256)

  • Strings: интернирование строк

  • Lists: предварительное выделение места для роста

  • Dicts: оптимизированные структуры с Python 3.6+

Интернирование и кеширование: оптимизации под капотом

Кеширование малых целых чисел

Python предварительно создаёт объекты для чисел от -5 до 256:

python# Демонстрация кеширования
def demonstrate_int_caching():
    # Малые числа всегда ссылаются на один объект
    a = 100
    b = 100
    print(f"a is b: {a is b}")        # True
    print(f"id(a): {id(a)}")
    print(f"id(b): {id(b)}")
    
    # Большие числа могут создавать новые объекты
    x = 1000
    y = 1000
    print(f"x is y: {x is y}")        # Обычно False
    print(f"id(x): {id(x)}")
    print(f"id(y): {id(y)}")

demonstrate_int_caching()
# Демонстрация кеширования
def demonstrate_int_caching():
    # Малые числа всегда ссылаются на один объект
    a = 100
    b = 100
    print(f"a is b: {a is b}")        # True
    print(f"id(a): {id(a)}")
    print(f"id(b): {id(b)}")
    
    # Большие числа могут создавать новые объекты
    x = 1000
    y = 1000
    print(f"x is y: {x is y}")        # Обычно False
    print(f"id(x): {id(x)}")
    print(f"id(y): {id(y)}")

demonstrate_int_caching()

Интернирование строк

Python автоматически интернирует строки, похожие на идентификаторы:

pythondef demonstrate_string_interning():
    # Автоматическое интернирование
    s1 = "hello"
    s2 = "hello"
    print(f"s1 is s2: {s1 is s2}")   # True
    
    # Строки с пробелами могут не интернироваться
    s3 = "hello world"
    s4 = "hello world"
    print(f"s3 is s4: {s3 is s4}")   # Может быть False
    
    # Принудительное интернирование
    import sys
    s5 = sys.intern("hello world")
    s6 = sys.intern("hello world")
    print(f"s5 is s6: {s5 is s6}")   # True

demonstrate_string_interning()
def demonstrate_string_interning():
    # Автоматическое интернирование
    s1 = "hello"
    s2 = "hello"
    print(f"s1 is s2: {s1 is s2}")   # True
    
    # Строки с пробелами могут не интернироваться
    s3 = "hello world"
    s4 = "hello world"
    print(f"s3 is s4: {s3 is s4}")   # Может быть False
    
    # Принудительное интернирование
    import sys
    s5 = sys.intern("hello world")
    s6 = sys.intern("hello world")
    print(f"s5 is s6: {s5 is s6}")   # True

demonstrate_string_interning()

Mutable vs Immutable: ключевое различие

Понимание разницы между изменяемыми (mutable) и неизменяемыми (immutable) объектами критично для работы с Python.

Immutable объекты

К неизменяемым относятся: int, float, str, tuple, frozenset, bytes:

pythondef immutable_example():
    # Создаём строку
    original = "Hello"
    modified = original + " World"
    
    print(f"original: {original}")      # Hello
    print(f"modified: {modified}")      # Hello World
    print(f"Same object: {original is modified}")  # False
    
    # "Изменение" создаёт новый объект
    number = 42
    print(f"ID before: {id(number)}")
    number += 1  # Создаётся новый объект!
    print(f"ID after: {id(number)}")
    print(f"Value: {number}")

immutable_example()
def immutable_example():
    # Создаём строку
    original = "Hello"
    modified = original + " World"
    
    print(f"original: {original}")      # Hello
    print(f"modified: {modified}")      # Hello World
    print(f"Same object: {original is modified}")  # False
    
    # "Изменение" создаёт новый объект
    number = 42
    print(f"ID before: {id(number)}")
    number += 1  # Создаётся новый объект!
    print(f"ID after: {id(number)}")
    print(f"Value: {number}")

immutable_example()

Mutable объекты

К изменяемым относятся: list, dict, set, пользовательские классы (по умолчанию):

pythondef mutable_example():
    # Список изменяется на месте
    original_list = [1, 2, 3]
    list_id_before = id(original_list)
    
    original_list.append(4)  # Изменение существующего объекта
    list_id_after = id(original_list)
    
    print(f"List: {original_list}")           # [1, 2, 3, 4]
    print(f"Same object: {list_id_before == list_id_after}")  # True
    
    # Словари тоже изменяемы
    d = {"a": 1}
    dict_id_before = id(d)
    d["b"] = 2
    dict_id_after = id(d)
    print(f"Dict same object: {dict_id_before == dict_id_after}")  # True

mutable_example()
def mutable_example():
    # Список изменяется на месте
    original_list = [1, 2, 3]
    list_id_before = id(original_list)
    
    original_list.append(4)  # Изменение существующего объекта
    list_id_after = id(original_list)
    
    print(f"List: {original_list}")           # [1, 2, 3, 4]
    print(f"Same object: {list_id_before == list_id_after}")  # True
    
    # Словари тоже изменяемы
    d = {"a": 1}
    dict_id_before = id(d)
    d["b"] = 2
    dict_id_after = id(d)
    print(f"Dict same object: {dict_id_before == dict_id_after}")  # True

mutable_example()

Передача аргументов: детальный анализ

В Python всё передаётся по ссылке на объект (object reference). Но поведение зависит от того, изменяемый объект или нет.

Передача immutable объектов

pythondef modify_immutable(x):
    print(f"Получен объект с ID: {id(x)}")
    x = x + 10  # Создаётся новый объект
    print(f"После изменения ID: {id(x)}")
    return x

original = 42
print(f"Исходный ID: {id(original)}")
result = modify_immutable(original)
print(f"Исходное значение: {original}")    # 42 - не изменилось
print(f"Результат: {result}")               # 52
def modify_immutable(x):
    print(f"Получен объект с ID: {id(x)}")
    x = x + 10  # Создаётся новый объект
    print(f"После изменения ID: {id(x)}")
    return x

original = 42
print(f"Исходный ID: {id(original)}")
result = modify_immutable(original)
print(f"Исходное значение: {original}")    # 42 - не изменилось
print(f"Результат: {result}")               # 52

Передача mutable объектов

pythondef modify_mutable(lst):
    print(f"Получен список с ID: {id(lst)}")
    lst.append(4)  # Изменяем существующий объект
    print(f"После добавления ID: {id(lst)}")  # ID не изменился
    
    lst = [100, 200]  # Создаём новый объект и переназначаем ссылку
    print(f"После переназначения ID: {id(lst)}")  # Новый ID
    return lst

original_list = [1, 2, 3]
print(f"Исходный ID: {id(original_list)}")
result = modify_mutable(original_list)
print(f"Исходный список: {original_list}")   # [1, 2, 3, 4] - изменился!
print(f"Возвращённый список: {result}")      # [100, 200]
def modify_mutable(lst):
    print(f"Получен список с ID: {id(lst)}")
    lst.append(4)  # Изменяем существующий объект
    print(f"После добавления ID: {id(lst)}")  # ID не изменился
    
    lst = [100, 200]  # Создаём новый объект и переназначаем ссылку
    print(f"После переназначения ID: {id(lst)}")  # Новый ID
    return lst

original_list = [1, 2, 3]
print(f"Исходный ID: {id(original_list)}")
result = modify_mutable(original_list)
print(f"Исходный список: {original_list}")   # [1, 2, 3, 4] - изменился!
print(f"Возвращённый список: {result}")      # [100, 200]

Продвинутые сценарии: когда интуиция подводит

Множественное присваивание

pythondef multiple_assignment_analysis():
    # Случай 1: immutable объекты
    a = b = c = [1, 2, 3]  # Все ссылаются на один список!
    print(f"a is b: {a is b}")  # True
    print(f"b is c: {b is c}")  # True
    
    a.append(4)
    print(f"После изменения a: {a}")  # [1, 2, 3, 4]
    print(f"b тоже изменился: {b}")   # [1, 2, 3, 4]
    print(f"c тоже изменился: {c}")   # [1, 2, 3, 4]
    
    # Правильный способ создания независимых списков
    a = [1, 2, 3]
    b = [1, 2, 3]
    c = [1, 2, 3]
    # Или используя copy
    a = [1, 2, 3]
    b = a.copy()
    c = list(a)

multiple_assignment_analysis()
def multiple_assignment_analysis():
    # Случай 1: immutable объекты
    a = b = c = [1, 2, 3]  # Все ссылаются на один список!
    print(f"a is b: {a is b}")  # True
    print(f"b is c: {b is c}")  # True
    
    a.append(4)
    print(f"После изменения a: {a}")  # [1, 2, 3, 4]
    print(f"b тоже изменился: {b}")   # [1, 2, 3, 4]
    print(f"c тоже изменился: {c}")   # [1, 2, 3, 4]
    
    # Правильный способ создания независимых списков
    a = [1, 2, 3]
    b = [1, 2, 3]
    c = [1, 2, 3]
    # Или используя copy
    a = [1, 2, 3]
    b = a.copy()
    c = list(a)

multiple_assignment_analysis()

Замыкания и мутабельные объекты

pythondef closure_trap():
    functions = []
    
    # Неправильно - все функции ссылаются на одну переменную
    for i in [1, 2, 3]:
        functions.append(lambda: i)  # i изменяется!
    
    print("Неправильный подход:")
    for f in functions:
        print(f())  # Напечатает 3, 3, 3
    
    # Правильно - создаём локальную копию
    functions_correct = []
    for i in [1, 2, 3]:
        functions_correct.append(lambda x=i: x)  # Захватываем значение
    
    print("Правильный подход:")
    for f in functions_correct:
        print(f())  # Напечатает 1, 2, 3

closure_trap()
def closure_trap():
    functions = []
    
    # Неправильно - все функции ссылаются на одну переменную
    for i in [1, 2, 3]:
        functions.append(lambda: i)  # i изменяется!
    
    print("Неправильный подход:")
    for f in functions:
        print(f())  # Напечатает 3, 3, 3
    
    # Правильно - создаём локальную копию
    functions_correct = []
    for i in [1, 2, 3]:
        functions_correct.append(lambda x=i: x)  # Захватываем значение
    
    print("Правильный подход:")
    for f in functions_correct:
        print(f())  # Напечатает 1, 2, 3

closure_trap()

Специальные случаи и оптимизации в Python 3.13

JIT-компиляция и управление памятью

Python 3.13.3 is the latest release, packed with a Just-in-Time (JIT) compiler, который влияет на работу с объектами:

pythondef jit_optimization_example():
    # JIT может оптимизировать создание объектов в циклах
    def hot_function(n):
        result = []
        for i in range(n):
            # JIT может оптимизировать создание int объектов
            result.append(i * 2)
        return result
    
    # При многократном вызове JIT оптимизирует код
    for _ in range(1000):
        hot_function(100)

jit_optimization_example()
def jit_optimization_example():
    # JIT может оптимизировать создание объектов в циклах
    def hot_function(n):
        result = []
        for i in range(n):
            # JIT может оптимизировать создание int объектов
            result.append(i * 2)
        return result
    
    # При многократном вызове JIT оптимизирует код
    for _ in range(1000):
        hot_function(100)

jit_optimization_example()

Улучшения в многопоточности

В Python 3.13 улучшена работа с памятью в многопоточных приложениях:

pythonimport threading
import time

def thread_memory_example():
    shared_data = {"counter": 0}
    
    def worker():
        # Каждый поток работает с одним объектом
        for _ in range(100000):
            shared_data["counter"] += 1  # Изменяем объект на месте
    
    threads = []
    for _ in range(4):
        t = threading.Thread(target=worker)
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print(f"Final counter: {shared_data['counter']}")
    # Результат может быть непредсказуемым без синхронизации!

# thread_memory_example()  # Раскомментируйте для тестирования
import threading
import time

def thread_memory_example():
    shared_data = {"counter": 0}
    
    def worker():
        # Каждый поток работает с одним объектом
        for _ in range(100000):
            shared_data["counter"] += 1  # Изменяем объект на месте
    
    threads = []
    for _ in range(4):
        t = threading.Thread(target=worker)
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    
    print(f"Final counter: {shared_data['counter']}")
    # Результат может быть непредсказуемым без синхронизации!

# thread_memory_example()  # Раскомментируйте для тестирования

Профилирование памяти: инструменты и техники

Встроенные инструменты

pythonimport sys
import gc
from collections import defaultdict

def memory_profiling():
    # Подсчёт объектов по типам
    def count_objects():
        counts = defaultdict(int)
        for obj in gc.get_objects():
            counts[type(obj).__name__] += 1
        return dict(counts)
    
    print("Объекты в памяти:")
    initial_counts = count_objects()
    
    # Создаём много объектов
    data = []
    for i in range(1000):
        data.append({"id": i, "value": f"item_{i}"})
    
    final_counts = count_objects()
    
    # Сравниваем изменения
    for obj_type in set(initial_counts.keys()) | set(final_counts.keys()):
        initial = initial_counts.get(obj_type, 0)
        final = final_counts.get(obj_type, 0)
        if final > initial:
            print(f"{obj_type}: +{final - initial}")

memory_profiling()
import sys
import gc
from collections import defaultdict

def memory_profiling():
    # Подсчёт объектов по типам
    def count_objects():
        counts = defaultdict(int)
        for obj in gc.get_objects():
            counts[type(obj).__name__] += 1
        return dict(counts)
    
    print("Объекты в памяти:")
    initial_counts = count_objects()
    
    # Создаём много объектов
    data = []
    for i in range(1000):
        data.append({"id": i, "value": f"item_{i}"})
    
    final_counts = count_objects()
    
    # Сравниваем изменения
    for obj_type in set(initial_counts.keys()) | set(final_counts.keys()):
        initial = initial_counts.get(obj_type, 0)
        final = final_counts.get(obj_type, 0)
        if final > initial:
            print(f"{obj_type}: +{final - initial}")

memory_profiling()

Использование slots для экономии памяти

pythonclass RegularClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedClass:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

def slots_comparison():
    # Создаём экземпляры
    regular = RegularClass(1, 2)
    slotted = SlottedClass(1, 2)
    
    print(f"Regular class size: {sys.getsizeof(regular) + sys.getsizeof(regular.__dict__)}")
    print(f"Slotted class size: {sys.getsizeof(slotted)}")
    
    # __slots__ экономит память, но ограничивает функциональность
    try:
        regular.z = 3  # Работает
        print("Regular: можно добавлять атрибуты")
    except:
        pass
    
    try:
        slotted.z = 3  # Ошибка!
        print("Slotted: можно добавлять атрибуты")
    except AttributeError as e:
        print(f"Slotted: {e}")

slots_comparison()
class RegularClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

class SlottedClass:
    __slots__ = ['x', 'y']
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

def slots_comparison():
    # Создаём экземпляры
    regular = RegularClass(1, 2)
    slotted = SlottedClass(1, 2)
    
    print(f"Regular class size: {sys.getsizeof(regular) + sys.getsizeof(regular.__dict__)}")
    print(f"Slotted class size: {sys.getsizeof(slotted)}")
    
    # __slots__ экономит память, но ограничивает функциональность
    try:
        regular.z = 3  # Работает
        print("Regular: можно добавлять атрибуты")
    except:
        pass
    
    try:
        slotted.z = 3  # Ошибка!
        print("Slotted: можно добавлять атрибуты")
    except AttributeError as e:
        print(f"Slotted: {e}")

slots_comparison()

Распространённые ошибки и их избежание

Ошибка 1: Мутация во время итерации

pythondef iteration_mutation_error():
    # НЕПРАВИЛЬНО
    items = [1, 2, 3, 4, 5]
    for item in items:
        if item % 2 == 0:
            items.remove(item)  # Изменяем список во время итерации!
    print(f"Результат (неправильно): {items}")  # Может быть не то, что ожидаете
    
    # ПРАВИЛЬНО - создаём новый список
    items = [1, 2, 3, 4, 5]
    items = [item for item in items if item % 2 != 0]
    print(f"Результат (правильно): {items}")
    
    # ПРАВИЛЬНО - итерируем по копии
    items = [1, 2, 3, 4, 5]
    for item in items[:]:  # Создаём копию для итерации
        if item % 2 == 0:
            items.remove(item)
    print(f"Результат (альтернатива): {items}")

iteration_mutation_error()
def iteration_mutation_error():
    # НЕПРАВИЛЬНО
    items = [1, 2, 3, 4, 5]
    for item in items:
        if item % 2 == 0:
            items.remove(item)  # Изменяем список во время итерации!
    print(f"Результат (неправильно): {items}")  # Может быть не то, что ожидаете
    
    # ПРАВИЛЬНО - создаём новый список
    items = [1, 2, 3, 4, 5]
    items = [item for item in items if item % 2 != 0]
    print(f"Результат (правильно): {items}")
    
    # ПРАВИЛЬНО - итерируем по копии
    items = [1, 2, 3, 4, 5]
    for item in items[:]:  # Создаём копию для итерации
        if item % 2 == 0:
            items.remove(item)
    print(f"Результат (альтернатива): {items}")

iteration_mutation_error()

Ошибка 2: Неглубокое копирование

pythondef shallow_copy_trap():
    original = [[1, 2], [3, 4]]
    
    # Поверхностное копирование
    shallow = original.copy()
    shallow[0].append(3)  # Изменяем вложенный список
    
    print(f"Original: {original}")   # [[1, 2, 3], [3, 4]] - изменился!
    print(f"Shallow: {shallow}")     # [[1, 2, 3], [3, 4]]
    
    # Глубокое копирование
    import copy
    original = [[1, 2], [3, 4]]
    deep = copy.deepcopy(original)
    deep[0].append(3)
    
    print(f"Original after deep copy: {original}")  # [[1, 2], [3, 4]] - не изменился
    print(f"Deep copy: {deep}")                     # [[1, 2, 3], [3, 4]]

shallow_copy_trap()
def shallow_copy_trap():
    original = [[1, 2], [3, 4]]
    
    # Поверхностное копирование
    shallow = original.copy()
    shallow[0].append(3)  # Изменяем вложенный список
    
    print(f"Original: {original}")   # [[1, 2, 3], [3, 4]] - изменился!
    print(f"Shallow: {shallow}")     # [[1, 2, 3], [3, 4]]
    
    # Глубокое копирование
    import copy
    original = [[1, 2], [3, 4]]
    deep = copy.deepcopy(original)
    deep[0].append(3)
    
    print(f"Original after deep copy: {original}")  # [[1, 2], [3, 4]] - не изменился
    print(f"Deep copy: {deep}")                     # [[1, 2, 3], [3, 4]]

shallow_copy_trap()

Оптимизация производительности

Предварительное выделение памяти

pythondef memory_preallocation():
    import time
    
    # Неэффективно - множественные расширения списка
    def slow_way(n):
        result = []
        for i in range(n):
            result.append(i)
        return result
    
    # Эффективно - предварительное выделение
    def fast_way(n):
        result = [None] * n
        for i in range(n):
            result[i] = i
        return result
    
    # Ещё эффективнее - генератор
    def fastest_way(n):
        return list(range(n))
    
    n = 100000
    
    start = time.time()
    slow_result = slow_way(n)
    slow_time = time.time() - start
    
    start = time.time()
    fast_result = fast_way(n)
    fast_time = time.time() - start
    
    start = time.time()
    fastest_result = fastest_way(n)
    fastest_time = time.time() - start
    
    print(f"Медленный способ: {slow_time:.4f}s")
    print(f"Быстрый способ: {fast_time:.4f}s")
    print(f"Самый быстрый: {fastest_time:.4f}s")

memory_preallocation()
def memory_preallocation():
    import time
    
    # Неэффективно - множественные расширения списка
    def slow_way(n):
        result = []
        for i in range(n):
            result.append(i)
        return result
    
    # Эффективно - предварительное выделение
    def fast_way(n):
        result = [None] * n
        for i in range(n):
            result[i] = i
        return result
    
    # Ещё эффективнее - генератор
    def fastest_way(n):
        return list(range(n))
    
    n = 100000
    
    start = time.time()
    slow_result = slow_way(n)
    slow_time = time.time() - start
    
    start = time.time()
    fast_result = fast_way(n)
    fast_time = time.time() - start
    
    start = time.time()
    fastest_result = fastest_way(n)
    fastest_time = time.time() - start
    
    print(f"Медленный способ: {slow_time:.4f}s")
    print(f"Быстрый способ: {fast_time:.4f}s")
    print(f"Самый быстрый: {fastest_time:.4f}s")

memory_preallocation()

Использование генераторов для экономии памяти

pythondef generator_memory_efficiency():
    # Список - создаёт все объекты в памяти
    def list_approach(n):
        return [x**2 for x in range(n)]
    
    # Генератор - создаёт объекты по требованию
    def generator_approach(n):
        return (x**2 for x in range(n))
    
    n = 1000000
    
    # Измеряем использование памяти
    import sys
    
    list_result = list_approach(n)
    print(f"Размер списка: {sys.getsizeof(list_result)} байт")
    
    gen_result = generator_approach(n)
    print(f"Размер генератора: {sys.getsizeof(gen_result)} байт")
    
    # Генератор использует во много раз меньше памяти!

generator_memory_efficiency()
def generator_memory_efficiency():
    # Список - создаёт все объекты в памяти
    def list_approach(n):
        return [x**2 for x in range(n)]
    
    # Генератор - создаёт объекты по требованию
    def generator_approach(n):
        return (x**2 for x in range(n))
    
    n = 1000000
    
    # Измеряем использование памяти
    import sys
    
    list_result = list_approach(n)
    print(f"Размер списка: {sys.getsizeof(list_result)} байт")
    
    gen_result = generator_approach(n)
    print(f"Размер генератора: {sys.getsizeof(gen_result)} байт")
    
    # Генератор использует во много раз меньше памяти!

generator_memory_efficiency()

Новые возможности Python 3.13

Улучшенная отладка с цветными трейсбеками

colorized tracebacks помогают лучше понимать проблемы с памятью:

pythondef colorized_traceback_example():
    def problematic_function():
        # Создаём проблемную ситуацию для демонстрации
        large_data = [i for i in range(1000000)]
        # Попытка доступа к несуществующему индексу
        return large_data[2000000]  # IndexError
    
    try:
        problematic_function()
    except IndexError as e:
        print(f"Поймали ошибку: {e}")
        # В Python 3.13 трейсбек будет цветным и более информативным

# colorized_traceback_example()  # Раскомментируйте для тестирования
def colorized_traceback_example():
    def problematic_function():
        # Создаём проблемную ситуацию для демонстрации
        large_data = [i for i in range(1000000)]
        # Попытка доступа к несуществующему индексу
        return large_data[2000000]  # IndexError
    
    try:
        problematic_function()
    except IndexError as e:
        print(f"Поймали ошибку: {e}")
        # В Python 3.13 трейсбек будет цветным и более информативным

# colorized_traceback_example()  # Раскомментируйте для тестирования

Практические рекомендации

1. Мониторинг использования памяти

pythondef memory_monitoring():
    import psutil
    import os
    
    process = psutil.Process(os.getpid())
    
    def get_memory_usage():
        return process.memory_info().rss / 1024 / 1024  # МБ
    
    print(f"Использование памяти в начале: {get_memory_usage():.2f} МБ")
    
    # Создаём много объектов
    data = []
    for i in range(100000):
        data.append({"index": i, "square": i**2, "data": f"item_{i}"})
    
    print(f"После создания объектов: {get_memory_usage():.2f} МБ")
    
    # Очищаем ссылки
    del data
    import gc
    gc.collect()
    
    print(f"После очистки: {get_memory_usage():.2f} МБ")

# memory_monitoring()  # Требует psutil
def memory_monitoring():
    import psutil
    import os
    
    process = psutil.Process(os.getpid())
    
    def get_memory_usage():
        return process.memory_info().rss / 1024 / 1024  # МБ
    
    print(f"Использование памяти в начале: {get_memory_usage():.2f} МБ")
    
    # Создаём много объектов
    data = []
    for i in range(100000):
        data.append({"index": i, "square": i**2, "data": f"item_{i}"})
    
    print(f"После создания объектов: {get_memory_usage():.2f} МБ")
    
    # Очищаем ссылки
    del data
    import gc
    gc.collect()
    
    print(f"После очистки: {get_memory_usage():.2f} МБ")

# memory_monitoring()  # Требует psutil

2. Оптимизация структур данных

pythondef data_structure_optimization():
    # Сравнение различных способов хранения данных
    import array
    
    # Обычный список
    regular_list = [i for i in range(1000)]
    print(f"Обычный список: {sys.getsizeof(regular_list)} байт")
    
    # Array - более эффективен для числовых данных
    int_array = array.array('i', range(1000))
    print(f"Array: {sys.getsizeof(int_array)} байт")
    
    # Bytes - ещё эффективнее для байтовых данных
    if all(0 <= x <= 255 for x in range(100)):
        byte_data = bytes(range(100))
        print(f"Bytes: {sys.getsizeof(byte_data)} байт")

data_structure_optimization()
def data_structure_optimization():
    # Сравнение различных способов хранения данных
    import array
    
    # Обычный список
    regular_list = [i for i in range(1000)]
    print(f"Обычный список: {sys.getsizeof(regular_list)} байт")
    
    # Array - более эффективен для числовых данных
    int_array = array.array('i', range(1000))
    print(f"Array: {sys.getsizeof(int_array)} байт")
    
    # Bytes - ещё эффективнее для байтовых данных
    if all(0 <= x <= 255 for x in range(100)):
        byte_data = bytes(range(100))
        print(f"Bytes: {sys.getsizeof(byte_data)} байт")

data_structure_optimization()

Заключение

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

  1. Всё является объектом - Python создаёт объекты в куче и работает со ссылками на них

  2. Различайте mutable и immutable - это определяет поведение при передаче в функции

  3. Используйте оптимизации - интернирование строк, кеширование чисел, slots для классов

  4. Мониторьте память - особенно важно в долгоживущих приложениях

  5. Изучайте новые возможности - Python 3.13 принёс JIT-компиляцию и улучшения в многопоточности

Современный Python становится всё более производительным, но знание основ остаётся ключом к написанию качественного кода. Надеюсь, эта статья поможет вам лучше понимать, что происходит "под капотом" вашего Python-кода.


Если у вас есть вопросы или дополнения - пишите в комментариях. Делитесь своими кейсами и примерами оптимизации памяти в Python!

Комментарии (5)


  1. nikolz
    17.06.2025 17:21

    Как устроены переменные в Python

    Можете объяснить, что нового Вы рассказали относительно учебников из интернета?

    Не лучший пересказ того, что есть в интернете.

    Например это:

    https://pythonchik.ru/osnovy/peremennye-v-python

    https://skillbox.ru/media/code/peremennye-v-python-chto-eto-takoe-i-kakie-oni-byvayut/

    --------------------

    Вот Алиса Яндекса рассказывает и дает ссылки на первоисточники :

    Внутреннее представление переменных в языке Python заключается в том, что переменная хранит не само значение, а ссылку на его адрес в памяти. Это связано с динамической типизацией языка: значения присваиваются переменным не при компиляции, а во время выполнения программы. skillbox.rupracticum.yandex.ru

    Структура переменных

    Переменная состоит из трёх частей:

    1. Имя (идентификатор) — название, придуманное программистом для обращения к переменной.

    2. Значение — информация, которая хранится в памяти компьютера и с которой работает программа. Значение всегда принадлежит к какому-либо типу данных.

    3. Адрес — номер ячейки памяти, в которой хранится значение переменной.

     skillbox.ru

    Тип переменной определяется исходя из значения, которое ей присвоено. Например, при присвоении строки в двойных или одинарных кавычках переменная имеет тип 

    str

    , а при присвоении целого числа — 

    int

    metanit.com

    Примеры кода

    Чтобы продемонстрировать внутреннее представление переменных, можно использовать функцию 

    id()

    . Она возвращает уникальный идентификатор объекта, на который ссылается переменная. skillbox.rudevpractice.rupythonchik.ru

    Пример:

    x = 4
    print(id(x))  # Выведет 2056817043856 — адрес объекта с числом 4.  
    Скопировать

    В этом примере при инициализации переменной 

    x

     программа создала объект с числом 4, принадлежащий к классу 

    int

    . Внутри переменной содержится не сам объект, а его адрес. skillbox.ru


  1. avshkol
    17.06.2025 17:21

    # Большие числа могут создавать новые объекты x = 1000 y = 1000 print(f"x is y: {x is y}") # Обычно False

    Вот это интересно: когда могут, а когда не могут?


  1. Alex283
    17.06.2025 17:21

    ужас, что только не придумают в новых языках программированиях, чтобы программа работала еще медленее, чем у её предшественников!


  1. dkir70
    17.06.2025 17:21

    В разделе про множественное присваивание два раза один и тот же пример.


  1. DmitriyReztsov
    17.06.2025 17:21

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