Duck Typing and Polymorphism in Python
The principle of duck typing states: "If it walks like a duck and quacks like a duck, then it is a duck." In Python, this means that an object's behavior determines its classification rather than its inheritance hierarchy. If multiple classes implement the same method signature, they can be treated uniformly.
For instance:
- An object with
__iter__()or__getitem__()is considered iterable. - An object implementing both
__iter__()and__next__()is recognized as an iterator. - Objects defining
__enter__()and__exit__()are context managers.
This flexibility allows developers to add such behaviors by simply implementing the required special methods—no explicit inheritance or interface declaration needed.
class Cat:
def speak(self):
print("Meow")
class Dog:
def speak(self):
print("Woof")
class Duck:
def speak(self):
print("Quack")
animals = [Cat(), Dog(), Duck()]
for animal in animals:
animal.speak()
Using Duck Typing with Built-in Functions
The built-in extend() method expects an iterable. By adding __getitem__() to a custom class, we make it compatible with extend(), even without formally inheriting from any sequence type.
class Team:
def __init__(self, members):
self.members = members
def __getitem__(self, index):
return self.members[index]
team = Team(["Alice", "Bob", "Charlie"])
group = ["David"]
group.extend(team)
print(group) # Output: ['David', 'Alice', 'Bob', 'Charlie']
Abstract Base Classes (ABC)
Python’s abc module enables enforcing method implementation through abstract base classes. This is useful for creating interfaces that derived classes must follow.
For example, using Sized from collections.abc checks whether a class implements __len__():
from collections.abc import Sized
class Department:
def __init__(self, staff):
self.staff = staff
def __len__(self):
return len(self.staff)
dept = Department(["John", "Jane"])
print(isinstance(dept, Sized)) # True
To enforce method definitions, use @abstractmethod:
import abc
class Storage(abc.ABC):
@abc.abstractmethod
def save(self, key, data):
pass
@abc.abstractmethod
def load(self, key):
pass
class MemoryStorage(Storage):
def __init__(self):
self._data = {}
def save(self, key, data):
self._data[key] = data
def load(self, key):
return self._data.get(key)
# The following would raise TypeError at instantiation:
# class BadStorage(Storage): pass
# bad = BadStorage() # Error: not all abstract methods implemented
Difference Between isinstance() and type()
isinstance() respects inheritance; type() does not.
class Parent:
pass
class Child(Parent):
pass
c = Child()
print(isinstance(c, Child)) # True
print(isinstance(c, Parent)) # True
print(type(c) is Child) # True
print(type(c) is Parent) # False
Class Variables vs Instance Variables
Class variables are shared across instances, while instance variables belong to individual objects.
class Item:
category = "electronics"
def __init__(self, name, price):
self.name = name
self.price = price
i1 = Item("Laptop", 999)
i2 = Item("Phone", 699)
print(i1.category) # electronics
print(i2.category) # electronics
Item.category = "gadgets"
print(i1.category) # gadgets
i1.category = "legacy" # Creates an instance variable
print(i1.category) # legacy
print(i2.category) # gadgets
Attribute lookup follows this order: instance namespace → class namespace → parent classes (via MRO).
Method Resolution Order (MRO) and Multiple Inheritance
Python uses the C3 linearization algorithm to determine method lookup order in multiple inheritance scenarios.
class X:
pass
class Y(X):
pass
class Z(X):
pass
class W(Y, Z):
pass
print(W.__mro__)
# (<class '__main__.W'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.X'>, <class 'object'>)
Instance Methods, Class Methods, and Static Methods
- Instance methods: Take
self, operate on instance data. - Class methods: Decorated with
@classmethod, takecls, used for alternative constructors. - Static methods: Decorated with
@staticmethod, no automatic reference passed; grouped logically within the class.
class Clock:
def __init__(self, hour, minute, second):
self.hour = hour
self.minute = minute
self.second = second
def tick(self):
self.second += 1
if self.second == 60:
self.second = 0
self.minute += 1
@classmethod
def from_string(cls, time_str):
h, m, s = map(int, time_str.split(':'))
return cls(h, m, s)
@staticmethod
def is_valid_time(time_str):
try:
h, m, s = map(int, time_str.split(':'))
return 0 <= h < 24 and 0 <= m < 60 and 0 <= s < 60
except ValueError:
return False
def __str__(self):
return f"{self.hour:02}:{self.minute:02}:{self.second:02}"
clock = Clock.from_string("14:35:20")
clock.tick()
print(clock) # 14:35:21
print(Clock.is_valid_time("25:00:00")) # False
Data Hiding and Name Mangling
Python doesn't have true private attributes, but naming an attribute with double underscores (__attr) triggers name mangling: it becomes _ClassName__attr.
class Person:
def __init__(self, birth_year):
self.__birth_year = birth_year
def get_age(self, current_year=2023):
return current_year - self.__birth_year
p = Person(1990)
print(p.get_age()) # 133
print(p._Person__birth_year) # 1990 (accessible via mangled name)
Introspection in Python
Python supports introspection—examining object structure at runtime.
obj.__dict__: Returns a dictionary of writable atributes.dir(obj): Lists all attribute and methods (names only).
class Employee:
title = "Engineer"
def __init__(self, name):
self.name = name
e = Employee("Sam")
print(e.__dict__) # {'name': 'Sam'}
e.__dict__['location'] = 'Berlin'
print(e.location) # Berlin
print(dir(e)[:5]) # Some attribute names including 'location', 'name', etc.
Understanding super() and Method Resolution
super() follows the MRO chain, not just immediate parent. It ensures cooperative multiple inheritance works correctly.
class A:
def __init__(self):
print("A init")
class B(A):
def __init__(self):
print("B init")
super().__init__()
class C(A):
def __init__(self):
print("C init")
super().__init__()
class D(B, C):
def __init__(self):
print("D init")
super().__init__()
d = D()
# Output:
# D init
# B init
# C init
# A init
print(D.__mro__)
Mixin Classes in Practice
Mixins provide reusable functionality without being standalone classes. Common in frameworks like Django REST Framework:
class ListViewMixin:
def list(self, request):
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
class RetrieveViewMixin:
def retrieve(self, request, pk):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
class ProductViewSet(ListViewMixin, RetrieveViewMixin):
queryset = Product.objects.all()
serializer_class = ProductSerializer
Key traits of mixins:
- Single responsibility.
- No dependency on specific base class.
- Avoid using
super()unless necessary. - Suffix name with
Mixinfor clarity.
Context Managers and __enter__/__exit__
Context managers manage resource setup and teardown using with blocks.
class Resource:
def __enter__(self):
print("Acquiring resource")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type:
print(f"Handling exception: {exc_val}")
print("Releasing resource")
return False # Propagate exceptions
def process(self):
print("Processing...")
with Resource() as res:
res.process()
Simplifiyng Context Managers with contextlib
The @contextmanager decorator turns a generator into a context manager.
from contextlib import contextmanager
@contextmanager
def managed_resource(name):
print(f"Setting up {name}")
resource = {}
try:
yield resource
finally:
print(f"Tearing down {name}")
with managed_resource("database") as db:
db["user"] = "admin"
print("Working with resource")