13_pattern_matching.py

Download
python 500 lines 14.0 KB
  1"""
  2Python Pattern Matching (Python 3.10+)
  3
  4Demonstrates:
  5- match/case statement basics
  6- Literal patterns
  7- Capture patterns
  8- Wildcard patterns
  9- OR patterns
 10- Guards (if clauses)
 11- Class patterns
 12- Sequence patterns
 13- Mapping patterns
 14- AS patterns
 15"""
 16
 17from dataclasses import dataclass
 18from typing import Any
 19
 20
 21def section(title: str) -> None:
 22    """Print a section header."""
 23    print("\n" + "=" * 60)
 24    print(f"  {title}")
 25    print("=" * 60)
 26
 27
 28# =============================================================================
 29# Basic Pattern Matching
 30# =============================================================================
 31
 32section("Basic Pattern Matching")
 33
 34
 35def describe_number(n: int) -> str:
 36    """Match literal values."""
 37    match n:
 38        case 0:
 39            return "zero"
 40        case 1:
 41            return "one"
 42        case 2:
 43            return "two"
 44        case _:
 45            return f"many ({n})"
 46
 47
 48print(f"describe_number(0): {describe_number(0)}")
 49print(f"describe_number(1): {describe_number(1)}")
 50print(f"describe_number(42): {describe_number(42)}")
 51
 52
 53# =============================================================================
 54# OR Patterns
 55# =============================================================================
 56
 57section("OR Patterns")
 58
 59
 60def classify_status_code(code: int) -> str:
 61    """Match multiple values."""
 62    match code:
 63        case 200 | 201 | 202:
 64            return "Success"
 65        case 400 | 401 | 403 | 404:
 66            return "Client Error"
 67        case 500 | 502 | 503:
 68            return "Server Error"
 69        case _:
 70            return "Unknown"
 71
 72
 73print(f"Status 200: {classify_status_code(200)}")
 74print(f"Status 404: {classify_status_code(404)}")
 75print(f"Status 500: {classify_status_code(500)}")
 76print(f"Status 999: {classify_status_code(999)}")
 77
 78
 79# =============================================================================
 80# Capture Patterns
 81# =============================================================================
 82
 83section("Capture Patterns")
 84
 85
 86def process_point(point: tuple) -> str:
 87    """Capture matched values."""
 88    match point:
 89        case (0, 0):
 90            return "Origin"
 91        case (0, y):
 92            return f"On Y-axis at y={y}"
 93        case (x, 0):
 94            return f"On X-axis at x={x}"
 95        case (x, y):
 96            return f"Point at ({x}, {y})"
 97
 98
 99print(f"(0, 0): {process_point((0, 0))}")
