@booklib/skills 1.0.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +122 -0
- package/README.md +20 -1
- package/ROADMAP.md +36 -0
- package/animation-at-work/evals/evals.json +44 -0
- package/animation-at-work/examples/after.md +64 -0
- package/animation-at-work/examples/before.md +35 -0
- package/animation-at-work/scripts/audit_animations.py +295 -0
- package/bin/skills.js +552 -42
- package/clean-code-reviewer/SKILL.md +109 -1
- package/clean-code-reviewer/evals/evals.json +121 -3
- package/clean-code-reviewer/examples/after.md +48 -0
- package/clean-code-reviewer/examples/before.md +33 -0
- package/clean-code-reviewer/references/api_reference.md +158 -0
- package/clean-code-reviewer/references/practices-catalog.md +282 -0
- package/clean-code-reviewer/references/review-checklist.md +254 -0
- package/clean-code-reviewer/scripts/pre-review.py +206 -0
- package/data-intensive-patterns/evals/evals.json +43 -0
- package/data-intensive-patterns/examples/after.md +61 -0
- package/data-intensive-patterns/examples/before.md +38 -0
- package/data-intensive-patterns/scripts/adr.py +213 -0
- package/data-pipelines/evals/evals.json +45 -0
- package/data-pipelines/examples/after.md +97 -0
- package/data-pipelines/examples/before.md +37 -0
- package/data-pipelines/scripts/new_pipeline.py +444 -0
- package/design-patterns/evals/evals.json +46 -0
- package/design-patterns/examples/after.md +52 -0
- package/design-patterns/examples/before.md +29 -0
- package/design-patterns/scripts/scaffold.py +807 -0
- package/domain-driven-design/SKILL.md +120 -0
- package/domain-driven-design/evals/evals.json +48 -0
- package/domain-driven-design/examples/after.md +80 -0
- package/domain-driven-design/examples/before.md +43 -0
- package/domain-driven-design/scripts/scaffold.py +421 -0
- package/effective-java/evals/evals.json +46 -0
- package/effective-java/examples/after.md +83 -0
- package/effective-java/examples/before.md +37 -0
- package/effective-java/scripts/checkstyle_setup.py +211 -0
- package/effective-kotlin/evals/evals.json +45 -0
- package/effective-kotlin/examples/after.md +36 -0
- package/effective-kotlin/examples/before.md +38 -0
- package/effective-python/SKILL.md +199 -0
- package/effective-python/evals/evals.json +44 -0
- package/effective-python/examples/after.md +56 -0
- package/effective-python/examples/before.md +40 -0
- package/effective-python/ref-01-pythonic-thinking.md +202 -0
- package/effective-python/ref-02-lists-and-dicts.md +146 -0
- package/effective-python/ref-03-functions.md +186 -0
- package/effective-python/ref-04-comprehensions-generators.md +211 -0
- package/effective-python/ref-05-classes-interfaces.md +188 -0
- package/effective-python/ref-06-metaclasses-attributes.md +209 -0
- package/effective-python/ref-07-concurrency.md +213 -0
- package/effective-python/ref-08-robustness-performance.md +248 -0
- package/effective-python/ref-09-testing-debugging.md +253 -0
- package/effective-python/ref-10-collaboration.md +175 -0
- package/effective-python/references/api_reference.md +218 -0
- package/effective-python/references/practices-catalog.md +483 -0
- package/effective-python/references/review-checklist.md +190 -0
- package/effective-python/scripts/lint.py +173 -0
- package/kotlin-in-action/evals/evals.json +43 -0
- package/kotlin-in-action/examples/after.md +53 -0
- package/kotlin-in-action/examples/before.md +39 -0
- package/kotlin-in-action/scripts/setup_detekt.py +224 -0
- package/lean-startup/evals/evals.json +43 -0
- package/lean-startup/examples/after.md +80 -0
- package/lean-startup/examples/before.md +34 -0
- package/lean-startup/scripts/new_experiment.py +286 -0
- package/microservices-patterns/SKILL.md +140 -0
- package/microservices-patterns/evals/evals.json +45 -0
- package/microservices-patterns/examples/after.md +69 -0
- package/microservices-patterns/examples/before.md +40 -0
- package/microservices-patterns/scripts/new_service.py +583 -0
- package/package.json +1 -1
- package/refactoring-ui/evals/evals.json +45 -0
- package/refactoring-ui/examples/after.md +85 -0
- package/refactoring-ui/examples/before.md +58 -0
- package/refactoring-ui/scripts/audit_css.py +250 -0
- package/skill-router/SKILL.md +142 -0
- package/skill-router/evals/evals.json +38 -0
- package/skill-router/examples/after.md +63 -0
- package/skill-router/examples/before.md +39 -0
- package/skill-router/references/api_reference.md +24 -0
- package/skill-router/references/routing-heuristics.md +89 -0
- package/skill-router/references/skill-catalog.md +156 -0
- package/skill-router/scripts/route.py +266 -0
- package/storytelling-with-data/evals/evals.json +47 -0
- package/storytelling-with-data/examples/after.md +50 -0
- package/storytelling-with-data/examples/before.md +33 -0
- package/storytelling-with-data/scripts/chart_review.py +301 -0
- package/system-design-interview/evals/evals.json +45 -0
- package/system-design-interview/examples/after.md +94 -0
- package/system-design-interview/examples/before.md +27 -0
- package/system-design-interview/scripts/new_design.py +421 -0
- package/using-asyncio-python/evals/evals.json +43 -0
- package/using-asyncio-python/examples/after.md +68 -0
- package/using-asyncio-python/examples/before.md +39 -0
- package/using-asyncio-python/scripts/check_blocking.py +270 -0
- package/web-scraping-python/evals/evals.json +46 -0
- package/web-scraping-python/examples/after.md +109 -0
- package/web-scraping-python/examples/before.md +40 -0
- package/web-scraping-python/scripts/new_scraper.py +231 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Before
|
|
2
|
+
|
|
3
|
+
Python ETL helper with a bare `except`, a mutable default argument bug, and a manual loop that should be a comprehension.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
import json
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
def fetch_orders(api_url, filters={}):
|
|
10
|
+
# filters dict persists across calls — mutable default arg bug
|
|
11
|
+
filters['status'] = 'completed'
|
|
12
|
+
try:
|
|
13
|
+
response = requests.get(api_url, params=filters)
|
|
14
|
+
data = response.json()
|
|
15
|
+
except:
|
|
16
|
+
# swallows every exception including KeyboardInterrupt
|
|
17
|
+
print("something went wrong")
|
|
18
|
+
return []
|
|
19
|
+
|
|
20
|
+
orders = []
|
|
21
|
+
for item in data['orders']:
|
|
22
|
+
if item['total'] > 0:
|
|
23
|
+
order = {
|
|
24
|
+
'id': item['id'],
|
|
25
|
+
'customer': item['customer_name'],
|
|
26
|
+
'total': item['total'],
|
|
27
|
+
}
|
|
28
|
+
orders.append(order)
|
|
29
|
+
return orders
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def summarize(orders):
|
|
33
|
+
totals = []
|
|
34
|
+
for o in orders:
|
|
35
|
+
totals.append(o['total'])
|
|
36
|
+
grand_total = 0
|
|
37
|
+
for t in totals:
|
|
38
|
+
grand_total = grand_total + t
|
|
39
|
+
return grand_total
|
|
40
|
+
```
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Chapter 1: Pythonic Thinking (Items 1-10)
|
|
2
|
+
|
|
3
|
+
## Item 1: Know Which Version of Python You're Using
|
|
4
|
+
- Use `python3` explicitly, not `python`
|
|
5
|
+
- Check version with `python3 --version` or `sys.version_info`
|
|
6
|
+
- Python 2 is end-of-life; always target Python 3
|
|
7
|
+
|
|
8
|
+
## Item 2: Follow the PEP 8 Style Guide
|
|
9
|
+
**Whitespace:**
|
|
10
|
+
- Use 4 spaces for indentation (never tabs)
|
|
11
|
+
- Lines should be 79 characters or fewer
|
|
12
|
+
- Continuations should be indented by 4 extra spaces
|
|
13
|
+
- Put two blank lines before/after top-level functions and classes
|
|
14
|
+
- One blank line between methods in a class
|
|
15
|
+
|
|
16
|
+
**Naming:**
|
|
17
|
+
- Functions, variables, attributes: `lowercase_underscore`
|
|
18
|
+
- Protected instance attributes: `_leading_underscore`
|
|
19
|
+
- Private instance attributes: `__double_leading_underscore`
|
|
20
|
+
- Classes and exceptions: `CapitalizedWord`
|
|
21
|
+
- Module-level constants: `ALL_CAPS`
|
|
22
|
+
- Instance methods use `self` as first parameter; class methods use `cls`
|
|
23
|
+
|
|
24
|
+
**Expressions & Statements:**
|
|
25
|
+
- Use inline negation (`if a is not b`) instead of negating positive (`if not a is b`)
|
|
26
|
+
- Don't check for empty containers with length (`if len(list) == 0`); use `if not list`
|
|
27
|
+
- Use `if list` to check for non-empty
|
|
28
|
+
- Avoid single-line `if`, `for`, `while`, `except`
|
|
29
|
+
- Always use absolute imports, not relative
|
|
30
|
+
- Put imports at top in order: stdlib, third-party, local
|
|
31
|
+
|
|
32
|
+
**Tools:** Use `pylint` for static analysis, `black` for formatting.
|
|
33
|
+
|
|
34
|
+
## Item 3: Know the Differences Between bytes and str
|
|
35
|
+
- `bytes` contains raw unsigned 8-bit values; `str` contains Unicode code points
|
|
36
|
+
- Use helper functions to convert between them:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
# BAD
|
|
40
|
+
def to_str(data):
|
|
41
|
+
if isinstance(data, bytes):
|
|
42
|
+
return data.decode('utf-8')
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
# GOOD — be explicit about encoding
|
|
46
|
+
def to_str(bytes_or_str):
|
|
47
|
+
if isinstance(bytes_or_str, bytes):
|
|
48
|
+
value = bytes_or_str.decode('utf-8')
|
|
49
|
+
else:
|
|
50
|
+
value = bytes_or_str
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
def to_bytes(bytes_or_str):
|
|
54
|
+
if isinstance(bytes_or_str, str):
|
|
55
|
+
value = bytes_or_str.encode('utf-8')
|
|
56
|
+
else:
|
|
57
|
+
value = bytes_or_str
|
|
58
|
+
return value
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- Use `'rb'` and `'wb'` modes for binary file I/O
|
|
62
|
+
- Specify encoding explicitly: `open(path, 'r', encoding='utf-8')`
|
|
63
|
+
|
|
64
|
+
## Item 4: Prefer Interpolated F-Strings Over C-style Format Strings and str.format
|
|
65
|
+
```python
|
|
66
|
+
# BAD — C-style
|
|
67
|
+
'Hello, %s. You are %d.' % (name, age)
|
|
68
|
+
|
|
69
|
+
# BAD — str.format
|
|
70
|
+
'Hello, {}. You are {}.'.format(name, age)
|
|
71
|
+
|
|
72
|
+
# GOOD — f-string
|
|
73
|
+
f'Hello, {name}. You are {age}.'
|
|
74
|
+
|
|
75
|
+
# F-strings support expressions
|
|
76
|
+
f'{key!r}: {value:.2f}'
|
|
77
|
+
f'result: {some_func(x)}'
|
|
78
|
+
|
|
79
|
+
# Multi-line f-strings
|
|
80
|
+
f'{key:<10} = {value:.2f}'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Item 5: Write Helper Functions Instead of Complex Expressions
|
|
84
|
+
- If an expression is hard to read, move it to a helper function
|
|
85
|
+
- Clarity over brevity: `if`/`else` is clearer than `or` for defaults
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# BAD
|
|
89
|
+
values = query_string.get('red', [''])
|
|
90
|
+
red = int(values[0]) if values[0] else 0
|
|
91
|
+
|
|
92
|
+
# GOOD
|
|
93
|
+
def get_first_int(values, key, default=0):
|
|
94
|
+
found = values.get(key, [''])
|
|
95
|
+
if found[0]:
|
|
96
|
+
return int(found[0])
|
|
97
|
+
return default
|
|
98
|
+
|
|
99
|
+
red = get_first_int(values, 'red')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Item 6: Prefer Multiple Assignment Unpacking Over Indexing
|
|
103
|
+
```python
|
|
104
|
+
# BAD
|
|
105
|
+
item = ('Peanut Butter', 3.50)
|
|
106
|
+
name = item[0]
|
|
107
|
+
price = item[1]
|
|
108
|
+
|
|
109
|
+
# GOOD
|
|
110
|
+
name, price = item
|
|
111
|
+
|
|
112
|
+
# Works with nested structures
|
|
113
|
+
((name1, cal1), (name2, cal2)) = snacks
|
|
114
|
+
|
|
115
|
+
# Use _ for unused values
|
|
116
|
+
_, price = item
|
|
117
|
+
|
|
118
|
+
# Swap without temp variable
|
|
119
|
+
a, b = b, a
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Item 7: Prefer enumerate Over range
|
|
123
|
+
```python
|
|
124
|
+
# BAD
|
|
125
|
+
for i in range(len(flavor_list)):
|
|
126
|
+
flavor = flavor_list[i]
|
|
127
|
+
print(f'{i + 1}: {flavor}')
|
|
128
|
+
|
|
129
|
+
# GOOD
|
|
130
|
+
for i, flavor in enumerate(flavor_list):
|
|
131
|
+
print(f'{i + 1}: {flavor}')
|
|
132
|
+
|
|
133
|
+
# Start from a different index
|
|
134
|
+
for i, flavor in enumerate(flavor_list, 1):
|
|
135
|
+
print(f'{i}: {flavor}')
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Item 8: Use zip to Process Iterators in Parallel
|
|
139
|
+
```python
|
|
140
|
+
# BAD
|
|
141
|
+
for i in range(len(names)):
|
|
142
|
+
print(f'{names[i]}: {counts[i]}')
|
|
143
|
+
|
|
144
|
+
# GOOD
|
|
145
|
+
for name, count in zip(names, counts):
|
|
146
|
+
print(f'{name}: {count}')
|
|
147
|
+
|
|
148
|
+
# When lengths differ, use zip_longest
|
|
149
|
+
from itertools import zip_longest
|
|
150
|
+
for name, count in zip_longest(names, counts, fillvalue=0):
|
|
151
|
+
print(f'{name}: {count}')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `zip` truncates to shortest iterator (use `itertools.zip_longest` if needed)
|
|
155
|
+
- zip is lazy — produces one tuple at a time
|
|
156
|
+
|
|
157
|
+
## Item 9: Avoid else Blocks After for and while Loops
|
|
158
|
+
- `else` on loops runs when the loop completes *without* `break`
|
|
159
|
+
- This is counterintuitive and confuses readers
|
|
160
|
+
- Instead, use a helper function with early return:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# BAD — confusing else on loop
|
|
164
|
+
for i in range(n):
|
|
165
|
+
if condition(i):
|
|
166
|
+
break
|
|
167
|
+
else:
|
|
168
|
+
handle_no_break()
|
|
169
|
+
|
|
170
|
+
# GOOD — helper function
|
|
171
|
+
def find_match(n):
|
|
172
|
+
for i in range(n):
|
|
173
|
+
if condition(i):
|
|
174
|
+
return i
|
|
175
|
+
return None # explicit "not found"
|
|
176
|
+
|
|
177
|
+
result = find_match(n)
|
|
178
|
+
if result is None:
|
|
179
|
+
handle_no_match()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Item 10: Prevent Repetition with Assignment Expressions (Walrus Operator)
|
|
183
|
+
```python
|
|
184
|
+
# BAD — repeated call or extra variable
|
|
185
|
+
count = fresh_fruit.get('lemon', 0)
|
|
186
|
+
if count:
|
|
187
|
+
make_lemonade(count)
|
|
188
|
+
|
|
189
|
+
# GOOD — walrus operator
|
|
190
|
+
if count := fresh_fruit.get('lemon', 0):
|
|
191
|
+
make_lemonade(count)
|
|
192
|
+
|
|
193
|
+
# Useful in while loops
|
|
194
|
+
while chunk := f.read(8192):
|
|
195
|
+
process(chunk)
|
|
196
|
+
|
|
197
|
+
# Useful in comprehensions
|
|
198
|
+
result = [y for x in data if (y := f(x)) is not None]
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
- Use `:=` when you need to both assign and test a value
|
|
202
|
+
- Don't overuse — only when it clearly reduces repetition
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Chapter 2: Lists and Dictionaries (Items 11-18)
|
|
2
|
+
|
|
3
|
+
## Item 11: Know How to Slice Sequences
|
|
4
|
+
```python
|
|
5
|
+
a = [1, 2, 3, 4, 5, 6, 7, 8]
|
|
6
|
+
|
|
7
|
+
# Basic slicing
|
|
8
|
+
a[:4] # [1, 2, 3, 4] — first 4
|
|
9
|
+
a[-3:] # [6, 7, 8] — last 3
|
|
10
|
+
a[3:5] # [4, 5]
|
|
11
|
+
|
|
12
|
+
# Don't use 0 for start or len for end
|
|
13
|
+
a[:5] # GOOD
|
|
14
|
+
a[0:5] # BAD — redundant 0
|
|
15
|
+
|
|
16
|
+
# Slicing makes a new list (shallow copy)
|
|
17
|
+
b = a[:] # copy of a
|
|
18
|
+
|
|
19
|
+
# Slice assignment replaces in place
|
|
20
|
+
a[2:4] = [10, 11] # can be different length
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Item 12: Avoid Striding and Slicing in a Single Expression
|
|
24
|
+
```python
|
|
25
|
+
# BAD — confusing stride + slice
|
|
26
|
+
x = a[2::2] # skip start, stride by 2
|
|
27
|
+
x = a[-2::-2] # reverse with stride
|
|
28
|
+
|
|
29
|
+
# GOOD — separate steps
|
|
30
|
+
y = a[::2] # stride first
|
|
31
|
+
z = y[1:3] # then slice
|
|
32
|
+
|
|
33
|
+
# Reverse a sequence
|
|
34
|
+
x = a[::-1] # OK for simple reversal, but avoid combining with slicing
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Item 13: Prefer Catch-All Unpacking Over Slicing
|
|
38
|
+
```python
|
|
39
|
+
# BAD — manual slicing
|
|
40
|
+
oldest = ages[0]
|
|
41
|
+
rest = ages[1:]
|
|
42
|
+
|
|
43
|
+
# GOOD — starred expression
|
|
44
|
+
oldest, *rest = ages
|
|
45
|
+
oldest, second, *rest = ages
|
|
46
|
+
first, *middle, last = ages
|
|
47
|
+
|
|
48
|
+
# Works with any iterable
|
|
49
|
+
it = iter(range(10))
|
|
50
|
+
first, second, *rest = it
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- Starred expressions always produce a list (may be empty)
|
|
54
|
+
- Cannot have more than one starred expression in a single assignment
|
|
55
|
+
|
|
56
|
+
## Item 14: Sort by Complex Criteria Using the key Parameter
|
|
57
|
+
```python
|
|
58
|
+
# Sort with key function
|
|
59
|
+
tools = [Tool('drill', 4), Tool('saw', 2)]
|
|
60
|
+
tools.sort(key=lambda x: x.weight)
|
|
61
|
+
|
|
62
|
+
# Multiple criteria — use tuple
|
|
63
|
+
tools.sort(key=lambda x: (x.name, x.weight))
|
|
64
|
+
|
|
65
|
+
# Reverse one criterion using negation (numeric)
|
|
66
|
+
tools.sort(key=lambda x: (-x.weight, x.name))
|
|
67
|
+
|
|
68
|
+
# For non-numeric reverse, use multiple sort passes (stable sort)
|
|
69
|
+
tools.sort(key=lambda x: x.name) # secondary first
|
|
70
|
+
tools.sort(key=lambda x: x.weight, reverse=True) # primary last
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Python sort is stable — equal elements maintain relative order
|
|
74
|
+
- Use `operator.attrgetter` for attribute access as key
|
|
75
|
+
|
|
76
|
+
## Item 15: Be Cautious When Relying on dict Insertion Order
|
|
77
|
+
- Since Python 3.7, dicts maintain insertion order
|
|
78
|
+
- But don't assume all dict-like objects do (e.g., custom classes)
|
|
79
|
+
- Use explicit ordering when you need it:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
# If order matters and you're creating a protocol
|
|
83
|
+
class MyDB:
|
|
84
|
+
def __init__(self):
|
|
85
|
+
self._data = {}
|
|
86
|
+
|
|
87
|
+
# Be explicit that order is part of the contract
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- For **kwargs, insertion order is preserved
|
|
91
|
+
- Standard dict methods (keys, values, items) follow insertion order
|
|
92
|
+
|
|
93
|
+
## Item 16: Prefer get Over in and KeyError to Handle Missing Dictionary Keys
|
|
94
|
+
```python
|
|
95
|
+
# BAD — check then access
|
|
96
|
+
if key in counters:
|
|
97
|
+
count = counters[key]
|
|
98
|
+
else:
|
|
99
|
+
count = 0
|
|
100
|
+
counters[key] = count + 1
|
|
101
|
+
|
|
102
|
+
# BAD — try/except
|
|
103
|
+
try:
|
|
104
|
+
count = counters[key]
|
|
105
|
+
except KeyError:
|
|
106
|
+
count = 0
|
|
107
|
+
counters[key] = count + 1
|
|
108
|
+
|
|
109
|
+
# GOOD — use get
|
|
110
|
+
count = counters.get(key, 0)
|
|
111
|
+
counters[key] = count + 1
|
|
112
|
+
|
|
113
|
+
# For complex default values, consider setdefault or defaultdict
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Item 17: Prefer defaultdict Over setdefault to Handle Missing Items in Internal State
|
|
117
|
+
```python
|
|
118
|
+
from collections import defaultdict
|
|
119
|
+
|
|
120
|
+
# BAD — setdefault (confusing API)
|
|
121
|
+
visits = {}
|
|
122
|
+
visits.setdefault('France', []).append('Paris')
|
|
123
|
+
|
|
124
|
+
# GOOD — defaultdict
|
|
125
|
+
visits = defaultdict(list)
|
|
126
|
+
visits['France'].append('Paris')
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- `defaultdict` is clearer when you control the dict creation
|
|
130
|
+
- `setdefault` is better when you don't control the dict (external data)
|
|
131
|
+
|
|
132
|
+
## Item 18: Know How to Construct Key-Dependent Default Values with __missing__
|
|
133
|
+
```python
|
|
134
|
+
# When the default value depends on the key, use __missing__
|
|
135
|
+
class Pictures(dict):
|
|
136
|
+
def __missing__(self, key):
|
|
137
|
+
value = open_picture(key) # default depends on key
|
|
138
|
+
self[key] = value
|
|
139
|
+
return value
|
|
140
|
+
|
|
141
|
+
pictures = Pictures()
|
|
142
|
+
handle = pictures[path] # calls __missing__ if path not present
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- Use when `defaultdict` isn't sufficient (default factory doesn't receive the key)
|
|
146
|
+
- `__missing__` is called by `__getitem__` when key is not found
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Chapter 3: Functions (Items 19-26)
|
|
2
|
+
|
|
3
|
+
## Item 19: Never Unpack More Than Three Variables When Functions Return Multiple Values
|
|
4
|
+
```python
|
|
5
|
+
# BAD — too many unpacked values, confusing
|
|
6
|
+
minimum, maximum, average, median, count = get_stats(data)
|
|
7
|
+
|
|
8
|
+
# GOOD — return a lightweight class or namedtuple
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
|
|
11
|
+
Stats = namedtuple('Stats', ['minimum', 'maximum', 'average', 'median', 'count'])
|
|
12
|
+
|
|
13
|
+
def get_stats(data):
|
|
14
|
+
return Stats(
|
|
15
|
+
minimum=min(data),
|
|
16
|
+
maximum=max(data),
|
|
17
|
+
average=sum(data)/len(data),
|
|
18
|
+
median=find_median(data),
|
|
19
|
+
count=len(data)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
result = get_stats(data)
|
|
23
|
+
print(result.average)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Three or fewer values is fine to unpack
|
|
27
|
+
- More than three: use a namedtuple, dataclass, or custom class
|
|
28
|
+
|
|
29
|
+
## Item 20: Prefer Raising Exceptions to Returning None
|
|
30
|
+
```python
|
|
31
|
+
# BAD — None is ambiguous
|
|
32
|
+
def careful_divide(a, b):
|
|
33
|
+
try:
|
|
34
|
+
return a / b
|
|
35
|
+
except ZeroDivisionError:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
# Caller can't distinguish None from 0
|
|
39
|
+
result = careful_divide(0, 5) # returns 0.0
|
|
40
|
+
if not result: # BUG: treats 0.0 as failure
|
|
41
|
+
|
|
42
|
+
# GOOD — raise an exception
|
|
43
|
+
def careful_divide(a, b):
|
|
44
|
+
try:
|
|
45
|
+
return a / b
|
|
46
|
+
except ZeroDivisionError as e:
|
|
47
|
+
raise ValueError('Invalid inputs') from e
|
|
48
|
+
|
|
49
|
+
# ALSO GOOD — type hints with Never-None return
|
|
50
|
+
def careful_divide(a: float, b: float) -> float:
|
|
51
|
+
"""Raises ValueError on invalid inputs."""
|
|
52
|
+
if b == 0:
|
|
53
|
+
raise ValueError('Invalid inputs')
|
|
54
|
+
return a / b
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Item 21: Know How Closures Interact with Variable Scope
|
|
58
|
+
```python
|
|
59
|
+
# Closures capture variables from enclosing scope
|
|
60
|
+
def sort_priority(values, group):
|
|
61
|
+
found = False
|
|
62
|
+
def helper(x):
|
|
63
|
+
nonlocal found # REQUIRED to modify enclosing variable
|
|
64
|
+
if x in group:
|
|
65
|
+
found = True
|
|
66
|
+
return (0, x)
|
|
67
|
+
return (1, x)
|
|
68
|
+
values.sort(key=helper)
|
|
69
|
+
return found
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- Without `nonlocal`, assignment creates a new local variable
|
|
73
|
+
- Prefer returning state over using `nonlocal` for complex cases
|
|
74
|
+
- For complex state, use a helper class instead
|
|
75
|
+
|
|
76
|
+
## Item 22: Reduce Visual Noise with Variable Positional Arguments (*args)
|
|
77
|
+
```python
|
|
78
|
+
# Accept variable args
|
|
79
|
+
def log(message, *values):
|
|
80
|
+
if not values:
|
|
81
|
+
print(message)
|
|
82
|
+
else:
|
|
83
|
+
values_str = ', '.join(str(x) for x in values)
|
|
84
|
+
print(f'{message}: {values_str}')
|
|
85
|
+
|
|
86
|
+
log('My numbers', 1, 2)
|
|
87
|
+
log('Hi')
|
|
88
|
+
|
|
89
|
+
# Pass a sequence as *args
|
|
90
|
+
favorites = [7, 33, 99]
|
|
91
|
+
log('Favorites', *favorites)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Caveats:**
|
|
95
|
+
- `*args` are converted to a tuple (memory issue with generators)
|
|
96
|
+
- Adding positional args before *args breaks callers if not careful
|
|
97
|
+
- Use keyword-only args after *args for new parameters
|
|
98
|
+
|
|
99
|
+
## Item 23: Provide Optional Behavior with Keyword Arguments
|
|
100
|
+
```python
|
|
101
|
+
def flow_rate(weight_diff, time_diff, *, period=1): # period is keyword-only
|
|
102
|
+
return (weight_diff / time_diff) * period
|
|
103
|
+
|
|
104
|
+
# Callers must use keyword
|
|
105
|
+
flow_rate(1, 2, period=3600)
|
|
106
|
+
flow_rate(1, 2, 3600) # TypeError!
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Keyword args make function calls more readable
|
|
110
|
+
- They provide default values for optional behavior
|
|
111
|
+
- Can be added to existing functions without breaking callers
|
|
112
|
+
|
|
113
|
+
## Item 24: Use None and Docstrings to Specify Dynamic Default Arguments
|
|
114
|
+
```python
|
|
115
|
+
# BAD — mutable default is shared across calls!
|
|
116
|
+
def log(message, when=datetime.now()): # BUG: evaluated once at import
|
|
117
|
+
print(f'{when}: {message}')
|
|
118
|
+
|
|
119
|
+
# GOOD — use None sentinel
|
|
120
|
+
def log(message, when=None):
|
|
121
|
+
"""Log a message with a timestamp.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
message: Message to print.
|
|
125
|
+
when: datetime of when the message occurred.
|
|
126
|
+
Defaults to the present time.
|
|
127
|
+
"""
|
|
128
|
+
if when is None:
|
|
129
|
+
when = datetime.now()
|
|
130
|
+
print(f'{when}: {message}')
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **Never use mutable objects** as default argument values (lists, dicts, sets, datetime.now())
|
|
134
|
+
- Use `None` and document the actual default in the docstring
|
|
135
|
+
- This also applies to type hints: `when: Optional[datetime] = None`
|
|
136
|
+
|
|
137
|
+
## Item 25: Enforce Clarity with Keyword-Only and Positional-Only Arguments
|
|
138
|
+
```python
|
|
139
|
+
# Keyword-only: after * in signature
|
|
140
|
+
def safe_division(number, divisor, *,
|
|
141
|
+
ignore_overflow=False,
|
|
142
|
+
ignore_zero_division=False):
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
# Positional-only: before / in signature (Python 3.8+)
|
|
146
|
+
def safe_division(numerator, denominator, /,
|
|
147
|
+
*, ignore_overflow=False):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
# Combined: positional-only / regular / keyword-only
|
|
151
|
+
def safe_division(numerator, denominator, /,
|
|
152
|
+
ndigits=10, *,
|
|
153
|
+
ignore_overflow=False):
|
|
154
|
+
pass
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `/` separates positional-only from regular params
|
|
158
|
+
- `*` separates regular from keyword-only params
|
|
159
|
+
- Use positional-only for params where the name is an implementation detail
|
|
160
|
+
- Use keyword-only for boolean flags and optional configuration
|
|
161
|
+
|
|
162
|
+
## Item 26: Define Function Decorators with functools.wraps
|
|
163
|
+
```python
|
|
164
|
+
from functools import wraps
|
|
165
|
+
|
|
166
|
+
# BAD — decorator hides original function metadata
|
|
167
|
+
def trace(func):
|
|
168
|
+
def wrapper(*args, **kwargs):
|
|
169
|
+
result = func(*args, **kwargs)
|
|
170
|
+
print(f'{func.__name__}({args}, {kwargs}) -> {result}')
|
|
171
|
+
return result
|
|
172
|
+
return wrapper
|
|
173
|
+
|
|
174
|
+
# GOOD — preserves function metadata
|
|
175
|
+
def trace(func):
|
|
176
|
+
@wraps(func)
|
|
177
|
+
def wrapper(*args, **kwargs):
|
|
178
|
+
result = func(*args, **kwargs)
|
|
179
|
+
print(f'{func.__name__}({args}, {kwargs}) -> {result}')
|
|
180
|
+
return result
|
|
181
|
+
return wrapper
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- `@wraps` copies the inner function's metadata (__name__, __module__, __doc__)
|
|
185
|
+
- Without it, debugging tools, serializers, and `help()` break
|
|
186
|
+
- **Always** use `@wraps` on decorator wrapper functions
|