杰瑞科技汇

Python lazyproperty如何实现延迟加载?

Of course! Let's dive deep into the lazyproperty in Python.

Python lazyproperty如何实现延迟加载?-图1
(图片来源网络,侵删)

What is a lazyproperty?

A lazyproperty is a decorator that turns a regular method into a "lazy" property. This means the method's code is executed only once, the first time the property is accessed. The result of that execution is then cached and returned on all subsequent accesses.

Think of it as a memoization technique for properties.


The Problem It Solves: Eager vs. Lazy Loading

To understand why lazyproperty is useful, let's look at the common alternative: an "eager" property.

Scenario: A Person Class with an Eager Property

Imagine a class Person that has a full_name property, which is a combination of first_name and last_name.

Python lazyproperty如何实现延迟加载?-图2
(图片来源网络,侵删)
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
        # This calculation happens IMMEDIATELY when the object is created.
        self.full_name = f"{self.first_name} {self.last_name}"
p = Person("John", "Doe")
print(p.full_name)  # Output: John Doe

This is simple, but what if calculating full_name was expensive? For example, what if it involved:

  • A complex string formatting operation.
  • A database lookup.
  • A network request to an API.
  • A heavy computation.

You wouldn't want to perform that expensive operation every time you create a Person object, especially if you never even end up needing the full_name!

This is where a lazyproperty shines. It defers the expensive calculation until the moment it's actually needed.


The Solution: Implementing a lazyproperty

Python's standard library doesn't have a built-in @lazyproperty decorator. However, it's very easy to create one using a combination of @property and a caching mechanism.

Python lazyproperty如何实现延迟加载?-图3
(图片来源网络,侵删)

Here is the most common and robust implementation using a private attribute to store the cached value.

Implementation 1: The Classic Caching Approach

This is the most widely used pattern. It's simple, effective, and easy to understand.

class lazyproperty:
    def __init__(self, func):
        self.func = func
        # Store the name of the attribute where the result will be cached.
        # func.__name__ gives us the name of the decorated method (e.g., 'full_name').
        self.attrname = None
        self.__doc__ = func.__doc__ # Preserve the original docstring
    def __get__(self, instance, owner):
        if instance is None:
            # Accessed on the class, not an instance (e.g., Person.full_name)
            return self
        if self.attrname is None:
            # Determine the attribute name on the first access.
            # This is a bit of a workaround for the fact that we don't know
            # the instance's attribute name until __get__ is called.
            # A simpler alternative is to pass the name to the decorator.
            self.attrname = f"_lazy_{self.func.__name__}"
        # Check if the cached value already exists on the instance
        if not hasattr(instance, self.attrname):
            # If not, call the original function and store its result
            setattr(instance, self.attrname, self.func(instance))
        # Return the cached value
        return getattr(instance, self.attrname)

How to Use It

Now, let's use our lazyproperty decorator on the Person class.

class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
        # No calculation happens here!
    @lazyproperty
    def full_name(self):
        print("Calculating full_name... (This should only happen once!)")
        return f"{self.first_name} {self.last_name}"
# --- Let's test it ---
p = Person("Jane", "Smith")
print("-" * 20)
# First access: triggers the calculation
print(f"First access: {p.full_name}")
# Output:
# --------------------
# Calculating full_name... (This should only happen once!)
# First access: Jane Smith
# Second access: uses the cached value
print(f"Second access: {p.full_name}")
# Output:
# Second access: Jane Smith
# (Notice no "Calculating..." message)
# You can see the cached attribute on the instance
print(f"Cached attribute: {p._lazy_full_name}")

When to Use lazyproperty

lazyproperty is a powerful tool, but it's not a universal replacement for @property. Use it when:

  1. Calculations are Expensive: The primary use case is for properties that require significant computation, I/O (disk, network), or other time-consuming operations.
  2. The Value is Immutable: The cached value is assumed not to change. If p.first_name changes after p.full_name has been accessed, the cached full_name will be stale.
  3. The Property Might Not Be Used: It's a way to optimize performance by avoiding work that might never be necessary.

When NOT to Use lazyproperty

Avoid lazyproperty when:

  1. The Value Can Change: If the underlying data can change, the cached value becomes incorrect. A standard @property is better because it re-calculates every time.

    # BAD: Using lazyproperty here leads to stale data
    class BankAccount:
        def __init__(self, balance):
            self.balance = balance
        @lazyproperty
        def formatted_balance(self):
            print("Formatting balance...")
            return f"${self.balance:.2f}"
    acc = BankAccount(100)
    print(acc.formatted_balance) # Formatting balance... $100.00
    acc.balance = 200
    print(acc.formatted_balance) # Still prints $100.00 (INCORRECT!)
  2. The Calculation is Cheap: If getting the value is fast (like self.first_name + " " + self.last_name), the overhead of checking for a cached value might even make it slower. A standard @property is more straightforward.

  3. State-Dependent Logic: If the property's logic depends on the state of other properties and you want the latest value every time, use @property.


Modern Alternative: @functools.cached_property

Since Python 3.8, the standard library includes functools.cached_property. It's the official, well-tested, and recommended way to create lazy properties.

Why prefer functools.cached_property?

  • It's in the standard library: No need to roll your own.
  • It's battle-tested: It's been reviewed and optimized by the core Python developers.
  • Cleaner Syntax: It's designed specifically for this purpose.

How to Use functools.cached_property

The usage is almost identical to our custom implementation.

from functools import cached_property
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
    @cached_property
    def full_name(self):
        print("Calculating full_name with cached_property...")
        return f"{self.first_name} {self.last_name}"
# --- Testing is the same ---
p = Person("Peter", "Jones")
print("-" * 20)
print(f"First access: {p.full_name}")
# Output:
# --------------------
# Calculating full_name with cached_property...
# First access: Peter Jones
print(f"Second access: {p.full_name}")
# Output:
# Second access: Peter Jones

The internal implementation of functools.cached_property is very similar to the one we wrote, but it's more robust and handles edge cases better.

Summary

Feature @property Custom @lazyproperty @functools.cached_property (Python 3.8+)
Purpose Provides controlled access to an attribute. Memoization for expensive property calculations. Memoization for expensive property calculations.
Execution Every time the property is accessed. Only once on first access. Result is cached. Only once on first access. Result is cached.
Performance Fast for simple logic. Overhead of cache lookup, but saves on expensive re-computation. Similar to custom lazyproperty, highly optimized.
Mutability Always returns the current value. Returns a stale value if underlying data changes. Returns a stale value if underlying data changes.
Availability Built-in. DIY or from a library. Built-in (Python 3.8+).

Recommendation: If you are using Python 3.8 or newer, always prefer @functools.cached_property. For older versions, the custom lazyproperty decorator shown above is an excellent and reliable choice.

分享:
扫描分享到社交APP
上一篇
下一篇