Trình trang trí thuộc tính tra cứu trì hoãn / ghi nhớ Python


109

Gần đây, tôi đã xem qua một cơ sở mã hiện có chứa nhiều lớp trong đó các thuộc tính phiên bản phản ánh các giá trị được lưu trữ trong cơ sở dữ liệu. Tôi đã cấu trúc lại rất nhiều thuộc tính này để hoãn việc tra cứu cơ sở dữ liệu của chúng, tức là. không được khởi tạo trong hàm tạo mà chỉ được khởi tạo khi đọc lần đầu. Các thuộc tính này không thay đổi trong suốt thời gian tồn tại của phiên bản, nhưng chúng là một nút thắt cổ chai thực sự để tính toán lần đầu tiên đó và chỉ thực sự được truy cập cho các trường hợp đặc biệt. Do đó, chúng cũng có thể được lưu vào bộ nhớ đệm sau khi chúng được truy xuất từ ​​cơ sở dữ liệu (do đó, điều này phù hợp với định nghĩa về memoisation trong đó đầu vào chỉ đơn giản là "không có đầu vào").

Tôi thấy mình đang gõ đoạn mã sau nhiều lần cho các thuộc tính khác nhau trên các lớp khác nhau:

class testA(object):

  def __init__(self):
    self._a = None
    self._b = None

  @property
  def a(self):
    if self._a is None:
      # Calculate the attribute now
      self._a = 7
    return self._a

  @property
  def b(self):
    #etc

Có một trình trang trí hiện có để thực hiện điều này bằng Python mà tôi đơn giản là không biết không? Hoặc, có một cách hợp lý đơn giản để xác định một người trang trí thực hiện điều này?

Tôi đang làm việc theo Python 2.5, nhưng các câu trả lời 2.6 vẫn có thể thú vị nếu chúng khác nhau đáng kể.

Ghi chú

Câu hỏi này đã được đặt ra trước khi Python bao gồm rất nhiều trình trang trí sẵn sàng cho việc này. Tôi đã cập nhật nó chỉ để sửa thuật ngữ.


Tôi đang sử dụng Python 2.7 và tôi không thấy bất cứ điều gì về trình trang trí làm sẵn cho việc này. Bạn có thể cung cấp một liên kết đến các trang trí làm sẵn được đề cập trong câu hỏi không?
Bamcclur

@Bamcclur xin lỗi, đã từng có những nhận xét khác nêu chi tiết về chúng, không rõ vì sao chúng bị xóa. Người duy nhất tôi có thể tìm thấy ngay bây giờ là một Python 3 một: functools.lru_cache().
detly

Bạn không chắc chắn có được xây dựng-in (ít nhất là Python 2.7), nhưng có thư viện Boltons của cachedproperty
guyarad

@guyarad Tôi không thấy nhận xét này cho đến bây giờ. Đó là một thư viện tuyệt vời! Đăng nó như một câu trả lời để tôi có thể ủng hộ nó.
detly

Câu trả lời:


12

Đối với tất cả các loại tiện ích tuyệt vời, tôi đang sử dụng bu lông .

Là một phần của thư viện đó, bạn có cacheproperty :

from boltons.cacheutils import cachedproperty

class Foo(object):
    def __init__(self):
        self.value = 4

    @cachedproperty
    def cached_prop(self):
        self.value += 1
        return self.value


f = Foo()
print(f.value)  # initial value
print(f.cached_prop)  # cached property is calculated
f.value = 1
print(f.cached_prop)  # same value for the cached property - it isn't calculated again
print(f.value)  # the backing value is different (it's essentially unrelated value)

124

Dưới đây là một ví dụ về triển khai trình trang trí thuộc tính lười biếng:

import functools

def lazyprop(fn):
    attr_name = '_lazy_' + fn.__name__

    @property
    @functools.wraps(fn)
    def _lazyprop(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fn(self))
        return getattr(self, attr_name)

    return _lazyprop


class Test(object):

    @lazyprop
    def a(self):
        print 'generating "a"'
        return range(5)

Phiên tương tác:

>>> t = Test()
>>> t.__dict__
{}
>>> t.a
generating "a"
[0, 1, 2, 3, 4]
>>> t.__dict__
{'_lazy_a': [0, 1, 2, 3, 4]}
>>> t.a
[0, 1, 2, 3, 4]

1
Ai đó có thể giới thiệu một cái tên thích hợp cho chức năng bên trong không? Tôi rất xấu tại danh nghĩa chính vào buổi sáng ...
Mike Boers

2
Tôi thường đặt tên cho hàm bên trong giống với hàm bên ngoài với dấu gạch dưới trước. Vì vậy, "_lazyprop" - theo "chỉ sử dụng nội" triết lý của pep 8.
spenthil

1
Điều này hoạt động tuyệt vời :) Tôi không biết tại sao nó không bao giờ xảy ra với tôi khi sử dụng trình trang trí trên một hàm lồng nhau như vậy.
detly

4
đưa ra giao thức mô tả không phải dữ liệu, giao thức này chậm hơn và kém thanh lịch hơn nhiều so với câu trả lời bên dưới bằng cách sử dụng__get__
Ronny

