Class และ Instance Variable บน Python มันต่างกันยังไง ?

ก่อนหน้านี้ที่เขียนเรื่องการ Clone Object ไป มา ๆ โผล่ ๆ ของตัวแปรต่าง ๆ ทำให้เรานึกถึงอีกพฤติกรรมนึงที่หลาย ๆ คนอาจจะคิดไม่ถึงบนภาษา Python โดยเฉพาะเมื่อเราเขียนโปรแกรมแบบ OOP บน Python เป็นเรื่องที่เมื่อก่อนเราเองก็ไม่รู้มาก่อนเลย พอมารู้ก็คือ ห่ะ เหรอ แบบนี้ก็ได้เหรอฟร๊ะ นั่นคือ Class Variable และ Instance Variable

ส่วนประกอบของ Class

โดยปกติตอนที่เราเรียน OOP กัน เราจะรู้ว่า ใน Class เราจะกำหนดของอยู่ 2 อย่างใหญ่ ๆ คือ Attribute และ Method (Constructor เป็น Method พิเศษตัวนึงเนอะ) ซึ่งตอนก่อนหน้าที่เราคุยเรื่องของ Decorator บน Class เราพูดถึง Method ไปแล้ว วันนี้เรามาโฟกัสที่ Attribute กัน

Attribute เป็นส่วนนึงของ Class ที่ใช้ในการบอกลักษณะต่าง ๆ ของ Class ตัวอย่างเช่น ถ้าเป็น รถ ก็จะเป็นจำนวนล้อ สี รุ่น ยี่ห้อ ต่าง ๆ ที่ใช้บ่งลักษณะของรถได้ ซึ่งโดยปกติแล้ว ส่วนของ Attribute เราจะทำการ Encapsulate เพื่อไม่ให้คนอื่นเข้าถึง หรือ แก้ไข Attribute ใน Object โดยตรง และ ข้อมูลใน Attribute จะเป็นของ Instance นั้น ๆ เลย ไม่ได้ใช้งานรวมกัน แต่เราอาจจะกำหนด Default ได้

การประกาศ Attribute ใน Python

from dataclass import dataclass

@dataclass
class Car :
    brand: str
    model: str
    colour: str
    no_wheel: int
    

เวลาเราจะประกาศ Attribute ใน Python เราสามารถทำได้หลายวิธีมาก ๆ เช่น Code ด้านบน เรากำหนดไว้ที่ด้านบนของ Class ได้เลย

class Car :
    def __init__ (brand, model, colour, no_wheel) :
        self.brand = brand
        self.model = model
        self.colour = colour
        self.no_wheel = no_wheel

อีกวิธีคือ เรากำหนดมันใน Constructor ก็ได้เหมือนกัน ในการใช้งานจริง มองเผิน ๆ การใช้งานไม่ต่างกันเลยนะ เราสามารถเข้าถึง และ แก้ไข Attribute ที่เรากำหนดได้หมดเลย หรือสนุกกว่านั้นอีก เราสามารถใช้ทั้ง 2 วิธีพร้อม ๆ กันเลยก็ได้นะ ถ้าสดชื่น จะเห็นได้เลยว่า Python เอง มันให้อิสระกับการเขียนของเรามาก เราจะเขียนแทบจะยังไงก็ได้แล้ว แต่จริง ๆ แล้วจะบอกว่า ไม่ว่าเราจะเขียนวิธีนี้ใน 2 วิธีนี้ มันมีความแตกต่างกันอยู่ด้วย

Instance Variable

class Car :
    def __init__ (brand, model, colour, no_wheel) :
        self.brand = brand
        self.model = model
        self.colour = colour
        self.no_wheel = no_wheel

มาเริ่มที่ตัวแรก ตัวที่เราน่าจะคุ้นเคยกันที่สุดก่อน เป็น Variable ที่เรากำหนดเป็น Attribute ภายใน Object เช่นตัวอย่างด้านบนนี้ เราจะลองเอามาสร้างเป็น Object กัน

car_a = Car('Honda', 'Civic', 'Red', 4)
car_b = Car('Honda', 'City', 'Red', 4)

เราเอา Class Car มาสร้างเป็น Object 2 ตัวด้วยกัน โดยที่เรากำหนด Attribute ที่ไม่เหมือนกันตัวนึงคือ Model แต่อันที่เราสนใจ จะเป็นตัวที่เหมือนกันดีกว่า เราลองมาเปลี่ยนสี car_b เป็น White ดูว่าจะเป็นยังไง

car_b.colour = 'White'
print('a', car_a.colour)
print('b', car_b.colour)
'Red'
'White'

เราจะเห็นเลยว่า ถ้าเราเปลี่ยนสีของ car_b ค่าสีของ car_a ไม่เปลี่ยนตามไปด้วย เพราะ car_a และ car_b เป็นคนละ Instance กันสิ่งที่เหมือนกันมีแค่มันสร้างมาจาก Class เดียวกันเท่านั้น ทำให้เราเรียก Variable ที่ผูกตาม Instance ว่า Instance Variable ตามชื่อเลย

Class Variable

from dataclass import dataclass

@dataclass
class Car :
    brand: str = 'Brand'
    model: str = 'Model'
    colour: str = 'Unknown'
    no_wheel: int = 4

มาลองดูอีกตัวคือ Class Variable ตัวอย่างแรกที่เรากำหนดไว้เลย คือเรากำหนดมันเอาไว้บน Class เลย ทำให้เราสามารถเข้าถึงได้ผ่าน Scope ภายใน Class ที่ย่อมลึกกว่านี้อยู่แล้ว เรามาลองอะไรสนุก ๆ กันดีกว่า เริ่มจากเราจะแก้ Code ด้านบนให้มันมี Default Value ก่อน

