07_descriptors.py

Download
python 494 lines 12.8 KB
  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""")