@booklib/skills 1.0.0 → 1.2.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/README.md +1 -0
- package/effective-python-skill/SKILL.md +199 -0
- package/effective-python-skill/ref-01-pythonic-thinking.md +202 -0
- package/effective-python-skill/ref-02-lists-and-dicts.md +146 -0
- package/effective-python-skill/ref-03-functions.md +186 -0
- package/effective-python-skill/ref-04-comprehensions-generators.md +211 -0
- package/effective-python-skill/ref-05-classes-interfaces.md +188 -0
- package/effective-python-skill/ref-06-metaclasses-attributes.md +209 -0
- package/effective-python-skill/ref-07-concurrency.md +213 -0
- package/effective-python-skill/ref-08-robustness-performance.md +248 -0
- package/effective-python-skill/ref-09-testing-debugging.md +253 -0
- package/effective-python-skill/ref-10-collaboration.md +175 -0
- package/package.json +8 -2
|
@@ -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
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Chapter 4: Comprehensions and Generators (Items 27-36)
|
|
2
|
+
|
|
3
|
+
## Item 27: Use Comprehensions Instead of map and filter
|
|
4
|
+
```python
|
|
5
|
+
# BAD
|
|
6
|
+
squares = map(lambda x: x**2, range(10))
|
|
7
|
+
even_squares = map(lambda x: x**2, filter(lambda x: x % 2 == 0, range(10)))
|
|
8
|
+
|
|
9
|
+
# GOOD
|
|
10
|
+
squares = [x**2 for x in range(10)]
|
|
11
|
+
even_squares = [x**2 for x in range(10) if x % 2 == 0]
|
|
12
|
+
|
|
13
|
+
# Also works for dicts and sets
|
|
14
|
+
chile_ranks = {rank: name for name, rank in names_and_ranks}
|
|
15
|
+
unique_lengths = {len(name) for name in names}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Item 28: Avoid More Than Two Control Subexpressions in Comprehensions
|
|
19
|
+
```python
|
|
20
|
+
# OK — two levels
|
|
21
|
+
flat = [x for row in matrix for x in row]
|
|
22
|
+
|
|
23
|
+
# OK — two conditions
|
|
24
|
+
filtered = [x for x in numbers if x > 0 if x % 2 == 0]
|
|
25
|
+
|
|
26
|
+
# BAD — too complex, hard to read
|
|
27
|
+
result = [x for sublist1 in my_lists
|
|
28
|
+
for sublist2 in sublist1
|
|
29
|
+
for x in sublist2]
|
|
30
|
+
|
|
31
|
+
# GOOD — use a loop or helper
|
|
32
|
+
result = []
|
|
33
|
+
for sublist1 in my_lists:
|
|
34
|
+
for sublist2 in sublist1:
|
|
35
|
+
result.extend(sublist2)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Rule of thumb: max two `for` subexpressions or two conditions
|
|
39
|
+
- Beyond that, use normal loops for readability
|
|
40
|
+
|
|
41
|
+
## Item 29: Avoid Repeated Work in Comprehensions by Using Assignment Expressions
|
|
42
|
+
```python
|
|
43
|
+
# BAD — calls get_batches twice
|
|
44
|
+
found = {name: batches for name in order
|
|
45
|
+
if (batches := get_batches(stock.get(name, 0), 8))}
|
|
46
|
+
|
|
47
|
+
# GOOD — walrus operator avoids repeated computation
|
|
48
|
+
found = {name: batches for name in order
|
|
49
|
+
if (batches := get_batches(stock.get(name, 0), 8))}
|
|
50
|
+
|
|
51
|
+
# The := expression in the condition makes 'batches' available in the value expression
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Use `:=` in the `if` clause to compute once and reuse in the value expression
|
|
55
|
+
- The walrus variable leaks into the enclosing scope (be careful with naming)
|
|
56
|
+
|
|
57
|
+
## Item 30: Consider Generators Instead of Returning Lists
|
|
58
|
+
```python
|
|
59
|
+
# BAD — builds entire list in memory
|
|
60
|
+
def index_words(text):
|
|
61
|
+
result = []
|
|
62
|
+
if text:
|
|
63
|
+
result.append(0)
|
|
64
|
+
for index, letter in enumerate(text):
|
|
65
|
+
if letter == ' ':
|
|
66
|
+
result.append(index + 1)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
# GOOD — generator yields one at a time
|
|
70
|
+
def index_words(text):
|
|
71
|
+
if text:
|
|
72
|
+
yield 0
|
|
73
|
+
for index, letter in enumerate(text):
|
|
74
|
+
if letter == ' ':
|
|
75
|
+
yield index + 1
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- Generators use memory proportional to one output, not all outputs
|
|
79
|
+
- Use for large or infinite sequences
|
|
80
|
+
- Easy to convert: replace `result.append(x)` with `yield x`
|
|
81
|
+
|
|
82
|
+
## Item 31: Be Defensive When Iterating Over Arguments
|
|
83
|
+
```python
|
|
84
|
+
# BAD — generator exhausted after first iteration
|
|
85
|
+
def normalize(numbers):
|
|
86
|
+
total = sum(numbers) # exhausts the generator
|
|
87
|
+
result = []
|
|
88
|
+
for value in numbers: # nothing left to iterate!
|
|
89
|
+
result.append(value / total)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
# GOOD — accept an iterable container, not iterator
|
|
93
|
+
def normalize(numbers):
|
|
94
|
+
total = sum(numbers) # iterates once
|
|
95
|
+
result = []
|
|
96
|
+
for value in numbers: # iterates again — works with lists, not generators
|
|
97
|
+
result.append(value / total)
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
# BETTER — use __iter__ protocol to detect single-use iterators
|
|
101
|
+
def normalize(numbers):
|
|
102
|
+
if iter(numbers) is numbers: # iterator, not container
|
|
103
|
+
raise TypeError('Must supply a container')
|
|
104
|
+
total = sum(numbers)
|
|
105
|
+
return [value / total for value in numbers]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Iterators are exhausted after one pass; containers are not
|
|
109
|
+
- Check `iter(x) is x` to detect iterators
|
|
110
|
+
- Or implement `__iter__` in a custom container class
|
|
111
|
+
|
|
112
|
+
## Item 32: Consider Generator Expressions for Large List Comprehensions
|
|
113
|
+
```python
|
|
114
|
+
# BAD — creates entire list in memory
|
|
115
|
+
values = [len(x) for x in open('my_file.txt')]
|
|
116
|
+
|
|
117
|
+
# GOOD — generator expression, lazy evaluation
|
|
118
|
+
values = (len(x) for x in open('my_file.txt'))
|
|
119
|
+
|
|
120
|
+
# Chain generator expressions
|
|
121
|
+
roots = ((x, x**0.5) for x in values)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
- Generator expressions use `()` instead of `[]`
|
|
125
|
+
- Lazy — only compute values as needed
|
|
126
|
+
- Can be chained together without memory overhead
|
|
127
|
+
|
|
128
|
+
## Item 33: Compose Multiple Generators with yield from
|
|
129
|
+
```python
|
|
130
|
+
# BAD — manual iteration
|
|
131
|
+
def chain_generators(gen1, gen2):
|
|
132
|
+
for item in gen1:
|
|
133
|
+
yield item
|
|
134
|
+
for item in gen2:
|
|
135
|
+
yield item
|
|
136
|
+
|
|
137
|
+
# GOOD — yield from
|
|
138
|
+
def chain_generators(gen1, gen2):
|
|
139
|
+
yield from gen1
|
|
140
|
+
yield from gen2
|
|
141
|
+
|
|
142
|
+
# Real example: tree traversal
|
|
143
|
+
def traverse(tree):
|
|
144
|
+
if tree is not None:
|
|
145
|
+
yield from traverse(tree.left)
|
|
146
|
+
yield tree.value
|
|
147
|
+
yield from traverse(tree.right)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `yield from` delegates to a sub-generator
|
|
151
|
+
- More readable and slightly faster than manual loop + yield
|
|
152
|
+
|
|
153
|
+
## Item 34: Avoid Injecting Data into Generators with send
|
|
154
|
+
- `generator.send(value)` is complex and hard to understand
|
|
155
|
+
- Prefer passing an iterator to the generator instead
|
|
156
|
+
- Use `send` only when absolutely necessary (coroutine patterns)
|
|
157
|
+
|
|
158
|
+
## Item 35: Avoid Causing State Transitions in Generators with throw
|
|
159
|
+
- `generator.throw(exception)` is confusing
|
|
160
|
+
- Use `__iter__` methods in a class instead for stateful iteration
|
|
161
|
+
- If you need exception handling in generators, prefer try/except inside the generator
|
|
162
|
+
|
|
163
|
+
## Item 36: Consider itertools for Working with Iterators and Generators
|
|
164
|
+
**Linking iterators:**
|
|
165
|
+
```python
|
|
166
|
+
import itertools
|
|
167
|
+
|
|
168
|
+
# Chain multiple iterators
|
|
169
|
+
itertools.chain(iter1, iter2)
|
|
170
|
+
|
|
171
|
+
# Repeat values
|
|
172
|
+
itertools.repeat('hello', 3)
|
|
173
|
+
|
|
174
|
+
# Cycle through an iterable
|
|
175
|
+
itertools.cycle([1, 2, 3])
|
|
176
|
+
|
|
177
|
+
# Parallel iteration with tee
|
|
178
|
+
it1, it2 = itertools.tee(iterator, 2)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Filtering:**
|
|
182
|
+
```python
|
|
183
|
+
# takewhile — yield while predicate is True
|
|
184
|
+
itertools.takewhile(lambda x: x < 5, values)
|
|
185
|
+
|
|
186
|
+
# dropwhile — skip while predicate is True
|
|
187
|
+
itertools.dropwhile(lambda x: x < 5, values)
|
|
188
|
+
|
|
189
|
+
# filterfalse — yield items where predicate is False
|
|
190
|
+
itertools.filterfalse(lambda x: x < 5, values)
|
|
191
|
+
|
|
192
|
+
# islice — slice an iterator
|
|
193
|
+
itertools.islice(values, 2, 8, 2) # start, stop, step
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Combining:**
|
|
197
|
+
```python
|
|
198
|
+
# product — cartesian product
|
|
199
|
+
itertools.product([1,2], ['a','b']) # (1,'a'), (1,'b'), (2,'a'), (2,'b')
|
|
200
|
+
|
|
201
|
+
# permutations and combinations
|
|
202
|
+
itertools.permutations([1,2,3], 2)
|
|
203
|
+
itertools.combinations([1,2,3], 2)
|
|
204
|
+
itertools.combinations_with_replacement([1,2,3], 2)
|
|
205
|
+
|
|
206
|
+
# accumulate — running totals
|
|
207
|
+
itertools.accumulate([1,2,3,4]) # 1, 3, 6, 10
|
|
208
|
+
|
|
209
|
+
# zip_longest
|
|
210
|
+
itertools.zip_longest([1,2], [1,2,3], fillvalue=0)
|
|
211
|
+
```
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Chapter 5: Classes and Interfaces (Items 37-43)
|
|
2
|
+
|
|
3
|
+
## Item 37: Compose Classes Instead of Nesting Many Levels of Built-in Types
|
|
4
|
+
```python
|
|
5
|
+
# BAD — deeply nested built-in types
|
|
6
|
+
grades = {} # dict of dict of list of tuples
|
|
7
|
+
grades['Math'] = {}
|
|
8
|
+
grades['Math']['test'] = [(95, 0.4), (87, 0.6)]
|
|
9
|
+
|
|
10
|
+
# GOOD — compose with named classes
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from collections import namedtuple
|
|
13
|
+
|
|
14
|
+
Grade = namedtuple('Grade', ('score', 'weight'))
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Subject:
|
|
18
|
+
grades: list
|
|
19
|
+
|
|
20
|
+
def average_grade(self):
|
|
21
|
+
total = sum(g.score * g.weight for g in self.grades)
|
|
22
|
+
total_weight = sum(g.weight for g in self.grades)
|
|
23
|
+
return total / total_weight
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Student:
|
|
27
|
+
subjects: dict # name -> Subject
|
|
28
|
+
|
|
29
|
+
class Gradebook:
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self._students = {}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- When nesting goes beyond dict of dict, refactor into classes
|
|
35
|
+
- Use `namedtuple` for lightweight immutable data containers
|
|
36
|
+
- Use `dataclass` for mutable data containers with behavior
|
|
37
|
+
- Bottom-up refactoring: start with the innermost type
|
|
38
|
+
|
|
39
|
+
## Item 38: Accept Functions Instead of Classes for Simple Interfaces
|
|
40
|
+
```python
|
|
41
|
+
# Python's hooks can accept any callable
|
|
42
|
+
names = ['Socrates', 'Archimedes', 'Plato']
|
|
43
|
+
names.sort(key=len) # function as interface
|
|
44
|
+
|
|
45
|
+
# Use __call__ for stateful callables
|
|
46
|
+
class CountMissing:
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self.added = 0
|
|
49
|
+
|
|
50
|
+
def __call__(self):
|
|
51
|
+
self.added += 1
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
counter = CountMissing()
|
|
55
|
+
result = defaultdict(counter, current_data) # uses __call__
|
|
56
|
+
print(counter.added)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Functions are first-class in Python — use them as interfaces
|
|
60
|
+
- For stateful behavior, define `__call__` on a class
|
|
61
|
+
- Simpler than defining full interface classes
|
|
62
|
+
|
|
63
|
+
## Item 39: Use @classmethod Polymorphism to Construct Objects Generically
|
|
64
|
+
```python
|
|
65
|
+
class InputData:
|
|
66
|
+
def read(self):
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
class PathInputData(InputData):
|
|
70
|
+
def __init__(self, path):
|
|
71
|
+
self.path = path
|
|
72
|
+
|
|
73
|
+
def read(self):
|
|
74
|
+
return open(self.path).read()
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def generate_inputs(cls, config):
|
|
78
|
+
"""Factory that creates instances from config."""
|
|
79
|
+
data_dir = config['data_dir']
|
|
80
|
+
for name in os.listdir(data_dir):
|
|
81
|
+
yield cls(os.path.join(data_dir, name))
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Use `@classmethod` as a polymorphic constructor
|
|
85
|
+
- Enables subclasses to provide their own construction logic
|
|
86
|
+
- Avoids hardcoding class names in factory functions
|
|
87
|
+
|
|
88
|
+
## Item 40: Initialize Parent Classes with super()
|
|
89
|
+
```python
|
|
90
|
+
# BAD — direct call to parent
|
|
91
|
+
class Child(Parent):
|
|
92
|
+
def __init__(self):
|
|
93
|
+
Parent.__init__(self) # breaks with multiple inheritance
|
|
94
|
+
|
|
95
|
+
# GOOD — always use super()
|
|
96
|
+
class Child(Parent):
|
|
97
|
+
def __init__(self):
|
|
98
|
+
super().__init__()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `super()` follows the MRO (Method Resolution Order) correctly
|
|
102
|
+
- Essential for multiple inheritance (diamond problem)
|
|
103
|
+
- Always call `super().__init__()` in `__init__` methods
|
|
104
|
+
- The MRO is deterministic: use `ClassName.__mro__` or `ClassName.mro()` to inspect
|
|
105
|
+
|
|
106
|
+
## Item 41: Consider Composing Functionality with Mix-in Classes
|
|
107
|
+
```python
|
|
108
|
+
# Mix-in: a class that provides extra functionality without its own state
|
|
109
|
+
class JsonMixin:
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_json(cls, data):
|
|
112
|
+
kwargs = json.loads(data)
|
|
113
|
+
return cls(**kwargs)
|
|
114
|
+
|
|
115
|
+
def to_json(self):
|
|
116
|
+
return json.dumps(self.__dict__)
|
|
117
|
+
|
|
118
|
+
class DatacenterRack(JsonMixin):
|
|
119
|
+
def __init__(self, switch=None, machines=None):
|
|
120
|
+
self.switch = switch
|
|
121
|
+
self.machines = machines
|
|
122
|
+
|
|
123
|
+
# Usage
|
|
124
|
+
rack = DatacenterRack.from_json(json_data)
|
|
125
|
+
json_str = rack.to_json()
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Mix-ins provide reusable behavior without instance state
|
|
129
|
+
- Classes can use multiple mix-ins via multiple inheritance
|
|
130
|
+
- Prefer mix-ins over deep inheritance hierarchies
|
|
131
|
+
- Name them with `Mixin` suffix for clarity
|
|
132
|
+
|
|
133
|
+
## Item 42: Prefer Public Attributes Over Private Ones
|
|
134
|
+
```python
|
|
135
|
+
# BAD — private attributes (__name mangling)
|
|
136
|
+
class MyObject:
|
|
137
|
+
def __init__(self):
|
|
138
|
+
self.__private_field = 10 # name-mangled to _MyObject__private_field
|
|
139
|
+
|
|
140
|
+
# GOOD — protected with convention
|
|
141
|
+
class MyObject:
|
|
142
|
+
def __init__(self):
|
|
143
|
+
self._protected_field = 10 # convention: internal use
|
|
144
|
+
|
|
145
|
+
# Access is still possible but signals "internal"
|
|
146
|
+
obj = MyObject()
|
|
147
|
+
obj._protected_field # works, but callers know it's internal
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `__double_underscore` causes name mangling — don't use it
|
|
151
|
+
- Use `_single_underscore` for protected/internal attributes
|
|
152
|
+
- Python philosophy: "We're all consenting adults"
|
|
153
|
+
- Name mangling breaks subclass access and makes debugging harder
|
|
154
|
+
- Only use `__` to avoid naming conflicts with subclasses (rare)
|
|
155
|
+
|
|
156
|
+
## Item 43: Inherit from collections.abc for Custom Container Types
|
|
157
|
+
```python
|
|
158
|
+
from collections.abc import Sequence
|
|
159
|
+
|
|
160
|
+
class FrequencyList(list):
|
|
161
|
+
def frequency(self):
|
|
162
|
+
counts = {}
|
|
163
|
+
for item in self:
|
|
164
|
+
counts[item] = counts.get(item, 0) + 1
|
|
165
|
+
return counts
|
|
166
|
+
|
|
167
|
+
# For custom containers, inherit from collections.abc
|
|
168
|
+
class BinaryNode(Sequence):
|
|
169
|
+
def __init__(self, value, left=None, right=None):
|
|
170
|
+
self.value = value
|
|
171
|
+
self.left = left
|
|
172
|
+
self.right = right
|
|
173
|
+
|
|
174
|
+
def __getitem__(self, index):
|
|
175
|
+
# Required by Sequence
|
|
176
|
+
...
|
|
177
|
+
|
|
178
|
+
def __len__(self):
|
|
179
|
+
# Required by Sequence
|
|
180
|
+
...
|
|
181
|
+
|
|
182
|
+
# count() and index() provided automatically by Sequence
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- `collections.abc` provides abstract base classes for containers
|
|
186
|
+
- Inheriting ensures you implement required methods
|
|
187
|
+
- You get mixin methods for free (e.g., `count`, `index` from `Sequence`)
|
|
188
|
+
- Available ABCs: `Sequence`, `MutableSequence`, `Set`, `MutableSet`, `Mapping`, `MutableMapping`, etc.
|