Kodokon kodokon.com

OOP: คลาส, __init__, เมทอด และการสืบทอด

จัดโครงสร้างโค้ดของคุณด้วยคลาสที่ออกแบบมาอย่างดี: การเริ่มต้นค่า แอตทริบิวต์ของอินสแตนซ์หรือของคลาส การสืบทอดและข้อแลกเปลี่ยนของมัน

10 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

คุณทำงานกับอ็อบเจกต์ของ Python อยู่ตลอดเวลา ไม่ว่าจะเป็นสายอักขระ ลิสต์ หรือไฟล์ การเขียนคลาสของตัวเองคือการรวม สถานะ (แอตทริบิวต์) และ พฤติกรรม (เมทอด) ไว้เบื้องหลังอินเทอร์เฟซที่สอดคล้องกัน มีจุดหนึ่งเรื่องคำศัพท์ที่มักเข้าใจผิดกัน: __init__ ไม่ใช่คอนสตรักเตอร์ อ็อบเจกต์มีอยู่แล้ว (ถูกสร้างโดย __new__) ตั้งแต่ตอนที่ __init__ ได้รับ self หน้าที่ของมันคือ เริ่มต้นค่า ให้อินสแตนซ์ และ self ก็ไม่มีอะไรวิเศษเลย มันคือพารามิเตอร์ตัวแรกของทุกเมทอดของอินสแตนซ์ ซึ่ง Python ส่งให้อัตโนมัติตอนเรียกใช้

PYTHON
class Account:
    def __init__(self, owner, balance=0.0):
        self.owner = owner
        self.balance = balance

    def deposit(self, amount):
        if amount <= 0:
            raise ValueError("amount must be > 0")
        self.balance += amount
        return self.balance

    def __repr__(self):
        return f"Account({self.owner!r}, {self.balance})"

account = Account("Ada")
account.deposit(120.0)
print(account)
คลาสที่สมบูรณ์: __init__ เมทอดเชิงธุรกิจ และ __repr__

ความแตกต่างที่สำคัญยิ่งในงานจริง: แอตทริบิวต์ของคลาส ถูกประกาศไว้ในตัวคลาสและถูกใช้ร่วมกันโดยทุกอินสแตนซ์ ส่วน แอตทริบิวต์ของอินสแตนซ์ ถูกสร้างบน self โดยปกติภายใน __init__ เวลาอ่านค่า Python จะมองที่อินสแตนซ์ก่อน แล้วจึงย้อนไปที่คลาส และต่อไปยังคลาสแม่ การกำหนดค่า self.x = ... จะสร้างแอตทริบิวต์ของอินสแตนซ์เสมอ ซึ่งจะบดบังแอตทริบิวต์ของคลาสที่ชื่อเดียวกัน กับดักคลาสสิกในโลกจริงคือ แอตทริบิวต์ของคลาสที่ เปลี่ยนแปลงได้ (mutable)

PYTHON
class Basket:
    items = []

    def add(self, item):
        self.items.append(item)

a = Basket()
b = Basket()
a.add("apple")
print(b.items)  # ['apple']: shared list!
กับดักของแอตทริบิวต์คลาสที่เปลี่ยนแปลงได้

การสืบทอดสื่อถึงความสัมพันธ์แบบ "เป็นชนิดหนึ่งของ": Car เป็น Vehicle ชนิดหนึ่ง คลาสลูกสืบทอดเมทอดมา สามารถเขียนทับได้ และ super() ทำให้มันเรียกเวอร์ชันของคลาสแม่ได้ ซึ่งขาดไม่ได้ใน __init__ เพื่อไม่ให้ลัดวงจรการเริ่มต้นค่าของคลาสแม่ Python แก้ปัญหาการเรียกด้วยการไล่ตาม MRO (Method Resolution Order) อย่าหยิบการสืบทอดมาใช้แบบอัตโนมัติทุกครั้ง หากต้องการนำโค้ดกลับมาใช้ซ้ำโดยไม่มีความสัมพันธ์แบบ "เป็นชนิดหนึ่งของ" ให้เลือก การประกอบ (composition) แทน (อ็อบเจกต์หนึ่งถือครองอีกอ็อบเจกต์หนึ่งไว้) ลำดับชั้นที่ลึกนั้นเปราะบาง คลาสแม่แต่ละตัวจะกลายเป็น API โดยปริยายสำหรับลูกทั้งหมดของมัน

PYTHON
class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def describe(self):
        return self.brand

class Car(Vehicle):
    def __init__(self, brand, doors):
        super().__init__(brand)
        self.doors = doors

    def describe(self):
        base = super().describe()
        return f"{base}, {self.doors} doors"

print(Car("Aster", 5).describe())
การสืบทอดและ super(): ต่อยอดโดยไม่ต้องเขียนใหม่

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. บทบาทที่แท้จริงของ __init__ คืออะไร?
    • สร้างอ็อบเจกต์ขึ้นในหน่วยความจำ
    • เริ่มต้นค่าให้อินสแตนซ์ที่ถูกสร้างขึ้นแล้ว
    • ประกาศแอตทริบิวต์ของคลาส
    • ถูกเรียกทุกครั้งที่มีการเข้าถึงแอตทริบิวต์
  2. คลาสหนึ่งประกาศ items = [] ไว้ในตัวคลาส หลังจาก a.add(1) ซึ่งรัน self.items.append(1) อินสแตนซ์อีกตัวอย่าง b จะเห็นอะไร?
    • b.items เป็นลิสต์ว่าง
    • b.items มี 1 อยู่ข้างใน: ลิสต์ถูกใช้ร่วมกัน
    • Python โยน AttributeError
  3. ใน Car.__init__ การเรียก super().__init__(brand) มีจุดประสงค์อะไร?
    • สร้างอินสแตนซ์ Vehicle ตัวที่สอง
    • รันการเริ่มต้นค่าที่นิยามไว้ในคลาสแม่
    • คัดลอกเมทอดของ Vehicle เข้ามาใน Car
    • ตรวจสอบว่า Car สืบทอดจาก Vehicle จริง