C13 Data Structures
Namedtuple
# Named tuples
# -----------------------------------------------------------------------------
# namedtuples give tuple-like objects readable field names without extra overhead.
from collections import namedtuple
# Create a namedtuple (label, fields)
# - The label will be used in the representation of the namedtuple
# - The fields will be used to access the namedtuple attributes
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
# Test the namedtuple representation
print(p)
# Test the namedtuple attributes
print(p.x)
print(p.y)
# Test the namedtuple index access
print(p[0])
print(p[1])
Dataclass
# Lightweight data classes
# -----------------------------------------------------------------------------
# Dataclasses eliminate boilerplate when defining classes meant primarily to store data.
import dataclasses
from dataclasses import dataclass, field
from typing import List
@dataclass
class DataClass(object):
name: str
value: int
def __post_init__(self):
self.value = self.value * 2
def __str__(self):
return f"{self.name}: {self.value}"
@dataclass
class DataClassWithDefaults(DataClass):
name: str = "MyData with defaults"
value: int = 0
data: List[int] = field(default_factory=list)
def add_value(self, value):
self.data.append(value)
def __str__(self):
return f"{self.name}: {self.value}, {self.data}"
if __name__ == "__main__":
my_data = DataClass(name="MyData", value=10)
print(my_data)
my_data = DataClassWithDefaults()
my_data.add_value(1)
print(my_data)
Array
# Efficient arrays
# -----------------------------------------------------------------------------
# The array module stores numeric values more efficiently than lists because all elements share the same type.
import array
def test_int():
# Create an array of integers
int_array = array.array('i', [1, 2, 3, 4, 5])
# Convert the array to a bytes object (useful for binary data)
bytes_data = int_array.tobytes()
print(bytes_data)
# Convert a bytes object back to an array
new_array = array.array('i')
new_array.frombytes(bytes_data)
print(new_array)
print()
def test_unicode():
# Use unicode characters
unicode_array = array.array('u', "Здравейте, хора!")
# Convert the array to a bytes object (useful for binary data)
bytes_data = unicode_array.tobytes()
print(bytes_data)
# Convert a bytes object back to an array
new_array = array.array('u')
new_array.frombytes(bytes_data)
print(new_array)
print()
if __name__ == "__main__":
test_int()
test_unicode()
Defaultdict
# Automatic keys with defaultdict
# -----------------------------------------------------------------------------
# defaultdict automatically creates missing keys so your code doesn't need explicit checks.
from collections import defaultdict
def test_default_factory(factory):
d = defaultdict(factory)
# Test the defaultdict
try:
print(d['key1'])
print(d['key2'])
print(d['key3'])
except KeyError as e:
print("KeyError: {}".format(e))
assert True
print()
if __name__ == "__main__":
default_factories = [
None, # Behaves like a regular dictionary
str, # Returns an empty string if the key is not found
int, # Returns 0 if the key is not found
float, # Returns 0.0 if the key is not found
list, # Returns an empty list if the key is not found
tuple, # Returns an empty tuple if the key is not found
dict, # Returns an empty dictionary if the key is not found
set, # Returns an empty set if the key is not found
lambda: 'value', # Returns a default value if the key is not found
]
for default_factory in default_factories:
test_default_factory(default_factory)
Ordered Dict
# Ordered dictionaries
# -----------------------------------------------------------------------------
# OrderedDict remembers the insertion order of keys, which can be useful for reproducible iteration.
from collections import OrderedDict
# Create an OrderedDict
od = OrderedDict()
# Add elements
od['a'] = 1
od['b'] = 2
od['c'] = 3
od['d'] = 4
# Print the OrderedDict
print(od)
# Move 'c' to the end
od.move_to_end('c')
# Print the OrderedDict
print(od)
# Move 'c' to the start
od.move_to_end('c', last=False)
# Print the OrderedDict
print(od)
Counter
# Counting with Counter
# -----------------------------------------------------------------------------
# A Counter is a dictionary subclass for tallying hashable objects quickly.
from collections import Counter
# Create a Counter object
a = Counter('abcdeabcdabcaba')
b = Counter(reversed("abcdeabcdabcaba"))
# Print the Counter object
print(a)
print(b)
# Print the three most common element
print(a.most_common(3))
print(b.most_common(3))
# Add two Counter objects
c = a + b
print(c)
# Subtract two Counter objects
d = a - b
print(d)
Queue
# LIFO queue with LifoQueue
# -----------------------------------------------------------------------------
# LifoQueue behaves like a stack while remaining safe for use with multiple threads.
from six.moves import queue
def empty_queue(customers):
# Test the queue
while True:
try:
item = customers.get(block=False)
print("Get item: {}".format(item))
except queue.Empty as e:
print("Empty: {}".format(e))
break
print()
def fill_queue(customers):
items = ['Ivan', 'Dragan', 'Petkan', 'Stoyan']
for item in items:
customers.put(item)
print("Add item: {}".format(item))
print()
if __name__ == "__main__":
# Create a LIFO queue
q = queue.LifoQueue()
fill_queue(q)
empty_queue(q)
Deque
# Deque as LIFO stack
# -----------------------------------------------------------------------------
# Using deque as a stack provides efficient push and pop operations.
from collections import deque
def emtpy_deque(plates):
# Test the deque
while True:
try:
# Get the item from the right
item = plates.pop()
print("Get item: {}".format(item))
except IndexError as e:
print("Empty: {}".format(e))
break
print()
def fill_deque(plates):
items = ['Soup plate', 'Salad plate', 'Dinner plate', 'Dessert plate']
for item in items:
plates.append(item)
print("Add item: {}".format(item))
print()
if __name__ == "__main__":
# Create a deque
d = deque()
# Fill the deque
fill_deque(d)
# Empty the deque
emtpy_deque(d)
Heapq
# heapq priority queue
# -----------------------------------------------------------------------------
# A heap lets you maintain a priority queue with O(log n) push/pop operations.
import heapq
def empty_heapq(h):
# Test the heapq
while True:
try:
print(heapq.heappop(h))
except IndexError as e:
print("Empty: {}".format(e))
break
print()
def fill_heapq(h):
# Add items to the heapq
heapq.heappush(h, 4)
heapq.heappush(h, 1)
heapq.heappush(h, 7)
if __name__ == "__main__":
# Create a heapq
heap = []
heapq.heapify(heap)
# Fill the heapq
fill_heapq(heap)
# Empty the heapq
empty_heapq(heap)
Chainmap
# Combining dictionaries with ChainMap
# -----------------------------------------------------------------------------
# ChainMap lets you combine several dictionaries into a single view without copying them.
from collections import ChainMap
# Create two dictionaries
dict1 = {'junior': 1, 'mid': 2}
dict2 = {'c': 3, 'd': 4}
# Create a ChainMap
chain = ChainMap(dict1, dict2)
# Print the ChainMap
print(chain)
# Print elements
print(list(chain.items()))
# Find value of a key from dict1
print(chain['a'])
# Find value of a key from dict2
print(chain['c'])