1. Property Decorators

Properties allow you to use methods like attributes, providing a clean way to implement getters, setters, and deleters.

class Temperature:
    def __init__(self, celsius):
        self._celsius = celsius

    @property
    def celsius(self):
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        if value < -273.15:
            raise ValueError("Temperature below absolute zero is not possible")
        self._celsius = value

    @property
    def fahrenheit(self):
        return (self.celsius * 9/5) + 32

    @fahrenheit.setter
    def fahrenheit(self, value):
        self.celsius = (value - 32) * 5/9

temp = Temperature(25)
print(temp.celsius)     # Output: 25
print(temp.fahrenheit)  # Output: 77.0
temp.fahrenheit = 100
print(temp.celsius)     # Output: 37.77777777777778

2. Class Methods and Static Methods

Class methods operate on the class itself, while static methods are utility functions that don’t need access to class or instance attributes.

class Pizza:
    def __init__(self, ingredients):
        self.ingredients = ingredients

    @classmethod
    def margherita(cls):
        return cls(["mozzarella", "tomatoes"])

    @classmethod
    def prosciutto(cls):
        return cls(["mozzarella", "tomatoes", "ham"])

    @staticmethod
    def is_vegetarian(ingredients):
        return "ham" not in ingredients

margherita = Pizza.margherita()
prosciutto = Pizza.prosciutto()
print(Pizza.is_vegetarian(margherita.ingredients))  # Output: True
print(Pizza.is_vegetarian(prosciutto.ingredients))  # Output: False

3. Abstract Base Classes

Abstract Base Classes (ABCs) define a common API for subclasses, ensuring that derived classes implement particular methods.

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

    @abstractmethod
    def move(self):
        pass

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

    def move(self):
        return "Running on four legs"

class Bird(Animal):
    def make_sound(self):
        return "Chirp!"

    def move(self):
        return "Flying"

dog = Dog()
bird = Bird()
print(dog.make_sound(), dog.move())   # Output: Woof! Running on four legs
print(bird.make_sound(), bird.move()) # Output: Chirp! Flying

4. Multiple Inheritance and Method Resolution Order (MRO)

Python supports multiple inheritance, allowing a class to inherit from multiple parent classes.

class A:
    def method(self):
        print("A method")

class B(A):
    def method(self):
        print("B method")

class C(A):
    def method(self):
        print("C method")

class D(B, C):
    pass

d = D()
d.method()  # Output: B method
print(D.mro())  # Output: [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

5. Metaclasses

Metaclasses are classes for classes. They define how classes behave, allowing you to customize class creation.

class SingletonMeta(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]

class Singleton(metaclass=SingletonMeta):
    def __init__(self):
        self.value = None

s1 = Singleton()
s2 = Singleton()
print(s1 is s2)  # Output: True

6. Descriptors

Descriptors define how attribute access is handled, allowing for sophisticated attribute management.

class Age:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        return getattr(obj, f"_{self.name}", None)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError("Age must be an integer")
        if value < 0:
            raise ValueError("Age must be positive")
        setattr(obj, f"_{self.name}", value)

class Person:
    age = Age()

    def __init__(self, name, age):
        self.name = name
        self.age = age

person = Person("Alice", 30)
print(person.age)  # Output: 30
person.age = 31    # This works
try:
    person.age = -5  # This raises a ValueError
except ValueError as e:
    print(e)  # Output: Age must be positive

7. Context Managers

Context managers allow you to allocate and release resources precisely using the with statement.

class File:
    def __init__(self, filename, mode):
        self.filename = filename
        self.mode = mode

    def __enter__(self):
        self.file = open(self.filename, self.mode)
        return self.file

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.file.close()

with File('example.txt', 'w') as f:
    f.write('Hello, World!')
# File is automatically closed after the with block