Getting to Know Duck Typing in Python | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Duck Typing: Behaving Like a Duck
If it walks like a duck and it quacks like a duck, then it must be a duck.duck typing is a type system where an object is considered compatible with a given type if it has all the methods and attributes that the type requires this type system supports the ability to use objects of independent and decoupled classes in a specific context as long as they adhere to some common interface class Duck: def swim(self): print("The duck is swimming") def fly(self): print("The duck is flying") class Swan: def swim(self): print("The swan is swimming") def fly(self): print("The swan is flying") class Albatross: def swim(self): print("The albatross is swimming") def fly(self): print("The albatross is flying")the classes above all have the same interface Python does not type check so the code below works >>> from birds_v1 import Duck, Swan, Albatross >>> birds = [Duck(), Swan(), Albatross()] >>> for bird in birds: ... bird.fly() ... bird.swim() ... The duck is flying The duck is swimming The swan is flying The swan is swimming The albatross is flying The albatross is swimming Duck Typing and Polymorphism
duck typing is a popular way to support polymorphism in Pythonobjects don't have to inherit from a common superclass |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Understanding the Pros and Cons of Duck Typing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Exploring Duck Typing in Python's Built-in Tools | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
built-in types, such as lists, tuples, strings, and dictionaries, support duck typing
numbers = [1, 2, 3] person = ("Jane", 25, "Python Dev") letters = "abc" ordinals = {"one": "first", "two": "second", "three": "third"} even_digits = {2, 4, 6, 8} collections = [numbers, person, letters, ordinals, even_digits] for collection in collections: for value in collection: print(value)
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Supporting Duck Typing in Custom Classes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
can support duck typing in custom classes using two different approaches
Using Regular Methods
file.txt
John 25 Engineer Jane 22 Designerfile.csv name,age,job John,25,Engineer Jane,22,Designerfile.json [ { "name": "John", "age": 25, "job": "Engineer" }, { "name": "Jane", "age": 22, "job": "Designer" } ]use duck typing to read the files import csv import json from itertools import batched # Python >= 3.12 class TextReader: def __init__(self, filename): self.filename = filename def read(self): with open(self.filename, encoding="utf-8") as file: return [ { "name": batch[0].strip(), "age": batch[1].strip(), "job": batch[2].strip(), } for batch in batched(file.readlines(), 3) ] class CSVReader: def __init__(self, filename): self.filename = filename def read(self): with open(self.filename, encoding="utf-8", newline="") as file: return list(csv.DictReader(file)) class JSONReader: def __init__(self, filename): self.filename = filename def read(self): with open(self.filename, encoding="utf-8") as file: return json.load(file)using the classes >>> from readers import TextReader, CSVReader, JSONReader >>> readers = [ ... TextReader("file.txt"), ... CSVReader("file.csv"), ... JSONReader("file.json"), ... ] >>> for reader in readers: ... print(reader.read()) ... [ {'name': 'John', 'age': '25', 'job': 'Engineer'}, {'name': 'Jane', 'age': '22', 'job': 'Designer'} ] [ {'name': 'John', 'age': '25', 'job': 'Engineer'}, {'name': 'Jane', 'age': '22', 'job': 'Designer'} ] [ {'name': 'John', 'age': 25, 'job': 'Engineer'}, {'name': 'Jane', 'age': 22, 'job': 'Designer'} ] Using Special Methods and Protocols
special methods are dundersprotocols are sets of special methods that support specific features of the language
needs to be iterable and support the built-in len() and reversed() functions needs to support the in operator from collections import deque class Queue: def __init__(self): self._elements = deque() def enqueue(self, element): self._elements.append(element) def dequeue(self): return self._elements.popleft() def __iter__(self): return iter(self._elements) def __len__(self): return len(self._elements) def __reversed__(self): return reversed(self._elements) def __contains__(self, element): return element in self._elementsclass uses a deque object to store the data. class has the classic queue operations enqueue and dequeue __iter__() allows supporting iterations implements 3 other dunder methods |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Exploring Alternatives to Duck Typing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Using Abstract Base Classes
Abstract base classes (ABCs) define a specific set of public methods and
attributes (API) that all their subclasses must implementuse the abc module from the standard library module provides a couple of ABC-related tools from abc import ABC, abstractmethod class Vehicle(ABC): def __init__(self, make, model, color): self.make = make self.model = model self.color = color @abstractmethod def start(self): raise NotImplementedError("start() must be implemented") @abstractmethod def stop(self): raise NotImplementedError("stop() must be implemented") @abstractmethod def drive(self): raise NotImplementedError("drive() must be implemented") # ... class Car(Vehicle): def start(self): print("The car is starting") def stop(self): print("The car is stopping") def drive(self): print("The car is driving") class Truck(Vehicle): def start(self): print("The truck is starting") def stop(self): print("The truck is stopping") def drive(self): print("The truck is driving") Checking Types Explicitly
to check whether an object comes from a given class, use the built-in
isinstance() function to check whether an object has a specific method or attribute, can use the built-in hasattr() function birds_v2.py class Duck: def fly(self): print("The duck is flying") def swim(self): print("The duck is swimming") class Pigeon: def fly(self): print("The pigeon is flying")classes partially support duck typing they work interchangeably if only the .fly() method is used Pigeon.swim() will fail with an AttributeError check the type before calling the swim() method >>> for bird in birds: ... if isinstance(bird, Duck): ... bird.swim() ... The duck is swimmingneed to know too much about the types when using isinstance() can explicitly check if the current object has the desired method using the hasattr()function for bird in birds: if hasattr(bird, "swim"): bird.swim() |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Using Duck Typing and Type Hints | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Understanding Type Hints and Static Duck Typing
type hints express the type of objects in a piece of codeassociated advantages include
a class can replace another if both have the same structure can use the len() function with all the classes that define the .__len__() method the method is part of the class's internal structure Using Protocols and ABCs
Python 3.8 introduced protocols in the type-hinting systema protocol specifies one or more methods that a class must implement to support a given feature protocols have to do with a class's internal structure protocols and abstract base classes fill the gap between duck typing and type hints help type checkers catch type-related issues and potential errors duck typing and type hints may not always work together >>> def mean(grades: list) -> float: ... return sum(grades) / len(grades) ... >>> mean([4, 3, 3, 4, 5]) 3.8 >>> mean((4, 3, 3, 4, 5)) 3.8function works but type hints limit its use to lists a union type expression makes the function duck type compatible def mean(grades: list | tuple | set) -> float: return sum(grades) / len(grades)type hint makes the function signature cumbersome a more generic solution is using the abstract base class Collection from the module collections.abc class expects support for ineration and the len() function from collections.abc import Collection def mean(grades: Collection) -> float: return sum(grades) / len(grades) Creating Custom Protocol Objects
can create custom classes to define protocols in code which relies
on duck typingbelow is a group of shape classes all the classes need to have .area() and .perimeter() methods shapes.py from math import pi def describe_shape(shape): print(f"{type(shape).__name__}") print(f" Area: {shape.area():.2f}") print(f" Perimeter: {shape.perimeter():.2f}") class Circle: def __init__(self, radius: float) -> None: self.radius = radius def area(self) -> float: return pi * self.radius**2 def perimeter(self) -> float: return 2 * pi * self.radius class Square: def __init__(self, side: float) -> None: self.side = side def area(self) -> float: return self.side**2 def perimeter(self) -> float: return 4 * self.side class Rectangle: def __init__(self, length: float, width: float) -> None: self.length = length self.width = width def area(self) -> float: return self.length * self.width def perimeter(self) -> float: return 2 * (self.length + self.width)all these classes have the required methods supporting duck typing also use type hints to describe the types of input arguments and the return values of every method >>> from shapes import Circle, Rectangle, Square, describe_shape >>> describe_shape(Circle(3)) Circle Area: 28.27 Perimeter: 18.85 >>> describe_shape(Square(5)) Square Area: 25.00 Perimeter: 20.00 >>> describe_shape(Rectangle(4, 5)) Rectangle Area: 20.00 Perimeter: 18.00there are no type hints for the method describe_shape argument can be any object whether or not it supports the desired interface create a Protocol subclass to describe the required set of methods from math import pi from typing import Protocol class Shape(Protocol): def area(self) -> float: ... def perimeter(self) -> float: ... # rest of shapes.pythe class inherits from Protocol and defines the required methods the methods don't have a proper implementation class uses an ellipsis to define the protocol's body to express these methods don't do anything can use a pass statement instead methods just define a custom protocol refactor describe_shape() adding type hints def describe_shape(shape: Shape) -> None: print(f"{type(shape).__name__}") print(f" Area: {shape.area():.2f}") print(f" Perimeter: {shape.perimeter():.2f}") |