ทำความเข้าใจว่า annotation ทำอะไรจริง ๆ ตอนรันไทม์ และดึงศักยภาพของ dataclass ออกมาให้เต็มที่ด้วย frozen, slots และ default_factory
เปิดบทเรียนนี้ใน KodokonType annotation คือ เมตาดาต้า ไม่ใช่ข้อบังคับ ตัวแปลภาษาเก็บมันไว้ใน __annotations__ และไม่เคยตรวจสอบมันตอนรันไทม์เลย การตรวจสอบเป็นหน้าที่ของตัววิเคราะห์แบบสถิต เช่น mypy หรือ pyright ซึ่งทำงานก่อนการรัน ผลที่ตามมาโดยตรงคือ โปรแกรมที่ใส่ชนิดข้อมูลผิดก็ยังรันได้โดยไม่บ่นสักคำ - และนั่นเป็นไปตามการออกแบบ gradual typing ช่วยให้คุณค่อย ๆ ใส่ annotation ให้โค้ดเบสที่มีอยู่แล้วทีละน้อย โดยไม่ทำให้อะไรพัง
from typing import get_type_hints
def add(a: int, b: int) -> int:
return a + b
print(add("py", "thon"))
print(add.__annotations__)
print(get_type_hints(add))ตั้งแต่ Python 3.9 เป็นต้นมา ชนิดข้อมูลในตัวเป็น generic ได้โดยตรง เขียน list[int] หรือ dict[str, float] ได้เลยโดยไม่ต้องนำเข้า typing.List ส่วนยูเนียนเขียนเป็น int | None ได้ตั้งแต่ 3.10 หากต้องการผูกชนิดของค่าที่รับเข้ากับชนิดของค่าที่คืน ให้ใช้ TypeVar - Python 3.12 ยังมีไวยากรณ์แบบกระชับอย่าง def first[T](items: list[T]) -> T ให้ด้วย จุดที่มักถูกมองข้ามคือ from __future__ import annotations ทำให้ทุก annotation กลายเป็นแบบ lazy (เก็บไว้เป็นสตริง) ซึ่งอนุญาตให้อ้างถึงคลาสที่นิยามไว้ด้านล่างได้ และลดต้นทุนตอนนำเข้าโมดูล
from collections.abc import Iterable, Iterator
from typing import TypeVar
T = TypeVar("T")
def dedupe(items: Iterable[T]) -> Iterator[T]:
seen: set[T] = set()
for item in items:
if item not in seen:
seen.add(item)
yield item
print(list(dedupe([3, 1, 3, 2, 1])))from dataclasses import dataclass, field
@dataclass(frozen=True, slots=True)
class Point:
x: float
y: float
@dataclass
class Basket:
items: list[str] = field(default_factory=list)
total: int = 0
def __post_init__(self) -> None:
self.total = len(self.items)
point = Point(1.0, 2.0)
print(point, hash(point))
print(Basket(["book", "pen"]))พารามิเตอร์สามตัวเปลี่ยน @dataclass ให้เป็นเครื่องมือระดับโปรดักชัน frozen=True ทำให้อินสแตนซ์ไม่เปลี่ยนแปลง และเมื่อรวมกับการเปรียบเทียบความเท่ากันที่ถูกสร้างให้ ก็ทำให้มัน แฮชได้ - ใช้เป็นคีย์ของดิกชันนารีได้ slots=True (Python 3.10+) ตัด __dict__ ประจำอินสแตนซ์ออก ใช้หน่วยความจำน้อยลง เข้าถึงแอตทริบิวต์เร็วขึ้น และการกำหนดค่าแอตทริบิวต์ที่ไม่ได้คาดไว้จะล้มเหลว สุดท้าย kw_only=True บังคับให้เรียกด้วยคีย์เวิร์ดอย่างชัดเจน ส่วน __post_init__ คือที่ที่ควรวาง invariant และฟิลด์ที่คำนวณจากฟิลด์อื่น สำหรับกลุ่มข้อมูลที่ไม่เปลี่ยนแปลงแบบเรียบง่าย NamedTuple ก็ยังสมเหตุสมผล แต่ทันทีที่มีพฤติกรรมหรือการตรวจสอบความถูกต้องเข้ามา ให้เลือก dataclass
add('py', 'thon') ในขณะที่ฟังก์ชันถูก annotate ไว้ว่า (a: int, b: int) -> int?items: list[str] = [] จึงถูกห้ามใช้เป็นค่าเริ่มต้นของฟิลด์ใน dataclass?list[str] ไม่ใช่ annotation ที่ถูกต้องสำหรับฟิลด์frozen=True ทำอะไรกับ dataclass?