@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,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.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# Chapter 6: Metaclasses and Attributes (Items 44-51)
|
|
2
|
+
|
|
3
|
+
## Item 44: Use Plain Attributes Instead of Setter and Getter Methods
|
|
4
|
+
```python
|
|
5
|
+
# BAD — Java-style getters/setters
|
|
6
|
+
class OldResistor:
|
|
7
|
+
def __init__(self, ohms):
|
|
8
|
+
self._ohms = ohms
|
|
9
|
+
|
|
10
|
+
def get_ohms(self):
|
|
11
|
+
return self._ohms
|
|
12
|
+
|
|
13
|
+
def set_ohms(self, ohms):
|
|
14
|
+
self._ohms = ohms
|
|
15
|
+
|
|
16
|
+
# GOOD — plain attributes
|
|
17
|
+
class Resistor:
|
|
18
|
+
def __init__(self, ohms):
|
|
19
|
+
self.ohms = ohms
|
|
20
|
+
|
|
21
|
+
# If you later need behavior, migrate to @property (Item 44)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- Start with simple public attributes
|
|
25
|
+
- If you need special behavior later, use `@property` without changing the API
|
|
26
|
+
- Never write explicit getter/setter methods in Python
|
|
27
|
+
|
|
28
|
+
## Item 45: Consider @property Instead of Refactoring Attributes
|
|
29
|
+
```python
|
|
30
|
+
class Bucket:
|
|
31
|
+
def __init__(self, period):
|
|
32
|
+
self.period = period
|
|
33
|
+
self.quota = 0
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def quota(self):
|
|
37
|
+
return self._quota
|
|
38
|
+
|
|
39
|
+
@quota.setter
|
|
40
|
+
def quota(self, value):
|
|
41
|
+
if value < 0:
|
|
42
|
+
raise ValueError('Quota must be >= 0')
|
|
43
|
+
self._quota = value
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- Use `@property` to add validation, logging, or computed behavior
|
|
47
|
+
- Keeps backward-compatible API (attribute access syntax)
|
|
48
|
+
- Don't do too much work in property getters — keep them fast
|
|
49
|
+
- If a property is getting complex, refactor to a normal method
|
|
50
|
+
|
|
51
|
+
## Item 46: Use Descriptors for Reusable @property Methods
|
|
52
|
+
```python
|
|
53
|
+
class Grade:
|
|
54
|
+
"""Reusable validation descriptor."""
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self._values = {}
|
|
57
|
+
|
|
58
|
+
def __get__(self, instance, instance_type):
|
|
59
|
+
if instance is None:
|
|
60
|
+
return self
|
|
61
|
+
return self._values.get(instance, 0)
|
|
62
|
+
|
|
63
|
+
def __set__(self, instance, value):
|
|
64
|
+
if not (0 <= value <= 100):
|
|
65
|
+
raise ValueError('Grade must be between 0 and 100')
|
|
66
|
+
self._values[instance] = value
|
|
67
|
+
|
|
68
|
+
class Exam:
|
|
69
|
+
math_grade = Grade()
|
|
70
|
+
writing_grade = Grade()
|
|
71
|
+
science_grade = Grade()
|
|
72
|
+
|
|
73
|
+
exam = Exam()
|
|
74
|
+
exam.math_grade = 95 # calls Grade.__set__
|
|
75
|
+
print(exam.math_grade) # calls Grade.__get__
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- Use descriptors when you'd copy-paste `@property` logic
|
|
79
|
+
- Store per-instance data using `WeakKeyDictionary` to avoid memory leaks:
|
|
80
|
+
```python
|
|
81
|
+
from weakref import WeakKeyDictionary
|
|
82
|
+
class Grade:
|
|
83
|
+
def __init__(self):
|
|
84
|
+
self._values = WeakKeyDictionary()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Item 47: Use __getattr__, __getattribute__, and __setattr__ for Lazy Attributes
|
|
88
|
+
```python
|
|
89
|
+
# __getattr__ — called only when attribute not found normally
|
|
90
|
+
class LazyRecord:
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.exists = 5
|
|
93
|
+
|
|
94
|
+
def __getattr__(self, name):
|
|
95
|
+
value = f'Value for {name}'
|
|
96
|
+
setattr(self, name, value) # cache it
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
# __getattribute__ — called for EVERY attribute access
|
|
100
|
+
class ValidatingRecord:
|
|
101
|
+
def __getattribute__(self, name):
|
|
102
|
+
value = super().__getattribute__(name)
|
|
103
|
+
# validate or log every access
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
# __setattr__ — called for EVERY attribute assignment
|
|
107
|
+
class SavingRecord:
|
|
108
|
+
def __setattr__(self, name, value):
|
|
109
|
+
super().__setattr__(name, value)
|
|
110
|
+
# save to database, etc.
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `__getattr__` is for lazy/dynamic attributes (called only on missing)
|
|
114
|
+
- `__getattribute__` intercepts ALL attribute access (use carefully)
|
|
115
|
+
- Always use `super()` in these methods to avoid infinite recursion
|
|
116
|
+
- `hasattr` and `getattr` also trigger `__getattribute__`
|
|
117
|
+
|
|
118
|
+
## Item 48: Validate Subclasses with __init_subclass__
|
|
119
|
+
```python
|
|
120
|
+
class Polygon:
|
|
121
|
+
sides = None
|
|
122
|
+
|
|
123
|
+
def __init_subclass__(cls, **kwargs):
|
|
124
|
+
super().__init_subclass__(**kwargs)
|
|
125
|
+
if cls.sides is None or cls.sides < 3:
|
|
126
|
+
raise ValueError('Polygons need 3+ sides')
|
|
127
|
+
|
|
128
|
+
class Triangle(Polygon):
|
|
129
|
+
sides = 3 # OK
|
|
130
|
+
|
|
131
|
+
class Line(Polygon):
|
|
132
|
+
sides = 2 # Raises ValueError at class definition time!
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `__init_subclass__` is called when a class is subclassed
|
|
136
|
+
- Use it for validation, registration, or class setup
|
|
137
|
+
- Much simpler than metaclasses for most use cases
|
|
138
|
+
- Works with multiple inheritance (use `**kwargs` to pass through)
|
|
139
|
+
|
|
140
|
+
## Item 49: Register Class Existence with __init_subclass__
|
|
141
|
+
```python
|
|
142
|
+
registry = {}
|
|
143
|
+
|
|
144
|
+
class Serializable:
|
|
145
|
+
def __init_subclass__(cls, **kwargs):
|
|
146
|
+
super().__init_subclass__(**kwargs)
|
|
147
|
+
registry[cls.__name__] = cls
|
|
148
|
+
|
|
149
|
+
class Point(Serializable):
|
|
150
|
+
def __init__(self, x, y):
|
|
151
|
+
self.x = x
|
|
152
|
+
self.y = y
|
|
153
|
+
|
|
154
|
+
# Point is automatically registered
|
|
155
|
+
assert registry['Point'] is Point
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
- Auto-registration pattern: base class registers all subclasses
|
|
159
|
+
- Useful for serialization, plugin systems, ORM models
|
|
160
|
+
- Replaces the need for explicit registration decorators or metaclasses
|
|
161
|
+
|
|
162
|
+
## Item 50: Annotate Class Attributes with __set_name__
|
|
163
|
+
```python
|
|
164
|
+
class Field:
|
|
165
|
+
def __set_name__(self, owner, name):
|
|
166
|
+
self.name = name # attribute name on the class
|
|
167
|
+
self.internal_name = '_' + name # storage name
|
|
168
|
+
|
|
169
|
+
def __get__(self, instance, instance_type):
|
|
170
|
+
if instance is None:
|
|
171
|
+
return self
|
|
172
|
+
return getattr(instance, self.internal_name, '')
|
|
173
|
+
|
|
174
|
+
def __set__(self, instance, value):
|
|
175
|
+
setattr(instance, self.internal_name, value)
|
|
176
|
+
|
|
177
|
+
class Customer:
|
|
178
|
+
first_name = Field() # __set_name__ called with name='first_name'
|
|
179
|
+
last_name = Field()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
- `__set_name__` is called automatically when a descriptor is assigned to a class attribute
|
|
183
|
+
- Eliminates the need to repeat the attribute name
|
|
184
|
+
- Works with descriptors to provide clean, DRY class definitions
|
|
185
|
+
|
|
186
|
+
## Item 51: Prefer Class Decorators Over Metaclasses for Composable Class Extensions
|
|
187
|
+
```python
|
|
188
|
+
# Class decorator — simple and composable
|
|
189
|
+
def my_class_decorator(cls):
|
|
190
|
+
# modify or wrap cls
|
|
191
|
+
original_init = cls.__init__
|
|
192
|
+
|
|
193
|
+
def new_init(self, *args, **kwargs):
|
|
194
|
+
print(f'Creating {cls.__name__}')
|
|
195
|
+
original_init(self, *args, **kwargs)
|
|
196
|
+
|
|
197
|
+
cls.__init__ = new_init
|
|
198
|
+
return cls
|
|
199
|
+
|
|
200
|
+
@my_class_decorator
|
|
201
|
+
class MyClass:
|
|
202
|
+
def __init__(self, value):
|
|
203
|
+
self.value = value
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- Class decorators are simpler than metaclasses
|
|
207
|
+
- They compose easily (stack multiple decorators)
|
|
208
|
+
- Use metaclasses only when you need to control the class creation process itself
|
|
209
|
+
- Prefer: `__init_subclass__` > class decorators > metaclasses
|