Tutorial

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

By Arnon Puitrakul - 14 กันยายน 2021

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

Read Next...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...

Humanise the Number in Python with "Humanize"

Humanise the Number in Python with "Humanize"

หลายวันก่อน เราทำงานแล้วเราต้องการทำงานกับตัวเลขเพื่อให้มันอ่านได้ง่ายขึ้น จะมานั่งเขียนเองก็เสียเวลา เลยไปนั่งหา Library มาใช้ จนไปเจอ Humanize วันนี้เลยจะเอามาเล่าให้อ่านกันว่า มันทำอะไรได้ แล้วมันล่นเวลาการทำงานของเราได้ยังไง...

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

การทำงานกับตัวเลขทศนิยมบนคอมพิวเตอร์มันมีความลับซ่อนอยู่ เราอาจจะเคยเจอเคสที่ เอา 0.3 + 0.6 แล้วมันได้ 0.899 ซ้ำไปเรื่อย ๆ ไม่ได้ 0.9 เพราะคอมพิวเตอร์ไม่ได้มองระบบทศนิยมเหมือนกับคนนั่นเอง บางตัวมันไม่สามารถเก็บได้ เลยจำเป็นจะต้องประมาณเอา เราเลยเรียกว่า Floating Point Approximation...