@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.
@@ -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.