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