Skip to content

Commit 63b5639

Browse files
committed
Create Encapsulation.ipynb
1 parent b535085 commit 63b5639

File tree

1 file changed

+297
-0
lines changed

1 file changed

+297
-0
lines changed
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
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

Comments
 (0)