Tutorial

Decorator กับ OOP บน Python

By Arnon Puitrakul - 03 กันยายน 2021 - 2 min read min(s)

Decorator กับ OOP บน Python

และเราก็ยังไม่ Move on จาก Decorator กันซะที วันนี้จะเป็นบทความสุดท้ายในชุด Decorator กันละ แต่บอกเลยว่า มันเป็นปัญหาที่เราว่า หลาย ๆ คนที่เขียน OOP ใน Python จะเจอ โดยเฉพาะเมื่อเราเคยเขียนโปรแกรมแบบ OOP ในภาษาอื่น ๆ มาเช่น Java ที่มันมีข้อบังคับเยอะมาก ๆ พอมาเขียน Python ก็คือ ต้องร้องว่าอิหยังว้าาา เพราะหลาย ๆ อย่างมันไม่บังคับหมดเลย อิสระมาก ๆ จนอาจทำให้มันเกิดปัญหาในการทำงานได้ วันนี้เราเลยจะมาแก้ปัญหา โดยการสร้าง Constrain ต่าง ๆ ที่จำเป็นกันด้วยการใช้ Decorator ใน Python กัน

Basic Class in Python

สำหรับคนที่ยังไม่คุ้นเคยกับการเขียนโปรแกรมแบบ OOP บน Python เราจะเขียนตัวอย่างของ Class คร่าว ๆ เพื่อเป็นพื้นฐานก่อน

class Student :
	def __init__ (self) :
    	self.name = 'Unknown'
    
    def get_name (self) :
    	return self.name

Code ด้านบนสิ่งที่เราทำคือ เราสร้าง Class ที่ชื่อว่า Student ขึ้นมา ใน Python Class เราจะสร้างออกมาเป็น Block นึงเลย ด้านใน ก็จะเป็น Method ต่าง ๆ ที่เวลาเราสร้าง เราจะใช้ลักษณะเดียวกับการสร้าง Function ทั่ว ๆ ไปใน Python เลย แต่ใน Argument เราจะต้องมีตัวมันเองเป็น Argument แรกเสมอ เพื่อให้เราสามารถเข้าถึง Instance ได้นั่นเอง ใน Method แรก เราจะเรียกว่า Constructor เป็น Method ที่จะถูกเรียกเมื่อ Class ถูกนำมาสร้างเป็น Object และ get_name เป็นอีก Method ธรรมดาที่เราสร้างขึ้นเพิ่มคิดซะว่า เป็น Function ที่เราเรียกได้ทั่ว ๆ ไป จากใน Object ได้เลย ถ้าสนใจพวก Concept ภายใน เดี๋ยวจะเปิด Course แยกให้เลย บอกเลยว่าโคตรสนุก รอติดตามได้

ตัว Python เองเขามี Standard Decorator ให้เรามาใช้เพื่อจัดการกับการเขียนโปรแกรมแบบ OOP ต่าง ๆ อยู่แล้วละ วันนี้เราจะมาเล่ากัน 3 ตัวหลัก ๆ ที่เราน่าจะได้พบเจอเวลาเราไปอ่าน Code คนอื่น คือ property, static_method และ class_method

@property

ตัวแรกสุด คือ @property เป็นตัวที่เรียกได้ว่า ใช้บ่อยมาก ๆ มันเอาไว้จัดการพวกสมบัติการทำ Encapsulation ได้เป็นอย่างดีเลยเชียว โดยปกติแล้ว ถ้าเราเขียน Attribute ขึ้นมา ตัว Python มันจะอนุญาติให้เราแก้ไข Attribute ได้เลยตรง ๆ ซึ่งมันจะขัดกับสมบัติข้อหนึ่งของ OOP อย่างรุนแรง และ เราไม่ก็ชอบเท่าไหร่ เพราะมันสร้างความชิบหายให้เรามาแล้วเช่นกัน

class Student :
	def __init__ (self, name:str):
        self.name = name
 
stu_a = Student('Thomas')
stu_a.name = "Robert"

ด้านบน เราจะเห็นได้เลยว่า Attribute name ของเราสามารถถูกแก้ไขได้ตรง ๆ เลย ไม่ใช่เรื่องดีแน่นอน เพราะเราต้องไปไล่ Validate ค่านี้ด้วย Code อีกมหาศาลมาก ๆ การทำแบบนี้คือ โคตรบาป !! อย่า หา ทำ เด็ดขาด

class Student :
	def __init__ (self, name:str):
        self.__name = name
 
stu_a = Student('Thomas')
stu_a.__name = "Robert"

