-
Notifications
You must be signed in to change notification settings - Fork 222
/
item_21_enforce_clarity.py
233 lines (168 loc) · 7.01 KB
/
item_21_enforce_clarity.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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Item 21: Enforce clarity with key-word only arguments
# Passing arguments by keyword is a powerful feature of Python functions (see
# Item 19: "Provide optimal behavior with keyword arguments"). The flexibility
# of keyword arguments enables you to write code that will be clear for your
# use cases.
# For example, say you want to divide one number by another but be very
# careful about special cases. Sometimes you want to ignore ZeroDivisionError
# exceptions and return infinity instead. Other times, you want to ignore
# OverflowError exceptions and return Zero instead.
def safe_division(number, divisor, ignore_overflow, ignore_zero_division):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
# Using this function is straightforward. This call will ignore the float
# overflow from division and will return zero.
result = safe_division(1, 100**500, True, False)
print(result)
# 0.0
# This call will ignore the error from dividing by zero and will return
# infinity.
result = safe_division(1, 0, False, True)
print(result)
# inf
# The problem is that it's easy to confuse the position of the two Boolean
# arguments that control the exception-ignoring behavior. This can easily
# cause bugs that are hard to track down. One way to improve the readability
# of this code is to use keyword arguments. By default, the function can be
# overly cautions and can always re-raise exceptions.
def safe_division_b(number, divisor,
ignore_overflow=False,
ignore_zero_division=False):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
# Then callers can use keyword arguments to specify which of the ignore flags
# they want to flip for specific operations, overriding the default behavior.
print(safe_division_b(1, 10**500, ignore_overflow=True))
print(safe_division_b(1, 0, ignore_zero_division=True))
# 0.0
# inf
# The problem is, since these keyword arguments are optional behavior, there's
# nothing forcing callers of your functions to use keyword arguments for
# clarity. Even with the new definition of safe_division_b, you can will still
# call it the old way with positional arguments.
print(safe_division_b(1, 10**500, True, False))
# 0.0
# With complex functions like this, it's better to require that callers are
# clear about their intentions. In Python 3, you can demand clarity by
# defining your functions with keyword-only arguments. These arguments can
# only be supplied by keyword, never by position.
# Here, I redefine the safe_division function to accept keyword-only
# arguments. The * symbol in the argument list indicates the end of positional
# arguments and the beginning of the keyword-only arguments.
def safe_division_c(number, divisor, *,
ignore_overflow=False,
ignore_zero_division=False):
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_division:
return float('inf')
else:
raise
# Now, calling the function with positional arguments for the keyword argument
# won't work.
# result = safe_division_c(1, 10**500, True, False)
# line 123, in <module>
# result = safe_division_c(1, 10**500, True, False)
# TypeError: safe_division_c() takes 2 positional arguments but 4 were given
# Keyword arguments and their default values work as expected.
result = safe_division_c(1, 0, ignore_zero_division=True) # OK
print(result)
# inf
try:
result = safe_division_c(1, 0)
print(result)
except ZeroDivisionError:
print("Exception ZeroDivisionError")
pass # Expected
# Exception ZeroDivisionError
# Keyword-only arguments in Python 2
# Unfortunately, Python 2 doesn't have explicit syntax for specifying
# keyword-only arguments like Python 3. But you can achieve the same behavior
# of raising TypeErrors for invalid function calls by using the ** operator in
# in argument list. The ** operator is similar to the * operator (see Item 18:
# "Reduce visual noise with variable positional arguments"), except that
# instead of accepting a variable number of positional arguments, it accepts
# any number of keyword arguments, even when they're not defined.
# Python 2
def print_args(*args, **kwargs):
print('Positional:', args)
print('Keyword: ', kwargs)
print_args(1, 2, foo='bar', stuff='meep')
# ('Positional:', (1, 2))
# ('Keyword: ', {'foo': 'bar', 'stuff': 'meep'})
# To make safe_division take keyword-only arguments in Python 2, you have the
# function accept **kwargs. Then you pop keyword arguments that you expect out
# of the kwargs dictionary, using the pop method's second argument to specify
# the default value when the key is messing. Finally, you makere sure there are
# no more keyword arguments left in kwargs to prevent callers from supplying
# arguments that are invalid.
# Python 2
def safe_division_d(number, divisor, **kwargs):
ignore_overflow = kwargs.pop('ignore_overflow', False)
ignore_zero_div = kwargs.pop('ignore_zero_division', False)
if kwargs:
raise TypeError('Unexpected **kwargs: %r' % kwargs)
try:
return number / divisor
except OverflowError:
if ignore_overflow:
return 0
else:
raise
except ZeroDivisionError:
if ignore_zero_div:
return float('inf')
else:
raise
# Now you can call the function with or without keyword arguments.
print(safe_division_d(1, 10.0))
print(safe_division_d(1, 0, ignore_zero_division=True))
print(safe_division_d(1, 10**500, ignore_overflow=True))
# 0.1
# inf
# 0.0
# Trying to pass keyword-only arguments by position won't work, just like in Python 3.
# safe_division_d(1, 0, False, True)
# line 209, in <module>
# safe_division_d(1, 0, False, True)
# TypeError: safe_division_d() takes 2 positional arguments but 4 were given
# Trying to pass unexpected keyword arguments also won't work.
safe_division_d(0, 0, unexpected=True)
# line 179, in safe_division_d
# raise TypeError('Unexpected **kwargs: %r' % kwargs)
# TypeError: Unexpected **kwargs: {'unexpected': True}
# Things to remember
# 1. Keyword arguments make the intention of a function call more clear.
# 2. Use keyword-only arguments to force callers to supply keyword arguments
# for potentially confusing functions, especially those that accept
# multiple Boolean flags.
# 3. Python 3 supports explicit syntax for keyword-only arguments in
# functions.
# 4. Python 2 can emulate keyword-only arguments for functions by using
# **kwargs and manually raising TypeError exceptions.