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