1
Mẹo: Đặt một @wraps(fn)dưới đây @propertyđể không mất chuỗi doc của bạn vv ( wrapsđến từ functools)
letmaik

111

Tôi đã viết cái này cho chính mình ... Được sử dụng cho các thuộc tính lười biếng được tính toán một lần thực sự . Tôi thích nó vì nó tránh dính các thuộc tính bổ sung trên các đối tượng và khi được kích hoạt sẽ không lãng phí thời gian kiểm tra sự hiện diện của thuộc tính, v.v.:

import functools

class lazy_property(object):
    '''
    meant to be used for lazy evaluation of an object attribute.
    property should represent non-mutable data, as it replaces itself.
    '''

    def __init__(self, fget):
        self.fget = fget

        # copy the getter function's docstring and other attributes
        functools.update_wrapper(self, fget)

    def __get__(self, obj, cls):
        if obj is None:
            return self

        value = self.fget(obj)
        setattr(obj, self.fget.__name__, value)
        return value


class Test(object):

    @lazy_property
    def results(self):
        calcs = 1  # Do a lot of calculation here
        return calcs

Lưu ý: lazy_propertyLớp là một bộ mô tả không phải dữ liệu , có nghĩa là nó ở chế độ chỉ đọc. Thêm một __set__phương thức sẽ ngăn nó hoạt động bình thường.


9
Điều này mất một chút thời gian để hiểu nhưng là một câu trả lời hoàn toàn tuyệt vời. Tôi thích cách bản thân hàm được thay thế bằng giá trị mà nó tính toán.
Paul Etherton

2
Đối với hậu thế: các phiên bản khác của điều này đã được đề xuất trong các câu trả lời khác kể từ (tham khảo 12 ). Có vẻ như đây là một công cụ phổ biến trong các khuôn khổ web Python (các dẫn xuất tồn tại trong Pyramid và Werkzeug).
André Caron

1
Cám ơn chú ý là Werkzeug có werkzeug.utils.cached_property: werkzeug.pocoo.org/docs/utils/#werkzeug.utils.cached_property
divieira

3
Tôi thấy phương pháp này nhanh hơn 7,6 lần so với câu trả lời đã chọn. (2,45 µs / 322 ns) Xem máy tính xách tay ipython
Dave Butler

1
NB: điều này không ngăn cản việc gán cho fgetcách @propertylàm. Để đảm bảo tính bất biến / tính không thay đổi, bạn cần thêm một __set__()phương thức nâng cao AttributeError('can\'t set attribute')(hoặc bất kỳ ngoại lệ / thông báo nào phù hợp với bạn, nhưng đây là điều propertylàm tăng). Điều này không may đi kèm với tác động hiệu suất của một phần nhỏ của micro giây vì __get__()nó sẽ được gọi trên mỗi lần truy cập thay vì kéo giá trị fget từ dict ở lần truy cập thứ hai và tiếp theo. Theo ý kiến ​​của tôi, rất đáng để duy trì tính bất biến / tính bất biến, đó là chìa khóa cho các trường hợp sử dụng của tôi, nhưng YMMV.
scanny

4

Dưới đây là một callable rằng có một đối số thời gian chờ tùy chọn, trong __call__bạn cũng có thể sao chép qua __name__, __doc__, __module__từ namespace func của:

import time

class Lazyproperty(object):

    def __init__(self, timeout=None):
        self.timeout = timeout
        self._cache = {}

    def __call__(self, func):
        self.func = func
        return self

    def __get__(self, obj, objcls):
        if obj not in self._cache or \
          (self.timeout and time.time() - self._cache[key][1] > self.timeout):
            self._cache[obj] = (self.func(obj), time.time())
        return self._cache[obj]

Ví dụ:

class Foo(object):

    @Lazyproperty(10)
    def bar(self):
        print('calculating')
        return 'bar'

>>> x = Foo()
>>> print(x.bar)
calculating
bar
>>> print(x.bar)
bar
...(waiting 10 seconds)...
>>> print(x.bar)
calculating
bar

3

propertylà một lớp học. Một bộ mô tả chính xác. Đơn giản chỉ cần bắt nguồn từ nó và thực hiện các hành vi mong muốn.

class lazyproperty(property):
   ....

class testA(object):
   ....
  a = lazyproperty('_a')
  b = lazyproperty('_b')

3

Những gì bạn thực sự muốn là trình trang trí reify(nguồn được liên kết!) Từ Pyramid:

Sử dụng như một trình trang trí phương thức lớp. Nó hoạt động gần giống như trình @propertytrang trí Python , nhưng nó đặt kết quả của phương thức mà nó trang trí vào instance dict sau lần gọi đầu tiên, thay thế một cách hiệu quả hàm mà nó trang trí bằng một biến instance. Theo cách nói của Python, nó là một bộ mô tả không phải dữ liệu. Sau đây là một ví dụ và cách sử dụng của nó:

>>> from pyramid.decorator import reify

>>> class Foo(object):
...     @reify
...     def jammy(self):
...         print('jammy called')
...         return 1

