Có một trang trí để chỉ đơn giản là trả lại các giá trị chức năng bộ đệm?


157

Hãy xem xét những điều sau đây:

@property
def name(self):

    if not hasattr(self, '_name'):

        # expensive calculation
        self._name = 1 + 1

    return self._name

Tôi là người mới, nhưng tôi nghĩ bộ nhớ đệm có thể được đưa vào trang trí. Chỉ có tôi không tìm thấy một cái như thế;)

PS tính toán thực không phụ thuộc vào các giá trị có thể thay đổi


Có thể có một người trang trí ngoài kia có một số khả năng như thế, nhưng bạn chưa chỉ định kỹ lưỡng những gì bạn muốn. Bạn đang sử dụng loại phụ trợ bộ nhớ đệm nào? Và giá trị sẽ được khóa như thế nào? Tôi giả sử từ mã của bạn rằng những gì bạn thực sự yêu cầu là một thuộc tính chỉ đọc được lưu trong bộ nhớ cache.
David Berger

Có các trang trí ghi nhớ thực hiện những gì bạn gọi là "bộ nhớ đệm"; chúng thường hoạt động trên các hàm như vậy (dù có nghĩa là trở thành phương thức hay không) mà kết quả của chúng phụ thuộc vào đối số của chúng (không phụ thuộc vào những thứ có thể thay đổi như bản thân! -) và do đó giữ một bản ghi nhớ riêng.
Alex Martelli

Câu trả lời:


206

Bắt đầu từ Python 3.2, có một trình trang trí tích hợp:

@functools.lru_cache(maxsize=100, typed=False)

Trình trang trí để bọc một chức năng với một cuộc gọi có thể ghi nhớ giúp tiết kiệm tối đa các cuộc gọi gần đây nhất. Nó có thể tiết kiệm thời gian khi một hàm ràng buộc I / O đắt tiền được gọi định kỳ với cùng các đối số.

Ví dụ về bộ đệm LRU để tính toán các số Fibonacci :

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> print([fib(n) for n in range(16)])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> print(fib.cache_info())
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Nếu bạn bị mắc kẹt với Python 2.x, đây là danh sách các thư viện ghi nhớ tương thích khác:



backport bây giờ có thể được tìm thấy ở đây: pypi.python.org/pypi/backports.funcools_lru_cache
Frederick Nord

@gerrit về lý thuyết, nó hoạt động cho các đối tượng có thể băm nói chung - mặc dù một số đối tượng có thể băm chỉ bằng nhau nếu chúng là cùng một đối tượng (như các đối tượng do người dùng định nghĩa mà không có hàm __hash __ () rõ ràng).
Jonathan

1
@Jonathan Nó hoạt động, nhưng sai. Nếu tôi chuyển một đối số có thể băm, có thể thay đổi và thay đổi giá trị của đối tượng sau lệnh gọi đầu tiên của hàm, cuộc gọi thứ hai sẽ trả về đối tượng đã thay đổi, không phải là đối tượng ban đầu. Đó gần như chắc chắn không phải là những gì người dùng muốn. Để nó hoạt động cho các đối số có thể thay đổi sẽ yêu cầu lru_cachetạo một bản sao của bất kỳ kết quả nào mà nó lưu vào bộ đệm và không có bản sao nào được tạo ra trong quá trình functools.lru_cachethực hiện. Làm như vậy cũng có nguy cơ tạo ra các vấn đề bộ nhớ khó tìm khi được sử dụng để lưu trữ một đối tượng lớn.
gerrit

@gerrit Would bạn phiền theo dõi ở đây: stackoverflow.com/questions/44583381/... ? Tôi đã không hoàn toàn làm theo ví dụ của bạn.
Jonathan

28

Có vẻ như bạn không yêu cầu trang trí ghi nhớ mục đích chung (nghĩa là bạn không quan tâm đến trường hợp chung nơi bạn muốn lưu trữ các giá trị trả về cho các giá trị đối số khác nhau). Đó là, bạn muốn có điều này:

x = obj.name  # expensive
y = obj.name  # cheap

trong khi một trang trí ghi nhớ mục đích chung sẽ cung cấp cho bạn điều này:

x = obj.name()  # expensive
y = obj.name()  # cheap

Tôi gửi rằng cú pháp gọi phương thức là kiểu tốt hơn, bởi vì nó cho thấy khả năng tính toán đắt tiền trong khi cú pháp thuộc tính gợi ý tra cứu nhanh.

[Cập nhật: Trình trang trí ghi nhớ dựa trên lớp mà tôi đã liên kết và trích dẫn ở đây trước đây không hoạt động cho các phương thức. Tôi đã thay thế nó bằng chức năng trang trí.] Nếu bạn sẵn sàng sử dụng một công cụ trang trí ghi nhớ đa năng, đây là một cách đơn giản:

