Python Collections Module

Python Collections Module

The Python collections module provides a variety of specialized container types to optimize different storage and retrieval operations. Here’s a breakdown of the key classes:

1. Counter

A Counter is a dictionary subclass for counting hashable objects. It stores elements as dictionary keys and their counts as dictionary values. You can initialize a Counter with an iterable, a dictionary, or keyword arguments.

Example:

from collections import Counter

# With sequence of items
print(Counter(['B','B','A','B','C','A','B','B','A','C']))
# Output: Counter({'B': 5, 'A': 3, 'C': 2})

# with dictionary
print(Counter({'A': 3, 'B': 5, 'C': 2}))
# Output: Counter({'B': 5, 'A': 3, 'C': 2})

# with keyword arguments
print(Counter(A=3, B=5, C=2))
# Output: Counter({'B': 5, 'A': 3, 'C': 2})
2. OrderedDict

OrderedDict is a dictionary subclass that maintains the order of items as they are inserted. Deleting and reinserting a key moves it to the end.

Example:

from collections import OrderedDict

# Regular dictionary
d = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
print("This is a Dict:")
for key, value in d.items():
    print(key, value)
# Output:
# a 1
# b 2
# c 3
# d 4

# Ordered dictionary
od = OrderedDict(d)
print("\nThis is an Ordered Dict:")
for key, value in od.items():
    print(key, value)
# Output:
# a 1
# b 2
# c 3
# d 4
3. DefaultDict

A DefaultDict provides default values for missing keys to avoid KeyError. It’s initialized with a default factory function.

Example:

from collections import defaultdict

# DefaultDict with int
d = defaultdict(int)
L = [1, 2, 3, 4, 2, 4, 1, 2]
for i in L:
    d[i] += 1
print(d)
# Output: defaultdict(<class 'int'>, {1: 2, 2: 3, 3: 1, 4: 2})

# DefaultDict with list
d = defaultdict(list)
for i in range(5):
    d[i].append(i)
print(d)
# Output: defaultdict(<class 'list'>, {0: [0], 1: [1], 2: [2], 3: [3], 4: [4]})
4. ChainMap

ChainMap combines multiple dictionaries into one. This is useful for managing multiple contexts in a single view.

Example:

from collections import ChainMap

d1 = {'a': 1, 'b': 2}
d2 = {'c': 3, 'd': 4}
d3 = {'e': 5, 'f': 6}
c = ChainMap(d1, d2, d3)
print(c)
# Output: ChainMap({'a': 1, 'b': 2}, {'c': 3, 'd': 4}, {'e': 5, 'f': 6})

# Accessing keys and values
print(c['a'])
# Output: 1
print(c.values())
# Output: ValuesView(ChainMap({'a': 1, 'b': 2}, {'c': 3, 'd': 4}, {'e': 5, 'f': 6}))
print(c.keys())
# Output: KeysView(ChainMap({'a': 1, 'b': 2}, {'c': 3, 'd': 4}, {'e': 5, 'f': 6}))

# Adding new dictionary
chain = c.new_child({'f': 7})
print(chain)
# Output: ChainMap({'f': 7}, {'a': 1, 'b': 2}, {'c': 3, 'd': 4}, {'e': 5, 'f': 6})
5. NamedTuple

NamedTuple extends tuple with named fields, making code clearer.

Example:

from collections import namedtuple

Student = namedtuple('Student', ['name', 'age', 'DOB'])
S = Student('Nandini', '19', '2541997')
print(S[1])  # Output: 19
print(S.name)  # Output: Nandini

# _make() and _asdict()
li = ['Manjeet', '19', '411997']
print(Student._make(li))
# Output: Student(name='Manjeet', age='19', DOB='411997')
print(S._asdict())
# Output: OrderedDict([('name', 'Nandini'), ('age', '19'), ('DOB', '2541997')])
6. Deque

A Deque (double-ended queue) allows fast appends and pops from both ends.

Example:

from collections import deque

# Declaring deque
queue = deque(['name', 'age', 'DOB'])
print(queue)
# Output: deque(['name', 'age', 'DOB'])