>>> f = Foo()
>>> v = f.jammy
jammy called
>>> print(v)
1
>>> f.jammy
1
>>> # jammy func not called the second time; it replaced itself with 1
>>> # Note: reassignment is possible
>>> f.jammy = 2
>>> f.jammy
2

1
Nice one, thực hiện chính xác những gì tôi cần ... mặc dù Kim tự tháp có thể là một sự phụ thuộc lớn đối với một trang trí:)
detly

@detly Việc triển khai decorator rất đơn giản và bạn có thể tự thực hiện nó, không cần pyramidphụ thuộc.
Peter Wood,

Do đó liên kết nói "nguồn liên kết": D
Antti Haapala

@AnttiHaapala Tôi đã nhận thấy, nhưng nghĩ rằng tôi muốn nhấn mạnh rằng nó đơn giản để triển khai cho những người không theo liên kết.
Peter Wood,

1

Cho đến nay, có sự pha trộn giữa các thuật ngữ và / hoặc sự nhầm lẫn của các khái niệm cả trong câu hỏi và câu trả lời.

Đánh giá lười biếng chỉ có nghĩa là một cái gì đó được đánh giá trong thời gian chạy ở thời điểm cuối cùng có thể khi một giá trị là cần thiết. Người @propertytrang trí tiêu chuẩn thực hiện điều đó. (*) Chức năng được trang trí chỉ được đánh giá và mọi lúc bạn cần giá trị của thuộc tính đó. (xem bài viết wikipedia về đánh giá lười biếng)

(*) Trên thực tế, đánh giá lười biếng thực sự (so sánh ví dụ như haskell) là rất khó đạt được trong python (và kết quả là mã khác xa với thành ngữ).

Ghi nhớ là thuật ngữ chính xác cho những gì người hỏi dường như đang tìm kiếm. Các hàm thuần túy không phụ thuộc vào các tác dụng phụ để đánh giá giá trị trả về có thể được ghi nhớ một cách an toàn và thực sự có một trình trang trí trong functools @functools.lru_cache nên không cần phải viết trình trang trí riêng trừ khi bạn cần hành vi chuyên biệt.


Tôi đã sử dụng thuật ngữ "lazy" vì trong lần triển khai ban đầu, thành viên đã được tính toán / truy xuất từ ​​DB tại thời điểm khởi tạo đối tượng và tôi muốn trì hoãn việc tính toán đó cho đến khi thuộc tính thực sự được sử dụng trong một mẫu. Điều này đối với tôi dường như phù hợp với định nghĩa của sự lười biếng. Tôi đồng ý rằng vì câu hỏi của tôi đã giả định một giải pháp sử dụng @property, "lười biếng" không có ý nghĩa gì ở điểm đó. (Tôi cũng nghĩ đến memoisation như bản đồ về đầu vào cho kết quả đầu ra được lưu trữ, và vì các đặc tính này chỉ có một đầu vào, không có gì, một bản đồ có vẻ như phức tạp hơn cần thiết.)
detly

Lưu ý rằng tất cả các giải pháp trang trí mà mọi người đã đề xuất là giải pháp "hữu ích" không tồn tại khi tôi hỏi điều này.
detly

Tôi đồng ý với Jason, đây là một câu hỏi về bộ nhớ đệm / ghi nhớ không lười biếng đánh giá.
poindexter

@poindexter - Bộ nhớ đệm không hoàn toàn bao gồm nó; nó không phân biệt việc tìm kiếm giá trị tại thời điểm bắt đầu đối tượng và bộ nhớ đệm nó với việc tìm kiếm giá trị và lưu vào bộ nhớ đệm khi thuộc tính được truy cập (đây là tính năng chính ở đây). Tôi nên gọi nó là gì? Trình trang trí "Cache-after-first-use"?
detly

@detly Ghi nhớ. Bạn nên gọi nó là Memoize. en.wikipedia.org/wiki/Memoization
poindexter

0

Bạn có thể làm điều này một cách tốt đẹp và dễ dàng bằng cách xây dựng một lớp từ thuộc tính gốc Python:

class cached_property(property):
    def __init__(self, func, name=None, doc=None):
        self.__name__ = name or func.__name__
        self.__module__ = func.__module__
        self.__doc__ = doc or func.__doc__
        self.func = func

    def __set__(self, obj, value):
        obj.__dict__[self.__name__] = value

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        value = obj.__dict__.get(self.__name__, None)
        if value is None:
            value = self.func(obj)
            obj.__dict__[self.__name__] = value
        return value

Chúng ta có thể sử dụng lớp thuộc tính này như thuộc tính lớp thông thường (Nó cũng hỗ trợ gán mục như bạn có thể thấy)

class SampleClass():
    @cached_property
    def cached_property(self):
        print('I am calculating value')
        return 'My calculated value'


c = SampleClass()
print(c.cached_property)
print(c.cached_property)
c.cached_property = 2
print(c.cached_property)
print(c.cached_property)

Giá trị chỉ được tính lần đầu tiên và sau đó chúng tôi đã sử dụng giá trị đã lưu của mình

Đầu ra:

I am calculating value
My calculated value
My calculated value
2
2
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.