ทำความเข้าใจพื้นฐานคลาสข้อมูลใน Python อย่างครบถ้วน

หากคุณต้องการสัมผัสประสบการณ์ Medium ด้วยตัวเอง ลองสนับสนุนฉันและนักเขียนคนอื่นๆ อีกหลายพันคนโดย สมัครสมาชิก มีค่าใช้จ่ายเพียง $5 ต่อเดือน ซึ่งสนับสนุนพวกเรา นักเขียน อย่างมาก และคุณสามารถเข้าถึงเรื่องราวที่น่าทึ่งทั้งหมดบน Medium

คลาสข้อมูลคืออะไร?

คลาสข้อมูลเป็นคุณสมบัติใหม่ที่มีใน Python ในตัวตั้งแต่เวอร์ชัน 3.7 พวกมันจัดเตรียมเครื่องมือตกแต่งและฟังก์ชันเพื่อสร้างคลาสที่ง่ายกว่า สะดวกกว่า และปลอดภัยกว่า ซึ่งส่วนใหญ่ใช้ในการประมวลผลข้อมูล จึงเป็นที่มาของชื่อ

ประโยชน์หลักประการหนึ่งของคลาสข้อมูลคือสร้างวิธีการพิเศษหลายวิธีให้คุณโดยอัตโนมัติ เช่น __init__, __repr__ และ __eq__ วิธีนี้ช่วยให้คุณประหยัดเวลาได้มากและโค้ดที่ซ้ำซ้อนเมื่อกำหนดคลาสที่ใช้ในการประมวลผลข้อมูลเป็นหลัก

ประโยชน์หลักอีกประการหนึ่งของคลาสข้อมูลคือการใช้การพิมพ์ที่รัดกุม ซึ่งช่วยให้มั่นใจว่ามีการกำหนดคุณลักษณะของอินสแตนซ์ ซึ่งสามารถทำได้โดยการใช้คำอธิบายประกอบประเภท ซึ่งช่วยให้คุณสามารถระบุประเภทของแต่ละแอ็ตทริบิวต์เมื่อกำหนดคลาส วิธีนี้สามารถป้องกันข้อบกพร่องเนื่องจากความคลุมเครือของประเภท และยังช่วยให้นักพัฒนารายอื่นจับโค้ดได้ง่ายขึ้น

วิธีใช้คลาสข้อมูล

หากต้องการใช้คลาสข้อมูล คุณต้องนำเข้าตัวตกแต่งคลาสข้อมูลจากโมดูล dataclasses ก่อน มัณฑนากรนี้รวมอยู่ใน Python 3.7 และสูงกว่าโดยกำเนิด

การใช้คลาสข้อมูลนั้นง่ายมาก เพียงตกแต่งคำจำกัดความของคลาสของคุณด้วย @dataclass มัณฑนากร เพื่อกำหนดคลาสข้อมูล

นี่คือตัวอย่างของคลาสข้อมูลอย่างง่ายที่มีพารามิเตอร์เริ่มต้น:

from dataclasses import dataclass, field

@dataclass
class Point:
    x: float
    y: float

p = Point(1.0, 2.0)
print(p)  # Output: Point(x=1.0, y=2.0)

เราได้กำหนดคลาสข้อมูล Point ด้วยสองฟิลด์ x และ y ซึ่งทั้งสองฟิลด์เป็นแบบลอยตัว เมื่อคุณสร้างอินสแตนซ์ของคลาส Point คุณสามารถระบุค่าสำหรับ x และ y เป็นอาร์กิวเมนต์ให้กับ Constructor ได้

ตามค่าเริ่มต้น คลาสข้อมูลจะสร้างเมธอด __init__ ให้คุณซึ่งรับฟิลด์ของคลาสเป็นอาร์กิวเมนต์ พวกเขายังจะสร้างเมธอด __repr__ ที่ส่งคืนการแสดงสตริงของออบเจ็กต์ ซึ่งเป็นสิ่งที่พิมพ์เมื่อคุณเรียก print(p) ในตัวอย่างด้านบน

คุณสามารถปรับแต่งเมธอดที่สร้างโดยมัณฑนากรได้โดยส่งอาร์กิวเมนต์เพิ่มเติม เช่น repr=False เพื่อปิดใช้งานเมธอด __repr__

ฟังก์ชันภาคสนาม

คุณยังสามารถระบุค่าเริ่มต้นสำหรับฟิลด์ของคลาสข้อมูลได้โดยใช้อาร์กิวเมนต์ default และ default_factory กับฟังก์ชัน field()

ตัวอย่างเช่น:

@dataclass
class Point:
    x: float = 0.0
    y: float = field(default=0.0)

p1 = Point(x=1.0, y=2.0)
print(p1)  # Output: Point(x=1.0, y=2.0)

p2 = Point(1.0)
print(p2)  # Output: Point(x=1.0, y=0.0)

ในตัวอย่างนี้ เราได้กำหนดคลาส Point โดยมีสองฟิลด์ คือ x และ y ฟิลด์ x มีค่าเริ่มต้น 0.0 ที่ระบุโดยตรงในคำจำกัดความของฟิลด์ ในขณะที่ฟิลด์ y มีค่าเริ่มต้น 0.0 ที่ระบุโดยใช้ฟังก์ชัน field()

เมื่อเราสร้างอินสแตนซ์ของคลาส Point เราสามารถระบุค่าสำหรับ x และ y เป็นอาร์กิวเมนต์ให้กับ Constructor ได้ หากเราไม่ระบุค่าสำหรับ y ระบบจะใช้ค่าเริ่มต้นที่ 0.0

ภายในคลาสข้อมูล การใช้งานทั้งสองเพื่อระบุค่าเริ่มต้นจะเทียบเท่ากับแอตทริบิวต์ ไม่เปลี่ยนรูป

อย่างไรก็ตาม ฟังก์ชัน field() ช่วยให้มีความยืดหยุ่นและมีตัวเลือกเพิ่มเติมในการกำหนดแอตทริบิวต์

การทำซ้ำฟิลด์และอาร์กิวเมนต์เริ่มต้น

อันที่จริง ฟังก์ชัน field() มีอาร์กิวเมนต์เริ่มต้น __repr__ และ __init__ ตั้งค่าเป็น True เราสามารถตั้งค่าเหล่านี้เป็น False เพื่อแก้ไขพฤติกรรมได้

@dataclass
class Point:
    x: float = field(default=0.0, repr=False)
    y: float = field(default=0.0, init=False)

p1 = Point(3.0) 
print(p1) # Output: Point(y=0.0)
p2 = Point(3.0, 2.0) # TypeError: Point.__init__() takes from 1 to 2 positional arguments but 3 were given

ในตัวอย่างแรก อินสแตนซ์ p1 ไม่แสดงค่า x ในการแสดงสตริง ซึ่งมีประโยชน์สำหรับการซ่อนตัวแปรชั่วคราวหรือตัวแปรที่มีความละเอียดอ่อน (อย่างน้อยก็จากการแสดงสตริง)

ในตัวอย่างที่สอง ไม่สามารถสร้าง instancep2 ได้เนื่องจากอาร์กิวเมนต์ init ถูกตั้งค่าเป็น False และเรากำลังพยายามเริ่มต้นตัวแปร y สิ่งนี้มีประโยชน์สำหรับตัวแปรที่ควรส่งคืนโดยเมธอดเท่านั้น มาเพิ่มวิธีการให้กับคลาสข้อมูลของเราที่ใช้เพียง y เป็นผลลัพธ์ของการคำนวณในฟังก์ชัน compute_y_with_x :

@dataclass
class Point:
    x: float = field(default=0.0, repr=False)
    y: float = field(default=0.0, init=False)
    def compute_y_with_x(self):
        self.y = self.x ** 2

p2 = Point(x=2.0)
p2.compute_y_with_x()
print(p2) # Output: Point(y=4.0)

ที่นี่ เราสังเกตเห็นว่า p2 ใช้ y เป็นตัวแปรที่ไม่ได้กำหนดค่าเริ่มต้น (init=False) ซึ่งเป็นผลมาจากการแปลงตัวแปรอินพุตของเรา x เราไม่สนใจเกี่ยวกับ x หลังจากการกำหนดค่าเริ่มต้น ดังนั้นเราจึงลบการแสดงสตริง (repr=False)

ฟิลด์ default_factory

คุณจำได้ไหมเมื่อเราคุยกันว่าการใช้อาร์กิวเมนต์เริ่มต้นของคลาสหรือการตั้งค่าฟิลด์ด้วยอาร์กิวเมนต์เริ่มต้นจะมีผลเช่นเดียวกันกับอ็อบเจ็กต์ที่ไม่เปลี่ยนรูป แต่ไม่ใช่กับอ็อบเจ็กต์ที่ ไม่แน่นอน

มาดูตัวอย่างที่เราพยายามเริ่มต้นแอตทริบิวต์ให้กับวัตถุที่ ไม่แน่นอน เช่น รายการ นอกจากนี้เรายังจะสร้างวิธีการที่ช่วยให้เราสามารถผนวกองค์ประกอบเข้ากับรายการที่เรียกว่า add_a_dimension:

@dataclass
class Points:
    coord: list = field(default=[])
    def add_a_dimension(self, element):
        self.coord.append(element)
# Output: ValueError

ไม่สามารถสร้างคลาสนี้ได้เนื่องจากตามที่ระบุไว้ “ไม่อนุญาตให้ใช้ค่าเริ่มต้นที่ไม่แน่นอน (เช่น รายการ) สำหรับฟิลด์ ‹coord›” นี่คือมาตรการรักษาความปลอดภัยที่เพิ่มโดยคลาสข้อมูล ซึ่งมีประโยชน์มากในการป้องกันข้อบกพร่องที่เราอาจพบกับคลาสปกติ

แท้จริงแล้ว หากเราต้องการกำหนด Points Data Class โดยใช้คลาสปกติ มันจะเทียบเท่ากับ:

class Points:
    coord = []
    def __init__(self, coord=coord):
        self.coord = coord
    def add_a_dimension(self, element):
        self.coord.append(element)

และเราจะไม่มีข้อผิดพลาดใดๆ เมื่อกำหนดคลาสนี้! อย่างไรก็ตาม หากเรากำหนดคลาสด้วยวิธีนี้โดยใช้คลาสปกติ เราจะเห็นพฤติกรรมที่ไม่ได้ตั้งใจ:

p1 = Points()
p2 = Points()
p1.coord, p2.coord # Output: ([],[])
p1.add_a_dimension(3)

ตกลง เราสร้างอินสแตนซ์รายการว่างและเพิ่ม 3 รายการให้กับอินสแตนซ์ p1 คุณคิดว่าค่าของ p1.coord และ p2.coord จะเป็นเท่าใด

p1.coord, p2.coord # Output: ([3], [3])

เหลือเชื่อ อินสแตนซ์ p2 ก็ได้รับผลกระทบจากการผนวก 3 รายการเข้ากับรายการด้วย

เนื่องจากใน Python เมื่อคุณสร้างอินสแตนซ์ของคลาส อินสแตนซ์นั้นจะแชร์ สำเนา เดียวกันของแอตทริบิวต์คลาส เนื่องจากรายการ ไม่แน่นอน ทั้ง p1 และ p2 จะใช้สำเนาเดียวกันของรายการ coord

หากต้องการใช้คลาสนี้อย่างถูกต้องเพื่อหลีกเลี่ยงผลลัพธ์ที่ไม่ได้ตั้งใจ Data Classes จะจัดเตรียมอาร์กิวเมนต์สำหรับ field() ที่เรียกว่า default_factory

อาร์กิวเมนต์นี้ช่วยให้คุณสามารถระบุฟังก์ชันที่จะถูกเรียกเพื่อสร้างค่าเริ่มต้นใหม่สำหรับฟิลด์ แต่ละครั้ง อินสแตนซ์ใหม่ของคลาสจะถูกสร้างขึ้น เพื่อให้แน่ใจว่าแต่ละอินสแตนซ์ของคลาสมี สำเนาเฉพาะของตัวเอง ของฟิลด์ แทนที่จะแชร์ออบเจ็กต์ ไม่แน่นอน เดียวกัน

@dataclass
class Points:
    coord: list = field(default_factory=lambda: [])
    def add_a_dimension(self, element):
        self.coord.append(element)
p1 = Points()
p2 = Points()
p1.coord, p2.coord # Output ([], [])

เป็นที่น่าสังเกตว่าไม่มี ValueError แม้ว่าเราจะกำหนดรายการที่ไม่แน่นอนเป็นค่าเริ่มต้นสำหรับ coord ก็ตาม

มาตรวจสอบผลลัพธ์หลังจากเรียกใช้เมธอด add_a_dimension บนอินสแตนซ์เดียวเท่านั้น:

p1.add_a_dimension(3)
p1.coord, p2.coord # Output ([3], [])

อ่า ในที่สุดเราก็ได้ผลลัพธ์ตามที่ต้องการ! อินสแตนซ์ p2 ไม่ได้ถูกเปลี่ยนแปลงโดยวิธีการที่เรียกใช้โดยอินสแตนซ์ p1 เนื่องจากแต่ละอินสแตนซ์มี สำเนาเฉพาะของตัวเอง ของฟิลด์ coord

ดังที่เราเห็น Data Classes ให้ความปลอดภัยโดยการจัดการอ็อบเจ็กต์ที่ไม่แน่นอนและไม่เปลี่ยนรูปแบบอย่างถูกต้อง

