|
| 1 | +# Protocols in Python |
| 2 | +Python can establish informal interfaces using protocols In order to improve code structure, reusability, and type checking. Protocols allow for progressive adoption and are more flexible than standard interfaces in other programming languages like JAVA, which are tight contracts that specify the methods and attributes a class must implement. |
| 3 | + |
| 4 | +>Before going into depth of this topic let's understand another topic which is pre-requisite od this topic \#TypingModule |
| 5 | +
|
| 6 | +## Typing Module |
| 7 | +This is a module in python which provides |
| 8 | +1. Provides classes, functions, and type aliases. |
| 9 | +2. Allows adding type annotations to our code. |
| 10 | +3. Enhances code readability. |
| 11 | +4. Helps in catching errors early. |
| 12 | + |
| 13 | +### Type Hints in Python: |
| 14 | +Type hints allow you to specify the expected data types of variables, function parameters, and return values. This can improve code readability and help with debugging. |
| 15 | + |
| 16 | +Here is a simple function that adds two numbers: |
| 17 | +```python |
| 18 | +def add(a,b): |
| 19 | + return a + b |
| 20 | +add(10,20) |
| 21 | +``` |
| 22 | +>Output: 30 |
| 23 | +
|
| 24 | +While this works fine, adding type hints makes the code more understandable and serves as documentation: |
| 25 | + |
| 26 | +```python |
| 27 | +def add(a:int, b:int)->int: |
| 28 | + return a + b |
| 29 | +print(add(1,10)) |
| 30 | +``` |
| 31 | +>Output: 11 |
| 32 | +
|
| 33 | +In this version, `a` and `b` are expected to be integers, and the function is expected to return an integer. This makes the function's purpose and usage clearer. |
| 34 | + |
| 35 | +#### let's see another example |
| 36 | + |
| 37 | +The function given below takes an iterable (it can be any off list, tuple, dict, set, frozeset, String... etc) and print it's content in a single line along with it's type. |
| 38 | + |
| 39 | +```python |
| 40 | +from typing import Iterable |
| 41 | +# type alias |
| 42 | + |
| 43 | +def print_all(l: Iterable)->None: |
| 44 | + print(type(l),end=' ') |
| 45 | + for i in l: |
| 46 | + print(i,end=' ') |
| 47 | + print() |
| 48 | + |
| 49 | +l = [1,2,3,4,5] # type: List[int] |
| 50 | +s = {1,2,3,4,5} # type: Set[int] |
| 51 | +t = (1,2,3,4,5) # type: Tuple[int] |
| 52 | + |
| 53 | +for iter_obj in [l,s,t]: |
| 54 | + print_all(iter_obj) |
| 55 | + |
| 56 | +``` |
| 57 | +Output: |
| 58 | +> <class 'list'> 1 2 3 4 5 |
| 59 | +> <class 'set'> 1 2 3 4 5 |
| 60 | +> <class 'tuple'> 1 2 3 4 5 |
| 61 | +
|
| 62 | +and now lets try calling the function `print_all` using a non-iterable object `int` as argument. |
| 63 | + |
| 64 | +```python |
| 65 | +a = 10 |
| 66 | +print_all(a) # This will raise an error |
| 67 | +``` |
| 68 | +Output: |
| 69 | +>TypeError: 'int' object is not iterable |
| 70 | +
|
| 71 | +This error occurs because `a` is an `integer`, and the `integer` class does not have any methods or attributes that make it work like an iterable. In other words, the integer class does not conform to the `Iterable` protocol. |
| 72 | + |
| 73 | +**Benefits of Type Hints** |
| 74 | +Using type hints helps in several ways: |
| 75 | + |
| 76 | +1. **Error Detection**: Tools like mypy can catch type-related problems during development, decreasing runtime errors. |
| 77 | +2. **Code Readability**: Type hints serve as documentation, making it easy to comprehend what data types are anticipated and returned. |
| 78 | +3. **Improved Maintenance**: With unambiguous type expectations, maintaining and updating code becomes easier, especially in huge codebases. |
| 79 | + |
| 80 | +Now that we have understood about type hints and typing module let's dive deep into protocols. |
| 81 | + |
| 82 | +## Understanding Protocols |
| 83 | + |
| 84 | +In Python, protocols define interfaces similar to Java interfaces. They let you specify methods and attributes that an object must implement without requiring inheritance from a base class. Protocols are part of the `typing` module and provide a way to enforce certain structures in your classes, enhancing type safety and code clarity. |
| 85 | + |
| 86 | +### What is a Protocol? |
| 87 | + |
| 88 | +A protocol specifies one or more method signatures that a class must implement to be considered as conforming to the protocol. |
| 89 | + This concept is often referred to as "structural subtyping" or "duck typing," meaning that if an object implements the required methods and attributes, it can be treated as an instance of the protocol. |
| 90 | + |
| 91 | +Let's write our own protocol: |
| 92 | + |
| 93 | +```python |
| 94 | +from typing import Protocol |
| 95 | + |
| 96 | +# Define a Printable protocol |
| 97 | +class Printable(Protocol): |
| 98 | + def print(self) -> None: |
| 99 | + """Print the object""" |
| 100 | + pass |
| 101 | + |
| 102 | +# Book class implements the Printable protocol |
| 103 | +class Book: |
| 104 | + def __init__(self, title: str): |
| 105 | + self.title = title |
| 106 | + |
| 107 | + def print(self) -> None: |
| 108 | + print(f"Book Title: {self.title}") |
| 109 | + |
| 110 | +# print_object function takes a Printable object and calls its print method |
| 111 | +def print_object(obj: Printable) -> None: |
| 112 | + obj.print() |
| 113 | + |
| 114 | +book = Book("Python Programming") |
| 115 | +print_object(book) |
| 116 | +``` |
| 117 | +Output: |
| 118 | +> Book Title: Python Programming |
| 119 | +
|
| 120 | +In this example: |
| 121 | + |
| 122 | +1. **Printable Protocol:** Defines an interface with a single method print. |
| 123 | +2. **Book Class:** Implements the Printable protocol by providing a print method. |
| 124 | +3. **print_object Function:** Accepts any object that conforms to the Printable protocol and calls its print method. |
| 125 | + |
| 126 | +we got our output because the class `Book` confirms to the protocols `printable`. |
| 127 | +similarly When you pass an object to `print_object` that does not conform to the Printable protocol, an error will occur. This is because the object does not implement the required `print` method. |
| 128 | +Let's see an example: |
| 129 | +```python |
| 130 | +class Team: |
| 131 | + def huddle(self) -> None: |
| 132 | + print("Team Huddle") |
| 133 | + |
| 134 | +c = Team() |
| 135 | +print_object(c) # This will raise an error |
| 136 | +``` |
| 137 | +Output: |
| 138 | +>AttributeError: 'Team' object has no attribute 'print' |
| 139 | +
|
| 140 | +In this case: |
| 141 | +- The `Team` class has a `huddle` method but does not have a `print` method. |
| 142 | +- When `print_object` tries to call the `print` method on a `Team` instance, it raises an `AttributeError`. |
| 143 | + |
| 144 | +> This is an important aspect of using protocols: they ensure that objects provide the necessary methods, leading to more predictable and reliable code. |
| 145 | +
|
| 146 | +**Ensuring Protocol Conformance** |
| 147 | +To avoid such errors, you need to ensure that any object passed to `print_object` implements the `Printable` protocol. Here's how you can modify the `Team` class to conform to the protocol: |
| 148 | +```python |
| 149 | +class Team: |
| 150 | + def __init__(self, name: str): |
| 151 | + self.name = name |
| 152 | + |
| 153 | + def huddle(self) -> None: |
| 154 | + print("Team Huddle") |
| 155 | + |
| 156 | + def print(self) -> None: |
| 157 | + print(f"Team Name: {self.name}") |
| 158 | + |
| 159 | +c = Team("Dream Team") |
| 160 | +print_object(c) |
| 161 | +``` |
| 162 | +Output: |
| 163 | +>Team Name: Dream Team |
| 164 | +
|
| 165 | +The `Team` class now implements the `print` method, conforming to the `Printable` protocol. and hence, no longer raises an error. |
| 166 | + |
| 167 | +### Protocols and Inheritance: |
| 168 | +Protocols can also be used in combination with inheritance to create more complex interfaces. |
| 169 | +we can do that by following these steps: |
| 170 | +**Step 1 - Base protocol**: Define a base protocol that specifies a common set of methods and attributes. |
| 171 | +**Step 2 - Derived Protocols**: Create derives protocols that extends the base protocol with addition requirements |
| 172 | +**Step 3 - Polymorphism**: Objects can then conform to multiple protocols, allowing for Polymorphic behavior. |
| 173 | + |
| 174 | +Let's see an example on this as well: |
| 175 | + |
| 176 | +```python |
| 177 | +from typing import Protocol |
| 178 | + |
| 179 | +# Base Protocols |
| 180 | +class Printable(Protocol): |
| 181 | + def print(self) -> None: |
| 182 | + """Print the object""" |
| 183 | + pass |
| 184 | + |
| 185 | +# Base Protocols-2 |
| 186 | +class Serializable(Protocol): |
| 187 | + def serialize(self) -> str: |
| 188 | + pass |
| 189 | + |
| 190 | +# Derived Protocol |
| 191 | +class PrintableAndSerializable(Printable, Serializable): |
| 192 | + pass |
| 193 | + |
| 194 | +# class with implementation of both Printable and Serializable |
| 195 | +class Book_serialize: |
| 196 | + def __init__(self, title: str): |
| 197 | + self.title = title |
| 198 | + |
| 199 | + def print(self) -> None: |
| 200 | + print(f"Book Title: {self.title}") |
| 201 | + |
| 202 | + def serialize(self) -> None: |
| 203 | + print(f"serialize: {self.title}") |
| 204 | + |
| 205 | +# function accepts the object which implements PrintableAndSerializable |
| 206 | +def test(obj: PrintableAndSerializable): |
| 207 | + obj.print() |
| 208 | + obj.serialize() |
| 209 | + |
| 210 | +book = Book_serialize("lean-in") |
| 211 | +test(book) |
| 212 | +``` |
| 213 | +Output: |
| 214 | +> Book Title: lean-in |
| 215 | +serialize: lean-in |
| 216 | + |
| 217 | +In this example: |
| 218 | + |
| 219 | +**Printable Protocol:** Specifies a `print` method. |
| 220 | +**Serializable Protocol:** Specifies a `serialize` method. |
| 221 | +**PrintableAndSerializable Protocol:** Combines both `Printable` and `Serializable`. |
| 222 | +**Book Class**: Implements both `print` and `serialize` methods, conforming to `PrintableAndSerializable`. |
| 223 | +**test Function:** Accepts any object that implements the `PrintableAndSerializable` protocol. |
| 224 | + |
| 225 | +If you try to pass an object that does not conform to the `PrintableAndSerializable` protocol to the test function, it will raise an `error`. Let's see an example: |
| 226 | + |
| 227 | +```python |
| 228 | +class Team: |
| 229 | + def huddle(self) -> None: |
| 230 | + print("Team Huddle") |
| 231 | + |
| 232 | +c = Team() |
| 233 | +test(c) # This will raise an error |
| 234 | +``` |
| 235 | +output: |
| 236 | +> AttributeError: 'Team' object has no attribute 'print' |
| 237 | +
|
| 238 | +In this case: |
| 239 | +The `Team` class has a `huddle` method but does not implement `print` or `serialize` methods. |
| 240 | +When test tries to call `print` and `serialize` on a `Team` instance, it raises an `AttributeError`. |
| 241 | + |
| 242 | +**In Conclusion:** |
| 243 | +>Python protocols offer a versatile and powerful means of defining interfaces, encouraging the decoupling of code, improving readability, and facilitating static type checking. They are particularly handy for scenarios involving file-like objects, bespoke containers, and any case where you wish to enforce certain behaviors without requiring inheritance from a specific base class. Ensuring that classes conform to protocols reduces runtime problems and makes your code more robust and maintainable. |
0 commit comments