print(Car.no_wheel)
4

และ เราลองไม่ต้องเอามาสร้างเป็น Object เราจะเรียก Attribute ผ่าน Class เลย ปรากฏว่า เห้ย เรียกได้เฉยเลย ค่าออกตรงด้วย ไม่มี Warning หรือ Error ใด ๆ ทั้งสิ้น

from dataclass import dataclass

@dataclass
class Car :
    brand: str = 'Brand'
    model: str = 'Model'
    no_wheel: int = 4
    
    def __init__ (self, colour) :
        self.colour = colour

เพื่อให้เห็นพฤติกรรมที่แปลก เราขอสร้าง Instance Variable เพิ่มเข้าไปเพื่อให้เห็นความแตกต่างละกัน

car_a = Car('Red')
car_b = Car('White')

เราสร้าง Object จาก Class Car ทั้งคู่ โดยการกำหนดสีของรถให้แตกต่างกัน เป็นสีแดง และ สีขาว จากก่อนหน้า เราจะคิดว่า Variable ใน Class และ Object หรือ Attribute มันควรจะเป็นของใครของมันแยกเป็น Instance ไป งั้นเรามาลอง ทำอะไรสนุก ๆ ดีกว่า

Car.no_wheel = 6
print('a', car_a.no.wheel)
print('b', car_b.no.wheel)
'a', 6
'b', 6

เห๋ ทำไมละ ทำไมออก 6 ละ ทั้ง ๆ ที่เราแก้ ที่ Car และ Object เราสร้างออกมาก่อนหน้าที่เราจะกำหนด no_wheel ใหม่ซะอีก ทำไมมันไป Update ที่ car_a และ car_b ด้วยละ สาเหตุนั่นเป็นเพราะ ทั้ง 2 Object นี้ถูกสร้างมาจาก Class Car และ Variable ตัวนี้ มันถูกกำหนดอยู่ใน Class ทำให้จริง ๆ แล้ว ตอนที่มันถูกสร้างมาเป็น Object ค่ามันไม่ได้ถูก Clone มาด้วย แต่มันเป็นการชี้ไปหาตัว Car เอง  ทำให้ค่าที่มันเปลี่ยนตาม Class เราจะเรียกพวกนี้ว่า Class Variable

โดยสรุปคือ Class Variable จะแตกต่างจาก Instance Variable อยู่ 2 เรื่องใหญ่ ๆ คือ Class Variable ไม่จำเป็นต้องสร้างผ่าน Constructor มันถูกกำหนดตั้งแต่เราสร้าง Class แล้ว ทำให้เราสามารถเข้าถึง และ แก้ไข ตั้งแต่มันยังไม่เป็น Object ได้เลย และถึงแม้ว่า เราจะสร้างออกไปเป็น Instance แล้ว Python จะไม่ได้ Clone ค่าออกมาแล้ว Assign ให้แต่ละ Instance เลย แต่มันจะใช้เป็นการชี้กลับไปที่ Class ตรง ๆ เลย นั่นทำให้เมื่อเราเปลี่ยนที่ Class ทุก Instance ที่ถูกสร้างจาก Class นั้น ๆ ก็จะโดนเปลี่ยนไปด้วย

car_a.no_wheel = 4
print('a', car_a.no.wheel)
print('b', car_b.no.wheel)
'a', 4
'b', 6

แต่ ๆ ที่เราเล่ามาใน Class Variable มันจะเป็นจริงแค่ขาเดียวเท่านั้น ถ้าเราลองกลับกันละ แทนที่เราจะกำหนดที่ Car โดยตรง เราไปกำหนดที่ Instance นั้น ๆ เลยละ มันจะเกิดอะไรขึ้น สิ่งที่มันเป็นคือ มันก็จะเปลี่ยนแค่ที่ Instance นั้น ๆ ที่เดียวเลย ไม่ได้เปลี่ยนทั้ง Car นั่นเป็นสาเหตุที่เวลาเราเขียน Class หลาย ๆ ครั้ง เราจะพบว่าการเขียนด้วย 2 วิธี เราจะเขียนแบบไหนก็ได้ เพราะส่วนใหญ่แล้ว เวลาเรากำหนด Attribute ไว้ด้านบน และ มาเติม ค่าผ่าน Constructor อยู่ดี ทำให้ค่ามันถูก Update ไป ก็จะเหมือนตัวอย่างที่เรายกให้ดูอันนี้นั่นเอง

ทำไมมันต้องมาแยก มันมีประโยชน์อะไร ?

ถ้าอ่านผ่าน ๆ อาจจะมองว่า อะไรของแกฟร๊ะ รู้ไปแล้วมันได้ประโยชน์อะไร สุดท้ายแล้ว เราก็แยกออกมาเป็น Instance อยู่ดี แต่เรามองว่า เราสามารถใช้ความสามารถนี้ในลดความซ้ำซ้อนของ Code เราได้เป็นอย่างดีเลยนะ ยิ่งถ้าเรามานั่งดูส่วนไหนที่เชื่อมกัน ส่วนไหนที่ไม่เชื่อมกัน แล้วจัดระเบียบมันตามการใช้งาน มันลดจำนวน Code ที่เราต้องเขียนมหาศาลมาก ๆ ตาม Concept DRY (Don't Repeat Yourselves) ที่เขาแนะนำว่า เราควรจะลดการเขียนส่วนที่ซ้ำกันด้วยการทำ Abstraction และ Data Normailsation