← Назад к вопросам

Какие типы данных эффективней используют память - изменяемые или неизменяемые?

1.0 Junior🔥 161 комментариев
#Python Core

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

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Эффективность памяти: Изменяемые vs Неизменяемые типы

Ответ зависит от контекста использования. Оба подхода имеют свои преимущества.

1. Неизменяемые типы (Immutable)

Примеры: int, float, str, tuple, frozenset

Преимущества по памяти

import sys

# Python интернирует малые объекты (кеширует)
a = 10
b = 10
print(a is b)  # True — одна ячейка памяти!
print(id(a), id(b))  # Одинаковые адреса

# Со строками тоже
str1 = "hello"
str2 = "hello"
print(str1 is str2)  # True — интернировано!

# Проверяем размер
print(sys.getsizeof(10))  # ~28 bytes
print(sys.getsizeof("hello"))  # ~54 bytes

# Это значит, что 1000 переменных с value 10 занимают почти как одна переменная!
a = [10] * 1000  # только один объект 10 в памяти, 1000 ссылок
print(len(set(id(x) for x in a)))  # 1 уникальный id

Когда неизменяемые эффективнее

# ✅ Хорошо: используем кортежи вместо списков для ключей
class CacheWithTuple:
    def __init__(self):
        self.cache = {}  # ключи — неизменяемые кортежи
    
    def get(self, user_id: int, timestamp: int):
        # Кортеж может быть ключом
        key = (user_id, timestamp)
        return self.cache.get(key)

# ❌ Плохо: список не может быть ключом
class CacheWithList:
    def __init__(self):
        self.cache = {}  # ValueError: unhashable type: 'list'
    
    def get(self, user_id: int, timestamp: int):
        key = [user_id, timestamp]  # Ошибка!
        # return self.cache.get(key)

# ✅ Кортежи экономят память в мультиключах
data = [(1, 'a'), (1, 'b'), (1, 'a'), (2, 'b')]  # (1, 'a') считается один раз
print(len(set(data)))  # 3

Кеширование неизменяемых объектов

# Python встроенно кеширует небольшие строки
small_str1 = "abc"
small_str2 = "abc"
print(small_str1 is small_str2)  # True

# Но построенные динамически — нет
large_str1 = "a" * 1000 + "b"
large_str2 = "a" * 1000 + "b"
print(large_str1 is large_str2)  # False — разные объекты
print(sys.getsizeof(large_str1) + sys.getsizeof(large_str2))  # 2KB

2. Изменяемые типы (Mutable)

Примеры: list, dict, set

Преимущества изменяемых

import sys

# Изменяемые типы экономят память при модификации
original_list = [1, 2, 3, 4, 5]  # 80 байт
print(sys.getsizeof(original_list))

# Вместо создания новой копии
original_list.append(6)  # Быстро!
print(sys.getsizeof(original_list))  # 96 байт (небольшой прирост)

# vs неизменяемое
original_tuple = (1, 2, 3, 4, 5)  # 80 байт
print(sys.getsizeof(original_tuple))

modified_tuple = original_tuple + (6,)  # Создаёт новый кортеж
print(sys.getsizeof(modified_tuple))  # 96 байт
print(sys.getsizeof(original_tuple))  # 80 байт (оба в памяти!)

# При большом количестве операций изменяемый + экономнее

Когда изменяемые эффективнее

# ✅ Хорошо: изменяемый список, модифицируем in-place
class DataProcessor:
    def accumulate_numbers(self):
        result = []  # изменяемый
        for i in range(1000000):
            result.append(i)
        return result

# ❌ Плохо: неизменяемый кортеж, создаём новый каждый раз
class DataProcessorTuple:
    def accumulate_numbers(self):
        result = ()  # неизменяемый
        for i in range(1000000):
            result = result + (i,)  # Создаёт новый кортеж КАЖДЫЙ раз!
        return result

# Первый вариант ~ 40MB памяти
# Второй вариант ~ 500MB+ памяти (из-за создания миллионов временных кортежей)

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

import timeit

# Mutable: быстро
code_mutable = """
result = []
for i in range(10000):
    result.append(i)
"""

# Immutable: медленно
code_immutable = """
result = ()
for i in range(10000):
    result = result + (i,)
"""

