-
Notifications
You must be signed in to change notification settings - Fork 222
/
item_30_consider_property.py
186 lines (143 loc) · 6.18 KB
/
item_30_consider_property.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# Item 30: Consider @property instead of refactoring attributes
from datetime import timedelta
import datetime
# The built-in @property decorator makes it easy for simple accesses of an
# instance's attributes to act smarter (see Item 29: "Use plain attributes
# instead of get and set methods"). One advanced but common use of @property
# is transitioning what was once a simple numerical attribute into an
# on-the-fly calculation. This is extremely helpful because it lets you
# migrate all existing usage of a class to have new behaviors without
# rewriting any of the call sites. It also provides an important stopgap for
# improving your interfaces over time.
# For example, say you want to implement a leaky bucket quota using plain
# Python objects. Here, the Bucket class represents how much quota remains
# and the duration for which the quota will be available:
class Bucket(object):
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.datetime.now()
self.quota = 0
def __repr__(self):
return 'Bucket(quota=%d)' % self.quota
# The leaky bucket algorithm works by ensuring that, whenever the bucket is
# filled, the amount of quota does not carry over from one period to the next.
def fill(bucket, amount):
now = datetime.datetime.now()
if now - bucket.reset_time > bucket.period_delta:
bucket.quota = 0
bucket.reset_time = now
bucket.quota += amount
# Each time a quota consumer wants to do something, it first must ensure that
# it can deduct the amount of quota it needs to use.
def deduct(bucket, amount):
now = datetime.datetime.now()
if now - bucket.reset_time > bucket.period_delta:
return False
if bucket.quota - amount < 0:
return False
bucket.quota -= amount
return True
# To use this class, first I fill the bucket.
bucket = Bucket(60)
fill(bucket, 100)
print(bucket)
# Bucket(quota=100)
# Then, I deduct the quota that I need.
if deduct(bucket, 99):
print('Had 99 quota')
else:
print('Not enough for 99 quota')
print(bucket)
# Had 99 quota
# Bucket(quota=1)
# Eventually, I'm prevented from making progress because I try to deduct more
# quota than is available. In this case, the bucket's quota level remains
# unchanged.
if deduct(bucket, 3):
print('Had 3 quota')
else:
print('Not enough for 3 quota')
print(bucket)
# Not enough for 3 quota
# Bucket(quota=1)
# The problem with this implementation is that I never know that quota level
# the bucket started with. The quota is deducted over the course of the period
# until it reaches zero. At that point, deduct will always return False. When
# that happens, it would be useful to know whether callers to deduct are being
# blocked because the Bucket ran out of quota or because the Bucket never had
# quota in the first place.
# To fix this, I can change the class to keep track of the max_quota issued in
# the period and the quota_consumed in the period.
class Bucket(object):
def __init__(self, period):
self.period_delta = timedelta(seconds=period)
self.reset_time = datetime.datetime.now()
self.max_quota = 0
self.quota_consumed = 0
def __repr__(self):
return ('Bucket(max_quota=%d, quota_consumed=%d)' %
(self.max_quota, self.quota_consumed))
# I use a @property method to compute the current level of quota on-the-fly
# using these new attributes.
@property
def quota(self):
return self.max_quota - self.quota_consumed
# When the quota attribute is assigned, I take special action matching the
# current interface of the class used by fill and decuct.
@quota.setter
def quota(self, amount):
delta = self.max_quota - amount
if amount == 0:
'''quota being reset for a new period'''
self.quota_consumed = 0
self.max_quota = 0
elif delta < 0:
'''quota being filled for the new period'''
assert self.quota_consumed == 0
self.max_quota = amount
else:
'''quota being consumed during the period'''
assert self.max_quota >= self.quota_consumed
self.quota_consumed += delta
# Rerunning the demo code from above produces the same results.
bucket = Bucket(60)
print('Initial', bucket)
fill(bucket, 100)
print('Filled', bucket)
if deduct(bucket, 99):
print('Had 99 quota')
else:
print('Not enough for 99 quota')
print('Now', bucket)
if deduct(bucket, 3):
print('Had 3 quota')
else:
print('Not enough for 3 quota')
print('Still', bucket)
# Initial Bucket(max_quota=0, quota_consumed=0)
# Filled Bucket(max_quota=100, quota_consumed=0)
# Had 99 quota
# Now Bucket(max_quota=100, quota_consumed=99)
# Not enough for 3 quota
# Still Bucket(max_quota=100, quota_consumed=99)
# The best part is that the code using Bucket.quota doesn't have to change or
# know that the class has changed. New usage of Bucket can do the right thing
# and access max_quota and quota_consumed directly.
# I especially like @property because it lets you make incremental progress
# toward a better data model over time. Reading the Bucket example above, you
# may have though to yourself, "fill and deduct should have been implemented
# as instance methods in the first place." Although you're probably right (see
# Item 22: "Prefer helper classes over bookkeeping with dictionaries and
# tuples"), in practice there are many situations in which objects start with
# poorly defined interfaces or act as dumb data containers. This happens when
# code grows over time, scope increases, multiple authors contribute without
# any one considering long-term hygiene, etc.
# @property is a tool to help you address problems you'll come across in real-
# world code. Don't overuse it. When you find yourself repeatedly extending
# @property methods, it's probably time to refactor your class instead of
# further paving over your code's poor design.
# Things to remember
# 1. Use @property to give existing instance attributes new functionality.
# 2. Make incremental progress toward better data models by using @property.
# 3. Consider refactoring a class and all call sites when you find yourself
# using @property too heavily.