Dataclasses
ปัญหาที่ dataclasses แก้ไข
หัวข้อที่มีชื่อว่า “ปัญหาที่ dataclasses แก้ไข”ใน TypeScript คุณมักเขียน class ขึ้นมาแค่เพื่อเก็บข้อมูลแล้วได้ constructor มาฟรี ๆ หรือไม่ก็ใช้ interface คู่กับ object literal ตอนที่ไม่ต้องการ method เลย
ฝั่ง Python ใช้ decorator @dataclass เชื่อมสองอย่างนี้เข้าด้วยกัน คือยังเป็น class เต็มตัว แต่สร้าง __init__, __repr__ และ __eq__ ให้อัตโนมัติจาก field annotation คุณจึงเขียนแค่ field ก็พอ
// TypeScript — class as data holderclass Point { constructor( public x: number, public y: number ) {}
toString(): string { return `Point(${this.x}, ${this.y})`; }}
const p = new Point(3, 4);console.log(p); // Point { x: 3, y: 4 }console.log(p.toString()); // Point(3, 4)from dataclasses import dataclass
@dataclassclass Point: x: float y: float
def distance_from_origin(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5
p = Point(3.0, 4.0)print(p) # Point(x=3.0, y=4.0)print(p.distance_from_origin()) # 5.0
# __eq__ ถูกสร้างอัตโนมัติprint(Point(1, 2) == Point(1, 2)) # Trueคุณได้ __init__, __repr__, และ __eq__ มาฟรีโดยไม่ต้องเขียน boilerplate decorator @dataclass อ่าน field annotations ตอน class definition time และสร้างเมธอดเหล่านั้นให้
Default values และ field()
หัวข้อที่มีชื่อว่า “Default values และ field()”Default values ทำงานเหมือน parameter defaults ของ TypeScript สำหรับ mutable defaults (lists, dicts) คุณ ต้อง ใช้ field(default_factory=...) — อย่า assign mutable literal ตรงๆ เพราะทุก instance จะแชร์ออบเจ็กต์เดียวกัน
// TypeScript — parameter defaultsclass Config { constructor( public host: string = "localhost", public port: number = 8080, public tags: string[] = [] // ⚠ shared ref bug possible ) {}}
// Correct: factory pattern or spread copyclass SafeConfig { host: string; port: number; tags: string[]; constructor(opts: Partial<SafeConfig> = {}) { this.host = opts.host ?? "localhost"; this.port = opts.port ?? 8080; this.tags = [...(opts.tags ?? [])]; }}from dataclasses import dataclass, field
@dataclassclass Config: host: str = "localhost" port: int = 8080 # field(default_factory=list) ป้องกัน shared-ref bug tags: list[str] = field(default_factory=list)
cfg1 = Config()cfg2 = Config(port=9000, tags=["prod"])
print(cfg1) # Config(host='localhost', port=8080, tags=[])print(cfg2) # Config(host='localhost', port=9000, tags=['prod'])
cfg1.tags.append("dev")print(cfg1.tags) # ['dev'] — ไม่กระทบ cfg2print(cfg2.tags) # ['prod']frozen=True: immutable records
หัวข้อที่มีชื่อว่า “frozen=True: immutable records”@dataclass(frozen=True) ทำให้ fields ทั้งหมดเป็น read-only หลัง __init__ นี่คือ Readonly<T> ของ TypeScript หรือ const object ยกเว้นว่า Python enforce จริงที่ runtime
// TypeScript — read-only interfaceinterface Color { readonly r: number; readonly g: number; readonly b: number;}
const red: Readonly<{ r: number; g: number; b: number }> = { r: 255, g: 0, b: 0};// red.r = 100; // TS compile errorfrom dataclasses import dataclass
@dataclass(frozen=True)class Color: r: int g: int b: int
red = Color(255, 0, 0)print(red) # Color(r=255, g=0, b=0)
# frozen=True ทำให้ instance เป็น hashable — ใช้เป็น dict key ได้!palette = {red: "primary red"}print(palette[Color(255, 0, 0)]) # primary red
try: red.r = 100 # type: ignoreexcept Exception as e: print(f"Error: {e}") # cannot assign to field 'r'แก้ไข frozen record: replace()
หัวข้อที่มีชื่อว่า “แก้ไข frozen record: replace()”เพราะ instance ที่ frozen แก้ค่าไม่ได้ วิธีทำคือสร้าง copy ที่แก้ค่าแล้ว ซึ่งเป็นท่าเดียวกับ object spread ของ TypeScript dataclasses.replace() ทำแบบนี้มานานแล้ว ส่วน Python 3.13 ยกระดับเป็น copy.replace() ที่ใช้ได้กับ object ใดก็ตามที่ implement __replace__ (รวมถึง dataclass และ datetime)
// TypeScript — spread เพื่อ copy พร้อมแก้ค่าconst red = { r: 255, g: 0, b: 0 } as const;const darker = { ...red, r: 128 };from dataclasses import dataclass, replaceimport copy
@dataclass(frozen=True)class Color: r: int g: int b: int
red = Color(255, 0, 0)
darker = replace(red, r=128) # dataclasses.replacedarker2 = copy.replace(red, r=128) # Python 3.13+ ผลลัพธ์เดียวกันprint(red, darker, darker2)
# copy.replace ใช้กับ type ใน stdlib ที่รองรับได้ด้วย:from datetime import dateprint(copy.replace(date(2026, 1, 1), year=2027)) # 2027-01-01ลองเล่น
หัวข้อที่มีชื่อว่า “ลองเล่น”from dataclasses import dataclass, field
@dataclassclass Point: x: float y: float
def distance_from_origin(self) -> float: return (self.x ** 2 + self.y ** 2) ** 0.5
@dataclassclass Config: host: str = "localhost" port: int = 8080 tags: list[str] = field(default_factory=list)
@dataclass(frozen=True)class Color: r: int g: int b: int
# Point demop = Point(3.0, 4.0)print(p)print(f"Distance: {p.distance_from_origin()}")print(f"Equal: {Point(1, 2) == Point(1, 2)}")
# Config democfg = Config(tags=["web"])print(cfg)cfg.tags.append("api")print(f"Tags: {cfg.tags}")
# Frozen demored = Color(255, 0, 0)print(red)print(f"Hashable: {hash(red)}")try: red.r = 50 # type: ignoreexcept Exception as e: print(f"Caught: {e}")Loading Python runtime (first run only)…