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'])