Python: Advanced 1

Contents

Recursion

Learn about recursive functions, base cases, and how recursion is used in Python. See the full playlist for more!

# Factorial (classic recursion)
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n-1)

print(factorial(5))  # 120

# Fibonacci (inefficient recursion)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print([fib(i) for i in range(7)])

# Recursion with memoization
def fib_memo(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

print([fib_memo(i) for i in range(10)])

# Recursion edge case: max depth
import sys
sys.setrecursionlimit(2000)
# Be careful: hitting the recursion limit will raise RecursionError

Advanced OOP: Abstract Base Classes, Multiple Inheritance, Mixins

Explore Python's abc module, multiple inheritance, and mixin patterns.

from abc import ABC, abstractmethod

# Abstract Base Class
class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, r):
        self.r = r
    def area(self):
        return 3.14 * self.r * self.r

# Multiple inheritance
class LoggerMixin:
    def log(self, msg):
        print(f"LOG: {msg}")

class Rectangle(Shape, LoggerMixin):
    def __init__(self, w, h):
        self.w = w
        self.h = h
    def area(self):
        self.log(f"Calculating area for {self.w}x{self.h}")
        return self.w * self.h

rect = Rectangle(3, 4)
print(rect.area())

# MRO (Method Resolution Order)
print(Rectangle.__mro__)

Metaclasses

Metaclasses let you control class creation. They are the "classes of classes" in Python.

# Basic metaclass
class UppercaseAttributes(type):
    def __new__(cls, name, bases, dct):
        uppercase = {}
        for k, v in dct.items():
            if not k.startswith('__'):
                uppercase[k.upper()] = v
            else:
                uppercase[k] = v
        return super().__new__(cls, name, bases, uppercase)

class MyClass(metaclass=UppercaseAttributes):
    foo = 'bar'

print(hasattr(MyClass, 'foo'))      # False
print(hasattr(MyClass, 'FOO'))      # True
print(MyClass.FOO)

Descriptors & Properties

Descriptors and the @property decorator let you control attribute access in Python.

# Descriptor example
class Positive:
    def __init__(self, name):
        self.name = name
    def __get__(self, instance, owner):
        return instance.__dict__[self.name]
    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Must be positive!")
        instance.__dict__[self.name] = value

class Account:
    balance = Positive('balance')
    def __init__(self, balance):
        self.balance = balance

a = Account(100)
a.balance = 200
# a.balance = -10  # Raises ValueError

# Property decorator
class Celsius:
    def __init__(self, temp=0):
        self._temp = temp
    @property
    def temp(self):
        return self._temp
    @temp.setter
    def temp(self, value):
        if value < -273.15:
            raise ValueError("Below absolute zero!")
        self._temp = value

c = Celsius(25)
c.temp = 30
# c.temp = -300  # Raises ValueError

Closures & Advanced Decorators

Closures capture variables from enclosing scopes. Decorators are functions that modify other functions.

# Closure example
def make_multiplier(x):
    def multiplier(n):
        return x * n
    return multiplier

double = make_multiplier(2)
print(double(5))  # 10

# Decorator with arguments
def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

greet("PySank")

# Class-based decorator
class CallCounter:
    def __init__(self, func):
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count}")
        return self.func(*args, **kwargs)

@CallCounter
def say_hi():
    print("Hi!")

say_hi()
say_hi()