100print(f"(0, 5): {process_point((0, 5))}")
101print(f"(3, 0): {process_point((3, 0))}")
102print(f"(3, 4): {process_point((3, 4))}")
103
104
105# =============================================================================
106# Guards (if clauses)
107# =============================================================================
108
109section("Guards (if clauses)")
110
111
112def categorize_point(point: tuple) -> str:
113    """Use guards to add conditions."""
114    match point:
115        case (x, y) if x == y:
116            return f"On diagonal: ({x}, {y})"
117        case (x, y) if x > 0 and y > 0:
118            return f"Quadrant I: ({x}, {y})"
119        case (x, y) if x < 0 and y > 0:
120            return f"Quadrant II: ({x}, {y})"
121        case (x, y) if x < 0 and y < 0:
122            return f"Quadrant III: ({x}, {y})"
123        case (x, y) if x > 0 and y < 0:
124            return f"Quadrant IV: ({x}, {y})"
125        case _:
126            return "On axis or origin"
127
128
129print(f"(5, 5): {categorize_point((5, 5))}")
130print(f"(3, 4): {categorize_point((3, 4))}")
131print(f"(-3, 4): {categorize_point((-3, 4))}")
132print(f"(3, -4): {categorize_point((3, -4))}")
133
134
135# =============================================================================
136# Sequence Patterns
137# =============================================================================
138
139section("Sequence Patterns")
140
141
142def analyze_list(data: list) -> str:
143    """Match sequences."""
144    match data:
145        case []:
146            return "Empty list"
147        case [x]:
148            return f"Single element: {x}"
149        case [x, y]:
150            return f"Two elements: {x}, {y}"
151        case [x, y, z]:
152            return f"Three elements: {x}, {y}, {z}"
153        case [first, *middle, last]:
154            return f"Multiple elements: first={first}, middle={middle}, last={last}"
155
156
157print(f"[]: {analyze_list([])}")
158print(f"[1]: {analyze_list([1])}")
159print(f"[1, 2]: {analyze_list([1, 2])}")
160print(f"[1, 2, 3]: {analyze_list([1, 2, 3])}")
161print(f"[1, 2, 3, 4, 5]: {analyze_list([1, 2, 3, 4, 5])}")
162
163
164# =============================================================================
165# Wildcard and Rest Patterns
166# =============================================================================
167
168section("Wildcard and Rest Patterns")
169
170
171def parse_command(cmd: list) -> str:
172    """Parse command with rest pattern."""
173    match cmd:
174        case ["quit"]:
175            return "Quitting..."
176        case ["help", topic]:
177            return f"Help for: {topic}"
178        case ["create", resource, *options]:
179            return f"Creating {resource} with options: {options}"
180        case ["delete", resource]:
181            return f"Deleting {resource}"
182        case _:
183            return "Unknown command"
184
185
186print(f"['quit']: {parse_command(['quit'])}")
187print(f"['help', 'commands']: {parse_command(['help', 'commands'])}")
188print(f"['create', 'user', '--admin']: {parse_command(['create', 'user', '--admin'])}")
189print(f"['create', 'file', 'a.txt', 'b.txt']: {parse_command(['create', 'file', 'a.txt', 'b.txt'])}")
190
191
192# =============================================================================
193# Mapping Patterns
194# =============================================================================
195
196section("Mapping Patterns")
197
198
199def handle_event(event: dict) -> str:
200    """Match dictionary patterns."""
201    match event:
202        case {"type": "click", "x": x, "y": y}:
203            return f"Click at ({x}, {y})"
204        case {"type": "keypress", "key": key}:
205            return f"Key pressed: {key}"
206        case {"type": "scroll", "direction": direction, **rest}:
207            return f"Scroll {direction}, data: {rest}"
208        case {"type": event_type}:
209            return f"Event: {event_type} (no additional data)"
210        case _:
211            return "Unknown event"
212
213
214print(handle_event({"type": "click", "x": 100, "y": 200}))
215print(handle_event({"type": "keypress", "key": "Enter"}))
216print(handle_event({"type": "scroll", "direction": "down", "delta": 10}))
217print(handle_event({"type": "custom"}))
218
219
220# =============================================================================
221# Class Patterns
222# =============================================================================
223
224section("Class Patterns")
225
226
227@dataclass
228class Point:
229    x: float
230    y: float
231
232
233@dataclass
234class Circle:
235    center: Point
236    radius: float
237
238
239@dataclass
240class Rectangle:
241    top_left: Point
242    width: float
243    height: float
244
245
246def describe_shape(shape) -> str:
247    """Match dataclass instances."""
248    match shape:
249        case Point(x=0, y=0):
250            return "Point at origin"
251        case Point(x=x, y=y):
252            return f"Point at ({x}, {y})"
253        case Circle(center=Point(x=cx, y=cy), radius=r):
254            return f"Circle centered at ({cx}, {cy}) with radius {r}"
255        case Rectangle(top_left=Point(x=x, y=y), width=w, height=h):
256            return f"Rectangle at ({x}, {y}), size {w}x{h}"
257        case _:
258            return "Unknown shape"
259
260
261p = Point(5, 10)
262c = Circle(Point(0, 0), 5)
263r = Rectangle(Point(10, 20), 30, 40)
264
265print(describe_shape(p))
266print(describe_shape(c))
267print(describe_shape(r))
268
269
270# =============================================================================
271# Nested Patterns
272# =============================================================================
273
274section("Nested Patterns")
275
276
277def evaluate(expr: list) -> float:
278    """Simple expression evaluator with nested patterns."""
279    match expr:
280        case ["+", a, b]:
281            return evaluate(a) + evaluate(b)
282        case ["-", a, b]:
283            return evaluate(a) - evaluate(b)
284        case ["*", a, b]:
285            return evaluate(a) * evaluate(b)
286        case ["/", a, b]:
287            return evaluate(a) / evaluate(b)
288        case int(n) | float(n):
289            return n
290        case _:
291            raise ValueError(f"Invalid expression: {expr}")
292
293
294expr1 = ["+", 10, 5]
295expr2 = ["*", ["+", 3, 2], 4]
296expr3 = ["/", ["-", 20, 5], 3]
297
298print(f"{expr1} = {evaluate(expr1)}")
299print(f"{expr2} = {evaluate(expr2)}")
300print(f"{expr3} = {evaluate(expr3)}")
301
302
303# =============================================================================
304# AS Patterns
305# =============================================================================
306
307section("AS Patterns (Capture and Match)")
308
309
310def process_data(data: Any) -> str:
311    """Use AS patterns to capture matched values."""
312    match data:
313        case [x, y] as point:
314            return f"2D point {point}: x={x}, y={y}"
315        case [x, y, z] as point:
316            return f"3D point {point}: x={x}, y={y}, z={z}"
317        case {"name": name, "age": age} as person:
318            return f"Person {person}: {name} is {age} years old"
319        case _:
320            return "Unknown data format"
321
322
323print(process_data([1, 2]))
324print(process_data([1, 2, 3]))
325print(process_data({"name": "Alice", "age": 30}))
326
327
328# =============================================================================
329# Type Patterns
330# =============================================================================
331
332section("Type Patterns")
333
334
335def handle_value(value: Any) -> str:
336    """Match by type."""
337    match value:
338        case int(n) if n < 0:
339            return f"Negative integer: {n}"
340        case int(n):
341            return f"Positive integer: {n}"
342        case float(x):
343            return f"Float: {x}"
344        case str(s):
345            return f"String: '{s}'"
346        case list(items):
347            return f"List with {len(items)} items"
348        case dict(mapping):
349            return f"Dict with {len(mapping)} keys"
350        case _:
351            return f"Other type: {type(value).__name__}"
352
353
354print(handle_value(42))
355print(handle_value(-10))
356print(handle_value(3.14))
357print(handle_value("hello"))
358print(handle_value([1, 2, 3]))
359print(handle_value({"a": 1, "b": 2}))
360
361
362# =============================================================================
363# Complex Example: JSON Processing
364# =============================================================================
365
366section("Complex Example: JSON Processing")
367
368
369def process_json(data: dict) -> str:
370    """Process JSON-like structure."""
371    match data:
372        case {
373            "type": "user",
374            "name": str(name),
375            "email": str(email),
376            "age": int(age)
377        } if age >= 18:
378            return f"Adult user: {name} ({email})"
379
380        case {
381            "type": "user",
382            "name": str(name),
383            "age": int(age)
384        } if age < 18:
385            return f"Minor user: {name}"
386
387        case {
388            "type": "post",
389            "title": str(title),
390            "author": str(author),
391            "tags": list(tags)
392        }:
393            return f"Post '{title}' by {author}, tags: {tags}"
394
395        case {
396            "type": "comment",
397            "text": str(text),
398            "replies": list(replies)
399        }:
400            return f"Comment with {len(replies)} replies: '{text}'"
401
402        case {"type": type_name, **rest}:
403            return f"Unknown type '{type_name}' with data: {rest}"
404
405        case _:
406            return "Invalid JSON structure"
407
408
409user1 = {"type": "user", "name": "Alice", "email": "alice@example.com", "age": 25}
410user2 = {"type": "user", "name": "Bob", "age": 16}
411post = {"type": "post", "title": "Python Tips", "author": "Charlie", "tags": ["python", "programming"]}
412comment = {"type": "comment", "text": "Great post!", "replies": []}
413
414print(process_json(user1))
415print(process_json(user2))
416print(process_json(post))
417print(process_json(comment))
418
419
420# =============================================================================
421# State Machine Example
422# =============================================================================
423
424section("State Machine Example")
425
426
427def transition(state: str, event: tuple) -> tuple[str, str]:
428    """Simple state machine using pattern matching."""
429    match (state, event):
430        case ("idle", ("start",)):
431            return ("running", "Started")
432
433        case ("running", ("pause",)):
434            return ("paused", "Paused")
435
436        case ("paused", ("resume",)):
437            return ("running", "Resumed")
438
439        case ("running", ("stop",)):
440            return ("idle", "Stopped")
441
442        case ("paused", ("stop",)):
443            return ("idle", "Stopped from pause")
444
445        case (current_state, (event_name,)):
446            return (current_state, f"Invalid event '{event_name}' in state '{current_state}'")
447
448
449state = "idle"
450events = [("start",), ("pause",), ("resume",), ("stop",)]
451
452print(f"Initial state: {state}")
453for event in events:
454    state, message = transition(state, event)
455    print(f"  Event {event[0]:6} -> state: {state:8} ({message})")
456
457
458# =============================================================================
459# Summary
460# =============================================================================
461
462section("Summary")
463
464print("""
465Pattern matching features (Python 3.10+):
4661. match/case - structural pattern matching
4672. Literal patterns - exact value matching
4683. Capture patterns - bind matched values to variables
4694. Wildcard (_) - match anything, don't capture
4705. OR patterns (|) - match multiple alternatives
4716. Guards (if) - add conditions to patterns
4727. Sequence patterns - match lists/tuples
4738. Mapping patterns - match dictionaries
4749. Class patterns - match dataclass/class instances
47510. AS patterns - capture while matching
47611. Type patterns - match by type
47712. Nested patterns - combine patterns
478
479Benefits:
480- More readable than if/elif chains
481- Exhaustiveness checking (with mypy)
482- Destructuring built-in
483- Declarative style
484- Better for complex matching scenarios
485
486Use cases:
487- Command parsing
488- Event handling
489- JSON/API response processing
490- State machines
491- Expression evaluation
492- Data validation
493
494Pattern matching vs if/elif:
495- Patterns: Better for structural matching, destructuring
496- if/elif: Better for simple conditions, complex logic
497
498Note: Pattern matching requires Python 3.10+
499""")