07: Flying with Python

In this course, we will explore how to utilize the advanced features of Python to solve complex problems and write efficient code. The main topics we will cover include various programming paradigms, advanced data structures, and the powerful built-in module functionalities that Python offers.

1. Advanced Programming Paradigms

Python is a multi-paradigm programming language. It supports procedural, object-oriented, and functional programming, allowing you to take advantage of each as needed. In this section, we will focus primarily on advanced techniques in object-oriented programming (OOP) and functional programming.

1.1 In-depth Object-Oriented Programming

The basic concept of OOP starts with the understanding of classes and objects. However, to design more complex programs, you need to know other concepts as well.

1.1.1 Inheritance and Polymorphism

Inheritance is a feature where a new class inherits the properties and methods of an existing class. By using inheritance, the reusability of the code can be enhanced. Polymorphism allows for the same interface to be used for objects of different classes.

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

def animal_sound(animal):
    print(animal.speak())

dog = Dog()
cat = Cat()

animal_sound(dog)  # Woof!
animal_sound(cat)  # Meow!

The above example is an illustration of polymorphism. By having the speak() method in different class objects, it can be called in the same way within the animal_sound function.

1.1.2 Abstraction and Interfaces

An abstract class is a class that defines a basic behavior, housing one or more abstract methods. An interface can be thought of as a collection of these abstract methods. In Python, abstraction is implemented through the ABC class of the abc module.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.1415 * self.radius * self.radius

circle = Circle(5)
print(circle.area())  # 78.5375

In the above example, the Shape class is an abstract class that defines the abstract method area. The Circle class inherits from Shape and implements the area method.

1.2 Functional Programming

Functional programming uses pure functions to reduce side effects and implements complex behaviors through function composition. Python provides strong functional tools to encourage this style.

1.2.1 Lambda Functions

Lambda functions are anonymous functions defined typically with a single expression. They are useful for writing short and concise functions.

add = lambda x, y: x + y
print(add(5, 3))  # 8

In the above example, lambda defines an anonymous function that adds two parameters.

1.2.2 Higher-Order Functions

A higher-order function is a function that takes another function as an argument or returns it. Python’s map, filter, and reduce are examples that utilize these functional programming techniques.

numbers = [1, 2, 3, 4, 5]
squared = map(lambda x: x**2, numbers)
print(list(squared))  # [1, 4, 9, 16, 25]

In the above example, the map function applies the lambda function to each element in the list to create a new iterator.

2. Advanced Data Structures

Utilizing advanced data structures allows for more efficient handling of complex data operations. Here we will address more complex data structures beyond basic types like lists and dictionaries.

2.1 Collections Module

The Python collections module provides several data structures with specialized purposes. Let’s take a look at a few of them.

2.1.1 defaultdict

defaultdict is a dictionary that automatically creates a default value when a non-existent key is referenced.

from collections import defaultdict

fruit_counter = defaultdict(int)
fruits = ['apple', 'banana', 'apple', 'orange', 'banana', 'apple']

for fruit in fruits:
    fruit_counter[fruit] += 1

print(fruit_counter)  # defaultdict(, {'apple': 3, 'banana': 2, 'orange': 1})

This example demonstrates how to easily count each fruit using defaultdict.

2.1.2 namedtuple

namedtuple is like a tuple but immutable while allowing access to fields by name which enhances the readability of the code.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)

print(p.x, p.y)  # 10 20

By using namedtuple, fields can be accessed by name, allowing for clearer code.

2.2 Heap Queue Module

The heapq module implements a heap queue algorithm, enabling a list to be used as a priority queue.

import heapq

numbers = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0]
heapq.heapify(numbers)  # Convert list to a priority queue

smallest = heapq.heappop(numbers)
print(smallest)  # 0

This allows for quick extraction of the minimum value in the data using a priority queue.

3. Utilizing Advanced Built-in Modules

The rich built-in modules of Python provide various functionalities. Here, we will introduce some modules for advanced tasks.

3.1 itertools Module

The itertools module offers useful functions for dealing with iterators. It is a powerful tool for repetitive data processing.

3.1.1 Combinations and Permutations

Combinations and permutations provide various methods for selecting elements from data sets.

from itertools import combinations, permutations

data = ['A', 'B', 'C']

# Combinations
print(list(combinations(data, 2)))  # [('A', 'B'), ('A', 'C'), ('B', 'C')]

# Permutations
print(list(permutations(data, 2)))  # [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')]

These functions allow for the quick generation of various list combinations.

3.1.2 Handling Iterator Collections

This module provides tools for various iterations such as infinite loops, counting increments, and periodic repetitions.

from itertools import count, cycle

# Infinite count
for i in count(10):
    if i > 15:
        break
    print(i, end=' ')  # 10 11 12 13 14 15

print()  # New line

# Periodic repetition
for i, char in zip(range(10), cycle('ABC')):
    print(char, end=' ')  # A B C A B C A B C A

The above example shows how to utilize infinite loops and periodic repetitions.

3.2 functools Module

The functools module provides functional programming tools, offering various utilities particularly useful for handling functions.

3.2.1 lru_cache Decorator

The @lru_cache decorator is used for memoization, storing computed results to avoid recalculating for the same input.

from functools import lru_cache

@lru_cache(maxsize=None)
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print([fibonacci(n) for n in range(10)])  # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

In the code above, the computed results for the Fibonacci sequence are stored in the cache, saving execution time for the same input.

Conclusion

In this article, we have discussed advanced topics in Python. By effectively utilizing these features, complex problems can be solved efficiently, and high-level code can be written. Let's delve into more topics in the next course and advance towards becoming Python experts.