@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,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
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Chapter 7: Concurrency and Parallelism (Items 52-64)
|
|
2
|
+
|
|
3
|
+
## Item 52: Use subprocess to Manage Child Processes
|
|
4
|
+
```python
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
# Run a command and capture output
|
|
8
|
+
result = subprocess.run(
|
|
9
|
+
['echo', 'Hello from subprocess'],
|
|
10
|
+
capture_output=True,
|
|
11
|
+
text=True
|
|
12
|
+
)
|
|
13
|
+
print(result.stdout)
|
|
14
|
+
|
|
15
|
+
# Set timeout
|
|
16
|
+
result = subprocess.run(
|
|
17
|
+
['sleep', '10'],
|
|
18
|
+
timeout=5 # raises TimeoutExpired after 5 seconds
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Pipe data to child process
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
['openssl', 'enc', '-aes-256-cbc', '-pass', 'pass:key'],
|
|
24
|
+
input=b'data to encrypt',
|
|
25
|
+
capture_output=True
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Run parallel child processes
|
|
29
|
+
procs = [subprocess.Popen(['cmd', arg]) for arg in args]
|
|
30
|
+
for proc in procs:
|
|
31
|
+
proc.communicate() # wait for each
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- Use `subprocess.run` for simple command execution
|
|
35
|
+
- Use `subprocess.Popen` for parallel or streaming processes
|
|
36
|
+
- Always set timeouts to prevent hanging
|
|
37
|
+
|
|
38
|
+
## Item 53: Use Threads for Blocking I/O, Avoid for Parallelism
|
|
39
|
+
```python
|
|
40
|
+
import threading
|
|
41
|
+
|
|
42
|
+
# Threads for I/O parallelism — GOOD
|
|
43
|
+
def download(url):
|
|
44
|
+
resp = urllib.request.urlopen(url)
|
|
45
|
+
return resp.read()
|
|
46
|
+
|
|
47
|
+
threads = [threading.Thread(target=download, args=(url,)) for url in urls]
|
|
48
|
+
for t in threads:
|
|
49
|
+
t.start()
|
|
50
|
+
for t in threads:
|
|
51
|
+
t.join()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- The GIL prevents true CPU parallelism with threads
|
|
55
|
+
- Threads ARE useful for blocking I/O (network, file system, etc.)
|
|
56
|
+
- For CPU-bound work, use `multiprocessing` or `concurrent.futures.ProcessPoolExecutor`
|
|
57
|
+
- Never use threads for CPU-intensive computation
|
|
58
|
+
|
|
59
|
+
## Item 54: Use Lock to Prevent Data Races in Threads
|
|
60
|
+
```python
|
|
61
|
+
from threading import Lock
|
|
62
|
+
|
|
63
|
+
class Counter:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.count = 0
|
|
66
|
+
self.lock = Lock()
|
|
67
|
+
|
|
68
|
+
def increment(self):
|
|
69
|
+
with self.lock: # context manager is cleanest
|
|
70
|
+
self.count += 1
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- The GIL does NOT prevent data races on Python objects
|
|
74
|
+
- Operations like `+=` are not atomic — they involve read + modify + write
|
|
75
|
+
- Always use `Lock` when multiple threads modify shared state
|
|
76
|
+
- Use `with lock:` context manager for clean acquire/release
|
|
77
|
+
|
|
78
|
+
## Item 55: Use Queue to Coordinate Work Between Threads
|
|
79
|
+
```python
|
|
80
|
+
from queue import Queue
|
|
81
|
+
from threading import Thread
|
|
82
|
+
|
|
83
|
+
def producer(queue):
|
|
84
|
+
for item in generate_items():
|
|
85
|
+
queue.put(item)
|
|
86
|
+
queue.put(None) # sentinel to signal done
|
|
87
|
+
|
|
88
|
+
def consumer(queue):
|
|
89
|
+
while True:
|
|
90
|
+
item = queue.get()
|
|
91
|
+
if item is None:
|
|
92
|
+
break
|
|
93
|
+
process(item)
|
|
94
|
+
queue.task_done()
|
|
95
|
+
|
|
96
|
+
queue = Queue(maxsize=10) # bounded for backpressure
|
|
97
|
+
Thread(target=producer, args=(queue,)).start()
|
|
98
|
+
Thread(target=consumer, args=(queue,)).start()
|
|
99
|
+
queue.join() # wait for all items to be processed
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `Queue` provides thread-safe FIFO communication
|
|
103
|
+
- Use `maxsize` for backpressure (producer blocks when full)
|
|
104
|
+
- Use `task_done()` + `join()` for completion tracking
|
|
105
|
+
- Use sentinel values (None) to signal shutdown
|
|
106
|
+
|
|
107
|
+
## Item 56: Know How to Recognize When Concurrency Is Necessary
|
|
108
|
+
- Concurrency is needed when you have fan-out (one task spawning many) and fan-in (collecting results)
|
|
109
|
+
- Signs you need concurrency: I/O-bound waits, independent tasks, pipeline processing
|
|
110
|
+
- Start simple (sequential), then add concurrency only when needed
|
|
111
|
+
|
|
112
|
+
## Item 57: Avoid Creating New Thread Instances for On-demand Fan-out
|
|
113
|
+
- Creating a thread per task doesn't scale (thread creation overhead, memory)
|
|
114
|
+
- Use thread pools instead (Item 58/59)
|
|
115
|
+
|
|
116
|
+
## Item 58: Understand How Using Queue for Concurrency Requires Refactoring
|
|
117
|
+
- Queue-based pipelines require significant refactoring
|
|
118
|
+
- Consider `concurrent.futures` for simpler patterns
|
|
119
|
+
|
|
120
|
+
## Item 59: Consider ThreadPoolExecutor When Threads Are Necessary for Concurrency
|
|
121
|
+
```python
|
|
122
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
123
|
+
|
|
124
|
+
def fetch_url(url):
|
|
125
|
+
return urllib.request.urlopen(url).read()
|
|
126
|
+
|
|
127
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
128
|
+
# Submit individual tasks
|
|
129
|
+
future = executor.submit(fetch_url, 'https://example.com')
|
|
130
|
+
result = future.result()
|
|
131
|
+
|
|
132
|
+
# Map over multiple inputs
|
|
133
|
+
results = list(executor.map(fetch_url, urls))
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- Simpler than manual thread + Queue management
|
|
137
|
+
- Automatically manages thread lifecycle
|
|
138
|
+
- `max_workers` controls parallelism
|
|
139
|
+
- Use `ProcessPoolExecutor` for CPU-bound tasks
|
|
140
|
+
|
|
141
|
+
## Item 60: Achieve Highly Concurrent I/O with Coroutines
|
|
142
|
+
```python
|
|
143
|
+
import asyncio
|
|
144
|
+
|
|
145
|
+
async def fetch_data(url):
|
|
146
|
+
# async I/O operation
|
|
147
|
+
reader, writer = await asyncio.open_connection(host, port)
|
|
148
|
+
writer.write(request)
|
|
149
|
+
data = await reader.read()
|
|
150
|
+
return data
|
|
151
|
+
|
|
152
|
+
async def main():
|
|
153
|
+
# Run multiple coroutines concurrently
|
|
154
|
+
results = await asyncio.gather(
|
|
155
|
+
fetch_data('url1'),
|
|
156
|
+
fetch_data('url2'),
|
|
157
|
+
fetch_data('url3'),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
asyncio.run(main())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Coroutines enable thousands of concurrent I/O operations
|
|
164
|
+
- Use `async def` and `await` keywords
|
|
165
|
+
- `asyncio.gather` runs multiple coroutines concurrently
|
|
166
|
+
- Far more efficient than threads for I/O-heavy workloads
|
|
167
|
+
|
|
168
|
+
## Item 61: Know How to Port Threaded I/O to asyncio
|
|
169
|
+
- Replace `threading.Thread` with `async def` coroutines
|
|
170
|
+
- Replace blocking I/O calls with `await async_version`
|
|
171
|
+
- Replace `Lock` with `asyncio.Lock`
|
|
172
|
+
- Replace `Queue` with `asyncio.Queue`
|
|
173
|
+
- Use `asyncio.run()` as the entry point
|
|
174
|
+
|
|
175
|
+
## Item 62: Mix Threads and Coroutines to Ease the Transition to asyncio
|
|
176
|
+
```python
|
|
177
|
+
# Run blocking code in a thread from async context
|
|
178
|
+
import asyncio
|
|
179
|
+
|
|
180
|
+
async def main():
|
|
181
|
+
loop = asyncio.get_event_loop()
|
|
182
|
+
result = await loop.run_in_executor(None, blocking_function, arg)
|
|
183
|
+
|
|
184
|
+
# Run async code from synchronous context
|
|
185
|
+
def sync_function():
|
|
186
|
+
loop = asyncio.new_event_loop()
|
|
187
|
+
result = loop.run_until_complete(async_function())
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- Use `run_in_executor` to call blocking code from async code
|
|
191
|
+
- Allows gradual migration from threads to asyncio
|
|
192
|
+
- Never call blocking functions directly in async code (it blocks the event loop)
|
|
193
|
+
|
|
194
|
+
## Item 63: Avoid Blocking the asyncio Event Loop to Maximize Responsiveness
|
|
195
|
+
- Never use `time.sleep()` in async code — use `await asyncio.sleep()`
|
|
196
|
+
- Never do CPU-heavy work in coroutines — use `run_in_executor`
|
|
197
|
+
- Never use blocking I/O calls — use async equivalents (aiohttp, aiofiles, etc.)
|
|
198
|
+
- Profile with `asyncio.get_event_loop().slow_callback_duration`
|
|
199
|
+
|
|
200
|
+
## Item 64: Consider concurrent.futures for True Parallelism
|
|
201
|
+
```python
|
|
202
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
203
|
+
|
|
204
|
+
def cpu_heavy(data):
|
|
205
|
+
return complex_computation(data)
|
|
206
|
+
|
|
207
|
+
with ProcessPoolExecutor() as executor:
|
|
208
|
+
results = list(executor.map(cpu_heavy, data_chunks))
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- `ProcessPoolExecutor` bypasses the GIL for true CPU parallelism
|
|
212
|
+
- Data is serialized between processes (use for independent tasks)
|
|
213
|
+
- Same API as `ThreadPoolExecutor` — easy to switch
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Chapter 8: Robustness and Performance (Items 65-76)
|
|
2
|
+
|
|
3
|
+
## Item 65: Take Advantage of Each Block in try/except/else/finally
|
|
4
|
+
```python
|
|
5
|
+
# Full structure
|
|
6
|
+
try:
|
|
7
|
+
# Code that might raise
|
|
8
|
+
result = dangerous_operation()
|
|
9
|
+
except SomeError as e:
|
|
10
|
+
# Handle specific error
|
|
11
|
+
log_error(e)
|
|
12
|
+
except (TypeError, ValueError):
|
|
13
|
+
# Handle multiple error types
|
|
14
|
+
handle_bad_input()
|
|
15
|
+
else:
|
|
16
|
+
# Runs ONLY if no exception was raised
|
|
17
|
+
# Use for code that depends on try succeeding
|
|
18
|
+
process(result)
|
|
19
|
+
finally:
|
|
20
|
+
# ALWAYS runs, even if exception was raised
|
|
21
|
+
# Use for cleanup (closing files, releasing locks)
|
|
22
|
+
cleanup()
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- `else` block: reduces code in `try`, makes it clear what you're protecting
|
|
26
|
+
- `finally` block: guaranteed cleanup
|
|
27
|
+
- Don't put too much in `try` — only the code that can raise the expected exception
|
|
28
|
+
|
|
29
|
+
## Item 66: Consider contextlib and with Statements for Reusable try/finally Behavior
|
|
30
|
+
```python
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
|
|
33
|
+
@contextmanager
|
|
34
|
+
def log_level(level, name):
|
|
35
|
+
logger = logging.getLogger(name)
|
|
36
|
+
old_level = logger.level
|
|
37
|
+
logger.setLevel(level)
|
|
38
|
+
try:
|
|
39
|
+
yield logger
|
|
40
|
+
finally:
|
|
41
|
+
logger.setLevel(old_level)
|
|
42
|
+
|
|
43
|
+
with log_level(logging.DEBUG, 'my-log') as logger:
|
|
44
|
+
logger.debug('Debug message')
|
|
45
|
+
# Level is automatically restored after the block
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- Use `contextlib.contextmanager` for simple context managers
|
|
49
|
+
- Use `with` statements instead of manual try/finally
|
|
50
|
+
- The `yield` in a context manager is where the `with` block executes
|
|
51
|
+
|
|
52
|
+
## Item 67: Use datetime Instead of time for Local Clocks
|
|
53
|
+
```python
|
|
54
|
+
from datetime import datetime, timezone
|
|
55
|
+
import pytz # or zoneinfo (Python 3.9+)
|
|
56
|
+
|
|
57
|
+
# BAD — time module is unreliable for timezones
|
|
58
|
+
import time
|
|
59
|
+
time.localtime() # platform-dependent behavior
|
|
60
|
+
|
|
61
|
+
# GOOD — datetime with explicit timezone
|
|
62
|
+
now = datetime.now(tz=timezone.utc)
|
|
63
|
+
|
|
64
|
+
# Convert between timezones
|
|
65
|
+
eastern = pytz.timezone('US/Eastern')
|
|
66
|
+
local_time = now.astimezone(eastern)
|
|
67
|
+
|
|
68
|
+
# Python 3.9+ — use zoneinfo
|
|
69
|
+
from zoneinfo import ZoneInfo
|
|
70
|
+
eastern = ZoneInfo('America/New_York')
|
|
71
|
+
local_time = now.astimezone(eastern)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Always store/transmit times in UTC
|
|
75
|
+
- Convert to local time only for display
|
|
76
|
+
- Use `pytz` or `zoneinfo` for timezone handling
|
|
77
|
+
- Never use the `time` module for timezone conversions
|
|
78
|
+
|
|
79
|
+
## Item 68: Make pickle Reliable with copyreg
|
|
80
|
+
```python
|
|
81
|
+
import copyreg
|
|
82
|
+
import pickle
|
|
83
|
+
|
|
84
|
+
class GameState:
|
|
85
|
+
def __init__(self, level=0, lives=4, points=0):
|
|
86
|
+
self.level = level
|
|
87
|
+
self.lives = lives
|
|
88
|
+
self.points = points
|
|
89
|
+
|
|
90
|
+
def pickle_game_state(game_state):
|
|
91
|
+
kwargs = game_state.__dict__
|
|
92
|
+
return unpickle_game_state, (kwargs,)
|
|
93
|
+
|
|
94
|
+
def unpickle_game_state(kwargs):
|
|
95
|
+
return GameState(**kwargs)
|
|
96
|
+
|
|
97
|
+
copyreg.pickle(GameState, pickle_game_state)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- `copyreg` makes pickle forward-compatible when classes change
|
|
101
|
+
- Register custom serialization functions for your classes
|
|
102
|
+
- Always provide default values for new attributes
|
|
103
|
+
|
|
104
|
+
## Item 69: Use decimal When Precision Matters
|
|
105
|
+
```python
|
|
106
|
+
from decimal import Decimal, ROUND_UP
|
|
107
|
+
|
|
108
|
+
# BAD — float precision issues
|
|
109
|
+
rate = 1.45
|
|
110
|
+
seconds = 222
|
|
111
|
+
cost = rate * seconds / 60 # 5.364999999999999
|
|
112
|
+
|
|
113
|
+
# GOOD — Decimal for exact arithmetic
|
|
114
|
+
rate = Decimal('1.45')
|
|
115
|
+
seconds = Decimal('222')
|
|
116
|
+
cost = rate * seconds / Decimal('60')
|
|
117
|
+
rounded = cost.quantize(Decimal('0.01'), rounding=ROUND_UP)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- Use `Decimal` for financial calculations, exact fractions
|
|
121
|
+
- Always construct from strings (`Decimal('1.45')`) not floats (`Decimal(1.45)`)
|
|
122
|
+
- Use `quantize` for rounding control
|
|
123
|
+
|
|
124
|
+
## Item 70: Profile Before Optimizing
|
|
125
|
+
```python
|
|
126
|
+
from cProfile import Profile
|
|
127
|
+
from pstats import Stats
|
|
128
|
+
|
|
129
|
+
profiler = Profile()
|
|
130
|
+
profiler.runcall(my_function, arg1, arg2)
|
|
131
|
+
|
|
132
|
+
stats = Stats(profiler)
|
|
133
|
+
stats.strip_dirs()
|
|
134
|
+
stats.sort_stats('cumulative')
|
|
135
|
+
stats.print_stats()
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
- Never guess where bottlenecks are — profile first
|
|
139
|
+
- Use `cProfile` for C-extension speed profiling
|
|
140
|
+
- `cumulative` time shows total time including sub-calls
|
|
141
|
+
- `tottime` shows time in the function itself (excluding sub-calls)
|
|
142
|
+
- Focus optimization on the top functions by cumulative time
|
|
143
|
+
|
|
144
|
+
## Item 71: Prefer deque for Producer-Consumer Queues
|
|
145
|
+
```python
|
|
146
|
+
from collections import deque
|
|
147
|
+
|
|
148
|
+
# FIFO queue operations
|
|
149
|
+
queue = deque()
|
|
150
|
+
queue.append('item') # O(1) add to right
|
|
151
|
+
item = queue.popleft() # O(1) remove from left
|
|
152
|
+
|
|
153
|
+
# BAD — list as queue
|
|
154
|
+
queue = []
|
|
155
|
+
queue.append('item') # O(1)
|
|
156
|
+
item = queue.pop(0) # O(n)! shifts all elements
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `list.pop(0)` is O(n); `deque.popleft()` is O(1)
|
|
160
|
+
- `deque` also supports `maxlen` for bounded buffers
|
|
161
|
+
- Use `deque` for any FIFO pattern
|
|
162
|
+
|
|
163
|
+
## Item 72: Consider Searching Sorted Sequences with bisect
|
|
164
|
+
```python
|
|
165
|
+
import bisect
|
|
166
|
+
|
|
167
|
+
sorted_list = [2, 5, 8, 12, 16, 23, 38, 56, 72, 91]
|
|
168
|
+
|
|
169
|
+
# Find insertion point
|
|
170
|
+
index = bisect.bisect_left(sorted_list, 12) # 3
|
|
171
|
+
index = bisect.bisect_right(sorted_list, 12) # 4
|
|
172
|
+
|
|
173
|
+
# Insert while maintaining sort order
|
|
174
|
+
bisect.insort(sorted_list, 15) # inserts 15 in correct position
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
- Binary search is O(log n) vs O(n) for linear search
|
|
178
|
+
- Use `bisect_left` for leftmost position, `bisect_right` for rightmost
|
|
179
|
+
- `insort` keeps list sorted after insertion
|
|
180
|
+
- Requires the sequence to already be sorted
|
|
181
|
+
|
|
182
|
+
## Item 73: Know How to Use heapq for Priority Queues
|
|
183
|
+
```python
|
|
184
|
+
import heapq
|
|
185
|
+
|
|
186
|
+
# Create a min-heap
|
|
187
|
+
heap = []
|
|
188
|
+
heapq.heappush(heap, 5)
|
|
189
|
+
heapq.heappush(heap, 1)
|
|
190
|
+
heapq.heappush(heap, 3)
|
|
191
|
+
|
|
192
|
+
# Pop smallest
|
|
193
|
+
smallest = heapq.heappop(heap) # 1
|
|
194
|
+
|
|
195
|
+
# Get n smallest/largest
|
|
196
|
+
heapq.nsmallest(3, data)
|
|
197
|
+
heapq.nlargest(3, data)
|
|
198
|
+
|
|
199
|
+
# Priority queue with tuples
|
|
200
|
+
heapq.heappush(heap, (priority, item))
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
- heapq provides O(log n) push and pop operations
|
|
204
|
+
- Always a min-heap (smallest first)
|
|
205
|
+
- For max-heap, negate the values
|
|
206
|
+
- Use for priority queues, top-K problems, merge sorted streams
|
|
207
|
+
|
|
208
|
+
## Item 74: Consider memoryview and bytearray for Zero-Copy Interactions with bytes
|
|
209
|
+
```python
|
|
210
|
+
# BAD — copying bytes on every slice
|
|
211
|
+
data = b'large data...'
|
|
212
|
+
chunk = data[10:20] # creates a new bytes object
|
|
213
|
+
|
|
214
|
+
# GOOD — zero-copy with memoryview
|
|
215
|
+
data = bytearray(b'large data...')
|
|
216
|
+
view = memoryview(data)
|
|
217
|
+
chunk = view[10:20] # no copy, just a view
|
|
218
|
+
chunk[:5] = b'hello' # writes directly to original data
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
- `memoryview` provides zero-copy slicing of bytes-like objects
|
|
222
|
+
- Essential for high-performance I/O and data processing
|
|
223
|
+
- Works with `bytearray`, `array.array`, NumPy arrays
|
|
224
|
+
- Use for socket I/O, file I/O, binary protocol parsing
|
|
225
|
+
|
|
226
|
+
## Item 75: Use repr Strings for Debugging Output
|
|
227
|
+
```python
|
|
228
|
+
class MyClass:
|
|
229
|
+
def __init__(self, value):
|
|
230
|
+
self.value = value
|
|
231
|
+
|
|
232
|
+
def __repr__(self):
|
|
233
|
+
return f'{self.__class__.__name__}({self.value!r})'
|
|
234
|
+
|
|
235
|
+
def __str__(self):
|
|
236
|
+
return f'MyClass with value {self.value}'
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
- `repr()` gives an unambiguous string for debugging
|
|
240
|
+
- `str()` gives a human-readable string
|
|
241
|
+
- Always implement `__repr__` on your classes
|
|
242
|
+
- Use `!r` in f-strings for repr formatting: `f'{obj!r}'`
|
|
243
|
+
|
|
244
|
+
## Item 76: Verify Related Behaviors in TestCase Subclasses
|
|
245
|
+
(Cross-reference with Chapter 9 Testing)
|
|
246
|
+
- Group related tests in TestCase subclasses
|
|
247
|
+
- Use descriptive test method names
|
|
248
|
+
- Test both success and failure cases
|