12_dataclasses_demo.py

Download
python 570 lines 13.0 KB
  1"""
  2Python Dataclasses
  3
  4Demonstrates:
  5- @dataclass decorator
  6- field() with default values and factories
  7- Post-initialization processing
  8- Frozen dataclasses (immutable)
  9- Inheritance with dataclasses
 10- Comparison with NamedTuple and attrs
 11- asdict() and astuple()
 12- __post_init__ hook
 13"""
 14
 15from dataclasses import dataclass, field, asdict, astuple, FrozenInstanceError
 16from typing import List, Optional, ClassVar
 17from collections import namedtuple
 18
 19
 20def section(title: str) -> None:
 21    """Print a section header."""
 22    print("\n" + "=" * 60)
 23    print(f"  {title}")
 24    print("=" * 60)
 25
 26
 27# =============================================================================
 28# Basic Dataclass
 29# =============================================================================
 30
 31section("Basic Dataclass")
 32
 33
 34@dataclass
 35class Point:
 36    """Simple 2D point."""
 37    x: float
 38    y: float
 39
 40
 41p1 = Point(10, 20)
 42p2 = Point(10, 20)
 43p3 = Point(30, 40)
 44
 45print(f"p1: {p1}")
 46print(f"p1.x = {p1.x}, p1.y = {p1.y}")
 47
 48# Automatic __eq__
 49print(f"\np1 == p2: {p1 == p2}")
 50print(f"p1 == p3: {p1 == p3}")
 51
 52# Automatic __repr__
 53print(f"repr(p1): {repr(p1)}")
 54
 55
 56# =============================================================================
 57# Default Values
 58# =============================================================================
 59
 60section("Default Values")
 61
 62
 63@dataclass
 64class Rectangle:
 65    """Rectangle with default dimensions."""
 66    width: float = 10.0
 67    height: float = 5.0
 68    color: str = "blue"
 69
 70
 71rect1 = Rectangle()
 72rect2 = Rectangle(width=20)
 73rect3 = Rectangle(30, 15, "red")
 74
 75print(f"rect1: {rect1}")
 76print(f"rect2: {rect2}")
 77print(f"rect3: {rect3}")
 78
 79
 80# =============================================================================
 81# field() with default_factory
 82# =============================================================================
 83
 84section("field() with default_factory")
 85
 86
 87@dataclass
 88class TodoList:
 89    """Todo list with mutable default."""
 90    owner: str
 91    items: List[str] = field(default_factory=list)  # Correct way for mutable defaults
 92    tags: List[str] = field(default_factory=lambda: ["general"])
 93
 94
 95todo1 = TodoList("Alice")
 96todo2 = TodoList("Bob")
 97
 98todo1.items.append("Buy milk")
 99todo2.items.append("Call dentist")
