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
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 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 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 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()