def memoize(function):
  memo = {}
  def wrapper(*args):
    if args in memo:
      return memo[args]
    else:
      rv = function(*args)
      memo[args] = rv
      return rv
  return wrapper

Ví dụ sử dụng:

@memoize
def fibonacci(n):
  if n < 2: return n
  return fibonacci(n - 1) + fibonacci(n - 2)

Một trang trí ghi nhớ khác với giới hạn về kích thước bộ đệm có thể được tìm thấy ở đây .


Không ai trong số các nhà trang trí được đề cập trong tất cả các câu trả lời làm việc cho các phương pháp! Có lẽ bởi vì họ dựa trên lớp học. Chỉ có một bản thân được thông qua? Những người khác hoạt động tốt, nhưng thật tệ khi lưu trữ các giá trị trong các hàm.
Tobias

2
Tôi nghĩ bạn có thể gặp vấn đề nếu args không thể băm được.
Không biết

1
@Un Unknown Có, trang trí đầu tiên mà tôi trích dẫn ở đây được giới hạn ở các loại có thể băm. Một trong ActiveState (với giới hạn kích thước bộ đệm) chọn các đối số thành một chuỗi (có thể băm), tất nhiên là đắt hơn nhưng tổng quát hơn.
Nhà bếp của Nathan

@vanity Cảm ơn bạn đã chỉ ra những hạn chế của các nhà trang trí dựa trên lớp. Tôi đã sửa đổi câu trả lời của mình để hiển thị chức năng trang trí, hoạt động cho các phương thức (tôi thực sự đã thử nghiệm phương pháp này).
Nhà bếp của Nathan

1
@SiminJie Trình trang trí chỉ được gọi một lần và hàm được gói mà nó trả về là cùng một hàm được sử dụng cho tất cả các lệnh gọi khác nhau fibonacci. Hàm đó luôn sử dụng cùng một memotừ điển.
Nhà bếp của Nathan

22
class memorize(dict):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args):
        return self[args]

    def __missing__(self, key):
        result = self[key] = self.func(*key)
        return result

Mẫu sử dụng:

>>> @memorize
... def foo(a, b):
...     return a * b
>>> foo(2, 4)
8
>>> foo
{(2, 4): 8}
>>> foo('hi', 3)
'hihihi'
>>> foo
{(2, 4): 8, ('hi', 3): 'hihihi'}

Lạ thật! Cái này hoạt động ra sao? Nó không giống như các trang trí khác mà tôi đã thấy.
PascalVKooten

1
Giải pháp này trả về TypeError nếu một người sử dụng các đối số từ khóa, ví dụ foo (3, b = 5)
kadee

1
Vấn đề của giải pháp là nó không có giới hạn bộ nhớ. Đối với các đối số được đặt tên, bạn chỉ có thể thêm chúng vào __ call__ và __ mất__ như ** nargs
Leonid Mednikov

16

Python 3.8 functools.cached_propertytrang trí

https://docs.python.org/dev/l Library / funcools.html # funcools.cached_property

cached_propertytừ Werkzeug đã được đề cập tại: https://stackoverflow.com/a/5295190/895245 nhưng một phiên bản được cho là có nguồn gốc sẽ được hợp nhất thành 3.8, thật tuyệt vời.

Trình trang trí này có thể được xem như bộ nhớ đệm @propertyhoặc là trình dọn dẹp @functools.lru_cachekhi bạn không có bất kỳ đối số nào.

Các tài liệu nói:

@functools.cached_property(func)

Chuyển đổi một phương thức của một lớp thành một thuộc tính có giá trị được tính toán một lần và sau đó được lưu vào bộ đệm như một thuộc tính bình thường cho vòng đời của thể hiện. Tương tự như property (), với việc thêm bộ đệm. Hữu ích cho các thuộc tính tính toán đắt tiền của các trường hợp có hiệu quả bất biến.

Thí dụ:

class DataSet:
    def __init__(self, sequence_of_numbers):
        self._data = sequence_of_numbers

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

    @cached_property
    def variance(self):
        return statistics.variance(self._data)

Mới trong phiên bản 3.8.

Lưu ý Trình trang trí này yêu cầu thuộc tính dict trên mỗi phiên bản là ánh xạ có thể thay đổi. Điều này có nghĩa là nó sẽ không hoạt động với một số loại, chẳng hạn như siêu dữ liệu (vì các thuộc tính dict trên các thể hiện loại là các proxy chỉ đọc cho không gian tên lớp) và các loại chỉ định các vị trí mà không bao gồm dict là một trong các vị trí được xác định (như các lớp đó không cung cấp một thuộc tính dict nào cả).



9

