|
| 1 | +{ |
| 2 | + "cells": [ |
| 3 | + { |
| 4 | + "cell_type": "markdown", |
| 5 | + "id": "8fdbefec-83b6-44d9-87ca-b27d6923ca9b", |
| 6 | + "metadata": {}, |
| 7 | + "source": [ |
| 8 | + "# **Encapsulation**\n", |
| 9 | + "\n", |
| 10 | + "Encapsulation is one of the four core principles of object-oriented programming (OOP). Encapsulation means **hiding internal data** from the outside world and only **allowing controlled access** via methods or interfaces.\n", |
| 11 | + "\n", |
| 12 | + "**Important Note: In Python, encapsulation is by convention, not by enforcement.**\n", |
| 13 | + "\n", |
| 14 | + "\n", |
| 15 | + "## **Why is it useful?**\n", |
| 16 | + "1. Prevents unintended modification of data.\n", |
| 17 | + "2. Allows validation before updating data.\n", |
| 18 | + "3. Maintains integrity and security of the object's state.\n", |
| 19 | + "\n", |
| 20 | + "## **How to Encapsulate in Python?**\n", |
| 21 | + "In Python, we **prefix variables with an underscore** `_` to signal they are **\"private\"** (by convention). Python **doesn't enforce** strict access, but **developers are expected to respect it**.\n", |
| 22 | + "```python\n", |
| 23 | + "class Student:\n", |
| 24 | + " def __init__(self, age):\n", |
| 25 | + " self._age = age # protected attribute\n", |
| 26 | + "```\n", |
| 27 | + "**But this alone doesn’t prevent wrong inputs!**\n", |
| 28 | + "\n", |
| 29 | + "## **Use of single underscore vs double underscore**\n", |
| 30 | + "The idea of the double underscore in Python is completely different. It was created as a means to override different methods of a class that is going to be extended several times, without the risk of having collisions with the method names. Even that is a too far-fetched use case as to justify the use of this mechanism.\n", |
| 31 | + "\n", |
| 32 | + "Double underscores are a non-Pythonic approach. If we need to define attributes as private, use a single underscore, and respect the Pythonic convention that it is a private attribute.\n", |
| 33 | + "\n", |
| 34 | + "Note that:\n", |
| 35 | + "- Using __double_underscore triggers name mangling — not true privacy.\n", |
| 36 | + "- But using _single_underscore is considered \"Pythonic\" — it indicates something is private, not enforces it.\n", |
| 37 | + "\n", |
| 38 | + "## **Private vs Name Mangling vs Magic Methods**\n", |
| 39 | + "\n", |
| 40 | + "| Symbol | Purpose |\n", |
| 41 | + "| ---------- | -------------------------------------------------------------- |\n", |
| 42 | + "| `_name` | You want to mark an attribute as private |\n", |
| 43 | + "| `__name` | Name Mangling: Avoid name clashes in inheritance. |\n", |
| 44 | + "| `__name__` | Special/Magic methods (e.g., `__init__`, `__str__`) i.e. dunder|\n", |
| 45 | + "\n", |
| 46 | + "## **Name Mangling**\n", |
| 47 | + "When you name an attribute like **__my_var** inside a class, Python internally changes its name to **_ClassName__my_var**.\n", |
| 48 | + "\n", |
| 49 | + "This is called name mangling, and it's used to prevent accidental overrides when classes are extended (i.e., inheritance)." |
| 50 | + ] |
| 51 | + }, |
| 52 | + { |
| 53 | + "cell_type": "code", |
| 54 | + "execution_count": 1, |
| 55 | + "id": "7eb84aab-2852-4f10-a85f-daa429bdb1b9", |
| 56 | + "metadata": {}, |
| 57 | + "outputs": [ |
| 58 | + { |
| 59 | + "name": "stdout", |
| 60 | + "output_type": "stream", |
| 61 | + "text": [ |
| 62 | + "-20\n" |
| 63 | + ] |
| 64 | + } |
| 65 | + ], |
| 66 | + "source": [ |
| 67 | + "class Student:\n", |
| 68 | + " def __init__(self, age):\n", |
| 69 | + " self._age = age # private attribute\n", |
| 70 | + "\n", |
| 71 | + "s = Student(-20)\n", |
| 72 | + "\n", |
| 73 | + "print(s._age) # Python doesn't enforce strict access" |
| 74 | + ] |
| 75 | + }, |
| 76 | + { |
| 77 | + "cell_type": "markdown", |
| 78 | + "id": "921b2907-138c-4a59-b53f-234189493883", |
| 79 | + "metadata": {}, |
| 80 | + "source": [ |
| 81 | + "## **Getters and Setters - @property and @attr.setter Decorator**\n", |
| 82 | + "- In OOP, objects are created to model real-world things.\n", |
| 83 | + "- These objects often contain data (like attributes) and behaviors (like methods).\n", |
| 84 | + "- Data accuracy/validity is critical — e.g., age should not be negative, email should be properly formatted.\n", |
| 85 | + "- So we write validation logic, especially in setters.\n", |
| 86 | + "- In Python, instead of writing separate get_x() and set_x() methods like in Java, we use @property — a cleaner, Pythonic way to handle this.\n", |
| 87 | + "- Keeps the interface clean (like obj.age) instead of calling obj.get_age().\n", |
| 88 | + "\n", |
| 89 | + "\n", |
| 90 | + "#### **What is @property?** \n", |
| 91 | + "The @property decorator in Python is used to define a method as a getter for a class attribute, allowing you to access it like a regular attribute while still including logic or validation behind the scenes. It is part of Python’s way of supporting encapsulation in an elegant and Pythonic way.\n", |
| 92 | + "\n", |
| 93 | + "@property makes a method look like an attribute. @property is mandatory before using @attr.setter.\n", |
| 94 | + "\n", |
| 95 | + "| Decorator | Purpose | Mandatory? |\n", |
| 96 | + "| -------------- | ----------------------------------------- | ------------------------------ |\n", |
| 97 | + "| `@property` | Turns a method into a **getter** property | Yes |\n", |
| 98 | + "| `@attr.setter` | Adds a **setter** to the property `attr` | Yes — only after `@property` |\n", |
| 99 | + "\n", |
| 100 | + "#### **Why is @property mandatory before @attr.setter?** \n", |
| 101 | + "- In Python, the @attr.setter decorator is not standalone.\n", |
| 102 | + "- It extends an existing property object.\n", |
| 103 | + "- That property object is first created using the @property decorator.\n", |
| 104 | + "\n", |
| 105 | + "#### **What is you skip @property?** \n", |
| 106 | + "If you try to use @attr.setter without first defining @property, you'll get: \n", |
| 107 | + "```\n", |
| 108 | + "AttributeError: 'function' object has no attribute 'setter'\n", |
| 109 | + "``` \n", |
| 110 | + "Because age was never defined as a @property in the first place, so there's nothing to extend with .setter\n", |
| 111 | + "\n", |
| 112 | + "#### **How does it work internally?** \n", |
| 113 | + "When you use:\n", |
| 114 | + "```python\n", |
| 115 | + "@property\n", |
| 116 | + "def age(self):\n", |
| 117 | + " return self._age\n", |
| 118 | + "```\n", |
| 119 | + "You are creating a property object named **age**. That object knows how to get the value.\n", |
| 120 | + "\n", |
| 121 | + "Now you can **extend** that property object to include setter behavior like follows:\n", |
| 122 | + "```python\n", |
| 123 | + "@age.setter\n", |
| 124 | + "def age(self, value):\n", |
| 125 | + " self._age = value\n", |
| 126 | + "```" |
| 127 | + ] |
| 128 | + }, |
| 129 | + { |
| 130 | + "cell_type": "code", |
| 131 | + "execution_count": 5, |
| 132 | + "id": "b88d9cc6-0353-4fb2-8cdb-010a75aae256", |
| 133 | + "metadata": {}, |
| 134 | + "outputs": [], |
| 135 | + "source": [ |
| 136 | + "class Person:\n", |
| 137 | + " def __init__(self, age):\n", |
| 138 | + " self.age = age # This triggers @age.setter\n", |
| 139 | + "\n", |
| 140 | + " @property\n", |
| 141 | + " def age(self):\n", |
| 142 | + " print(\"Getter called\")\n", |
| 143 | + " return self._age\n", |
| 144 | + " \n", |
| 145 | + " @age.setter\n", |
| 146 | + " def age(self, value):\n", |
| 147 | + " print(\"Setter called\")\n", |
| 148 | + " if value < 0:\n", |
| 149 | + " raise ValueError(\"Age cannot be negative\")\n", |
| 150 | + " self._age = value" |
| 151 | + ] |
| 152 | + }, |
| 153 | + { |
| 154 | + "cell_type": "code", |
| 155 | + "execution_count": 6, |
| 156 | + "id": "f1233d49-d4ea-4c52-a4a0-917685214b56", |
| 157 | + "metadata": {}, |
| 158 | + "outputs": [ |
| 159 | + { |
| 160 | + "name": "stdout", |
| 161 | + "output_type": "stream", |
| 162 | + "text": [ |
| 163 | + "Setter called\n", |
| 164 | + "Getter called\n", |
| 165 | + "25\n", |
| 166 | + "{'_age': 25}\n" |
| 167 | + ] |
| 168 | + } |
| 169 | + ], |
| 170 | + "source": [ |
| 171 | + "p = Person(25) # Call the setter\n", |
| 172 | + "print(p.age) # Call the getter\n", |
| 173 | + "print(p.__dict__)" |
| 174 | + ] |
| 175 | + }, |
| 176 | + { |
| 177 | + "cell_type": "code", |
| 178 | + "execution_count": 7, |
| 179 | + "id": "bdedac9c-acc6-42f8-8a95-c962d37ddd52", |
| 180 | + "metadata": {}, |
| 181 | + "outputs": [ |
| 182 | + { |
| 183 | + "name": "stdout", |
| 184 | + "output_type": "stream", |
| 185 | + "text": [ |
| 186 | + "Setter called\n", |
| 187 | + "Getter called\n", |
| 188 | + "30\n", |
| 189 | + "{'_age': 30}\n" |
| 190 | + ] |
| 191 | + } |
| 192 | + ], |
| 193 | + "source": [ |
| 194 | + "p.age = 30 # Call the setter\n", |
| 195 | + "print(p.age) # Call the getter\n", |
| 196 | + "print(p.__dict__)" |
| 197 | + ] |
| 198 | + }, |
| 199 | + { |
| 200 | + "cell_type": "code", |
| 201 | + "execution_count": 8, |
| 202 | + "id": "59165647-5831-4b31-8d09-ede5c89ccb4a", |
| 203 | + "metadata": {}, |
| 204 | + "outputs": [ |
| 205 | + { |
| 206 | + "name": "stdout", |
| 207 | + "output_type": "stream", |
| 208 | + "text": [ |
| 209 | + "Setter called\n" |
| 210 | + ] |
| 211 | + }, |
| 212 | + { |
| 213 | + "ename": "ValueError", |
| 214 | + "evalue": "Age cannot be negative", |
| 215 | + "output_type": "error", |
| 216 | + "traceback": [ |
| 217 | + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", |
| 218 | + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", |
| 219 | + "Cell \u001b[0;32mIn[8], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mp\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mage\u001b[49m \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m1\u001b[39m \u001b[38;5;66;03m# Call the setter\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28mprint\u001b[39m(p\u001b[38;5;241m.\u001b[39mage) \u001b[38;5;66;03m# Call the getter\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28mprint\u001b[39m(p\u001b[38;5;241m.\u001b[39m\u001b[38;5;18m__dict__\u001b[39m)\n", |
| 220 | + "Cell \u001b[0;32mIn[5], line 14\u001b[0m, in \u001b[0;36mPerson.age\u001b[0;34m(self, value)\u001b[0m\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mSetter called\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m value \u001b[38;5;241m<\u001b[39m \u001b[38;5;241m0\u001b[39m:\n\u001b[0;32m---> 14\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mAge cannot be negative\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 15\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_age \u001b[38;5;241m=\u001b[39m value\n", |
| 221 | + "\u001b[0;31mValueError\u001b[0m: Age cannot be negative" |
| 222 | + ] |
| 223 | + } |
| 224 | + ], |
| 225 | + "source": [ |
| 226 | + "p.age = -1 # Call the setter\n", |
| 227 | + "print(p.age) # Call the getter\n", |
| 228 | + "print(p.__dict__)" |
| 229 | + ] |
| 230 | + }, |
| 231 | + { |
| 232 | + "cell_type": "markdown", |
| 233 | + "id": "3bacd358-0037-4f16-8732-c6fc2512e523", |
| 234 | + "metadata": {}, |
| 235 | + "source": [ |
| 236 | + "## **Real Time Example of Encapsulation**\n", |
| 237 | + "\n", |
| 238 | + "Consider the example of a geographical system that needs to deal with coordinates. There is only a certain range of values for which latitude and longitude make sense. Outside of those values, a coordinate cannot exist. We can create an object to represent a coordinate, but in doing so we must ensure that the values for latitude are at all times within the acceptable ranges. And for this, we can use properties:" |
| 239 | + ] |
| 240 | + }, |
| 241 | + { |
| 242 | + "cell_type": "code", |
| 243 | + "execution_count": 9, |
| 244 | + "id": "617ebc4d-30a7-4e2d-bd1f-cec2f8a6919e", |
| 245 | + "metadata": {}, |
| 246 | + "outputs": [], |
| 247 | + "source": [ |
| 248 | + "class Coordinate:\n", |
| 249 | + " def __init__(self, lat: float, long: float) -> None:\n", |
| 250 | + " self._latitude = self._longitude = None\n", |
| 251 | + " self.latitude = lat # This triggers @latitude.setter\n", |
| 252 | + " self.longitude = long # This triggers @longitude.setter\n", |
| 253 | + "\n", |
| 254 | + " @property\n", |
| 255 | + " def latitude(self) -> float:\n", |
| 256 | + " return self._latitude\n", |
| 257 | + "\n", |
| 258 | + " @latitude.setter\n", |
| 259 | + " def latitude(self, lat_value: float) -> None:\n", |
| 260 | + " if lat_value not in range(-90, 90 + 1):\n", |
| 261 | + " raise ValueError(f\"{lat_value} is an invalid value for latitude\")\n", |
| 262 | + " self._latitude = lat_value\n", |
| 263 | + "\n", |
| 264 | + " @property \n", |
| 265 | + " def longitude(self) -> float: \n", |
| 266 | + " return self._longitude\n", |
| 267 | + "\n", |
| 268 | + " @longitude.setter\n", |
| 269 | + " def longitude(self, long_value: float) -> None:\n", |
| 270 | + " if long_value not in range(-180, 180 + 1):\n", |
| 271 | + " raise ValueError(f\"{long_value} is an invalid value for longitude\")\n", |
| 272 | + " self._longitude = long_value" |
| 273 | + ] |
| 274 | + } |
| 275 | + ], |
| 276 | + "metadata": { |
| 277 | + "kernelspec": { |
| 278 | + "display_name": "Python 3 (ipykernel)", |
| 279 | + "language": "python", |
| 280 | + "name": "python3" |
| 281 | + }, |
| 282 | + "language_info": { |
| 283 | + "codemirror_mode": { |
| 284 | + "name": "ipython", |
| 285 | + "version": 3 |
| 286 | + }, |
| 287 | + "file_extension": ".py", |
| 288 | + "mimetype": "text/x-python", |
| 289 | + "name": "python", |
| 290 | + "nbconvert_exporter": "python", |
| 291 | + "pygments_lexer": "ipython3", |
| 292 | + "version": "3.9.6" |
| 293 | + } |
| 294 | + }, |
| 295 | + "nbformat": 4, |
| 296 | + "nbformat_minor": 5 |
| 297 | +} |
0 commit comments