1"""
2Python Descriptors
3
4Demonstrates:
5- Descriptor protocol (__get__, __set__, __delete__)
6- Data vs non-data descriptors
7- property() implementation
8- Validation descriptors
9- Cached properties
10- Type checking descriptors
11"""
12
13from typing import Any, Optional, Callable
14import functools
15
16
17def section(title: str) -> None:
18 """Print a section header."""
19 print("\n" + "=" * 60)
20 print(f" {title}")
21 print("=" * 60)
22
23
24# =============================================================================
25# Basic Descriptor
26# =============================================================================
27
28section("Basic Descriptor Protocol")
29
30
31class Descriptor:
32 """Basic descriptor demonstrating __get__, __set__, __delete__."""
33
34 def __init__(self, name: str):
35 self.name = name
36
37 def __get__(self, instance, owner):
38 """Called when attribute is accessed."""
39 if instance is None:
40 # Accessed on class, not instance
41 print(f" __get__ called on class {owner.__name__}")
42 return self
43
44 print(f" __get__: {self.name} from {instance}")
45 return instance.__dict__.get(self.name, None)
46
47 def __set__(self, instance, value):
48 """Called when attribute is assigned."""
49 print(f" __set__: {self.name} = {value}")
50 instance.__dict__[self.name] = value
51
52 def __delete__(self, instance):
53 """Called when attribute is deleted."""
54 print(f" __delete__: {self.name}")
55 del instance.__dict__[self.name]
56
57
58class MyClass:
59 """Class using descriptor."""
60 attr = Descriptor("attr")
61
62
63obj = MyClass()
64print("Setting attribute:")
65obj.attr = 42
66
67print("\nGetting attribute:")
68value = obj.attr
69print(f" Value: {value}")
70
71print("\nDeleting attribute:")
72del obj.attr
73
74print("\nAccessing on class:")
75MyClass.attr
76
77
78# =============================================================================
79# Data vs Non-Data Descriptors
80# =============================================================================
81
82section("Data vs Non-Data Descriptors")
83
84
85class DataDescriptor:
86 """Data descriptor - has __get__ AND __set__."""
87
88 def __init__(self, name):
89 self.name = name
90
91 def __get__(self, instance, owner):
92 if instance is None:
93 return self
94 return instance.__dict__.get(self.name, "default")
95
96 def __set__(self, instance, value):
97 instance.__dict__[self.name] = value
98
99
100class NonDataDescriptor:
101 """Non-data descriptor - only has __get__."""
102
103 def __init__(self, name):
104 self.name = name
105
106 def __get__(self, instance, owner):
107 if instance is None:
108 return self
109 return instance.__dict__.get(self.name, "default")
110
111
112class TestClass:
113 data_desc = DataDescriptor("data_desc")
114 non_data_desc = NonDataDescriptor("non_data_desc")
115
116
117obj = TestClass()
118
119print("Data descriptor (has priority over instance dict):")
120obj.__dict__["data_desc"] = "instance value"
121print(f" obj.data_desc = {obj.data_desc}") # Still goes through descriptor
122
123print("\nNon-data descriptor (instance dict has priority):")
124obj.__dict__["non_data_desc"] = "instance value"
125print(f" obj.non_data_desc = {obj.non_data_desc}") # Uses instance dict
126
127
128# =============================================================================
129# Validation Descriptor
130# =============================================================================
131
132section("Validation Descriptor")
133
134
135class PositiveNumber:
136 """Descriptor that validates positive numbers."""
137
138 def __init__(self, name: str):
139 self.name = name
140
141 def __get__(self, instance, owner):
142 if instance is None:
143 return self
144 return instance.__dict__.get(self.name)
145
146 def __set__(self, instance, value):
147 if not isinstance(value, (int, float)):
148 raise TypeError(f"{self.name} must be a number")
149 if value <= 0:
150 raise ValueError(f"{self.name} must be positive")
151 instance.__dict__[self.name] = value
152
153
154class Product:
155 """Product with validated price and quantity."""
156
157 price = PositiveNumber("price")
158 quantity = PositiveNumber("quantity")
159
160 def __init__(self, name: str, price: float, quantity: int):
161 self.name = name
162 self.price = price
163 self.quantity = quantity
164
165 def total_value(self) -> float:
166 return self.price * self.quantity
167
168
169product = Product("Widget", 19.99, 100)
170print(f"Product: {product.name}")
171print(f" Price: ${product.price}")
172print(f" Quantity: {product.quantity}")
173print(f" Total value: ${product.total_value()}")
174
175print("\nTrying invalid values:")
176try:
177 product.price = -10
178except ValueError as e:
179 print(f" Error: {e}")
180
181try:
182 product.quantity = "not a number"
183except TypeError as e:
184 print(f" Error: {e}")
185
186
187# =============================================================================
188# Type Checking Descriptor
189# =============================================================================
190
191section("Type Checking Descriptor")
192
193
194class TypedDescriptor:
195 """Descriptor with type checking."""
196
197 def __init__(self, name: str, expected_type: type):
198 self.name = name
199 self.expected_type = expected_type
200
201 def __get__(self, instance, owner):
202 if instance is None:
203 return self
204 return instance.__dict__.get(self.name)
205
206 def __set__(self, instance, value):
207 if not isinstance(value, self.expected_type):
208 raise TypeError(
209 f"{self.name} must be {self.expected_type.__name__}, "
210 f"got {type(value).__name__}"
211 )
212 instance.__dict__[self.name] = value
213
214
215class Person:
216 """Person with type-checked attributes."""
217
218 name = TypedDescriptor("name", str)
219 age = TypedDescriptor("age", int)
220 salary = TypedDescriptor("salary", float)
221
222 def __init__(self, name: str, age: int, salary: float):
223 self.name = name
224 self.age = age
225 self.salary = salary
226
227
228person = Person("Alice", 30, 75000.0)
229print(f"Person: {person.name}, {person.age} years old, ${person.salary}")
230
231try:
232 person.age = "thirty"
233except TypeError as e:
234 print(f"\nType error: {e}")
235
236
237# =============================================================================
238# property() - Built-in Descriptor
239# =============================================================================
240
241section("property() - Built-in Descriptor")
242
243
244class Temperature:
245 """Temperature with Celsius/Fahrenheit conversion."""
246
247 def __init__(self, celsius: float):
248 self._celsius = celsius
249
250 @property
251 def celsius(self) -> float:
252 """Get temperature in Celsius."""
253 print(" Getting celsius")
254 return self._celsius
255
256 @celsius.setter
257 def celsius(self, value: float):
258 """Set temperature in Celsius."""
259 print(f" Setting celsius to {value}")
260 self._celsius = value
261
262 @property
263 def fahrenheit(self) -> float:
264 """Get temperature in Fahrenheit."""
265 print(" Computing fahrenheit")
266 return self._celsius * 9/5 + 32
267
268 @fahrenheit.setter
269 def fahrenheit(self, value: float):
270 """Set temperature via Fahrenheit."""
271 print(f" Setting fahrenheit to {value}")
272 self._celsius = (value - 32) * 5/9
273
274
275temp = Temperature(25)
276print(f"Celsius: {temp.celsius}°C")
277print(f"Fahrenheit: {temp.fahrenheit}°F")
278
279print("\nSetting via Fahrenheit:")
280temp.fahrenheit = 100
281print(f"Celsius: {temp.celsius}°C")
282
283
284# =============================================================================
285# Cached Property
286# =============================================================================
287
288section("Cached Property Descriptor")
289
290
291class CachedProperty:
292 """Descriptor that caches computed property value."""
293
294 def __init__(self, func: Callable):
295 self.func = func
296 self.name = func.__name__
297
298 def __get__(self, instance, owner):
299 if instance is None:
300 return self
301
302 # Check if value is cached
303 cache_name = f"_cached_{self.name}"
304 if not hasattr(instance, cache_name):
305 print(f" Computing {self.name}...")
306 value = self.func(instance)
307 setattr(instance, cache_name, value)
308 else:
309 print(f" Using cached {self.name}")
310
311 return getattr(instance, cache_name)
312
313
314class DataProcessor:
315 """Processor with expensive computations."""
316
317 def __init__(self, data: list):
318 self.data = data
319
320 @CachedProperty
321 def average(self) -> float:
322 """Compute average (expensive operation)."""
323 import time
324 time.sleep(0.1) # Simulate expensive computation
325 return sum(self.data) / len(self.data)
326
327 @CachedProperty
328 def total(self) -> float:
329 """Compute total (expensive operation)."""
330 import time
331 time.sleep(0.1) # Simulate expensive computation
332 return sum(self.data)
333
334
335processor = DataProcessor([1, 2, 3, 4, 5])
336
337print("First access (computed):")
338print(f" Average: {processor.average}")
339
340print("\nSecond access (cached):")
341print(f" Average: {processor.average}")
342
343print("\nAccessing total:")
344print(f" Total: {processor.total}")
345
346
347# =============================================================================
348# Using functools.cached_property
349# =============================================================================
350
351section("functools.cached_property")
352
353
354class WebPage:
355 """Web page with cached properties."""
356
357 def __init__(self, url: str):
358 self.url = url
359
360 @functools.cached_property
361 def content(self) -> str:
362 """Fetch page content (cached)."""
363 print(f" Fetching {self.url}...")
364 import time
365 time.sleep(0.1)
366 return f"Content from {self.url}"
367
368
369page = WebPage("https://example.com")
370print(f"Content (first): {page.content[:30]}...")
371print(f"Content (cached): {page.content[:30]}...")
372
373
374# =============================================================================
375# Read-Only Descriptor
376# =============================================================================
377
378section("Read-Only Descriptor")
379
380
381class ReadOnly:
382 """Read-only descriptor."""
383
384 def __init__(self, value):
385 self.value = value
386
387 def __get__(self, instance, owner):
388 return self.value
389
390 def __set__(self, instance, value):
391 raise AttributeError("Cannot modify read-only attribute")
392
393
394class Config:
395 """Configuration with read-only values."""
396
397 VERSION = ReadOnly("1.0.0")
398 MAX_CONNECTIONS = ReadOnly(100)
399
400
401config = Config()
402print(f"Config.VERSION: {config.VERSION}")
403print(f"Config.MAX_CONNECTIONS: {config.MAX_CONNECTIONS}")
404
405try:
406 config.VERSION = "2.0.0"
407except AttributeError as e:
408 print(f"\nError: {e}")
409
410
411# =============================================================================
412# Lazy Descriptor
413# =============================================================================
414
415section("Lazy Descriptor")
416
417
418class LazyProperty:
419 """Descriptor that lazily initializes value."""
420
421 def __init__(self, init_func: Callable):
422 self.init_func = init_func
423 self.name = init_func.__name__
424
425 def __get__(self, instance, owner):
426 if instance is None:
427 return self
428
429 attr_name = f"_lazy_{self.name}"
430 if not hasattr(instance, attr_name):
431 print(f" Lazy-initializing {self.name}")
432 value = self.init_func(instance)
433 setattr(instance, attr_name, value)
434
435 return getattr(instance, attr_name)
436
437
438class Application:
439 """Application with lazy-loaded components."""
440
441 @LazyProperty
442 def database(self):
443 """Initialize database connection."""
444 print(" Connecting to database...")
445 return "DatabaseConnection"
446
447 @LazyProperty
448 def cache(self):
449 """Initialize cache."""
450 print(" Connecting to cache...")
451 return "CacheConnection"
452
453
454app = Application()
455print("Application created (no connections yet)")
456
457print("\nAccessing database:")
458print(f" {app.database}")
459
460print("\nAccessing database again:")
461print(f" {app.database}")
462
463
464# =============================================================================
465# Summary
466# =============================================================================
467
468section("Summary")
469
470print("""
471Descriptor patterns covered:
4721. Descriptor protocol - __get__, __set__, __delete__
4732. Data vs non-data descriptors - lookup priority
4743. Validation descriptors - enforce constraints
4754. Type checking descriptors - runtime type validation
4765. property() - built-in descriptor for getters/setters
4776. Cached property - compute once, cache result
4787. Read-only descriptor - prevent modification
4798. Lazy property - initialize on first access
480
481Descriptor use cases:
482- Validation and type checking
483- Computed properties
484- Caching expensive computations
485- Lazy initialization
486- ORM field definitions
487- Method binding
488
489Descriptors are the mechanism behind:
490- @property decorator
491- @classmethod and @staticmethod
492- Functions (binding to methods)
493""")