Tôi đã mã hóa lớp trang trí đơn giản này để phản hồi chức năng bộ đệm. Tôi thấy nó RẤT hữu ích cho các dự án của tôi:

from datetime import datetime, timedelta 

class cached(object):
    def __init__(self, *args, **kwargs):
        self.cached_function_responses = {}
        self.default_max_age = kwargs.get("default_cache_max_age", timedelta(seconds=0))

    def __call__(self, func):
        def inner(*args, **kwargs):
            max_age = kwargs.get('max_age', self.default_max_age)
            if not max_age or func not in self.cached_function_responses or (datetime.now() - self.cached_function_responses[func]['fetch_time'] > max_age):
                if 'max_age' in kwargs: del kwargs['max_age']
                res = func(*args, **kwargs)
                self.cached_function_responses[func] = {'data': res, 'fetch_time': datetime.now()}
            return self.cached_function_responses[func]['data']
        return inner

Việc sử dụng rất đơn giản:

import time

@cached
def myfunc(a):
    print "in func"
    return (a, datetime.now())

@cached(default_max_age = timedelta(seconds=6))
def cacheable_test(a):
    print "in cacheable test: "
    return (a, datetime.now())


print cacheable_test(1,max_age=timedelta(seconds=5))
print cacheable_test(2,max_age=timedelta(seconds=5))
time.sleep(7)
print cacheable_test(3,max_age=timedelta(seconds=5))

1
Đầu tiên của bạn @cachedlà thiếu dấu ngoặc đơn. Khác, nó sẽ chỉ trả lại cachedđối tượng ở vị trí myfuncvà khi được gọi là myfunc()sau đó innersẽ luôn được trả về dưới dạng giá trị trả về
Markus Meskanen

6

TUYÊN BỐ TỪ CHỐI: Tôi là tác giả của Kids.cache .

Bạn nên kiểm tra kids.cache, nó cung cấp một trình @cachetrang trí hoạt động trên python 2 và python 3. Không phụ thuộc, ~ 100 dòng mã. Ví dụ, rất đơn giản để sử dụng mã của bạn, bạn có thể sử dụng nó như thế này:

pip install kids.cache

Sau đó

from kids.cache import cache
...
class MyClass(object):
    ...
    @cache            # <-- That's all you need to do
    @property
    def name(self):
        return 1 + 1  # supposedly expensive calculation

Hoặc bạn có thể đặt @cachetrang trí sau @property(kết quả tương tự).

Sử dụng bộ đệm trên một thuộc tính được gọi là đánh giá lười biếng , kids.cachecó thể làm nhiều hơn thế (nó hoạt động trên chức năng với bất kỳ đối số, thuộc tính, bất kỳ loại phương thức và thậm chí các lớp ...). Đối với người dùng nâng cao, các kids.cachehỗ trợ cachetoolscung cấp lưu trữ bộ đệm ưa thích cho python 2 và python 3 (bộ nhớ cache LRU, LFU, TTL, RR).

LƯU Ý QUAN TRỌNG : kho lưu trữ bộ đệm mặc định của kids.cachelà một lệnh chính, không được khuyến nghị cho chương trình chạy dài với các truy vấn khác nhau vì nó sẽ dẫn đến một kho lưu trữ bộ đệm ngày càng tăng. Đối với việc sử dụng này, bạn có thể bổ sung các cửa hàng bộ nhớ cache khác bằng cách sử dụng ( @cache(use=cachetools.LRUCache(maxsize=2))để trang trí chức năng / thuộc tính / lớp / phương thức của bạn ...)


Mô-đun này dường như dẫn đến thời gian nhập chậm trên python 2 ~ 0,9 (xem: pastebin.com/raw/aA1ZBE9Z ). Tôi nghi ngờ rằng điều này là do dòng này github.com/0k/kids.cache/blob/master/src/kids/__init__.py#L3 (điểm nhập cf setuptools). Tôi đang tạo ra một vấn đề cho việc này.
Att Righ

Đây là một vấn đề cho github.com/0k/kids.cache/issues/9 .
Att Righ

Điều này sẽ dẫn đến rò rỉ bộ nhớ.
Timothy Zhang

@vaab tạo một thể hiện ccủa MyClass, và kiểm tra nó với objgraph.show_backrefs([c], max_depth=10), có một chuỗi ref từ đối tượng lớp MyClassđể c. Điều đó có nghĩa là, csẽ không bao giờ được phát hành cho đến khi MyClassđược phát hành.
Timothy Zhang

@TimothyZhang bạn được mời và hoan nghênh thêm mối quan tâm của bạn vào github.com/0k/kids.cache/issues/10 . Stackoverflow không phải là nơi thích hợp để có một cuộc thảo luận thích hợp về điều đó. Và cần làm rõ thêm. Cảm ơn phản hôi của bạn.
vaab


4

