ทำความเข้าใจพื้นฐานคลาสข้อมูลใน 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 โดยตรง
หรือรับโพสต์ทั้งหมดของฉันในกล่องจดหมายของคุณทำที่นี่!