Notice: This page requires JavaScript to function properly.
Please enable JavaScript in your browser settings or update your browser.
Вивчайте Lazy-Loading Descriptors | Building Useful Descriptors
Python Descriptors Explained

Lazy-Loading Descriptors

Свайпніть щоб показати меню

Some attributes are expensive to compute – they require a database query, a file read, or a heavy calculation. Lazy loading defers this work until the attribute is first accessed, then caches the result so subsequent accesses are instant. A descriptor is the cleanest way to implement this pattern.

The Problem with Eager Computation

Computing expensive attributes in __init__ wastes resources when the attribute is never used:

123456789101112131415
import time # Eager computation – always runs, even if result is never used class AnalyticsReport: def __init__(self, report_id): self.report_id = report_id self.summary = self._compute_summary() # Always runs def _compute_summary(self): time.sleep(0.5) # Simulating an expensive computation return f"Summary for {self.report_id}" # Creating 10 reports – all summaries computed upfront reports = [AnalyticsReport(f"R-{report_id:03d}") for report_id in range(10)] # 5 seconds wasted even if we only read one summary

A Lazy-Loading Descriptor

A lazy descriptor computes the value on first access and stores it directly in the instance __dict__. On subsequent accesses, the instance attribute shadows the descriptor – no recomputation:

123456789101112131415161718192021222324252627282930313233343536373839
import time # Lazy descriptor – computes once, then stores in instance __dict__ class LazyAttribute: def __init__(self, compute_func): self._compute_func = compute_func self._name = None def __set_name__(self, owner, name): self._name = name def __get__(self, obj, objtype=None): if obj is None: return self # Computing the value and storing it in instance __dict__ value = self._compute_func(obj) obj.__dict__[self._name] = value # Shadows the descriptor on next access return value class AnalyticsReport: def __init__(self, report_id): self.report_id = report_id @LazyAttribute def summary(self): time.sleep(0.1) # Simulating expensive computation return f"Summary for {self.report_id}" report = AnalyticsReport("R-001") # First access – computation runs print(report.summary) # "Summary for R-001" # Second access – returns cached value from __dict__, no computation print(report.summary) # "Summary for R-001" – instant # Confirming it is now stored in instance __dict__ print("summary" in report.__dict__) # True

Why This Works

After the first access, obj.__dict__["summary"] exists. On the next lookup, Python finds the instance attribute before reaching the descriptor – because LazyAttribute is a non-data descriptor (it only defines __get__). The instance attribute shadows it permanently.

This is the key design decision: lazy descriptors must be non-data descriptors. If they defined __set__, they would take priority over the instance __dict__ and the cached value would never be returned.

Using LazyAttribute as a Decorator

1234567891011121314151617181920
import time class DataPipeline: def __init__(self, pipeline_id, raw_records): self.pipeline_id = pipeline_id self.raw_records = raw_records @LazyAttribute def processed_records(self): time.sleep(0.1) # Simulating data transformation return [record.strip().upper() for record in self.raw_records] @LazyAttribute def record_count(self): return len(self.processed_records) pipeline = DataPipeline("P-001", [" alice ", " bob ", " carol "]) print(pipeline.record_count) # 3 – triggers both lazy attributes print(pipeline.processed_records) # ['ALICE', 'BOB', 'CAROL'] – already cached

Lazy Descriptor vs functools.cached_property

Python 3.8 introduced functools.cached_property, which implements exactly this pattern:

123456789101112131415
import functools import time class MarketReport: def __init__(self, market_id): self.market_id = market_id @functools.cached_property def volatility_index(self): time.sleep(0.1) # Simulating expensive calculation return round(hash(self.market_id) % 100 / 10, 2) report = MarketReport("NYSE") print(report.volatility_index) # Computed once print(report.volatility_index) # Cached

cached_property is preferred for simple cases. Build a custom lazy descriptor when you need additional control – expiry, invalidation, or conditional caching.

question mark

Why must a lazy-loading descriptor be a non-data descriptor rather than a data descriptor?

Виберіть правильну відповідь

Все було зрозуміло?

Як ми можемо покращити це?

Дякуємо за ваш відгук!

Секція 3. Розділ 2

Запитати АІ

expand

Запитати АІ

ChatGPT

Запитайте про що завгодно або спробуйте одне із запропонованих запитань, щоб почати наш чат

Секція 3. Розділ 2
some-alt