fastcache , đó là "C triển khai Python 3 funcools.lru_cache. Cung cấp tốc độ tăng tốc 10-30x so với thư viện chuẩn."

Giống như câu trả lời được chọn , chỉ cần nhập khác nhau:

from fastcache import lru_cache
@lru_cache(maxsize=128, typed=False)
def f(a, b):
    pass

Ngoài ra, nó được cài đặt trong Anaconda , không giống như funcools cần được cài đặt .


1
functoolslà một phần của thư viện tiêu chuẩn, liên kết bạn đã đăng là đến một ngã ba git ngẫu nhiên hoặc một cái gì đó khác ...
cz


3

Nếu bạn đang sử dụng Django Framework, nó có thuộc tính như vậy để lưu trữ chế độ xem hoặc phản hồi của API bằng cách sử dụng @cache_page(time)và cũng có thể có các tùy chọn khác.

Thí dụ:

@cache_page(60 * 15, cache="special_cache")
def my_view(request):
    ...

Thông tin chi tiết có thể được tìm thấy ở đây .


2

Cùng với ví dụ Ghi nhớ tôi đã tìm thấy các gói python sau:

  • bộ đệm ; Nó cho phép thiết lập ttl và \ hoặc số lượng cuộc gọi cho các chức năng được lưu trong bộ nhớ cache; Ngoài ra, người ta có thể sử dụng bộ đệm dựa trên tệp được mã hóa ...
  • percache

1

Tôi đã triển khai một cái gì đó như thế này, sử dụng dưa chua để duy trì và sử dụng sha1 cho các ID ngắn gần như chắc chắn duy nhất. Về cơ bản, bộ đệm đã băm mã của hàm và lịch sử của các đối số để có được một sha1 sau đó tìm kiếm một tệp có sha1 đó trong tên. Nếu nó tồn tại, nó đã mở nó và trả về kết quả; nếu không, nó gọi hàm và lưu kết quả (tùy chọn chỉ lưu nếu mất một khoảng thời gian nhất định để xử lý).

Điều đó nói rằng, tôi thề tôi đã tìm thấy một mô-đun hiện có đã làm điều này và thấy mình ở đây đang cố gắng tìm mô-đun đó ... Cái gần nhất tôi có thể tìm thấy là cái này, có vẻ đúng: http: //chase-seibert.github. io / blog / 2011/11/23 / pythondjango-đĩa-dựa-cacheing-decorator.html

Vấn đề duy nhất tôi thấy đó là nó sẽ không hoạt động tốt đối với các đầu vào lớn vì nó băm str (arg), không phải là duy nhất cho các mảng khổng lồ.

Sẽ thật tuyệt nếu có một giao thức unique_hash () có một lớp trả về một hàm băm an toàn cho nội dung của nó. Về cơ bản, tôi đã thực hiện thủ công cho các loại tôi quan tâm.



1

Nếu bạn đang sử dụng Django và muốn lưu trữ lượt xem, hãy xem câu trả lời của Nikhil Kumar .


Nhưng nếu bạn muốn lưu trữ bất kỳ kết quả chức năng nào, bạn có thể sử dụng django-cache-utils .

Nó sử dụng lại bộ đệm Django và cung cấp cachedtrang trí dễ sử dụng :

from cache_utils.decorators import cached

@cached(60)
def foo(x, y=0):
    print 'foo is called'
    return x+y

1

@lru_cache không hoàn hảo với các giá trị hàm mặc định

memtrang trí của tôi :

import inspect


def get_default_args(f):
    signature = inspect.signature(f)
    return {
        k: v.default
        for k, v in signature.parameters.items()
        if v.default is not inspect.Parameter.empty
    }


def full_kwargs(f, kwargs):
    res = dict(get_default_args(f))
    res.update(kwargs)
    return res


def mem(func):
    cache = dict()

    def wrapper(*args, **kwargs):
        kwargs = full_kwargs(func, kwargs)
        key = list(args)
        key.extend(kwargs.values())
        key = hash(tuple(key))
        if key in cache:
            return cache[key]
        else:
            res = func(*args, **kwargs)
            cache[key] = res
            return res
    return wrapper

và mã để thử nghiệm:

from time import sleep


@mem
def count(a, *x, z=10):
    sleep(2)
    x = list(x)
    x.append(z)
    x.append(a)
    return sum(x)


def main():
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5))
    print(count(1,2,3,4,5, z=6))
    print(count(1,2,3,4,5, z=6))
    print(count(1))
    print(count(1, z=10))


if __name__ == '__main__':
    main()

kết quả - chỉ 3 lần với giấc ngủ

nhưng với @lru_cachenó sẽ là 4 lần, bởi vì điều này:

print(count(1))
print(count(1, z=10))

sẽ được tính hai lần (làm việc xấu với mặc định)

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.