# Append and appendleft
de = deque([1, 2, 3])
de.append(4)
print(de)
# Output: deque([1, 2, 3, 4])
de.appendleft(6)
print(de)
# Output: deque([6, 1, 2, 3, 4])

# Pop and popleft
de.pop()
print(de)
# Output: deque([6, 1, 2, 3])
de.popleft()
print(de)
# Output: deque([1, 2, 3])
7. UserDict, UserList, UserString

These classes allow users to subclass and create modified dictionary, list, or string behaviors by acting as wrappers around built-in types.

UserDict Example:

from collections import UserDict

class MyDict(UserDict):
    def __del__(self):
        raise RuntimeError("Deletion not allowed")
    def pop(self, s=None):
        raise RuntimeError("Deletion not allowed")
    def popitem(self, s=None):
        raise RuntimeError("Deletion not allowed")

d = MyDict({'a': 1, 'b': 2, 'c': 3})
d.pop(1)
# Output: RuntimeError: Deletion not allowed

UserList

from collections import UserList

class MyList(UserList):
    def remove(self, s=None):
        raise RuntimeError("Deletion not allowed")
    def pop(self, s=None):
        raise RuntimeError("Deletion not allowed")

L = MyList([1, 2, 3, 4])
L.append(5)
print(L)
# Output: [1, 2, 3, 4, 5]
L.remove()
# Output: RuntimeError: Deletion not allowed

UserString

from collections import UserString

class MyString(UserString):
    def append(self, s):
        self.data += s
    def remove(self, s):
        self.data = self.data.replace(s, "")

s1 = MyString("Hello")
s1.append(" World")
print(s1)
# Output: Hello World
s1.remove("l")
print(s1)
# Output: Heo Word

NamedTuple in Python

In Python, NamedTuple from the collections module allows you to create simple, lightweight data structures that are similar to classes but don’t require a full class definition. NamedTuples combine the flexibility of dictionaries with the advantages of iteration and named access. This is particularly useful for creating objects with a predefined structure.

Python NamedTuple Syntax

To define a NamedTuple, use:

namedtuple(typename, field_names)
  • typename – The name of the NamedTuple.
  • field_names – A list of fields or attributes within the NamedTuple.

NamedTuple Example in Python

# Importing namedtuple from collections
from collections import namedtuple

# Defining a NamedTuple for Student
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Adding values to the NamedTuple
S = Student('Alex', '21', '01011999')

# Access using index
print("The student's age by index is:", S[1])

# Access using name
print("The student's name by field name is:", S.name)

Output:

The student's age by index is: 21
The student's name by field name is: Alex
Operations on NamedTuple

Operations supported by NamedTuples include:

1. Creating a NamedTuple in Python: You can create a NamedTuple class with namedtuple().

from collections import namedtuple

# Defining a Point NamedTuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(x=3, y=4)
print(p.x, p.y)

Output:

3 4

2. Accessing NamedTuple Elements: NamedTuples provide different access methods.

3. Access by Index: Fields in a NamedTuple are ordered, allowing access by index.

from collections import namedtuple

# Defining Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Creating an instance
S = Student('Alex', '21', '01011999')

# Accessing age using index
print("The student's age by index is:", S[1])

Output:

The student's age by index is: 21

4. Access by Field Name: NamedTuple fields can be accessed by their field names.

from collections import namedtuple

# Creating a Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Adding values
S = Student('Alex', '21', '01011999')

# Accessing by field name
print("The student's name by field name is:", S.name)

Output:

The student's name by field name is: Alex

5. Access Using getattr(): Another way to retrieve values is with getattr().

from collections import namedtuple

# Defining Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Creating an instance
S = Student('Alex', '21', '01011999')

# Using getattr to access DOB
print("The student's DOB is:", getattr(S, 'DOB'))

Output:

The student's DOB is: 01011999
Conversion Operations for NamedTuple

NamedTuples offer conversion functions:

1. Conversion Using _make(): This function converts an iterable into a NamedTuple instance.

from collections import namedtuple

# Defining Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Creating an iterable
data = ['Chris', '20', '02111999']

# Converting the list to a NamedTuple
student_instance = Student._make(data)
print("NamedTuple from list:", student_instance)