ถ้าเราเข้าไปอ่านมาหลาย ๆ คนจะบอกว่าให้ใส่ Underscore 2 ตัวไว้ที่ด้านหน้าของ Attribute แต่มันก็เป็นแค่ Naming Convention เท่านั้น Python หาได้แคร์ กับสิ่งนี้ไม่ มันแค่ทำให้เรารู้ว่า อย่าแหยมไปเรียกเท่านั้นถึง ถ้าแหกกฏไปมันก็ทำได้อยู่ แต่เราก็ไม่อยากให้มันทำได้ เราจะต้องใช้ข้อบังคับอะไรบางอย่างในการจัดการเรื่องนี้ สิ่งนั้นก็คือ @property นั่นเอง

class Student :
    def __init__ (self, name:str):
        self.__name = name
    
    @property
    def name (self) :
        print('Name Getter')
        return self.name
    
    @name.setter
    def name (self, name:str) :
        print('Name Setter')
        self.__name = name

นึกถึงเวลาเราเขียน OOP ปกติเลย เราจะเขียนพวก Getter และ Setter เอาไว้ดึงค่า และ กำหนดค่าของ Attribute ใน @property เราจะ Decorate เข้าไปที่ getter funtion ของเราได้เลย และ ส่วนของ Setter เราจะสร้างเป็นอีก Method ขึ้นมาชื่อเดียวกันเลย และ Decorate ด้วย ชื่อ property ที่เราตั้งตามด้วย setter เราก็จะได้ Setter Method ออกมาใช้งานได้เลย

stu_a = Student('Thomas')
stu_a.name = "Robert"
print(stu_a.name)
Name Setter
Name Getter
Robert

อ่านดูแล้ว อาจจะ งง ว่า แล้วมันต่างจากเดิมอย่างไร คำตอบมันอยู่ที่ Output ด้านบนนี้เลย ก่อนหน้านี้ ถ้าเราไม่ได้วาง Constrain อะไรให้มันเลย เราจะเข้าถึง Attribute ได้ตรง ๆ เลย ทำให้เราไม่สามารถ Validate หรือ Pre-Process อะไรก่อนที่จะเอาค่าเข้า และ ออกจาก Object เราได้เลย แต่สังเกตว่า ตอนที่พยายามกำหนดค่าให้ name เราเขียนเท่ากับปกติเลย ดูเหมือนว่า เราจะเข้าถึง Attribute ตรง ๆ เลย แต่ถ้าเราดูจาก Output จะรู้เลยว่า ไม่ใช่แบบนั้นเลย ใน Class เรา Debug ไว้ให้ดู ใน Getter และ Setter เราใส่ print() ไว้ให้ดูด้วย แปลว่า จริง ๆ แล้ว Python มันเข้าไปทำงานจาก Function ที่เรากำหนดไว้นั่นเอง ไม่ได้เข้าถึง และกำหนดค่าให้กับ Attribute ใหม่ตรง ๆ ดังนั้น ถ้าเราต้องการเขียน Logic เพื่อกำหนด Attribute เราก็แค่เขียนไว้ใน Getter หรือ Setter Function ก็ใช้ได้แล้ว และ ก็ไม่ต้องเปลี่ยนรูปแบบการเขียนเพื่อเรียก และ กำหนดอีกด้วย

class Student :
    def __init__ (self, name:str, surname:str):
        self.__name = name
        self.__surname = surname
    
    @property
    def name (self) :
        print('Name Getter')
        return self.name
    
    @name.setter
    def name (self, name:str) :
        print('Name Setter')
        self.__name = name
    
    @property
    def full_name (self) :
        return self.__name + ' ' + self.__surname

a = Student('John', 'Anderson')
print(a.full_name)
John Anderson

ตัวอย่างด้านบน เราจะเห็นได้ว่า เราเพิ่ม Property ชื่อว่า full_name ขึ้นมา แต่จริง ๆ แล้วเราไม่ได้กำหนดเป็น Property จริง ๆ ใน Class เลย แต่เราสามารถใช้ความเป็น Getter กำหนดมันขึ้นมาได้เช่นกัน แต่มันแค่จะแปลก ๆ หน่อยเท่านั้นเอง

staticmethod

