Tutorial

dataclass บน Python ที่จะช่วยให้จัดการ Class ได้ง่ายขึ้น

By Arnon Puitrakul - 15 พฤศจิกายน 2021 - 1 min read min(s)

dataclass บน Python ที่จะช่วยให้จัดการ Class ได้ง่ายขึ้น

โดยปกติเวลาเราเขียน Python และเรามีพวกข้อมูลที่อาจจะมีหลาย ๆ ค่าในหนึ่งตัว เช่น Coordinate หรือพวกชื่อคนต่าง ๆ เราก็อาจจะใช้วิธีง่าย ๆ อย่างการสร้าง Dictionary เข้ามาเก็บ หรือไม่ก็ลงทุนหน่อยเปิดเป็น Class แล้วเอาข้อมูลยัดเลย ถ้าเราใช้วิธีแรก เราก็อาจจะเจอปัญหาที่ว่า การจัดการมันยากมาก ๆ กับเราไม่สามารถควบคุมเรื่องการแก้ไขได้เลย หรือถ้าเราเปิดเป็น Class มันก็ทำให้เราต้องยุ่งยากเขียน Class เขียน Constructor และ Method เองเยอะมาก ๆ ทำให้เราเสียเวลาทำมาหากินมาก ๆ มันจะดีกว่ามั้ยถ้ามันจะมี Class สำเร็จรูปที่เกิดมาเพื่อการเก็บข้อมูลโดยเฉพาะเลย ไม่ได้ต้องการพวก Method อะไรที่ซับซ้อนเลย ใน Python 3.7 dataclass ก็ถือกำเนิดขึ้นมาเพื่อแก้ปัญหานี้เลย วันนี้เราจะมาดูกันว่ามันใช้งานยังไง และทำให้งานเราง่าย และ เร็วขึ้นได้อย่างไร

ทดลองสร้าง dataclass object อันแรก

from dataclasses import dataclass

@dataclass
class Coordinate :
    x: int
    y: int

การสร้าง dataclass ไม่ใช่เรื่องยากเลย Python เขาคิดมาให้เราหมดแล้ว เราสามารถ Import มันเข้ามา และเรียกใช้งานผ่าน Decorator ได้เลย ถ้าใครที่ไม่คุ้นเคยกับ Concept ของ Decorator เราเคยเขียนเล่าไว้ ที่นี่

ในตัว Class เราไม่ต้อง Implement อะไรเลย นอกจาก Attribute ที่เราต้องการไว้ได้เลย โดยที่เราเลือกใช้ Typing เพื่อเป็นการบอกด้วยว่า แต่ละตัวที่เราใช้มันจะเป็น Data Type ไหนบ้าง เพื่อให้ Linter ช่วยเราเวลาเราเขียน จะได้ลดโอกาสผิดด้วย หรือเราสามารถที่จะกำหนด Default Value ได้ที่นี่เลย

>>> point_1 = Coordinate(20,10)
>>> point_1.__dir__()

