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

Зачем нужен frozenset?

1.2 Junior🔥 111 комментариев
#Python Core

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

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

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

# frozenset: Неизменяемое множество

frozenset — это неизменяемая версия set. Это значит, что после создания frozenset не может быть изменен. Это мощный инструмент для определенных случаев.

Основная разница: set vs frozenset

# set — изменяемый
s = {1, 2, 3}
s.add(4)       # Работает
s.remove(1)    # Работает
s[0]           # TypeError — нет индексирования

# frozenset — неизменяемый
fs = frozenset([1, 2, 3])
fs.add(4)      # AttributeError — нет метода add
fs.remove(1)   # AttributeError — нет метода remove
fs[0]          # TypeError — нет индексирования

# Зато frozenset hashable
hash(fs)       # Работает: 8765432101
hash(s)        # TypeError: unhashable type: 'set'

Главные причины использовать frozenset

1. Использование как ключа в dict (САМАЯ ЧАСТАЯ ПРИЧИНА)

Поскольку frozenset hashable, его можно использовать как ключ в словаре или элемент другого set.

# Плохо — set не может быть ключом
data = {}
key = {1, 2, 3}
data[key] = "value"  # TypeError: unhashable type: 'set'

# Хорошо — frozenset может быть ключом
data = {}
key = frozenset([1, 2, 3])
data[key] = "value"  # Работает!
print(data[key])     # "value"

Практический пример: Кеширование комбинаций

def calculate_result(parameters):
    """Вычисляет результат для набора параметров."""
    cache = {}
    
    # Параметры — это кортежи (хеш-устойчивые)
    # Но если параметры более сложные, нужен frozenset
    
    # Приходит набор настроек: {"debug": True, "mode": "prod", ...}
    # Хочу кешировать по всем этим настройкам
    
    # Вариант 1: Плохо — convert to string
    key = str(sorted(parameters.items()))  # Может быть не уникально
    
    # Вариант 2: Хорошо — используем frozenset
    key = frozenset(parameters.items())
    
    if key in cache:
        return cache[key]
    
    result = do_expensive_calculation(parameters)
    cache[key] = result
    return result

2. Элемент другого set

# Плохо
sets_of_sets = {
    {1, 2, 3},  # TypeError: unhashable type: 'set'
    {4, 5, 6}
}

# Хорошо
sets_of_sets = {
    frozenset([1, 2, 3]),
    frozenset([4, 5, 6]),
    frozenset([1, 2, 3])  # Дублер будет удален
}
print(len(sets_of_sets))  # 2 (дублер удален благодаря set-у)

# Используем для удаления дублей списков списков
lists = [
    [1, 2, 3],
    [4, 5, 6],
    [1, 2, 3]  # Дублер
]

unique_lists = [list(fs) for fs in {frozenset(l) for l in lists}]
print(unique_lists)  # [[1, 2, 3], [4, 5, 6]]

3. Правильная работа с множественными наборами

# Часто нужно найти, какие наборы параметров мы уже обработали
processed_configs = set()

# Плохо: кортежи могут быть медленнее для больших данных
config1 = ("debug", "true", "env", "prod", "feature_flags", "v2")
processed_configs.add(config1)

# Хорошо: frozenset для произвольного порядка
config1 = frozenset({("debug", "true"), ("env", "prod"), ("feature_flags", "v2")})
processed_configs.add(config1)

# Плюс: порядок не важен
config2 = frozenset({("feature_flags", "v2"), ("env", "prod"), ("debug", "true")})
print(config1 == config2)  # True!
print(config1 in processed_configs)  # True

4. Constancy в API (информирование о неизменяемости)

def get_allowed_colors() -> frozenset:
    """Возвращает неизменяемый набор допустимых цветов."""
    return frozenset(["red", "green", "blue"])

# Потребитель функции видит в типе, что это неизменяемо
colors = get_allowed_colors()
colors.add("yellow")  # AttributeError — явный сигнал

# vs
def get_allowed_colors() -> set:
    """Возвращает набор допустимых цветов."""
    return {"red", "green", "blue"}

# Потребитель может по ошибке изменить
colors = get_allowed_colors()
colors.add("yellow")  # Работает, но может сломать логику

Практические примеры

Пример 1: Поиск пересечений и удаление дублей

def find_duplicate_lists(lists: list[list]) -> list[list]:
    """Находит дублирующиеся списки."""
    seen = set()
    duplicates = []
    
    for lst in lists:
        fs = frozenset(lst)  # Преобразуем в frozenset для hashability
        if fs in seen:
            duplicates.append(lst)
        seen.add(fs)
    
    return duplicates

lists = [
    [1, 2, 3],
    [4, 5, 6],
    [2, 1, 3],  # Дублер (порядок не важен)
    [1, 2, 3],  # Дублер
]

print(find_duplicate_lists(lists))  # [[2, 1, 3], [1, 2, 3]]

Пример 2: Граф и поиск циклов