Output:

NamedTuple from list: Student(name='Chris', age='20', DOB='02111999')
-

2. Conversion Using _asdict(): Converts a NamedTuple to an OrderedDict.

from collections import namedtuple

# Defining a Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])
S = Student('Alex', '21', '01011999')

# Converting to OrderedDict
print("OrderedDict instance:", S._asdict())

Output:

OrderedDict instance: OrderedDict([('name', 'Alex'), ('age', '21'), ('DOB', '01011999')])

3. Conversion Using ** Operator: Converts a dictionary into a NamedTuple.

from collections import namedtuple

# Defining Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Creating a dictionary
data_dict = {'name': 'Lily', 'age': 23, 'DOB': '15051998'}

# Converting to NamedTuple
student_instance = Student(**data_dict)
print("NamedTuple from dictionary:", student_instance)

Output:

NamedTuple from dictionary: Student(name='Lily', age=23, DOB='15051998')
Additional NamedTuple Attributes and Methods

1. _fields: Provides all field names as a tuple.

from collections import namedtuple

# Defining a NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])
S = Student('Alex', '21', '01011999')

print("Field names:", S._fields)

Output:

Field names: ('name', 'age', 'DOB')

2. _replace(): Creates a new NamedTuple with one or more values replaced.

from collections import namedtuple

# Defining a Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])
S = Student('Alex', '21', '01011999')

# Replacing a field value
new_student = S._replace(name='John')
print("Updated NamedTuple:", new_student)

Output:

Updated NamedTuple: Student(name='John', age='21', DOB='01011999')

3. __new__(): Creates a new instance by directly calling __new__ on the NamedTuple class.

from collections import namedtuple

# Defining Student NamedTuple
Student = namedtuple('Student', ['name', 'age', 'DOB'])

# Creating a new instance using __new__()
new_instance = Student.__new__(Student, 'Emily', '22', '05061999')
print("New instance:", new_instance)

Output:

New instance: Student(name='Emily', age='22', DOB='05061999')

Deque in Python

In Python, the deque (Doubly Ended Queue) is implemented using the collections module. Deques are more efficient than lists when performing append and pop operations from both ends, providing O(1) time complexity for these operations compared to O(n) in lists.

Types of Restricted Deque

1. Input Restricted Deque: Allows insertions at only one end, but deletions can happen from both ends.
2. Output Restricted Deque: Allows deletions at only one end, but insertions can happen at both ends.

Example: Creating a deque

from collections import deque

# Creating a deque with initial elements
queue = deque(['item1', 'item2', 'item3'])

print("Initial deque:", queue)

Output:

Initial deque: deque(['item1', 'item2', 'item3'])
Basic Operations on deque

1. Appending Items

  • append(): Adds an element to the right end of the deque.
  • appendleft(): Adds an element to the left end of the deque.
from collections import deque

# Initializing deque
deq = deque([10, 20, 30])

# Adding elements
deq.append(40)
deq.appendleft(5)

print("Deque after append and appendleft:", deq)

Output:

Deque after append and appendleft: deque([5, 10, 20, 30, 40])

2. Popping Items

  • pop(): Removes an element from the right end.
  • popleft(): Removes an element from the left end.
from collections import deque

# Initializing deque
deq = deque([10, 20, 30, 40])

# Removing elements
deq.pop()
deq.popleft()

print("Deque after pop and popleft:", deq)

Output:

Deque after pop and popleft: deque([20, 30])

3. Accessing and Modifying Items

  • index(ele, start, end): Finds the first occurrence of ele between the start and end indices.
  • insert(i, a): Inserts element a at position i.
  • remove(): Removes the first occurrence of a specified element.
  • count(): Counts the occurrences of an element.
from collections import deque

# Initializing deque
deq = deque([1, 2, 2, 3, 4, 3, 2])

# Using index and count
position = deq.index(3, 0, 6)
deq.insert(2, 5)
count_two = deq.count(2)
deq.remove(3)

print("Position of first 3:", position)
print("Deque after insert and remove:", deq)
print("Count of 2 in deque:", count_two)

Output:

Position of first 3: 3
Deque after insert and remove: deque([1, 2, 5, 2, 4, 3, 2])
Count of 2 in deque: 3

4. Finding Size of a deque

  • len(): Returns the current size of the deque.
from collections import deque

# Initializing deque
deq = deque([1, 2, 3, 4])

# Checking the size of deque
print("Size of deque:", len(deq))

Output:

Size of deque: 4

5. Accessing Front and Back Elements

  • deq[0]: Accesses the front element.
  • deq[-1]: Accesses the back element.
from collections import deque

# Initializing deque
deq = deque([10, 20, 30])

# Accessing front and back elements
print("Front element:", deq[0])
print("Back element:", deq[-1])

Output:

Front element: 10
Back element: 30
Complexity Analysis
MethodTime ComplexityAuxiliary Space
append()O(1)O(1)
appendleft()O(1)O(1)
pop()O(1)O(1)
popleft()O(1)O(1)
index()O(N)O(1)
insert()O(N)O(1)
remove()O(N)O(1)
count()O(N)O(1)
extend()O(K)O(1)
extendleft()O(K)O(1)
reverse()O(N)O(1)
rotate()O(K)O(1)

This covers the core functions and efficiency of deque in Python.

ChainMap in Python

In Python, the ChainMap container in the collections module allows you to combine multiple dictionaries into a single view. This is especially useful when managing multiple contexts or namespaces, as ChainMap prioritizes the order of the dictionaries, giving precedence to the first dictionary in case of duplicate keys.

Example of Using ChainMap

# Importing ChainMap from collections
from collections import ChainMap

# Initializing dictionaries
dict1 = {'x': 10, 'y': 20}
dict2 = {'z': 30, 'w': 40}
dict3 = {'m': 50, 'n': 60}

# Defining a ChainMap
chain = ChainMap(dict1, dict2, dict3)

# Displaying the ChainMap
print(chain)

Output:

ChainMap({'x': 10, 'y': 20}, {'z': 30, 'w': 40}, {'m': 50, 'n': 60})
Operations on ChainMap

Access Operations

  • keys(): Displays all keys across the dictionaries in the ChainMap.
  • values(): Displays values for each key in the ChainMap.
  • maps: Displays all key-value pairs across all dictionaries.
# Importing collections for ChainMap operations
import collections

# Initializing dictionaries
dict_a = {'p': 1, 'q': 2}
dict_b = {'q': 3, 'r': 4}

# Initializing ChainMap
combined_chain = collections.ChainMap(dict_a, dict_b)

# Printing the ChainMap contents
print("ChainMap contents:", combined_chain.maps)

# Displaying all keys
print("All keys in ChainMap:", list(combined_chain.keys()))

# Displaying all values
print("All values in ChainMap:", list(combined_chain.values()))

Output:

ChainMap contents: [{'p': 1, 'q': 2}, {'q': 3, 'r': 4}]
All keys in ChainMap: ['p', 'q', 'r']
All values in ChainMap: [1, 2, 4]

Manipulating Operations

  • new_child(): Adds a new dictionary at the start of the ChainMap.
  • reversed(): Reverses the order of dictionaries in the ChainMap.

Example:

# Importing collections for ChainMap operations
import collections

# Initializing dictionaries
dict_x = {'p': 5, 'q': 6}
dict_y = {'q': 7, 'r': 8}
dict_z = {'s': 9}

# Creating ChainMap
chain_map = collections.ChainMap(dict_x, dict_y)

# Displaying the ChainMap contents
print("Original ChainMap:", chain_map.maps)

# Using new_child() to add a new dictionary
new_chain_map = chain_map.new_child(dict_z)

# Displaying the updated ChainMap
print("Updated ChainMap with new child:", new_chain_map.maps)

# Accessing the value of 'q' before reversing
print("Value associated with 'q' before reversing:", new_chain_map['q'])

# Reversing the order of ChainMap dictionaries
new_chain_map.maps = reversed(new_chain_map.maps)

# Accessing the value of 'q' after reversing
print("Value associated with 'q' after reversing:", new_chain_map['q'])

Output:

