← Назад к вопросам
Какие типы данных эффективней используют память - изменяемые или неизменяемые?
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, а не абстрактно.