['x', 'y', '__module__', '__annotations__', '__dict__', '__weakref__', '__doc__', '__dataclass_params__', '__dataclass_fields__', '__init__', '__repr__', '__eq__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

การสร้าง Object ออกมาก็ไม่ยากเลย เพราะ dataclass เขาจัดการเรื่อง Constructor ให้เราเสร็จหมดแล้ว เราสามารถใส่ค่าตามลำดับของ Attribute ที่เรากำหนดไว้ได้เลย ทีนี้ เราอาจจะสงสัยว่า แล้วเวลาเราจะเรียกค่าออกมา หรือจะแก้ไข เราจะทำได้อย่างไร เราเลยลองเรียก Dunder Function อย่าง dir ออกมาให้ดู เราจะเห็นว่า มันจะมี x และ y ที่เรากำหนดให้เป็น Attribute อยู่ ทำให้เราสามารถเรียกมันขึ้นมา เพื่อเอาค่า หรือเราอาจจะใช้กับ Assign Operator เพื่อกำหนดค่าใหม่ได้ตรง ๆ เลย

point_1.x = 25

ตัวอย่างเช่น ถ้าเราต้องการ Update  x จากเดิม 20 เปลี่ยนเป็น 25 เราก็สามารถเรียกแบบด้านบนได้ตรง ๆ เลย ไม่ต้องกลัวเรื่อง Encapsulation เลย เรียกมันตรง ๆ นี่แหละ

>>> point_1
Coordinate(x=25, y=10)

ส่วน Default String Representation มันก็จะได้ออกมาเป็นแบบด้านบนเลยคือ เป็นชื่อ Class ตามด้วยวงเล็บ และก็จะมี Attribute อยู่ทั้งหมดเลย ง่าย ๆ แบบนี้แหละ ไม่ได้ซับซ้อนอะไรเท่าไหร่

Mutable or Immutable?

ถ้าเกิดว่า เราไม่อยากให้มีการแก้ไขข้อมูลเกิดขึ้นละ เราจะทำได้อย่างไร เพราะตอนนี้เราไม่ได้เขียนเองแล้ว เราเรียกใช้ของเขาตรง ๆ เลย แต่ไม่ต้องกลัวไป เพราะใน dataclass เขาก็คิดเรื่องนี้มาให้เราแล้วเหมือนกัน โดยการใส่ argument ที่ชื่อว่า frozen เข้าไปใน Decorator ด้วยเลย

@dataclass(frozen=True)
class ImmutablePoint :
    x: int
    y: int
 

จากตัวอย่างด้านบน เราทำการสร้าง Class ใหม่ที่ชื่อว่า ImmutablePoint ทำมาเหมือนกับ Class ก่อนหน้าทุกประการยกเว้น เราเติม Argument Frozen ลงไปเพื่อบอกให้ dataclass ไม่ให้ใครแก้ค่าอะไรเลย หลังจากที่เราทำการสร้าง Object ออกมาแล้ว

>>> point_2 = ImmutablePoint(10,20)
>>> point_2.x = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'x'

ดังนั้น เมื่อเราทดลอง Update ค่า x ใหม่ มันก็จะเด้ง Error ออกมาเลย บอกว่า มันไม่สามารถที่จะเอาค่าใส่ลงไปได้นะ ก็จะทำให้เราสามารถใช้เพื่อเก็บค่าอะไรบางอย่างที่เราไม่อยากให้มันหายไปนั่นเอง

แปลงร่างเป็น Dictionary ได้ด้วย

ข้อดีของพวก Class และ Object คือ เราสามารถแก้ไข และ จัดการได้ง่ายมาก ๆ เพราะมันมีการจัดการที่เป็นระเบียบ และ เราเห็นลักษณะของข้อมูลจาก Code ได้เลย แต่พอมาเป็นตัวอื่น ต้องยอมรับว่า พวก Library มันก็รองรับมากกว่าหลาย ๆ เท่าเลย ทำให้บางที เราก็อาจจะต้องมีการแปลงเป็นข้อมูลประเภทต่าง ๆ ซึ่งใน Python ก็คิดแล้วเหมือนกัน

>>> from dataclasses import asdict

>>> print(asdict(point_2))
{'x': 10, 'y': 20}

เขาเขียนคำสั่งสำหรับการแปลง dataclass ให้กลายเป็น Dictionary มาให้เราเลย เราไม่ต้องไปนั่งหาทำเขียนอะไรเองเลย เราก็เรียกเข้ามานี่แหละง่าย ๆ ถ้าเรามีเป็น List เลย เราก็อาจจะใช้ List Comprehension ช่วยก็ได้ จะได้สั้น ๆ หน่อยในการแปลงจาก dataclass เป็น Dictionary

นั่นทำให้เวลาเราจะเอาไปใช้งานจริง ๆ เรามองว่า มันทำให้เราง่ายขึ้นเยอะมาก ๆ เช่น เราบอกว่า เราจะเอาไปแปลงเป็น Pandas DataFrame มันก็ทำได้อาจจะต้องใช้ List Comprehension ช่วยเพื่อให้มันอยู่ใน Format ที่พร้อมสำหรับการแปลงเป็น DataFrame อีกที แต่ก็ดีกว่าไม่มีอะไรเลยฮ่า ๆ

Inheritance

@dataclass
class PlanePoint (Coordinate) :
    z: int

ด้วยความที่มันเป็น Class ปกติ ทำให้เรายังสามารถใช้สมบัติของการสืบทอด (Inheritance)  ได้อยู่ ซึ่งแน่นอนว่า เราสามารถทำมันได้อย่างง่ายดายเลย เพียงแค่เราบอกว่า เราจะ Inherit ลงมา แล้วที่เหลือ dataclass มันจะจัดการให้เราทั้งหมดเลย จากตัวอย่างด้านบน จะเห็นได้ว่า เราทำการสร้าง Class ใหม่ พร้อมกับ Inherit จาก Class Coordinate ที่เราสร้างไว้ก่อนหน้านี้

>>> plane_point_1 = PlanePoint(1,2,3)
>>> plane_point_1

PlanePoint(x=1, y=2, z=3)

การใช้งานก็ไม่ต่างจากเดิมเลย เพียงแค่ Attribute ของทั้งสายตระกูลมันก็จะมาอยู่ใน Object ให้เราเรียกใช้ได้เลย ทำให้การเขียนมันยิ่งดูสะอาด และ ง่ายเข้าไปอีกเยอะมาก ๆ

Adding Method

จากเดิม เรามี Class อยู่แล้ว พอมันเป็น Class ถามว่า เราเพิ่ม Method ได้มั้ยคำตอบคือได้ (ไม่งั้นจะมีหัวข้อนี้มั้ย ฮ่า ๆ) ตอนนี้เรามีจุด งั้นเรามาลองเพิ่มการคำนวณระยะดีกว่า

import math

@dataclass
class PlanePoint (Coordinate) :
    z: int
    
    def distance_to (self, point : PlanePoint) -> float :
        return math.sqrt((self.x - point.x)**2 + (self.y - point.y)**2 + (self.z - point.z)**2)

ถ้าจำกันได้ Eucadian distance เราก็เอา แต่ละแกนของทั้ง 2 จุดลบกัน ยกกำลัง 2 แล้วเอามาบวกกันหมด แล้วทั้งหมดก็ใส่ Square Root เราก็จะได้ระยะระหว่างจุดสองจุดออกมา ก็คือตามที่เรา Implement ด้านบนเลย

>>> plane_point_2 = PlanePoint(10,1,4)
>>> plane_point_1.distance_to(plane_point_2)

9.1104335791443

เพื่อการทดสอบ เราสร้างจุดใหม่ขึ้นมา เปลี่ยนพิกัดนิดหน่อย แล้วลองเอาจุดแรกเรียกหา Distance ไปที่จุดที่เราสร้างใหม่ เราก็จะเห็นได้เลยว่า เราสามารถเรียก Method ได้ตรง ๆ เหมือนกับ Object ทั่ว ๆ ไปเลย

สรุป

dataclass เป็น Class จาก Python เองที่ออกมา เพื่อให้เราสามารถเก็บข้อมูลในรูปแบบของ Class ได้เลย โดยที่เราไม่ต้องมานั่ง Implement ทุก ๆ ส่วนเองเหมือนกับ Class ปกติ ลดเวลาในการเขียน รวมไปถึงทำให้ Code ของเราดูสะอาดมากขึ้นอีกด้วย นอกจากนั้น ตัวมันเองยัง Implement Method สำหรับการแปลงไปเป็น Data Structure อย่าง Dictionary อีกด้วย ทำให้เมื่อเราต้องการจะไปทำงานบน Data Structure อื่น ๆ เราก็สามารถแปลงต่ออีกทอดได้ง่าย ๆ เลย ทั้งหมดนี่ มันเลย ทำให้เราทำงานได้เร็วขึ้น และง่ายขึ้นมาก ๆ ชอบ ๆ แนะนำให้ลองไปเอาใช้