Original ChainMap: [{'p': 5, 'q': 6}, {'q': 7, 'r': 8}]
Updated ChainMap with new child: [{'s': 9}, {'p': 5, 'q': 6}, {'q': 7, 'r': 8}]
Value associated with 'q' before reversing: 6
Value associated with 'q' after reversing: 7

Python Counter Objects elements()

The Counter class in Python’s collections module is a specialized type of container used to count the frequency of hashable objects. When you pass an iterable to Counter, it creates a hash table with elements as keys and their counts as values.

The elements() method of the Counter class provides an iterator over elements with positive counts, effectively repeating each element according to its count.

  • Parameters: None
  • Return Type: Returns an iterator over the elements with positive counts.

Example 1: Using elements() on a Counter object

# Importing Counter class from collections module
from collections import Counter

# Creating a Counter object from a string
counter_example = Counter("exampleexample")

# Printing the elements of the Counter object
for element in counter_example.elements():
    print(element, end=" ")

Output:

e e e e x x a a m m p l

Example 2: Creating a Counter object from a list

from threading import Timer

def time_up():
    print("\nTime's up! You took too long to respond.")

def ask_question():
    print("What is your favorite color?")
    timer = Timer(5, time_up)  # Set a 5-second timer
    timer.start()

    answer = input()
    timer.cancel()  # Cancel the timer if input is received on time
    return answer

response = ask_question()
print("Your favorite color is:", response)

Output:

Counter({30: 3, 10: 2, 20: 2, 40: 2})
10 : 2
20 : 2
30 : 3
40 : 2
[10, 20, 30, 40]
[2, 2, 3, 2]

OrderedDict in Python

An OrderedDict is a dictionary subclass that retains the order of key insertions. Unlike a regular dict, which does not guarantee order, OrderedDict preserves insertion sequence when iterating.

OrderedDict vs. dict in Python

An OrderedDict keeps items in the order they are added, while a regular dict may not. This behavior makes OrderedDict ideal when the order of entries is important.

Example:
This code demonstrates the differences between a standard dictionary (dict) and an ordered dictionary (OrderedDict). First, it displays the entries in a regular dictionary, where insertion order isn’t assured.

from collections import OrderedDict

print("This is a Dict:\n")
d = {}
d['x'] = 1
d['y'] = 2
d['z'] = 3
d['w'] = 4

for key, value in d.items():
    print(key, value)

print("\nThis is an Ordered Dict:\n")
od = OrderedDict()
od['x'] = 1
od['y'] = 2
od['z'] = 3
od['w'] = 4

for key, value in od.items():
    print(key, value)

Output:

This is a Dict:
x 1
y 2
z 3
w 4

This is an Ordered Dict:
x 1
y 2
z 3
w 4
Key-Value Changes in OrderedDict

Changing the value associated with a key doesn’t affect its position within an OrderedDict. The example below illustrates changing a key’s value while maintaining order.

Example:

from collections import OrderedDict

print("Before:\n")
od = OrderedDict()
od['x'] = 1
od['y'] = 2
od['z'] = 3
od['w'] = 4
for key, value in od.items():
    print(key, value)

print("\nAfter:\n")
od['z'] = 5
for key, value in od.items():
    print(key, value)

Output:

Before:
x 1
y 2
z 3
w 4

After:
x 1
y 2
z 5
w 4
Equality Comparison

Two OrderedDict objects are considered equal only if their contents and insertion order match.

Example:

from collections import OrderedDict

# Two ordered dictionaries with different key order
od1 = OrderedDict([('x', 1), ('y', 2), ('z', 3)])
od2 = OrderedDict([('z', 3), ('y', 2), ('x', 1)])

# Comparing for equality
print(od1 == od2)

Output:

False
Reversing OrderedDict

Though OrderedDict lacks a reverse() method, Python’s reversed() function combined with list() and items() can reverse its contents.

Example:

from collections import OrderedDict

my_dict = OrderedDict([('x', 1), ('y', 2), ('z', 3)])

# Reverse the OrderedDict
reversed_dict = OrderedDict(reversed(list(my_dict.items())))

# Print reversed dictionary
for key, value in reversed_dict.items():
    print(key, value)

Output:

z 3
y 2
x 1