มรดก

จุดสุดท้ายที่ต้องพิจารณาคือวิธีที่ Data Classes ทำงานกับการสืบทอด ตามค่าเริ่มต้น คลาสข้อมูลไม่มีเมธอด __init__ ดังนั้นจึงจำเป็นต้องมีเมธอดที่อนุญาตให้คุณเขียนทับแอตทริบิวต์ที่สืบทอดมาได้ วิธีการนี้เรียกว่า __post_init__ และถูกเรียกหลังจากอินสแตนซ์ได้รับการเตรียมใช้งานแล้ว

นี่คือตัวอย่างเพื่อช่วยอธิบายแนวคิดนี้:

@dataclass
class Point:
    x: float = field(default=0.0)
    y: float = field(default=0.0)
    
    def __post_init__(self):
        self.x = self.x ** 2
        self.y = self.y ** 2

@dataclass
class ColoredPoint(Point):
    color: str = field(default='black')
    
    def __post_init__(self):
        self.color = self.color.upper()

ในตัวอย่างนี้ คลาส Point มีเมธอด __post_init__ ที่จะยกกำลังสองค่าของ x และ y คลาส ColoredPoint สืบทอดมาจาก Point และยังมีเมธอด __post_init__ ของตัวเองที่เพิ่มค่าของแอตทริบิวต์ color ให้เป็นตัวพิมพ์ใหญ่

มาสร้างอินสแตนซ์ของ Point:

p0 = Point(2.0,2.0)
print(p0) # Output: Point(x=4.0, 4.0)

เราสังเกตเห็นจากเอาต์พุตว่ามีการเรียกใช้เมธอด __post_init__ และทั้งค่าของ x และ y นั้นเป็นกำลังสอง

ตอนนี้เรามาสร้างอินสแตนซ์ของ ColoredPoint:

p1 = ColoredPoint(2.0, 2.0, 'red')
print(p1) # Output: ColoredPoint(x=2.0, y=2.0, color='RED')

เมื่อสร้างอินสแตนซ์ของ ColoredPoint ระบบจะเรียกใช้เมธอด __post_init__ และค่าของ color จะเป็นตัวพิมพ์ใหญ่ แต่ค่าของ x และ y จะไม่ถูกยกกำลังสอง คุณรู้ไหมว่าทำไม

เนื่องจากไม่ได้เรียกเมธอด __post_init__ ของ Point ในเมธอด __post_init__ ของ ColoredPoint!

หากต้องการเรียกใช้เมธอด __post_init__ ของคลาสพื้นฐาน คุณสามารถใช้ฟังก์ชัน super() ซึ่งจะส่งคืนการอ้างอิงไปยังคลาสฐาน นี่คือเวอร์ชันที่แก้ไขแล้วของ ColoredPoint:

@dataclass
class ColoredPoint(Point):
    color: str = field(default='red')

    def __post_init__(self):
        super().__post_init__()
        self.color = self.color.upper()
p2 = ColoredPoint(2.0, 2.0, 'red')
print(p2) # Output: ColoredPoint(x=4.0, y=4.0, color='RED')

ด้วยการเปลี่ยนแปลงนี้ เมธอด __post_init__ ของคลาส Point จะถูกเรียกเมื่อมีการสร้างอินสแตนซ์ของ ColoredPoint และค่าของ x และ y จะถูกยกกำลังสองตามที่คาดไว้

ฉันดีใจที่สามารถช่วยให้คุณเรียนรู้เกี่ยวกับคลาสข้อมูลได้ ดังที่คุณเห็นแล้วว่า Data Classes มีเวอร์ชันปรับปรุงของคลาสปกติที่ปลอดภัยกว่าและเน้นการพิมพ์ที่แข็งแกร่ง ฉันหวังว่าคุณจะมั่นใจมากขึ้นเกี่ยวกับความสามารถในการใช้คลาสข้อมูลในโครงการในอนาคต

โดยไม่มีค่าใช้จ่ายเพิ่มเติม คุณสามารถสมัครสมาชิกสื่อผ่านลิงก์ผู้แนะนำของฉันได้



«เข้าร่วม Medium ด้วยลิงก์ผู้แนะนำของฉัน — Arli
อ่านเรื่องราวทุกเรื่องจาก Arli และนักเขียนคนอื่นๆ อีกหลายพันคนบน Medium ค่าสมาชิกของคุณสนับสนุน Arli และ...medium.com โดยตรง



หรือรับโพสต์ทั้งหมดของฉันในกล่องจดหมายของคุณทำที่นี่!