time_mutable = timeit.timeit(code_mutable, number=100)
time_immutable = timeit.timeit(code_immutable, number=100)

print(f"Mutable: {time_mutable}s")
print(f"Immutable: {time_immutable}s")
# Mutable ~0.02s
# Immutable ~20s (в 1000 раз медленнее!)

3. Сравнение по типам данных

import sys

# ============== Строки ==============

# Immutable: экономят память благодаря интернированию
small_strings = ["hello"] * 10000  # Один объект в памяти
print(sys.getsizeof(small_strings))  # ~80KB (только ссылки)

# Мutable эквивалент: списки строк
string_lists = [["h", "e", "l", "l", "o"]] * 10000
print(sys.getsizeof(string_lists))  # ~150KB (больше)

# ============== Числа ==============

# Immutable: интернирование малых чисел
small_numbers = [5] * 10000  # Один объект 5
print(len(set(id(x) for x in small_numbers)))  # 1

large_numbers = [5000000] * 10000  # Разные объекты
print(len(set(id(x) for x in large_numbers)))  # 10000

# ============== Коллекции ==============

# Dict vs frozenset: обе хешируемые
my_dict = {'a': 1, 'b': 2}
my_frozenset = frozenset([('a', 1), ('b', 2)])

print(sys.getsizeof(my_dict))  # 240 bytes
print(sys.getsizeof(my_frozenset))  # 200 bytes

# frozenset экономнее!

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

Используй неизменяемые когда:

# 1. Нужны ключи словаря или элементы set
class UserCache:
    def __init__(self):
        self.cache = {}  # ключи — кортежи (неизменяемые)
    
    def get(self, user_id: int, version: int):
        return self.cache.get((user_id, version))

# 2. Конфигурация (не должна меняться)
APP_CONFIG = frozenset([('DEBUG', True), ('PORT', 8000)])
# Не случайно изменится

# 3. Данные передаются между потоками (потокобезопасно)
from threading import Thread

def worker(immutable_tuple):
    # Безопасно читать, невозможно случайно изменить
    value = immutable_tuple[0]
    print(value)

data = (42, 'important')
thread = Thread(target=worker, args=(data,))
thread.start()

# 4. Кеширование (мемоизация)
from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_computation(a: int, b: int, c: int) -> int:
    # Кеш работает с неизменяемыми аргументами
    return a + b + c

Используй изменяемые когда:

# 1. Много добавлений/удалений
result = []  # список, не кортеж
for item in huge_dataset:
    if should_include(item):
        result.append(item)  # быстро!

# 2. Нужна модификация на месте
data = [3, 1, 2]
data.sort()  # in-place, без создания копии

# 3. Возможны изменения структуры
config = {'debug': False}  # dict, может измениться
config['debug'] = True  # обновляем

# 4. Большие структуры данных
people = []  # list, экономнее чем tuple при 10к+ добавлений
for i in range(10000):
    people.append({'name': f'user{i}', 'age': i % 100})

5. Гибридный подход

from typing import NamedTuple
from dataclasses import dataclass

# ✅ NamedTuple: неизменяемая, но с именованными полями
class Point(NamedTuple):
    x: int
    y: int

p1 = Point(1, 2)
print(p1 in {p1})  # Хешируемая, можно в dict

# Можно использовать как эффективное хранилище
points = [Point(i, i*2) for i in range(1000000)]
print(sys.getsizeof(points))  # экономно

# ✅ dataclass: удобна для изменения
@dataclass
class User:
    id: int
    name: str
    email: str

user = User(1, 'John', 'john@example.com')
user.email = 'john.new@example.com'  # легко менять

Итоговая таблица

СценарийЛучшеПричина
Ключи dict/setНеизменяемыеХешируемость
Много добавленийИзменяемыеO(1) append vs O(n) создание
Большие структурыИзменяемыеIn-place операции
Константные данныеНеизменяемыеПотокобезопасность
КешированиеНеизменяемыеИнтернирование
Передача между потокамиНеизменяемыеБезопасность

Вывод: Неизменяемые экономят память благодаря интернированию и хешированию. Изменяемые экономят память при частых модификациях. Выбирай в зависимости от use case, а не абстрактно.

Какие типы данных эффективней используют память - изменяемые или неизменяемые? | PrepBro