14.4 Frozenset: The Immutable Set
A frozenset is an immutable, hashable version of Python’s built-in set object. While sets are mutable and therefore cannot be used as keys in dictionaries or elements of other sets, frozensets overcome this limitation by guaranteeing that their contents cannot change after creation. This immutability is the cornerstone of their utility and behavior. Conceptually, a frozenset is to a set what a tuple is to a list: a fixed collection for safe use in contexts requiring hashability.
Creation and Basic Operations
A frozenset is created using the frozenset() constructor. It can be built from any iterable, such as a list, tuple, string, or even another set. If no argument is provided, an empty frozenset is created.
# Creating frozensets from different iterables
list_based = frozenset([1, 2, 3, 2, 1]) # Duplicates are removed
tuple_based = frozenset(('a', 'b', 'c'))
string_based = frozenset('hello') # {'h', 'e', 'l', 'o'}
set_based = frozenset({10, 20, 30})
empty_fs = frozenset()
print(list_based) # frozenset({1, 2, 3})
print(string_based) # frozenset({'e', 'h', 'l', 'o'})
Once created, you can perform most standard set operations that return a new frozenset without modifying the original. This includes union (|), intersection (&), difference (-), and symmetric difference (^). These operations work seamlessly with other frozensets and regular sets.
fs1 = frozenset([1, 2, 3, 4])
fs2 = frozenset([3, 4, 5, 6])
s1 = {2, 4, 6}
# Operations with another frozenset
union_result = fs1 | fs2 # frozenset({1, 2, 3, 4, 5, 6})
intersection_result = fs1 & s1 # frozenset({2, 4}) - works with a regular set
print(union_result)
print(intersection_result)
Why Immutability Enables Hashability
The key reason frozenset exists is to provide a hashable set type. An object is hashable if it has a hash value that remains constant throughout its lifetime. Mutable objects like list and set cannot be hashed because a change in their contents would change their hash value, breaking the fundamental contract of hash-based data structures like dictionaries and sets. Since a frozenset is immutable, its contents cannot change, and thus a single, consistent hash value can be computed for it upon creation. This allows it to be used as a dictionary key or an element within another set.
# Using frozensets as dictionary keys
student_courses = {}
fs_math = frozenset(['Math101', 'Calc202'])
fs_bio = frozenset(['Bio101', 'Genetics300'])
student_courses[fs_math] = 'Alice'
student_courses[fs_bio] = 'Bob'
# Even a frozenset can be a key in a set of unique keys
unique_keys = {fs_math, fs_bio}
print(student_courses[fs_math]) # 'Alice'
print(unique_keys) # {frozenset({'Calc202', 'Math101'}), frozenset({'Bio101', 'Genetics300'})}
Membership Testing and Iteration
Like regular sets, frozensets are optimized for fast membership testing using the in keyword. This operation is平均常数时间 complexity, O(1), on average. This makes frozensets excellent for deduplication and membership checks, even when used in large-scale data structures. You can also iterate over the elements of a frozenset, though the order is arbitrary and should not be relied upon.
config_flags = frozenset(['--verbose', '--debug', '--safe-mode'])
# Fast membership test
if '--verbose' in config_flags:
print("Verbose mode is enabled.")
# Iteration (order is not guaranteed)
for flag in config_flags:
print(f"Flag: {flag}")
Common Pitfalls and Best Practices
A frequent pitfall is assuming that a frozenset containing mutable elements is completely immutable. The frozenset itself is immutable—you cannot add, remove, or change its elements. However, if those elements are mutable objects (like lists), the contents of those objects can still be changed. This does not affect the frozenset’s hash because the frozenset only holds references to the objects; the identity of the references remains constant.
# Pitfall: A frozenset holding mutable elements
mutable_list = [1, 2]
fs_with_list = frozenset([mutable_list, 3]) # This will raise a TypeError!
# You cannot put an unhashable list inside any set or frozenset.
# The correct approach is to use tuples if you need immutable sequences.
immutable_tuple = (1, 2)
fs_ok = frozenset([immutable_tuple, 3]) # This works
print(fs_ok) # frozenset({3, (1, 2)})
The best practice is to use frozensets whenever you need an immutable collection of unique elements, particularly for use as keys. They are also valuable for representing fixed configurations, constant lookup tables, or any data that should be considered final and safe from accidental modification. For temporary operations or data that needs to change, the regular set is the appropriate choice.