Decorator ตัวต่อไปคือ staticmethod จริง ๆ มันก็คือการกำหนด Static Method หรือ Method ที่เราสามารถเรียกได้ตรง ๆ เลยโดยที่เราไม่ต้องเอา Class มาสร้างเป็น Object เลย ซึ่งแน่นอนว่า ภายใน เราจะไม่ควรเรียกค่า หรือของต่าง ๆ ที่ไม่ได้อยู่ใน Instance ของ Object ทั้งนั้น ไม่งั้นเราก็จะเจอกับ Error นั่นเอง ทำให้จริง ๆ แล้ว Static Method มันจะเป็น เหมือน Function ตัวนึงที่เราวางไว้นอก Class เลย แต่สาเหตุที่บางครั้งเราจะต้องใช้ Static Method เพื่อให้เวลาเราเรียกใช้มันดู Make Sense มากกว่า ทำให้เราอ่านแล้วเข้าใจ Code ได้ง่ายขึ้น

from datetime import date

class Student :
    def __init__ (self, name:str, surname:str, birth_year:int):
        self.__name = name
        self.__surname = surname
        self.__birth_year = birth_year
    
    @property
    def name (self) :
        return self.name
    
    @property
    def birth_year (self) :
        return self.__birth_year
    
    @name.setter
    def name (self, name:str) :
        self.__name = name
    
    @staticmethod
    def cal_age (birth_year) :
    	return date.today().year - birth_year

