Welcome! In this lesson, we'll explore Python Generics. Generics let you write code that can handle different data types without duplicating code, making programs more flexible and reusable.
Think of generics like a Swiss Army knife. Instead of carrying a knife and a screwdriver separately, you have one tool that switches functions effortlessly. Sounds useful, right?
By the end of this lesson, you'll know how to define and use generics in Python. Let's get started!
Before diving into generics, let's revisit type annotations. Type annotations specify the kind of data a variable should hold, making our code more understandable and error-free.
Generics come in when we want a function or class to work with any data type. For example, a list
can hold integers, strings, or objects. Wouldn't it be nice to write code that works with any type, just like a list? Generics help us achieve this.
Python's typing
module provides tools for generics, like TypeVar
, which lets us define a variable type. Let's explore how to use it in code.
We define a generic class using TypeVar
from the typing
module. This acts as a placeholder for any data type. It's like labeling a blank space, which we'll fill with an actual data type later.
Here's the Stack
class example:
Python1from typing import TypeVar, Generic 2 3# T can be any type 4T = TypeVar('T') 5 6class Stack(Generic[T]): 7 def __init__(self) -> None: 8 self.__container: list[T] = []
Here, we declare the T
type and create a Stack
class that stores a list of items of type T
. It is achieved by defining the class as Generic[T]
. Generic[T] is used to define a class or function that can operate on any data type specified by T. Using this trick, we can define Stacks that hold any data type; for example, we can create a Stack
of integers or a Stack
of strings.
Continuing with our Stack
class, let's implement its methods:
Python1class Stack(Generic[T]): 2 def __init__(self) -> None: 3 self.__container: list[T] = [] 4 5 def push(self, item: T) -> None: 6 self.__container.append(item) 7 8 def pop(self) -> T: 9 return self.__container.pop() 10 11 def size(self) -> int: 12 return len(self.__container)
Let's break down the methods in our Stack
class:
push(self, item: T) -> None
: Adds an item of typeT
to the stack.pop(self) -> T
: Removes and returns the last item in the stack. Note how we use-> T
to specify that the returned item will be of the typeT
.size(self) -> int
: Returns the number of items in the stack.
Let's add a peek
method, which uses Optional
from the typing
module to indicate the result might be None
if the stack is empty.
Python1from typing import Optional 2 3class Stack(Generic[T]): 4 def __init__(self) -> None: 5 self.__container: list[T] = [] 6 7 def push(self, item: T) -> None: 8 self.__container.append(item) 9 10 def pop(self) -> T: 11 return self.__container.pop() 12 13 def peek(self) -> Optional[T]: 14 if self.size() == 0: 15 return None 16 return self.__container[-1] 17 18 def size(self) -> int: 19 return len(self.__container)
The peek(self) -> Optional[T]
method checks if the stack is empty. If it is, it returns None
; otherwise, it returns the last item. Optional[T]
indicates the result could be either T
or None
.
Here's how to create a stack for integers:
Python1if __name__ == "__main__": 2 stack = Stack[int]() 3 stack.push(1) 4 stack.push(2) 5 stack.push(3) 6 print(f"Stack of Integers: {stack}") # Stack of Integers: <__main__.Stack object at 0x...> 7 print(f"Top item: {stack.peek()}") # Top item: 3 8 print(f"Size of stack: {stack.size()}") # Size of stack: 3 9 stack.pop() 10 print(f"Top item after pop: {stack.peek()}") # Top item after pop: 2 11 print(f"Size of stack after pop: {stack.size()}") # Size of stack after pop: 2
With Stack[int]
we specify that this time T
will be int
.
Here's how to create a stack for strings:
Python1if __name__ == "__main__": 2 stack = Stack[str]() 3 stack.push("apple") 4 stack.push("banana") 5 stack.push("cherry") 6 print(f"Stack of Strings: {stack}") # Stack of Strings: <__main__.Stack object at 0x...> 7 print(f"Top item: {stack.peek()}") # Top item: cherry 8 print(f"Size of stack: {stack.size()}") # Size of stack: 3 9 stack.pop() 10 print(f"Top item after pop: {stack.peek()}") # Top item after pop: banana 11 print(f"Size of stack after pop: {stack.size()}") # Size of stack after pop: 2
This time, T
is str
. This flexibility shows how generics make our code reusable and consistent, regardless of type.
Great job! Today, we learned about Python Generics and how they help write reusable and type-safe code. We covered:
- The concept of generics and their usefulness.
- How to define a generic class using
TypeVar
. - Implementing generic methods in a class.
- Real-life examples of creating and using generic classes.
By understanding and using generics, you can create flexible and reusable code components. Now, you're ready for hands-on practice to create your own generic classes and methods.
Now it's time to put your knowledge into practice! You'll create and use generic classes and methods to solidify your understanding. This hands-on practice will show you how powerful and flexible generics can be in real-world coding scenarios. Good luck!