Merge branch 'master' of github.com:pydanny/cached-property
[cached-property.git] / README.rst
1 ===============================
2 cached-property
3 ===============================
4
5 .. image:: https://img.shields.io/pypi/v/cached-property.svg
6 :target: https://pypi.python.org/pypi/cached-property
7
8 .. image:: https://github.com/pydanny/cached-property/workflows/Python%20package/badge.svg
9 :target: https://github.com/pydanny/cached-property/actions
10
11
12 .. image:: https://img.shields.io/badge/code%20style-black-000000.svg
13 :target: https://github.com/ambv/black
14 :alt: Code style: black
15
16
17 A decorator for caching properties in classes.
18
19 Why?
20 -----
21
22 * Makes caching of time or computational expensive properties quick and easy.
23 * Because I got tired of copy/pasting this code from non-web project to non-web project.
24 * I needed something really simple that worked in Python 2 and 3.
25
26 How to use it
27 --------------
28
29 Let's define a class with an expensive property. Every time you stay there the
30 price goes up by $50!
31
32 .. code-block:: python
33
34 class Monopoly(object):
35
36 def __init__(self):
37 self.boardwalk_price = 500
38
39 @property
40 def boardwalk(self):
41 # In reality, this might represent a database call or time
42 # intensive task like calling a third-party API.
43 self.boardwalk_price += 50
44 return self.boardwalk_price
45
46 Now run it:
47
48 .. code-block:: python
49
50 >>> monopoly = Monopoly()
51 >>> monopoly.boardwalk
52 550
53 >>> monopoly.boardwalk
54 600
55
56 Let's convert the boardwalk property into a ``cached_property``.
57
58 .. code-block:: python
59
60 from cached_property import cached_property
61
62 class Monopoly(object):
63
64 def __init__(self):
65 self.boardwalk_price = 500
66
67 @cached_property
68 def boardwalk(self):
69 # Again, this is a silly example. Don't worry about it, this is
70 # just an example for clarity.
71 self.boardwalk_price += 50
72 return self.boardwalk_price
73
74 Now when we run it the price stays at $550.
75
76 .. code-block:: python
77
78 >>> monopoly = Monopoly()
79 >>> monopoly.boardwalk
80 550
81 >>> monopoly.boardwalk
82 550
83 >>> monopoly.boardwalk
84 550
85
86 Why doesn't the value of ``monopoly.boardwalk`` change? Because it's a **cached property**!
87
88 Invalidating the Cache
89 ----------------------
90
91 Results of cached functions can be invalidated by outside forces. Let's demonstrate how to force the cache to invalidate:
92
93 .. code-block:: python
94
95 >>> monopoly = Monopoly()
96 >>> monopoly.boardwalk
97 550
98 >>> monopoly.boardwalk
99 550
100 >>> # invalidate the cache
101 >>> del monopoly.__dict__['boardwalk']
102 >>> # request the boardwalk property again
103 >>> monopoly.boardwalk
104 600
105 >>> monopoly.boardwalk
106 600
107
108 Working with Threads
109 ---------------------
110
111 What if a whole bunch of people want to stay at Boardwalk all at once? This means using threads, which
112 unfortunately causes problems with the standard ``cached_property``. In this case, switch to using the
113 ``threaded_cached_property``:
114
115 .. code-block:: python
116
117 from cached_property import threaded_cached_property
118
119 class Monopoly(object):
120
121 def __init__(self):
122 self.boardwalk_price = 500
123
124 @threaded_cached_property
125 def boardwalk(self):
126 """threaded_cached_property is really nice for when no one waits
127 for other people to finish their turn and rudely start rolling
128 dice and moving their pieces."""
129
130 sleep(1)
131 self.boardwalk_price += 50
132 return self.boardwalk_price
133
134 Now use it:
135
136 .. code-block:: python
137
138 >>> from threading import Thread
139 >>> from monopoly import Monopoly
140 >>> monopoly = Monopoly()
141 >>> threads = []
142 >>> for x in range(10):
143 >>> thread = Thread(target=lambda: monopoly.boardwalk)
144 >>> thread.start()
145 >>> threads.append(thread)
146
147 >>> for thread in threads:
148 >>> thread.join()
149
150 >>> self.assertEqual(m.boardwalk, 550)
151
152
153 Working with async/await (Python 3.5+)
154 --------------------------------------
155
156 The cached property can be async, in which case you have to use await
157 as usual to get the value. Because of the caching, the value is only
158 computed once and then cached:
159
160 .. code-block:: python
161
162 from cached_property import cached_property
163
164 class Monopoly(object):
165
166 def __init__(self):
167 self.boardwalk_price = 500
168
169 @cached_property
170 async def boardwalk(self):
171 self.boardwalk_price += 50
172 return self.boardwalk_price
173
174 Now use it:
175
176 .. code-block:: python
177
178 >>> async def print_boardwalk():
179 ... monopoly = Monopoly()
180 ... print(await monopoly.boardwalk)
181 ... print(await monopoly.boardwalk)
182 ... print(await monopoly.boardwalk)
183 >>> import asyncio
184 >>> asyncio.get_event_loop().run_until_complete(print_boardwalk())
185 550
186 550
187 550
188
189 Note that this does not work with threading either, most asyncio
190 objects are not thread-safe. And if you run separate event loops in
191 each thread, the cached version will most likely have the wrong event
192 loop. To summarize, either use cooperative multitasking (event loop)
193 or threading, but not both at the same time.
194
195
196 Timing out the cache
197 --------------------
198
199 Sometimes you want the price of things to reset after a time. Use the ``ttl``
200 versions of ``cached_property`` and ``threaded_cached_property``.
201
202 .. code-block:: python
203
204 import random
205 from cached_property import cached_property_with_ttl
206
207 class Monopoly(object):
208
209 @cached_property_with_ttl(ttl=5) # cache invalidates after 5 seconds
210 def dice(self):
211 # I dare the reader to implement a game using this method of 'rolling dice'.
212 return random.randint(2,12)
213
214 Now use it:
215
216 .. code-block:: python
217
218 >>> monopoly = Monopoly()
219 >>> monopoly.dice
220 10
221 >>> monopoly.dice
222 10
223 >>> from time import sleep
224 >>> sleep(6) # Sleeps long enough to expire the cache
225 >>> monopoly.dice
226 3
227 >>> monopoly.dice
228 3
229
230 **Note:** The ``ttl`` tools do not reliably allow the clearing of the cache. This
231 is why they are broken out into seperate tools. See https://github.com/pydanny/cached-property/issues/16.
232
233 Credits
234 --------
235
236 * Pip, Django, Werkzueg, Bottle, Pyramid, and Zope for having their own implementations. This package originally used an implementation that matched the Bottle version.
237 * Reinout Van Rees for pointing out the `cached_property` decorator to me.
238 * My awesome wife `@audreyr`_ who created `cookiecutter`_, which meant rolling this out took me just 15 minutes.
239 * @tinche for pointing out the threading issue and providing a solution.
240 * @bcho for providing the time-to-expire feature
241
242 .. _`@audreyr`: https://github.com/audreyr
243 .. _`cookiecutter`: https://github.com/audreyr/cookiecutter
244
245 Support This Project
246 ---------------------------
247
248 This project is maintained by volunteers. Support their efforts by spreading the word about:
249
250 Django Crash Course
251 ++++++++++++++++++++++++++++++++++++
252
253 .. image:: https://cdn.shopify.com/s/files/1/0304/6901/files/Django-Crash-Course-300x436.jpg
254 :name: Django Crash Course: Covers Django 3.0 and Python 3.8
255 :align: center
256 :alt: Django Crash Course
257 :target: https://www.roygreenfeld.com/products/django-crash-course
258
259 Django Crash Course for Django 3.0 and Python 3.8 is the best cheese-themed Django reference in the universe!