Lazy Fields¶
The lazyfields
module provides advanced lazy evaluation fields for Python classes, supporting both synchronous and asynchronous initialization, with optional per-instance locking for thread safety. This is useful for expensive computations or resources that should be initialized only when accessed.
Why?¶
Consider the following code snippet:
class MyClass:
def __init__(self):
self._expensive = None
@property
def expensive(self):
if self._expensive is None:
print("Computing...")
self._expensive = 42 # Simulate an expensive computation
return self._expensive
lazyfields
module simplifies this by providing decorators that handle lazy initialization, caching, and locking automatically.
from escudeiro.lazyfields import lazyfield, asynclazyfield, mark_class
class MyClass:
@lazyfield
def expensive(self):
print("Computing...")
return 42 # Simulate an expensive computation
Features¶
- Synchronous and asynchronous lazy fields (
@lazyfield
,@asynclazyfield
) - Per-instance locking for thread safety (optional)
- Class marking for isolation of locks
- Manual reset and deletion of lazy values
- Type-safe and dataclass-friendly
Usage¶
Basic Synchronous Lazy Field¶
from escudeiro.lazyfields import lazyfield
class MyClass:
@lazyfield
def expensive(self):
print("Computing...")
return 42
obj = MyClass()
print(obj.expensive) # Prints "Computing..." then 42
print(obj.expensive) # Prints 42 (no recomputation)
Asynchronous Lazy Field¶
from escudeiro.lazyfields import asynclazyfield
import asyncio
class MyAsyncClass:
@asynclazyfield
async def expensive(self):
print("Computing async...")
await asyncio.sleep(1)
return 99
async def main():
obj = MyAsyncClass()
print(await obj.expensive()) # Prints "Computing async..." then 99
print(await obj.expensive()) # Prints 99 (no recomputation)
asyncio.run(main())
Using Locks for Thread Safety¶
import threading
from escudeiro.lazyfields import lazyfield, mark_class
def lock_factory():
return threading.Lock()
@mark_class(ctx_factory=lock_factory)
class ThreadSafe:
@lazyfield
def value(self):
# expensive computation
return 123
API Reference¶
Decorators¶
@lazyfield
¶
@overload
def lazyfield[SelfT, T](
func: Callable[[SelfT], T], /
) -> LazyField[SelfT, T]: ...
@overload
def lazyfield[SelfT, T](
func: None = None,
/,
lock_factory: Callable[[], contextlib.AbstractContextManager] = contextlib.nullcontext,
) -> Callable[[Callable[[SelfT], T]], LazyField[SelfT, T]]: ...
- Description: Decorator for defining a synchronous lazy field.
- Parameters:
func
: The method to compute the value.lock_factory
: Optional, provides a context manager for locking.
@asynclazyfield
¶
@overload
def asynclazyfield[SelfT, T](
func: Callable[[SelfT], Coroutine[Any, Any, T]], /
) -> AsyncLazyField[SelfT, T]: ...
@overload
def asynclazyfield[SelfT, T](
func: None = None,
/,
lock_factory: Callable[[], contextlib.AbstractAsyncContextManager] = contextlib.nullcontext,
) -> Callable[[Callable[[SelfT], Coroutine[Any, Any, T]]], AsyncLazyField[SelfT, T]]: ...
- Description: Decorator for defining an asynchronous lazy field.
- Parameters:
func
: The async method to compute the value.lock_factory
: Optional, provides an async context manager for locking.
mark_class
¶
def mark_class(
ctx_factory: Callable[[], contextlib.AbstractContextManager] | None = None,
actx_factory: Callable[[], contextlib.AbstractAsyncContextManager] | None = None,
)
- Description: Marks a class so each instance gets its own lock for lazy fields.
- Parameters:
ctx_factory
: Factory for sync locks.actx_factory
: Factory for async locks.
Utilities¶
is_initialized(instance, attr)
: ReturnsTrue
if the lazy field is initialized.dellazy(instance, lazyf)
: Resets the lazy field (sync or async).getlazyfield(instance, attr)
: Returns the lazy field descriptor.
Notes¶
- Use
@mark_class
if you want per-instance locking (recommended for thread safety). - For async fields, always use
await obj.field()
. - You can manually reset a field using
dellazy(obj, MyClass.expensive)
.