10000 Update README.md · Cmatrix1/Python-Descriptors@3183555 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3183555

Browse files
authored
Update README.md
1 parent 84d5a68 commit 3183555

File tree

1 file changed

+213
-1
lines changed

1 file changed

+213
-1
lines changed

README.md

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,213 @@
1-
# Python-Descriptors
1+
2+
![safjsd](https://github.com/Cmatrix1/Python-Descriptors/assets/74909796/de5bce1d-802e-4942-98bf-faf121896c4d)
3+
4+
# Python Descriptors
5+
Complete Tuturial of Python Descriptors
6+
7+
## Introduction
8+
9+
Welcome to the intriguing world of Python descriptors! This article may seem a bit complex, but trust me, understanding descriptors is absolutely worth it.
10+
11+
They unlock the inner workings of properties, methods, slots, and even functions, providing a solid foundation for Python mastery.
12+
13+
In this section, we'll primarily focus on two types of descriptors: non-data descriptors and data descriptors. These two types behave slightly differently, and we'll delve into their distinctions.
14+
15+
As we progress, we'll learn how to craft and utilize custom data descriptors. You'll discover the unique advantages and practical applications of writing your own data descriptors.
16+
17+
Additionally, we'll navigate through some common pitfalls related to storing data associated with data descriptors. By learning how to avoid these pitfalls, you'll enhance your descriptor implementation skills.
18+
19+
As we explore deeper into this section, we'll tackle advanced topics such as weak references and their relationship to weak dictionaries. These concepts will broaden your understanding and empower you to create more robust and efficient code. 😄 There's no denying that this article is packed with captivating content. So, without further ado, let's dive in and unravel the mysteries of Python descriptors together! Thank you for joining me.
20+
21+
22+
## Problem We Want to Solve
23+
Suppose we want a User class whose age must always be integer and name must always be string
24+
25+
Of course we can use a property with getter and setter methods
26+
27+
Let's implement it with propertys first:
28+
```python
29+
class User:
30+
@property
31+
def name(self):
32+
return self._name
33+
34+
@name.setter
35+
def name(self, value):
36+
self._name = str(value)
37+
38+
@property
39+
def age(self):
40+
return self._age
41+
42+
@age.setter
43+
def age(self, value):
44+
self._age = int(value)
45+
```
46+
this is tedious, repetitive boiler plate code! **better way needed!**
47+
48+
# Descriptors
49+
50+
And this is where the descriptor protocol comes in !
51+
52+
Python descriptors are simply objects that implement the descriptor protocol.
53+
54+
The protocol is comprised of the following special methods - not all are required.
55+
56+
- `__get__`: retrieve the property value
57+
- `__set__`: store the property value
58+
- `__del__`: delete a property from the instance
59+
- `__set_name__`: capture the property name as it is being defined.
60+
61+
There are two types of Descriptors:
62+
63+
1. Non-data descriptors: these are descriptors that only implement `__get__` (and optionally `__set_name__`)
64+
2. Data descriptors: these implement the `__set__` method, and normally, also the `__get__` method.
65+
66+
#### Let's create a simple non-data descriptor:
67+
```python
68+
from datetime import datetime
69+
70+
class TimeUTC:
71+
def __get__(self, instance, owner_class):
72+
return datetime.utcnow().isoformat()
73+
```
74+
75+
So `TimeUTC` is a class that implements the `__get__` method only, and is therefore considered a non-data descriptor.
76+
77+
We can now use it to create properties in other classes:
78+
```python
79+
class Logger:
80+
current_time = TimeUTC()
81+
```
82+
---
83+
**NOTE** :
84+
That `current_time` is a class attribute:
85+
```python
86+
Logger.__dict__
87+
```
88+
```bash
89+
mappingproxy({'__module__': '__main__',
90+
'current_time': <__main__.TimeUTC at 0x7fdcd84bbd68>,
91+
'__dict__': <attribute '__dict__' of 'Logger' objects>,
92+
'__weakref__': <attribute '__weakref__' of 'Logger' objects>,
93+
'__doc__': None})
94+
```
95+
---
96+
97+
98+
We can access that attribute from an instance of the `Logger` class:
99+
```python
100+
l = Logger()
101+
l.current_time
102+
'2023-10-27T08:11:58.319429'
103+
```
104+
105+
We can also access it from the class itself, and for now it behaves the same (we'll come back to that later):
106+
```python
107+
Logger.current_time
108+
'2023-10-27T08:11:58.327000'
109+
```
110+
111+
112+
#### Let's consider another example.
113+
Suppose we want to create class that allows us to select a random status and random favorite color for the user instance.
114+
We could approach it this way:
115+
```python
116+
from random import choice, seed
117+
118+
class Person:
119+
@property
120+
def status(self):
121+
return choice(('😃', '😄', '😊', '😉', '😍', '🤩', '😎', '🥳', '😇', '🙌'))
122+
123+
@property
124+
def favorite_color(self):
125+
colors = ('🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '🟤', '', '', '🌈')
126+
return choice(colors)
127+
128+
```
129+
130+
This was pretty easy, but as you can see both properties essentially did the same thing - they picked a random choice from some iterable.
131+
Let's rewrite this using a custom descriptor:
132+
133+
```python
134+
class Choice:
135+
def __init__(self, *choices):
136+
self.choices = choices
137+
138+
def __get__(self, instance, owner_class):
139+
return choice(self.choices)
140+
141+
```
142+
And now we can rewrite our `Person` class this way:
143+
144+
```python
145+
class Person:
146+
status = Choice('😃', '😄', '😊', '😉', '😍', '🤩', '😎', '🥳', '😇', '🙌')
147+
favorite_color = Choice('🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '🟤', '', '', '🌈')
148+
```
149+
150+
Of course we are not limited to just cards, we could use it in other classes too:
151+
```python
152+
class Dice:
153+
die_1 = Choice(1,2,3,4,5,6)
154+
die_2 = Choice(1,2,3,4,5,6)
155+
die_3 = Choice(1,2,3,4,5,6)
156+
```
157+
158+
## Getters and Setters
159+
160+
So far we have seen how the `__get__` method is called when we assign an instance of a descriptors to a class attribute.
161+
But we can access that attribute either from the class itself, or the instance - as we saw in the last lecture, both accesses end up calling the `__get__` method.
162+
So, when __get__ is called, we may want to know:
163+
- which instance was used (if any) -> None if called from class
164+
- what class owns the TimeUTC (descriptor) instance -> Logger in our case
165+
166+
this is why we have the signature: `__get__(self, instance, owner_class)`
167+
So we can return different values from __get__ depending on:
168+
- called from class
169+
- called from instance
170+
171+
```python
172+
from datetime import datetime
173+
174+
class TimeUTC:
175+
def __get__(self, instance, owner_class):
176+
print(f'__get__ called, self={self}, instance={instance}, owner_class={owner_class}')
177+
return datetime.utcnow().isoformat()
178+
179+
class Logger1:
180+
current_time = TimeUTC()
181+
```
182+
Now let's access `current_time` from the class itself:
183+
```python
184+
Logger1.current_time
185+
'__get__ called, self=<__main__.TimeUTC object at 0x0000026BF3C87210>, instance=None, owner_class=<class '__main__.Logger1'>'
186+
'2023-10-27T09:58:00.586792'
187+
```
188+
As you can see, the `instance` was `None` - this was because we called the descriptor from the `Logger1` class, not an instance of it. The `owner_class` tells us this descriptor instance is defined in the `Logger1` class.
189+
190+
But if we call the descriptor via an instance instead:
191+
```python
192+
l1 = Logger1()
193+
l1.current_time
194+
'__get__ called, self=<__main__.TimeUTC object at 0x0000026BF3C87210>, instance=<__main__.Logger1 object at 0x0000026BF41DEC50>, owner_class=<class '__main__.Logger1'>'
195+
```
196+
197+
198+
very often, we choose to:
199+
- return the descriptor (TimeUTC) instance when called from class itself (Logger class) gives us an easy handle to the descriptor instance
200+
- return the attribute value when called from an instance of the class
201+
202+
```python
203+
from datetime import datetime
204+
205+
class TimeUTC:
206+
def __get__(self, instance, owner_class):
207+
if instance is None:
208+
# called from class
209+
return self
210+
else:
211+
# called from instance
212+
return datetime.utcnow().isoformat()
213+
```

0 commit comments

Comments
 (0)
0