class Graph:
    def __init__(self):
        self.edges = set()
    
    def add_edge(self, u, v):
        """Добавляет неориентированное ребро (порядок не важен)."""
        edge = frozenset([u, v])  # {u, v} == {v, u}
        self.edges.add(edge)
    
    def has_edge(self, u, v):
        """Проверяет наличие ребра."""
        edge = frozenset([u, v])
        return edge in self.edges

g = Graph()
g.add_edge(1, 2)
g.add_edge(2, 3)

print(g.has_edge(1, 2))  # True
print(g.has_edge(2, 1))  # True (порядок не важен!)
print(len(g.edges))      # 2

Пример 3: Кеширование функций с множественными параметрами

from functools import lru_cache

# Проблема: @lru_cache не работает с unhashable типами
@lru_cache(maxsize=128)
def process_items(items: list):
    # TypeError: unhashable type: 'list'
    return sum(items)

# Решение: используем frozenset
from functools import lru_cache

@lru_cache(maxsize=128)
def process_items(items: frozenset):
    return sum(items)  # Работает!

result1 = process_items(frozenset([1, 2, 3]))
result2 = process_items(frozenset([3, 2, 1]))  # Кеш попадает!

Пример 4: Атомарность операций в многопоточности

import threading

class PermissionManager:
    def __init__(self):
        self.user_permissions = {}  # user_id -> frozenset permissons
        self.lock = threading.Lock()
    
    def set_permissions(self, user_id, permissions: list):
        """Устанавливает permissions (неизменяемо)."""
        with self.lock:
            self.user_permissions[user_id] = frozenset(permissions)
    
    def get_permissions(self, user_id) -> frozenset:
        """Получает permissions (не может быть изменено снаружи)."""
        with self.lock:
            return self.user_permissions.get(user_id, frozenset())
    
    def has_permission(self, user_id, permission: str) -> bool:
        perms = self.get_permissions(user_id)
        return permission in perms

manager = PermissionManager()
manager.set_permissions(1, ["read", "write"])

# Потребитель получает frozenset
perms = manager.get_permissions(1)
perms.add("delete")  # AttributeError — не может изменить!

# Это гарантирует, что permissions остаются корректными

Пример 5: Валидация множественного выбора

class PizzaOrder:
    ALLOWED_TOPPINGS = frozenset(["pepperoni", "mushrooms", "onions", "sausage"])
    
    def __init__(self, toppings: list):
        selected = frozenset(toppings)
        
        # Проверяем, что все выбранные топпинги допустимы
        if not selected.issubset(self.ALLOWED_TOPPINGS):
            invalid = selected - self.ALLOWED_TOPPINGS
            raise ValueError(f"Invalid toppings: {invalid}")
        
        self.toppings = selected
    
    def get_excluded_toppings(self):
        """Возвращает топпинги, которые НЕ выбраны."""
        return self.ALLOWED_TOPPINGS - self.toppings

order = PizzaOrder(["pepperoni", "mushrooms"])
print(order.get_excluded_toppings())  # frozenset({'onions', 'sausage'})

Производительность

import timeit

# set
t_set = timeit.timeit(lambda: {1, 2, 3, 4, 5} in [{1, 2, 3, 4, 5}], number=100000)
# TypeError — set не может быть в list, требует convert

# frozenset
t_frozenset = timeit.timeit(
    lambda: frozenset([1, 2, 3, 4, 5]) in [frozenset([1, 2, 3, 4, 5])],
    number=100000
)
print(f"frozenset: {t_frozenset:.4f}s")

# frozenset в dict
d = {frozenset([1, 2, 3]): "value"}
t_dict_lookup = timeit.timeit(
    lambda: d[frozenset([1, 2, 3])],
    number=100000
)
print(f"dict lookup: {t_dict_lookup:.4f}s")

Когда НЕ использовать frozenset

# Плохо: если нужны быстро менять данные
config = frozenset({"debug": True})
config.add("prod": False)  # AttributeError
# Нужно переделать:
config = frozenset({("debug", True), ("prod", False)})

# Плохо: если нужно сохранять порядок
fs = frozenset([3, 1, 2])
# frozenset не гарантирует порядок
# Используй tuple вместо этого

# Плохо: если нужна большая изменяемость
# frozenset лучше всего для "напиши один раз, читай много раз"

Ключевые выводы

  1. frozenset — неизменяемый set, может быть ключом в dict
  2. Главное применение: Использование как ключа в dict или элемента другого set
  3. Кеширование: Когда нужно кешировать по множеству параметров
  4. API контракт: frozenset явно показывает, что данные неизменяемы
  5. Производительность: O(1) для проверки принадлежности (как set)
  6. Используй когда:
    • Нужен hashable набор
    • Хочешь гарантировать immutability
    • Нужно дедублировать списки списков
    • Кешируешь по множественным параметрам
  7. Не используй когда:
    • Нужна частая модификация
    • Важен порядок элементов