100
101print(f"todo1: {todo1}")
102print(f"todo2: {todo2}")
103print("Lists are separate (not shared)")
104
105
106# =============================================================================
107# field() Options
108# =============================================================================
109
110section("field() Options")
111
112
113@dataclass
114class Product:
115    """Product with various field options."""
116    name: str
117    price: float
118    quantity: int = 0
119
120    # Excluded from __repr__
121    _internal_id: str = field(default="", repr=False)
122
123    # Excluded from __init__
124    total_value: float = field(init=False, repr=True)
125
126    # Excluded from comparison
127    last_updated: str = field(default="", compare=False)
128
129    # Class variable (not instance field)
130    category: ClassVar[str] = "General"
131
132    def __post_init__(self):
133        """Calculate derived fields."""
134        self.total_value = self.price * self.quantity
135
136
137prod = Product("Widget", 19.99, 10, _internal_id="INTERNAL-123")
138print(f"Product: {prod}")
139print(f"total_value: {prod.total_value}")
140print(f"Category (class var): {Product.category}")
141
142
143# =============================================================================
144# __post_init__ Hook
145# =============================================================================
146
147section("__post_init__ Hook")
148
149
150@dataclass
151class User:
152    """User with validation and derived fields."""
153    username: str
154    email: str
155    age: int
156    full_name: str = field(init=False)
157
158    def __post_init__(self):
159        """Validate and initialize derived fields."""
160        # Validation
161        if self.age < 0:
162            raise ValueError("Age cannot be negative")
163        if "@" not in self.email:
164            raise ValueError("Invalid email")
165
166        # Derived field
167        self.full_name = f"User: {self.username}"
168
169
170user = User("alice", "alice@example.com", 30)
171print(f"User: {user}")
172print(f"full_name: {user.full_name}")
173
174try:
175    invalid_user = User("bob", "invalid-email", 25)
176except ValueError as e:
177    print(f"\nValidation error: {e}")
178
179
180# =============================================================================
181# Frozen Dataclasses (Immutable)
182# =============================================================================
183
184section("Frozen Dataclasses (Immutable)")
185
186
187@dataclass(frozen=True)
188class ImmutablePoint:
189    """Immutable point - cannot modify after creation."""
190    x: float
191    y: float
192
193    def move(self, dx: float, dy: float) -> 'ImmutablePoint':
194        """Return new point with offset."""
195        return ImmutablePoint(self.x + dx, self.y + dy)
196
197
198p = ImmutablePoint(10, 20)
199print(f"Original: {p}")
200
201p_moved = p.move(5, 3)
202print(f"Moved: {p_moved}")
203print(f"Original unchanged: {p}")
204
205try:
206    p.x = 100  # This will fail
207except FrozenInstanceError as e:
208    print(f"\nCannot modify frozen instance: {e}")
209
210
211# =============================================================================
212# Comparison and Ordering
213# =============================================================================
214
215section("Comparison and Ordering")
216
217
218@dataclass(order=True)
219class Version:
220    """Version with automatic ordering."""
221    major: int
222    minor: int
223    patch: int
224
225
226v1 = Version(1, 0, 0)
227v2 = Version(1, 5, 2)
228v3 = Version(2, 0, 0)
229
230print(f"v1: {v1}")
231print(f"v2: {v2}")
232print(f"v3: {v3}")
233print(f"\nv1 < v2: {v1 < v2}")
234print(f"v2 < v3: {v2 < v3}")
235print(f"sorted([v3, v1, v2]): {sorted([v3, v1, v2])}")
236
237
238# =============================================================================
239# Custom Ordering
240# =============================================================================
241
242section("Custom Ordering")
243
244
245@dataclass(order=True)
246class Person:
247    """Person with custom sort order."""
248    sort_index: int = field(init=False, repr=False)
249    name: str
250    age: int
251    salary: float
252
253    def __post_init__(self):
254        # Sort by salary (primary), then age (secondary)
255        self.sort_index = self.age  # Can customize as needed
256
257
258people = [
259    Person("Alice", 30, 75000),
260    Person("Bob", 25, 60000),
261    Person("Charlie", 35, 90000),
262]
263
264print("Original:")
265for p in people:
266    print(f"  {p.name}: age={p.age}, salary={p.salary}")
267
268print("\nSorted by age (via sort_index):")
269for p in sorted(people):
270    print(f"  {p.name}: age={p.age}, salary={p.salary}")
271
272
273# =============================================================================
274# Inheritance
275# =============================================================================
276
277section("Inheritance with Dataclasses")
278
279
280@dataclass
281class Animal:
282    """Base animal class."""
283    name: str
284    age: int
285
286
287@dataclass
288class Dog(Animal):
289    """Dog extends Animal."""
290    breed: str
291    is_good_boy: bool = True
292
293
294dog = Dog("Buddy", 5, "Golden Retriever")
295print(f"Dog: {dog}")
296print(f"Is good boy? {dog.is_good_boy}")
297
298
299# =============================================================================
300# asdict() and astuple()
301# =============================================================================
302
303section("asdict() and astuple()")
304
305
306@dataclass
307class Book:
308    """Book with author information."""
309    title: str
310    author: str
311    year: int
312    pages: int
313
314
315book = Book("Python Tricks", "Dan Bader", 2017, 301)
316
317# Convert to dict
318book_dict = asdict(book)
319print(f"asdict(): {book_dict}")
320print(f"Type: {type(book_dict)}")
321
322# Convert to tuple
323book_tuple = astuple(book)
324print(f"\nastuple(): {book_tuple}")
325print(f"Type: {type(book_tuple)}")
326
327
328# =============================================================================
329# Nested Dataclasses
330# =============================================================================
331
332section("Nested Dataclasses")
333
334
335@dataclass
336class Address:
337    """Address information."""
338    street: str
339    city: str
340    zip_code: str
341
342
343@dataclass
344class Company:
345    """Company with address."""
346    name: str
347    address: Address
348    employees: int
349
350
351address = Address("123 Main St", "Springfield", "12345")
352company = Company("Acme Inc", address, 50)
353
354print(f"Company: {company}")
355
356# asdict with nested dataclasses
357company_dict = asdict(company)
358print(f"\nasdict() (nested): {company_dict}")
359
360
361# =============================================================================
362# Comparison with NamedTuple
363# =============================================================================
364
365section("Comparison with NamedTuple")
366
367# NamedTuple
368PersonTuple = namedtuple('PersonTuple', ['name', 'age'])
369
370
371@dataclass
372class PersonDataclass:
373    """Person as dataclass."""
374    name: str
375    age: int
376
377
378pt = PersonTuple("Alice", 30)
379pd = PersonDataclass("Alice", 30)
380
381print("NamedTuple:")
382print(f"  Creation: {pt}")
383print(f"  Immutable: {True}")
384print(f"  Default values: Limited (via defaults)")
385print(f"  Methods: Can't add methods to instance")
386
387print("\nDataclass:")
388print(f"  Creation: {pd}")
389print(f"  Immutable: {False} (unless frozen=True)")
390print(f"  Default values: Full support with field()")
391print(f"  Methods: Can add methods")
392
393
394# =============================================================================
395# Match Pattern (Python 3.10+)
396# =============================================================================
397
398section("Pattern Matching with Dataclasses")
399
400
401@dataclass
402class Circle:
403    """Circle shape."""
404    radius: float
405
406
407@dataclass
408class Rectangle:
409    """Rectangle shape."""
410    width: float
411    height: float
412
413
414def area(shape):
415    """Calculate area using pattern matching."""
416    match shape:
417        case Circle(radius=r):
418            return 3.14159 * r * r
419        case Rectangle(width=w, height=h):
420            return w * h
421        case _:
422            return 0
423
424
425circle = Circle(5)
426rect = Rectangle(4, 6)
427
428print(f"Circle area: {area(circle)}")
429print(f"Rectangle area: {area(rect)}")
430
431
432# =============================================================================
433# Comparison with attrs (Conceptual)
434# =============================================================================
435
436section("Dataclasses vs attrs")
437
438print("""
439Dataclasses (stdlib, Python 3.7+):
440  @dataclass
441  class Point:
442      x: int
443      y: int
444
445  Pros:
446  - Built into Python 3.7+
447  - No external dependencies
448  - Standard library support
449  - Good IDE integration
450
451  Cons:
452  - Less feature-rich than attrs
453  - No validators (need __post_init__)
454  - No converters
455
456attrs (third-party, more features):
457  @attrs.define
458  class Point:
459      x: int
460      y: int
461
462  Pros:
463  - More features (validators, converters)
464  - Works with Python 2.7+
465  - More mature
466  - Slots support
467
468  Cons:
469  - External dependency
470  - Additional package to maintain
471
472When to use:
473- Dataclasses: Default choice for Python 3.7+
474- attrs: Need advanced features or Python 2.7 support
475- NamedTuple: Need immutable, tuple-like behavior
476- Regular class: Need complex custom behavior
477""")
478
479
480# =============================================================================
481# Real-World Example
482# =============================================================================
483
484section("Real-World Example")
485
486
487@dataclass
488class BlogPost:
489    """Blog post with metadata."""
490    title: str
491    content: str
492    author: str
493    tags: List[str] = field(default_factory=list)
494    published: bool = False
495    views: int = 0
496    slug: str = field(init=False)
497
498    def __post_init__(self):
499        """Generate slug from title."""
500        self.slug = self.title.lower().replace(" ", "-")
501
502    def publish(self):
503        """Mark post as published."""
504        self.published = True
505
506    def increment_views(self):
507        """Increment view count."""
508        self.views += 1
509
510    def summary(self) -> str:
511        """Get post summary."""
512        return f"{self.title} by {self.author} ({self.views} views)"
513
514
515post = BlogPost(
516    title="Python Dataclasses Guide",
517    content="Lorem ipsum...",
518    author="Alice",
519    tags=["python", "tutorial"]
520)
521
522print(f"Post: {post}")
523print(f"Slug: {post.slug}")
524
525post.publish()
526post.increment_views()
527post.increment_views()
528
529print(f"Summary: {post.summary()}")
530print(f"Published: {post.published}")
531
532
533# =============================================================================
534# Summary
535# =============================================================================
536
537section("Summary")
538
539print("""
540Dataclass features:
5411. @dataclass decorator - auto-generate methods
5422. field() - customize field behavior
5433. default_factory - mutable defaults
5444. __post_init__ - post-initialization processing
5455. frozen=True - immutable instances
5466. order=True - comparison operators
5477. asdict()/astuple() - conversion utilities
5488. Inheritance - works naturally
5499. Pattern matching - structural pattern matching support
550
551Generated methods:
552- __init__ - initialization
553- __repr__ - string representation
554- __eq__ - equality comparison
555- __lt__, __le__, __gt__, __ge__ - ordering (if order=True)
556- __hash__ - hashing (if frozen=True)
557
558Use dataclasses when:
559- Need simple data containers
560- Want automatic __init__, __repr__, __eq__
561- Need type hints
562- Want clean, readable code
563- Python 3.7+ is available
564
565Avoid when:
566- Need complex custom initialization
567- Require validation beyond __post_init__
568- Need converters/validators (use attrs instead)
569""")