-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexamples.py
More file actions
317 lines (243 loc) · 10.3 KB
/
Copy pathexamples.py
File metadata and controls
317 lines (243 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
"""
Chapter 1: The Python Data Model
=================================
Original implementations exploring Python's special method protocol.
Key concepts demonstrated:
- __repr__, __str__, __len__, __getitem__, __contains__
- __bool__, __abs__, __add__, __mul__
- How protocols enable duck typing
- The difference between calling len() vs .__len__()
NOT a copy of the book — these are original extensions exploring the same concepts.
"""
from __future__ import annotations
import sys
sys.stdout.reconfigure(encoding="utf-8")
import collections
import math
import random
from typing import Iterator
# =============================================================================
# PART 1: Extending the FrenchDeck Concept
# =============================================================================
# The book shows a simple FrenchDeck. Here we build a more complete Card system
# that demonstrates how implementing just __len__ and __getitem__ unlocks the
# entire Python sequence ecosystem.
Card = collections.namedtuple("Card", ["rank", "suit"])
SUIT_SYMBOLS = {
"spades": "♠",
"hearts": "♥",
"diamonds": "♦",
"clubs": "♣",
}
SUIT_VALUES = {"spades": 3, "hearts": 2, "diamonds": 1, "clubs": 0}
RANK_VALUES = {
rank: value
for value, rank in enumerate(
["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"],
start=2,
)
}
class FrenchDeck:
"""
A standard 52-card French deck implemented using only __len__ and __getitem__.
By implementing these two methods, we gain for FREE:
- len(deck) → __len__
- deck[0], deck[-1] → __getitem__
- deck[0:5] → __getitem__ with slice
- for card in deck → __getitem__ with sequential integer keys
- 'card in deck' → sequential __getitem__ scan (no __contains__ needed)
- random.choice(deck) → uses len() and __getitem__ internally
- random.shuffle(deck) → needs __setitem__ too (shown below in MutableDeck)
- sorted(deck, ...) → uses __getitem__ iteration
- reversed(deck) → uses __len__ and __getitem__
This is the power of Python's data model: one small surface area unlocks
a massive ecosystem of functionality.
"""
RANKS = [str(n) for n in range(2, 11)] + list("JQKA")
SUITS = ["clubs", "diamonds", "hearts", "spades"]
def __init__(self) -> None:
self._cards: list[Card] = [
Card(rank, suit) for suit in self.SUITS for rank in self.RANKS
]
def __len__(self) -> int:
return len(self._cards)
def __getitem__(self, position: int | slice) -> Card | list[Card]:
return self._cards[position]
def __repr__(self) -> str:
top = self._cards[0]
bottom = self._cards[-1]
return (
f"FrenchDeck({len(self)} cards, "
f"top={top.rank}{SUIT_SYMBOLS[top.suit]}, "
f"bottom={bottom.rank}{SUIT_SYMBOLS[bottom.suit]})"
)
def __contains__(self, card: object) -> bool:
"""
Override the default __contains__ (sequential scan) with O(1) lookup.
Without this, 'card in deck' iterates through all cards.
With this, it's instant.
LESSON: Python falls back to __getitem__ iteration for 'in', but
you should provide __contains__ when you can do better than O(n).
"""
return isinstance(card, Card) and card in set(self._cards)
def spades_high_key(card: Card) -> int:
"""Sort key: higher rank = higher value; spades beat others at same rank."""
return RANK_VALUES[card.rank] * 4 + SUIT_VALUES[card.suit]
# =============================================================================
# PART 2: Vector2D — Numeric Protocol
# =============================================================================
class Vector2D:
"""
A 2D vector implementing the numeric special method protocol.
Demonstrates:
- __repr__ (developer-facing) vs __str__ (user-facing)
- __abs__ for magnitude
- __bool__ for truthiness (zero vector is falsy)
- __add__ and __mul__ for arithmetic
- __eq__ for equality
- __hash__ (must implement when __eq__ is defined)
- __iter__ for unpacking: x, y = vector
Design decision: we make Vector2D immutable (no __setattr__ override)
so it's hashable and safe to use as dict key.
"""
__slots__ = ("_x", "_y") # memory optimization: no __dict__
def __init__(self, x: float, y: float) -> None:
self._x = float(x)
self._y = float(y)
@property
def x(self) -> float:
return self._x
@property
def y(self) -> float:
return self._y
def __repr__(self) -> str:
"""Developer representation: unambiguous, reconstructable."""
return f"Vector2D({self._x!r}, {self._y!r})"
def __str__(self) -> str:
"""User representation: readable coordinates."""
return f"({self._x}, {self._y})"
def __abs__(self) -> float:
"""Magnitude of the vector using the Pythagorean theorem."""
return math.hypot(self._x, self._y)
def __bool__(self) -> bool:
"""
A zero vector (0, 0) is falsy; any non-zero vector is truthy.
LESSON: __bool__ should return bool, not int. While Python accepts
any integer return, being explicit avoids subtle bugs.
Alternative: return bool(abs(self)) — but this is less efficient
for a zero-check. Direct comparison is O(1) vs computing hypot.
"""
return bool(self._x or self._y)
def __eq__(self, other: object) -> bool:
if isinstance(other, Vector2D):
return (self._x, self._y) == (other._x, other._y)
return NotImplemented # Let Python try the other side
def __hash__(self) -> int:
"""
Must define __hash__ when defining __eq__.
Python removes __hash__ from classes that define __eq__ without __hash__,
making them unhashable (can't be used as dict keys or in sets).
"""
return hash((self._x, self._y))
def __add__(self, other: Vector2D) -> Vector2D:
"""Vector addition: component-wise."""
if not isinstance(other, Vector2D):
return NotImplemented
return Vector2D(self._x + other._x, self._y + other._y)
def __mul__(self, scalar: float) -> Vector2D:
"""Scalar multiplication: Vector2D * number."""
if not isinstance(scalar, (int, float)):
return NotImplemented
return Vector2D(self._x * scalar, self._y * scalar)
def __rmul__(self, scalar: float) -> Vector2D:
"""
Reflected multiplication: number * Vector2D.
Without __rmul__, `2 * Vector2D(1, 2)` raises TypeError because
int.__mul__ doesn't know about Vector2D and returns NotImplemented.
Python then tries the right operand's __rmul__ — that's this method.
"""
return self.__mul__(scalar)
def __iter__(self) -> Iterator[float]:
"""
Makes the vector unpackable: x, y = Vector2D(3, 4)
Also enables: list(vector), tuple(vector)
LESSON: __iter__ is the primary protocol. If absent, Python falls
back to __getitem__ with integer indices starting at 0.
"""
yield self._x
yield self._y
def dot(self, other: Vector2D) -> float:
"""Dot product: not a dunder but a domain method."""
return self._x * other._x + self._y * other._y
# =============================================================================
# PART 3: Protocol Demonstration — What You Get For Free
# =============================================================================
class RangeSet:
"""
A lazy integer range that behaves like a set using only:
- __len__ for cardinality
- __contains__ for membership (O(1) math check)
- __iter__ for iteration
This demonstrates how you can implement a minimal protocol surface
and still get rich behavior from the standard library.
"""
def __init__(self, start: int, stop: int) -> None:
self.start = start
self.stop = stop
def __len__(self) -> int:
return max(0, self.stop - self.start)
def __contains__(self, item: object) -> bool:
"""O(1) membership test — no iteration needed."""
return isinstance(item, int) and self.start <= item < self.stop
def __iter__(self) -> Iterator[int]:
return iter(range(self.start, self.stop))
def __repr__(self) -> str:
return f"RangeSet({self.start}, {self.stop})"
# =============================================================================
# DEMO FUNCTIONS
# =============================================================================
def demo_french_deck() -> None:
"""Show how __len__ + __getitem__ unlocks the ecosystem."""
deck = FrenchDeck()
print("=== FrenchDeck Demo ===")
print(f"repr: {deck!r}")
print(f"len: {len(deck)}")
print(f"First card: {deck[0]}")
print(f"Last card: {deck[-1]}")
print(f"Top 3: {deck[:3]}")
print(f"Random card: {random.choice(deck)}")
print(f"Ace of spades in deck: {Card('A', 'spades') in deck}")
print("\nTop 5 cards by rank (spades-high sort):")
for card in sorted(deck, key=spades_high_key)[-5:]:
print(f" {card.rank}{SUIT_SYMBOLS[card.suit]}")
def demo_vector2d() -> None:
"""Show the numeric protocol in action."""
v1 = Vector2D(3, 4)
v2 = Vector2D(1, 2)
print("\n=== Vector2D Demo ===")
print(f"repr(v1): {v1!r}")
print(f"str(v1): {v1}")
print(f"abs(v1): {abs(v1)}")
print(f"bool(v1): {bool(v1)}")
print(f"bool(Vector2D(0,0)): {bool(Vector2D(0, 0))}")
print(f"v1 + v2: {v1 + v2!r}")
print(f"v1 * 3: {v1 * 3!r}")
print(f"3 * v1: {3 * v1!r}")
x, y = v1 # Works because of __iter__
print(f"Unpacking: x={x}, y={y}")
vectors = {v1, v2, Vector2D(3, 4)} # Works because of __hash__
print(f"In a set (v1 and Vector2D(3,4) are same): {len(vectors)} unique vectors")
def demo_range_set() -> None:
"""Show minimal protocol surface with rich behavior."""
rs = RangeSet(1, 101)
print("\n=== RangeSet Demo ===")
print(f"repr: {rs!r}")
print(f"len: {len(rs)}")
print(f"42 in rs: {42 in rs}")
print(f"150 in rs: {150 in rs}")
print(f"sum(rs): {sum(rs)}") # Works via __iter__
print(f"list(rs)[:5]: {list(rs)[:5]}") # Works via __iter__
if __name__ == "__main__":
demo_french_deck()
demo_vector2d()
demo_range_set()