a = Student('John', 'Anderson', 1997)
print(Student.cal_age(a.birth_year)

ตัวอย่างนี้จริง ๆ เราว่ามันยังไม่ทำให้เห็นภาพชัดเท่าไหร่ถึง Static Method แต่ จากที่เราเห็น เราทำการสร้าง cal_age พร้อมกับใส่ @staticmethod เพื่อบอกให้รู้ว่ามันคือ Static Method และเราเพิ่ม birth_year หรือปีเกิดเข้าไปเป็นอีกหนึ่ง Attribute และใส่ Getter และ Setter ให้กับมัน ทำให้เราสามารถเรียก ปีเกิดออกมาได้ และ ถ้าเราอยากรู้อายุ เราก็สามารถเรียก Static Method สำหรับการคำนวณอายุที่เราสร้างเอาไว้ได้ ไม่ต้องเก็บไว้ใน Instance เพราะมันก็แอบเปลืองเนอะ บางอย่างเราคำนวณเองได้ง่าย ๆ เราก็ไม่ต้องเก็บก็ได้ สาเหตุที่เราบอกว่า ตัวอย่างนี้มันไม่ค่อยชัดเจน เพราะ cal_age() จริง ๆ ชื่อมันชัดเจนในตัวของมันเองแล้ว ทำให้ มันเลยไม่มีเหตุผลอะไรที่เราจะเอามันมาใส่เป็น Static Method ใน Class เลย สรุปคือ Static Method เราจะใช้เพื่อทำให้มันดู Make Sense ในที่ ๆ มันอยู่นั่นเอง ไม่งั้น ชื่อเราจะต้องตั้งซ้ำ ๆ ใกล้ ๆ กัน กำกวมไป เวลาใช้จะลำบาก การอยู่ใน Class จะทำให้เข้าใจง่ายกว่าเยอะ

classmethod

ก่อนที่เราจะมาพูดถึง classmethod เราขอพูดถึง Method อีกประเภทที่เราจะได้เจอในการเขียนโปรแกรมคือ Instance Method สั้น ๆ คือมันเป็น Method ที่จะถูกเรียกผ่าน Object ทำให้เราสามารถเข้าถึงพวก Instance Method อื่น ๆ และ Attribute ที่อยู่ใน Object ก้อนนั้น ๆ ได้ ตอนเราเขียน Python เราก็จะเรียกผ่าน Syntax ที่ชื่อว่า self นั่นเอง

แต่ Class Method เป็น Method อีกประเภท ที่ทำงานตรงตามชื่อเลยคือ มันจะทำงานผ่านการเรียกที่ Class เท่านั้น นั่นหมายความว่า เวลาเราจะเรียกใช้งานเราจะต้องใช้ Class เป็น Argument ด้วย ซึ่งปกติแล้วตาม Convention ของการเขียนโปรแกรม เราจะใช้ชื่อว่า cls กัน ส่วนใน Python เราจะเติม Decorator ที่ชื่อว่า classmethod ลงไปด้วย

Use Case ที่ทำให้เราตัดสินใจใช้งาน classmethod ซะเยอะคือ การทำ Class Factory หมายความว่า เราจะพยายามสร้างหลาย ๆ วิธีในการนำเข้าข้อมูลเพื่อให้เราได้ Object ออกมา ตัวอย่างเช่น Student ที่เราเขียนกันมาตลอด เราจะเห็นว่าใน Constructor เรารับค่าของ Attribute แต่ละตัวเข้ามาตรง ๆ เลย แต่ถ้าเราเปลี่ยน Data Source และ Pattern ละ เราก็ต้องมาเขียน Function สำหรับการแปลงข้อมูลให้อยู่ในรูปแบบที่เราเขียน Constructor ไว้ ดูจะเป็นการแก้ปัญหาแล้ว แต่....... ลองคิดเพิ่มว่า ถ้าเกิด เรามี Code ที่ทำลักษณะนี้หลาย ๆ จุดละ เราไม่ต้องเขียน Code สำหรับแปลงนี้ซ้ำ ๆ กันเหรอ มันก็ไม่โอเค ดังนั้น มันน่าจะดีกว่าที่เราจะ Reuse Code ส่วนนี้ โดยการอัดลงไปใน Class เลย แต่ถ้าเราอัดเป็น Method ทั่ว ๆ ไป เราก็จะเรียกจาก Class ไม่ได้ ทำให้ Class Method เป็น Solution ที่ดีในการจัดการกับปัญหานี้เลยทีเดียว

สำหรับคนที่มาจากภาษาพวก Java มันคือ Overloaded Constructors ที่เราเขียนเพื่อรับลักษณะข้อมูลที่แตกต่างกัน หลักการเดียวกันเลย แค่เปลี่ยนชื่อเท่านั้นแหละ

class Student :
    def __init__ (self, name:str, surname:str, birth_year:int):
        self.__name = name
        self.__surname = surname
        self.__birth_year = birth_year
 
    @classmethod
    def from_dict (cls, stu_info : dict) :
        return cls (
            name = stu_info['name'],
            surname = stu_info['surname'],
            birth_year = stu_info['birth_year']
        )

    @property
    def name (self) :
        return self.__name
       
       
a = Student.from_dict({
    'name' : 'John', 
    'surname' : 'Anderson', 
    'birth_year' : 1997
})

ด้านบนนี้เป็นตัวอย่างของการใช้ classmethod จะเห็นได้ว่า เราทำการสร้าง Method ที่ชื่อว่า from_dict() ขึ้นมา เพื่อเป็น Constructor ตัวนึงที่ใช้รับ Dictionary เข้ามา และทำการสร้างออกมาเป็น Class ถ้าเราลองสังเกตให้ลึกขึ้นอีกนิด เราจะเห็น Argument ของ from_dict() ที่ปกติเราจะรับ self เข้ามา แต่อันนี้เราจะรับ cls เข้ามาแทนอย่างที่เราบอกเรื่องของ Convention และสุดท้าย สิ่งที่เราทำคือ เราคืน Object ที่สร้างจาก cls กลับไป ซึ่งใน Python มันจะไปแทนค่ากลับเป็น Student ให้เราเองเลย เราไม่ต้องทำอะไรทั้งนั้น ผลที่ได้เราก็จะได้ Object ที่สร้างจาก Class Student มาโดยที่เรายัด Logic ในการอ่าน Dicionary ทั้งหมดไว้ใน Class เพื่อความเข้าใจ และ จัดระเบียบได้ดีขึ้นนั่นเองนี่แหละคือความ Factory อย่างที่บอก มันสามารถ Generate Object ได้เหมือนกับโรงงาน

สรุป

3 Decorator ที่เรายกมาเล่าในวันนี้เป็น Decorator ที่ทำให้การเขียน Class บน Python ทำได้ง่ายขึ้นมาก ช่วยให้เราจัด Constrain อย่างที่มันควรจะเป็น ลดความผิดพลาดที่อาจจะเกิดขึ้นในการทำงานได้มหาศาลมาก ๆ จริง ๆ การที่ Python มันให้อิสระมันก็ดีแหละ แต่มีมากไป มันก็อาจจะทำให้เราชิบหาย หาบัคกันไม่เจอเลยก็ได้ การสร้าง Constrain ก็เป็นวิธีนึงในการป้องกันความผิดพลาดที่อาจจะเกิดได้ดีเลย เขียนแบบนี้ไม่ได้ มันก็ Error บอกเลยว่า ตรงนี้ทำไม่ได้ ดีกว่า ไปบอกที่ไกล ๆ แล้วต้องไล่กลับมาอีกว่าตรงไหนว๊าาาาาา ทำให้เราค่อนข้างชอบ Concept ของภาษาที่มีการบังคับที่เข้มกว่านี้มากกว่า พอมาเขียน Python ถ้ามันมีพวกส่วนที่บังคับให้อยู่ ก็ทำให้เราอุ่นใจขึ้นเยอะเลย