"The place where code transcends functionality and becomes an art form"
Mixins are a sort of class designed to offer optional methods or functionality to other classes.
They are a form of multiple inheritance, allowing developers to add the same functionality to multiple classes without repeating code.
IMPORTANT: Unlike traditional base classes, mixins are specifically designed to be combined with other classes, not to stand on their own.
They are used to modularize functionality, making it easy to add or remove features from objects without afecting the inheritance hierarchy of the classes. They can:
- Provide a set of methods that can be used in different classes.
- Compose behaviors in classes.
- Add functionality to classes without modifying them directly.
Generally, the main idea is to make mixins generic as possible, defining functionality to use them in different classes which serve different purpose.
A mixin is typically implemented as a class that does not work by itself. As I mentioned before, it must be combined with another class to make sense.
class JsonMixin:
import json
def to_json(self):
return self.json.dumps(self.__dict__)
class Vehicle:
def __init__(self, brand, model):
self.brand = brand
self.model = model
class Car(JsonMixin, Vehicle): # Mixin
def __init__(self, brand, model, engine_type):
super().__init__(brand, model)
self.engine_type = engine_type
my_car = Car("Tesla", "Model S", "Electric")
print(my_car.to_json()) # Call external functionality frim ``JsonMixin`` class
{"brand": "Tesla", "model": "Model S", "engine_type": "Electric"}
In this example, JsonMixin
provides a to_json
method, which is then available to the Car
class through multiple inheritance. This is a simple yet powerful way to add functionality to classes without affecting their logic.
Let's create another mixin that adds logging functionality to any class.
import logging
class LoggingMixin:
@property
def logger(self):
name = '.'.join([self.__class__.__module__, self.__class__.__name__])
return logging.getLogger(name)
def log_info(self, message):
self.logger.info(message)
def log_error(self, message):
self.logger.error(message)
logging.basicConfig(level=logging.INFO)
class DataProcessor(LoggingMixin):
def process_data(self, data):
self.log_info(f"Processing data: {data}")
if data == "error":
self.log_error("An error occurred while processing data.")
processor = DataProcessor()
processor.process_data("some data")
processor.process_data("error")
INFO:__main__.DataProcessor:Processing data: some data
INFO:__main__.DataProcessor:Processing data: error
ERROR:__main__.DataProcessor:An error occurred while processing data.
LoggingMixin
uses the @property
decorator for logger
to ensure that the logger is specific to the class it's mixed into, using the class's fully qualified name.
This makes the logs clearer and more informative because they're automatically tagged with the class name. As well it remains generic ass possible and can be injected into any class being an extremely powerful tool.
1.Single Responsibility: Each mixin should be focused on a single, clear purpose, adhering to the Single Responsibility Principle.
import json
class JsonMixin:
def to_json(self):
"""Serialize data to a JSON format."""
return json.dumps(self.__dict__)
class Person(JsonMixin):
def __init__(self, name, age):
self.name = name
self.age = age
# Usage
person = Person("John Doe", 30)
print(person.to_json()) # {"name": "John Doe", "age": 30}
IMPORTANT: JsonMixin
can be integrated in ANY class, I am intentionally including the same example as above to show that we can use it not only with a class Car
but with a Person
as well.
{"name": "John Doe", "age": 30}
2.Avoid State in Mixins: Ideally, mixins should not store state. If they must, be cautious of conflicts with the classes they are mixed into.
class DebuggingMixin:
def debug_method_call(self, method_name):
print(f"Method called: {method_name}")
class Calculation(DebuggingMixin):
def add(self, a, b):
self.debug_method_call('add')
return a + b
def subtract(self, a, b):
self.debug_method_call('subtract')
return a - b
# Usage
calc = Calculation()
print(calc.add(10, 5))
print(calc.subtract(10, 5))
Method called: add
15
Method called: subtract
5
3.Use Descriptive Names: Since mixins can be combined in various ways, their names should be as descriptive as possible to clarify their purpose and functionality, as everything in Python.
import base64
class EncryptionDecryptionMixin:
def encrypt_data(self, data):
"""Encrypts data using base64 encoding."""
return base64.b64encode(data.encode('utf-8'))
def decrypt_data(self, data):
"""Decrypts data using base64 decoding."""
return base64.b64decode(data).decode('utf-8')
class SecureCommunicator(EncryptionDecryptionMixin):
def send_secure_message(self, message):
encrypted_message = self.encrypt_data(message)
print(f"Sending encrypted message: {encrypted_message}")
def receive_secure_message(self, encrypted_message):
message = self.decrypt_data(encrypted_message)
print(f"Received message: {message}")
comm = SecureCommunicator()
encrypted = comm.encrypt_data("Hello World")
print(f"Encrypted: {encrypted}")
print("Decrypted:", comm.decrypt_data(encrypted))
Encrypted: b'SGVsbG8gV29ybGQ='
Decrypted: Hello World
4.Be Mindful of the Method Resolution Order (MRO): Python's method resolution order means that the order of base classes affects the methods used.
class BaseFeature:
def feature(self):
print("Base feature")
class EnhancementMixin:
def feature(self):
print("Enhanced feature")
class AdditionalFeatureMixin:
def feature(self):
super().feature()
print("Additional feature")
class Product(EnhancementMixin, AdditionalFeatureMixin, BaseFeature):
pass
product = Product()
product.feature() # Will call EnhancementMixin's feature due to MRO
Again, based on my experience mixins are very underestimated, but hopefully you will be able to find where they could be applied potentially.
Metaclasses define how a class behaves, they define the rules for class objects. A class is an instance of a metaclass, just as an object is an instance of a class.
In Python, the type type
is the built-in metaclass that is used by default, but custom metaclasses can be created by inheriting from type
. In order to create a metaclass you need to be inheriting from type
and defining the __new__
or __init__
method.
class Meta(type):
def __new__(cls, name, bases, dct):
# custom logic here
return super().__new__(cls, name, bases, dct)
To use a metaclass, you specify it in the class definition using the metaclass
keyword.
class MyClass(metaclass=Meta):
pass
The __new__
method in Python
is a special method used for creating and returning a new instance of a class.
It is important to understand, unlike __init__
, which initializes an already existing instance, __new__
is responsible for actually creating the instance.
class MyClass:
def __new__(cls):
print("Creating instance")
instance = super().__new__(cls)
return instance
def __init__(self):
print("Initializing instance")
# When you create a new instance of MyClass
obj = MyClass()
# Creating instance
# Initializing instance
In some cases, you might override __new__
when you need to control the creation of a new instance, such as enforcing a Singleton Pattern (ensuring a class only ever has one instance) or modifying the instantiation process of a subclass.
Suppose you want to add a debugging method to a range of classes to print their attributes in a formatted manner for easier debugging.
Instead of adding the method to each class manually, you can create a metaclass that automatically injects this method into any class that uses it, that's how we ensure that we have a controll on the stage of creation of a new instance.
class DebugMeta(type):
def __new__(cls, name, bases, dct):
# Define a new method
def debug(self):
attrs = "\n".join(f"{key}={value}" for key, value in self.__dict__.items())
print(f"Debugging {self.__class__.__name__}:\n{attrs}")
# Add the method to the class
dct["debug"] = debug
return super().__new__(cls, name, bases, dct)
class Product(metaclass=DebugMeta):
def __init__(self, name, price):
self.name = name
self.price = price
class Customer(metaclass=DebugMeta):
def __init__(self, id, name):
self.id = id
self.name = name
# While creating instances
# (before ``__init__`` ``__new__`` will be called and assign attributes of classes which use ``DebugMeta``
product = Product("Laptop", 1200)
customer = Customer("001", "John Doe")
# Using the new method
product.debug()
customer.debug()
Debugging Product:
name=Laptop
price=1200
Debugging Customer:
id=001
name=John Doe
- The
DebugMeta
metaclass defines adebug
method inside the__new__
method and adds it to every class that specifiesDebugMeta
as its metaclass. - By using this metaclass, any class can automatically gain the
debug
method without explicitly defining it. This is powerful for adding functionality like logging, debugging, serialization, and more across multiple classes.
Now, developers have flexibility in extending class functionality and this can be particularly useful in large applications or libraries.
Django, Python web framework, uses metaclasses to define models that represent database tables. The ModelBase
metaclass in Django's ORM system allows developers to define models using simple class syntax, which the metaclass then translates into database fields and tables.
This abstraction enables developers to work with databases in a more Pythonic way, without having to write SQL queries for basic operations.
Example Use Case:
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Meta:
db_table = 'person_table'
models.Model
uses a metaclass to process the class attributes (name
and age
) and convert them into database columns.
The metaclass also handles inheritance, database schema generation, and integrates with Django's migrations system to apply changes to the database schema over time.
Unfortunately, Django framework is beyond the scope of this book, as it is enormously big framework which will require a separate book to be written in order to describe its functionality, but you can refere to the official documentation and try building web apps by yourself, as your knowledge should be enough already.
SQLAlchemy, a popular SQL toolkit and Object-Relational Mapping (ORM) library for Python, utilizes metaclasses to define a declarative base class.
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String)
age = Column(Integer)
The declarative_base()
function in SQLAlchemy uses a metaclass to create a base class, which then automatically maps class properties to database table columns. Due to metaclasses the ORM simplifies the creation of models and their associated database operations.
IMPORTANT: Be careful using metaclasses, they can introduce complexity and should only be used when simpler solutions like class inheritance or decorators are insufficient.
Metaclasses can automatically validate or modify member attributes. This can be useful for type checking or automatically adding getter/setter
methods to attributes.
You can annotate variables, function parameters, and return types using custom classes just as you would with built-in types. This tells the reader of the code, as well as static type checkers, exactly what kind of object is expected.
Example:
class Car:
def __init__(self, make: str, model: str):
self.make = make
self.model = model
def display_car_info(car: Car) -> str:
return f"{car.make} {car.model}"
# Correct usage
my_car: Car = Car("Tesla", "Model S")
info: str = display_car_info(my_car)
wrong_car: int = 123
display_car_info(wrong_car) # Mypy will flag this as an error
When working with type hints that refer to classes defined in external modules, you might encounter a situation where you want to avoid importing those modules directly at runtime.
Python's typing.TYPE_CHECKING
constant can be used in these cases to conditionally import modules for type annotations without affecting runtime performance.
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from some_external_module import ExternalClass
def func(arg: 'ExternalClass') -> None:
pass
Note the use of a string literal 'ExternalClass'
in the function signature. This is a forward declaration, which is necessary because the actual ExternalClass
is not imported at runtime.
Having this practices can reduce circular imports, which is the worst error to be tackled, as well, improve readability and the code will be less prune to errors.
Python's dynamic nature allows for flexibility in handling different types, but there are scenarios where enforcing type safety at runtime is necessary, especially when interfacing with external systems or libraries.
The isinstance()
function checks if an object is an instance of a specific class or a tuple of classes. It's a straightforward way to validate types at runtime, ensuring that the data conforms to the expected type before proceeding.
def process_data(data):
if not isinstance(data, dict):
raise ValueError("Expected a dictionary")
# process data here
In this case we want to work only with a dict
class and its subclasses, inheritance is taken into consideration as well.
Suppose we have a class hierarchy where Animal
is a base class, and Dog
and Cat
are subclasses. We might want to write a function that behaves differently based on whether it's given a Dog
or a Cat
.
class Animal:
pass
class Dog(Animal):
def bark(self):
return "Woof!"
class Cat(Animal):
def meow(self):
return "Meow!"
def make_sound(animal):
if isinstance(animal, Dog):
print(animal.bark())
elif isinstance(animal, Cat):
print(animal.meow())
else:
raise TypeError("make_sound only accepts Dog or Cat instances")
# Usage
make_sound(Dog())
make_sound(Cat())
Woof!
Meow!
In this way, we handle different scenarios based on the object type, calling isinstance()
function.
While isinstance()
checks an object's type against a class or a tuple of classes considering inheritance, type()
is used to get the exact type of an object without considering subclassing. This can be useful for type checking that needs to ignore the inheritance hierarchy.
class MyBaseClass:
pass
class MyDerivedClass(MyBaseClass):
pass
obj = MyDerivedClass()
if type(obj) is MyDerivedClass:
print("obj is exactly MyDerivedClass")
else:
print("obj is not exactly MyDerivedClass")
obj is exactly MyDerivedClass
Or we can implement something similar to the example avoiding subclass interference.
if type(obj) is MyBaseClass:
print("obj is exactly MyBaseClass and not a subclass")
else:
print("obj is a subclass of MyBaseClass or an unrelated class")
obj is a subclass of MyBaseClass or an unrelated class
Based on the table we could see when and which function should be used.
Personally, I don't like using type()
in production code, as it can be dangerous if we want
to work within inheritance paradigm and can lead to unexpected bugs or changing behaivours. But again, you will not find out what is best for your application unless trying.
Feature | isinstance() |
type() |
---|---|---|
Checks inheritance | Yes, considers an object an instance if it's derived from the class. | No, checks for the object's immediate type only. |
Use case | Ideal for polymorphic behavior where subclass instances should be treated as instances of the base class. | When you need to distinguish an object's exact type, especially to differentiate between a class and its subclass. |
Syntax | isinstance(object, class_or_tuple) |
type(object) is SomeClass |
Type guards are constructs that explicitly check and narrow down the type of variables within a certain scope, making it safer to perform operations that are type-specific. You would want to use them when dealing with union types or the Any
type, where the specific type might not be clear
from typing import Union
def double(value: Union[int, str]):
if isinstance(value, str):
# The type of value is narrowed down to str here
return value * 2
elif isinstance(value, int):
# The type of value is narrowed down to int here
return value + value
print(double(11))
print(double('11'))
22
1111
In this code, value
can be either an int
or a str
. The isinstance()
checks act as type guards, narrowing down the type of value
within each block and allowing for type-specific operations. That can useful in case we want to handle different operations with different approaches.
The main concept of duck typing is that, a function can accept any object that has the required attributes or methods, regardless of the object's class.
"If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck"
def quack_and_fly(thing):
thing.quack()
thing.fly()
print("This thing looks like a duck and quacks like a duck.")
class Duck:
def quack(self):
print("Quack, quack!")
def fly(self):
print("Flap, flap!")
class Airplane:
def fly(self):
print("Whoosh!")
class Person:
def quack(self):
print("I'm quacking like a duck!")
def fly(self):
print("I'm flapping my arms!")
duck = Duck()
airplane = Airplane()
person = Person()
quack_and_fly(duck) # This works fine
quack_and_fly(person) # This works fine
# This will raise an AttributeError, because `airplane` has no `quack` method.
quack_and_fly(airplane)
Quack, quack!
Flap, flap!
This thing looks like a duck and quacks like a duck.
I'm quacking like a duck!
I'm flapping my arms!
This thing looks like a duck and quacks like a duck.
Traceback (most recent call last):
File "<string>", line 32, in <module>
File "<string>", line 2, in quack_and_fly
AttributeError: 'Airplane' object has no attribute 'quack'
- Despite their different types, all these objects can be used in the
quack_and_fly()
function - In case with airplane, we can use
Type Guards
to check the types passed or callhasattr()
function to check if an object has a certain attribute or method before calling it. - Due to the fact that
Python
is a dynamical language, we can't really strictly enforce Python to define the type explicitly.
Duck typing is more than just a theoretical concept. It has practical applications in real-world scenarios, especially in web development.
Consider a web application framework that needs to handle different types of HTTP
requests. Instead of checking the type of request, you can rely on the presence of a method to handle it.
class GetRequestHandler:
def handle(self, request):
# process the GET request
return "Handling GET request"
class PostRequestHandler:
def handle(self, request):
# process the POST request
return "Handling POST request"
def process_request(handler, request):
return handler.handle(request)
# Both GetRequestHandler and PostRequestHandler can be passed to process_request
get_handler = GetRequestHandler()
post_handler = PostRequestHandler()
print(process_request(get_handler, 'test')) # As long as it has a handle method, it works
print(process_request(post_handler, 'test'))
Handling GET request
Handling POST request
In data analysis, you might encounter different data sources. Here's how duck typing allows you to write generic data loading functions:
class CSVDataLoader:
def load_data(self, source):
print(f"Loading data from a CSV file: {source}")
class ExcelDataLoader:
def load_data(self, source):
print(f"Loading data from an Excel file: {source}")
class SQLDataLoader:
def load_data(self, source):
print(f"Loading data from a SQL database: {source}")
def load_data_from_any_source(loader, source):
loader.load_data(source)
csv_loader = CSVDataLoader()
excel_loader = ExcelDataLoader()
sql_loader = SQLDataLoader()
load_data_from_any_source(csv_loader, "data.csv")
load_data_from_any_source(excel_loader, "data.xlsx")
load_data_from_any_source(sql_loader, "database.sqldb")
Loading data from a CSV file: data.csv
Loading data from an Excel file: data.xlsx
Loading data from a SQL database: database.sqldb
IMPORTANT: Duck typing requires careful handling, though it has a high flexebility, it can lead to potential pitfalls inside the app and situations where it's a disadvantage, rather than an advantage.
During designing your application, don't hesitate to look at this table, it can help you to decide the best approach to be used:
OOP Concept | Advantages | Potential Drawbacks | Use-Case Scenarios |
---|---|---|---|
Duck Typing | - Flexibility - Less boilerplate - Natural polymorphism |
- Possible runtime errors - Less explicit type safety |
- Small scripts - When behavior is a priority over type |
class Duck:
def quack(self):
print("Quack")
def make_sound(animal):
animal.quack()
daffy = Duck()
make_sound(daffy)
OOP Concept | Advantages | Potential Drawbacks | Use-Case Scenarios |
---|---|---|---|
Explicit Type Checking | - Clear type contracts - Compile-time error detection |
- More boilerplate - Less flexibility |
- Large systems - Safety-critical applications |
def process_data(data):
if not isinstance(data, dict):
raise ValueError("Expected a dictionary")
# process data here
OOP Concept | Advantages | Potential Drawbacks | Use-Case Scenarios |
---|---|---|---|
Abstract Base Classes | - Enforces an interface - Explicit contracts between code parts |
- Requires more upfront design - Can be overkill for simple cases |
- Plugin systems - Framework development |
from abc import ABC, abstractmethod
class AbstractAnimal(ABC):
@abstractmethod
def sound(self):
pass
class Dog(AbstractAnimal):
def sound(self):
print("Woof!")
fido = Dog()
fido.sound()
OOP Concept | Advantages | Potential Drawbacks | Use-Case Scenarios |
---|---|---|---|
Static Typing | - Early error detection - Improved IDE support and code completion |
- Additional complexity in annotations - Steeper learning curve for new Python users |
- Large codebases - Applications with a long lifecycle |
def greet(name: str) -> str:
return f'Hello, {name}'
greeting = greet("Alice")
print(greeting)
Ultimately, the choice of when and how to use these concepts depends on the specific requirements of your project, your team's preferences, and the need to balance development speed with code safety and maintainability.
Personally, I prefer more explicit type checking, static typing for large projects, and Abstract Base Classes. Not a big fun of duck typing, as all the time you have a feeling that something will break :)
Enough theory for today, try it yourself!
What is the primary purpose of using mixins in object-oriented programming?
A) To serve as the primary base class for all classes in an application.
B) To provide a set of methods that can be used in various unrelated classes to enhance functionality without using inheritance.
C) To enforce strict data typing in Python.
D) To replace the functionality of interfaces in Python.
In the context of duck typing, which of the following statements is true?
A) Duck typing refers to the type system used by Python to manage memory more efficiently.
B) Duck typing allows a function to accept any object that meets the required interface, regardless of its specific type.
C) Duck typing is a programming style where the type of the variable is known only at runtime.
D) Duck typing requires classes to inherit from a common base to be interchangeable in a function.
What does this sippet illustrate?
class EncryptionMixin:
def encrypt(self, message):
# Simulated encryption logic
return message[::-1]
class DecryptionMixin:
def decrypt(self, message):
# Simulated decryption logic
return message[::-1]
class SecureCommunicator(EncryptionMixin, DecryptionMixin):
pass
comm = SecureCommunicator()
encrypted = comm.encrypt("hello")
print(comm.decrypt(encrypted))
A) Singleton pattern
B) Factory method pattern
C) Use of mixins for adding encryption and decryption methods to a class
D) The use of the decorator pattern to enhance method functionality
What is a metaclass in Python primarily used for?
A) To prevent the modification of a class once it has been created.
B) To define a class for an ORM framework like SQLAlchemy or Django.
C) To create classes and control the behavior of class creation.
D) To implement mandatory methods in derived classes like an abstract base class.
Which Python feature allows the creation of a class whose instances can only have one instantiation no matter how many times the class is instantiated?
A) Encapsulation
B) Inheritance
C) Metaclass with Singleton Pattern
D) Duck typing with type hints
I want you to be creative and sumbit H/W tasks for review in the related MR. Find your own usage of patterns and techniques described above!