[Python Series 8] Class Design and Dataclasses

한국어 버전

After seven parts of scripting and automation, it is time to improve your code structure so your projects stay reusable and maintainable. We'll bundle state and behavior with classes and apply the dataclasses module to build data-focused objects quickly.

Key terms

  1. Instance: A concrete object created from a class; self points to this instance.
  2. dataclass: A standard-library decorator that auto-generates constructors and comparison methods for data containers.
  3. slots: A Python feature that locks down the allowed attribute names to save memory and catch typos.
  4. Protocol: A typing tool that declares required methods so different implementations can share the same interface.

Core ideas

Study memo

  • Time: 60 minutes
  • Prereqs: Comfortable splitting code into functions/modules plus experience with requests and file IO
  • Goal: Define classes that tie state+behavior together and use dataclass models with conversion helpers

Classes combine state and behavior. Dataclasses reduce boilerplate for data containers so you can focus on transformations.

Code examples

A minimal class

class MealFetcher:
    def __init__(self, school_code: str):
        self.school_code = school_code

    def build_url(self, date: str) -> str:
        return f"https://school.example/api/meals/{self.school_code}?date={date}"

    def parse(self, raw: dict) -> list[str]:
        return raw["menu"]

This ties the shared state (self.school_code) and reusable methods so your CLI or batch job calls the same logic everywhere.

Instantiate and call methods

fetcher = MealFetcher("demo-high")
today_menu = fetcher.parse({"menu": ["Pork cutlet", "Salad"]})

self always references the instance, so you never pass it manually. Pairing classes with typing keeps IDE suggestions and type checking happy.

Class methods and factory helpers

from datetime import date

class MealFetcher:
    @classmethod
    def for_today(cls, school_code: str) -> "MealFetcher":
        instance = cls(school_code)
        instance.query_date = date.today().isoformat()
        return instance

Use @classmethod for alternative constructors, like injecting API tokens or shared config.

Define data models with dataclasses

from dataclasses import dataclass

@dataclass(slots=True)
class Meal:
    menu: list[str]
    calories: int
    origin: list[str]

@dataclass auto-generates __init__, __repr__, and comparisons. slots=True shrinks memory usage and prevents accidental attributes.

🧩 dataclass in one line A decorator that builds constructor + representation methods for data-centric classes.

Defaults and conversion helpers

from dataclasses import dataclass, field

@dataclass
class Meal:
    menu: list[str]
    calories: int = 0
    origin: list[str] = field(default_factory=list)

    @classmethod
    def from_json(cls, payload: str) -> "Meal":
        data = json.loads(payload)
        return cls(menu=data["menu"], calories=data.get("calories", 0))

    def to_markdown(self) -> str:
        return "\n".join(f"- {item}" for item in self.menu)

field(default_factory=list) is mandatory for mutable defaults. Conversion helpers like from_json and to_markdown become bridges between API responses and presentations.

Inheritance and protocols

from typing import Protocol

class Notifier(Protocol):
    def notify(self, message: str) -> None: ...

class SlackNotifier:
    def __init__(self, webhook: str):
        self.webhook = webhook

    def notify(self, message: str) -> None:
        print("send to slack", message)

Protocols capture the "what" (method signature) without dictating the "how". They're perfect when you want duck typing plus static guarantees, such as swapping notification channels or injecting test doubles.

📌 What is a Protocol? An interface declaration that lists required attributes/methods. Implementations stay flexible while guaranteeing behavior.

MealFetcherState + behaviorMeal dataclassData modelNotifier ProtocolNotification impl JSON -> objectto_markdown() outputDependency injection

Diagramming responsibilities shows who creates data, who formats it, and where protocols abstract the interaction.

Why it matters

Separating responsibilities with classes lets you evolve API calls, parsing, and notifications independently. Dataclass conversion helpers bridge JSON and Markdown so your automation stays understandable to humans.

Practice

  • Follow along: Implement MealFetcher and Meal, then confirm to_markdown() prints the expected list.
  • Extend: Create ConsoleNotifier and FileNotifier classes that fulfill the Notifier protocol, then inject them into MealFetcher.
  • Debug: Intentionally set a list literal as the default in a dataclass, observe the mutable-default warning, and fix it with field(default_factory=list).
  • Done when: At least two classes collaborate while a dataclass conversion method round-trips JSON strings and objects.

Wrap up

Once you structure data with classes and dataclasses, API calls, caching, and notifications fall into clear modules. Next we'll reinforce that structure with robust error handling and logging strategies.

💬 댓글

이 글에 대한 의견을 남겨주세요