φυβλαςのβλογ
บล็อกของ phyblas



[python] สร้างแอตทริบิวต์ที่มีคุณสมบัติพิเศษในคลาสด้วย property
เขียนเมื่อ 2016/04/30 17:56
แก้ไขล่าสุด 2021/09/28 16:42
ในภาษาไพธอนนั้นปกติเวลาที่เราสร้างคลาสขึ้นมาเราสามารถป้อนค่าให้กับแอตทริบิวต์ให้ได้อย่างอิสระด้วยการใช้ = และสามารถดูค่า
class Hoge:
    0

ho = Hoge()
ho.fuga = '&%)*%#*%$](#&*@('
print(ho.fuga) # ได้ &%)*%#*%$](#&*@(

แต่บางครั้งเราก็อาจจะสามารถใช้วิธีโดยอ้อมเพื่อปรับแก้หรือดูค่าของแอตทริบิวต์ได้ นั่นคือใช้เมธอด
class Hoge:
    def get_fuga(self):
        return self.fuga
    def set_fuga(self,x):
        self.fuga = x

ho = Hoge()
ho.set_fuga('*$()&@^&$^%^{][=+')
print(ho.get_fuga()) # ได้ *$()&@^&$^%^{][=+

ในตัวอย่างนี้แอตทริบิวต์ fuga ถูกตั้งค่าด้วยเมธอด set_fuga และเอาค่าได้จากเมธอด get_fuga ซึ่งก็ไม่ต่างจากตัวอย่างด้านบน

อย่างไรก็ตามวิธีนี้มีข้อดีคือเราสามารถจะตั้งให้เวลาที่ป้อนค่าไปค่ามีการตกแต่ง หรือปรับเปลี่ยนอะไรบางอย่างก่อน และสามารถตั้งให้เวลาที่จะเอาค่าสามารถแสดงผลให้ต่างออกไปได้

ยกตัวอย่าง สร้างแอตทริบิวต์ที่เมื่อรับค่าอะไรเข้าไปจะถูกเปลี่ยนเป็นสายอักขระ และเวลาแสดงออกจะมีส่วนแต่งเติมเพิ่มเติม
class Saya:
    def get_s(self):
        return 's = <<"'+self.s+'">>'
    def set_s(self,s):
        self.s = str(s)

ay = Saya()
ay.set_s(129.3)
print(ay.get_s()) # ได้ s = <<"129.3">>

จะเห็นว่าแอตทริบิวต์ s ถูกป้อนค่าให้ด้วยเมธอด set_s โดยค่าที่ป้อนให้ฟังก์ชันจะถูกเปลี่ยนเป็นสายอักขระก่อนด้วย str()

จากนั้นค่าของ s จะเอาได้จากเมธอด get_s โดยค่าที่คืนกลับมาจะมีการแต่งเติมเพิ่ม 's = <<"' และ '">>' ลงไป

อย่างไรก็ตามการจะต้องมาคอยใช้ฟังก์ชันอยู่ตลอดมันก็ยุ่งยาก ถ้าสามารถพิมพ์เป็น ay.s = 129.3 แบบนี้ได้โดยตรงก็จะสะดวกกว่า แต่หากทำแบบนั้นก็จะเป็นแค่การแทนค่าลงไปธรรมดา ผลที่ได้ก็ไมไ่ด้ถูกแปลงเป็นสายอักขระตามที่ต้องการ

มีอยู่วิธีหนึ่งที่จะสามารถทำให้การป้อนค่าและเอาค่าแอตทริบิวต์สามารถทำได้ง่ายโดยไม่ต้องเขียนในรูปเมธอดโดยตรง

นั่นก็คือการตั้งให้แอตทริบิวต์ที่ต้องการกลายเป็นสิ่งที่เรียกว่าพรอเพอร์ตี (property)

หากว่ากันตามความหมายของคำศัพท์โดยทั่วไปแล้ว ความหมายของคำว่าพรอเพอร์ตีนั้นจริงๆแล้วไม่ต่างจากคำว่าแอตทริบิวต์มากนัก แต่ในภาษาไพธอนนั้น พรอเพอร์ตีหมายถึงแอตทริบิวต์พิเศษรูปแบบหนึ่ง

การสร้างพรอเพอร์ตีนั้นมีอยู่หลายรูปแบบในการเขียน จะขอเริ่มจากวิธีที่เข้าใจง่ายที่สุดก่อน โดยจากตัวอย่างข้างต้นหากต้องการให้ s เป็นพรอเพอร์ตีจะเขียนเป็น
class Saya:
    def get_s(self):
        return 's = <<"'+self.__s+'">>'
    def set_s(self,s):
        self.__s = str(s)
    s = property(get_s,set_s)

จะเห็นว่าคล้ายเดิม แต่ตัวแปร self.s เปลี่ยนเป็น self.__s และมีบรรทัดสุดท้ายคือ s = property(get_s,set_s) เพิ่มเข้ามา

พอเขียนแบบนี้แล้วตัวแปร s จะไม่ใช่แอตทริบิวต์ที่แท้จริงของออบเจ็กต์โดยตรง แต่แอตทริบิวต์จริงๆคือตัวแปร __s แต่เวลาเราเรียกใช้จะทำผ่านตัวแปร s เสมือนมันเป็นแอตทริบิวต์ตัวหนึ่ง

ลองทดสอบใช้ดู
ay = Saya()
ay.s = 392.1
print(ay.s) # ได้ s = <<"392.1">>

จะเห็นว่าได้ผลเหมือนกับการใช้เมธอด get_s และ set_s

ดูเผินๆอาจดูเหมือนประหลาดที่ว่าเราป้อนค่าตัวเลข 392.1 ลงไป แต่พอให้แสดงผลออกมามันกลับกลายเป็นสายอักขระยาว

เพราะในที่นี้ ay.s = 392.1 ไม่ใช่การแทนค่าแล้ว แต่กลายเป็นการเรียกใช้เมธอด set_s ซึ่งจะเป็นการแทนค่าใน ay.__s แทน

นี่คือผลจากการทำงานของ property

property นั้นเป็นออบเจ็กต์ชนิดหนึ่ง เวลาที่เรียกใช้จะรับอาร์กิวเมนต์ ๓ ตัวคือ
- ฟังก์ชันที่จะให้ทำงานเมื่อมีการเอาค่า
- ฟังก์ชันที่จะให้ทำงานเมื่อมีการแทนค่าลงไปด้วย =
- ฟังก์ชันที่จะให้ทำงานเมื่อมีการลบด้วยคำสั่ง del

เมื่อประกาศให้ s=property(...,...,...) แบบนี้ก็ทำให้ s กลายเป็นพรอเพอร์ตี ซึ่งเมื่อมีการทำอะไรกับ s ฟังก์ชันที่นิยามไว้ก็จะทำงานโดยส่งผลโดยอ้อมต่อค่า __s

ในที่นี้เราไม่ได้ใส่อาร์กิวเมนต์ตัวที่ ๓ ลงไป ดังนั้นฟังก์ชันสำหรับตอนลบจะไม่ได้ถูกตั้ง

เพื่อให้สมบูรณ์คราวนี้เราลองเขียนใหม่โดยเพิ่มฟังก์ชันสำหรับตอนถูกลบไปด้วย
class Saya:
    def get_s(self):
        print('ดูค่า s')
        return 's = <<"'+self.__s+'">>'
    def set_s(self,s):
        print('ป้อนค่า %f ให้ s'%s)
        self.__s = str(s)
    def del_s(self):
        print('ลบ s ทิ้งแล้ว')
        del self.__s
    s = property(get_s,set_s,del_s)
    
ay = Saya()
ay.s = 392.1 # set_s
print(ay.s) # get_s
del ay.s # del_s

ในที่นี้ใส่คำสั่ง print ลงไปเพื่อให้มีข้อความขึ้นทุกครั้งที่มีการใช้งานพรอเพอร์ตี s ด้วย

ผลที่ได้คือ
ป้อนค่า 392.100000 ให้ s
ดูค่า s
s = <<"392.1">>
ลบ s ทิ้งแล้ว

คำสั่งที่ใส่จะใส่อะไรไปก็ได้ ไม่จำเป็นว่า get จะต้องเป็นการเอาค่า set เป็นการตั้งค่า และ del เป็นการลบ แต่ว่าโดยทั่วไปมักจะเป็นแบบนั้น แค่เติมแต่งผลบางอย่างเข้ามาเท่านั้น

จากนั้นมาลองดูวิธีการเขียนอีกแบบที่สามารถทำได้ ขอเขียนตัวอย่างเดิมซึ่งมีทั้ง set get del อยู่นั้นใหม่โดยตัดส่วน print ออก

จะได้เป็น
class Saya:
    def get_s(self):
        return 's = <<"'+self.__s+'">>'
    def set_s(self,s):
        self.__s = str(s)
    def del_s(self):
        del self.__s
    s = property()
    s = s.getter(get_s)
    s = s.setter(set_s)
    s = s.deleter(del_s)

ตรงส่วนท้ายที่จากเดิมมีแค่บรรทัดเดียว ตอนนี้เพิ่มมาเป็น ๔ บรรทัด โดยในที่นี้ s มีการเรียกใช้เมธอด getter setter และ deleter เพื่อตั้งให้ get_s, set_s ลแะ del_s กลายมาเป็นเมธอดสำหรับเอาค่า ตั้งค่า และลบค่า ตามลำดับ

การเขียนแบบนี้ดูยุ่งยากกว่าเดิม แต่หากมองดูรูปแบบแล้วจะเห็นว่าเป็นรูปแบบที่สามารถใช้สัญลักษณ์ @ เขียนได้ ซึ่งจะได้เป็น
class Saya:
    s = property()
    @s.getter
    def s(self):
        return 's = <<"'+self.__s+'">>'
    @s.setter
    def s(self,s):
        self.__s = str(s)
    @s.deleter
    def s(self):
        del self.__s

แต่ว่า getter นั้นแต่เดิมก็สามารถได้มาจากอาร์กิวเมนต์ตัวแรกของ property ดังนั้นสามารถเขียนเป็น
class Saya:
    @property
    def s(self):
        return 's = <<"'+self.__s+'">>'
    @s.setter
    def s(self,s):
        self.__s = str(s)
    @s.deleter
    def s(self):
        del self.__s

อนึ่ง ชื่อตัวแปร __s นี้เป็นการตั้งชื่อตัวแปรในรูปแบบพิเศษ ดังที่อธิบายไว้ในเนื้อหาภาษาไพธอนเบื้องต้น บทที่ ๒๓ แอตทริบิวต์ที่มีขีดล่างนำหน้าสองตัว __ แบบนี้จะไม่สามารถเข้าถึงโดยตรงได้ แต่ต้องพิมพ์ชื่อเป็น ay._Saya__s

ภาษาไพธอนไม่มีการซ่อนแอตทริบิวต์อย่างสมบูรณ์ แต่ถึงอย่างนั้นการตั้งชื่อตัวแปรแบบนี้ก็เป็นธรรมเนียมปฏิบัติให้รู้ว่าไม่ควรจะไปยุ่งกับตัวแปรนี้โดยตรง

การประยุกต์ใช้อย่างหนึ่งของพรอเพอร์ตีคือ อาจลองเอาไว้ใช้เพื่อสร้างข้อมูลที่สามารถเปิดดูได้อย่างเดียวแต่ไม่สามารถแก้ค่าได้

เช่นตัวอย่างนี้ สร้างคลาสพนักงานขึ้นมาให้มีแอตทริบิวต์ ๒ อย่างคือ ชื่อ และ เงินเดือน โดยเราต้องการให้เงินเดือนไม่สามารถแก้ค่าได้ ดังนั้นก็ต้องเขียนคลาสแบบนี้
class Phanakngan:
    def __init__(self,chue,ngoenduean):
        self.chue = chue
        self.__ngoenduean = ngoenduean
    @property
    def ngoenduean(self):
        return  '{%s เงินเดือน: %f บาท}'%(self.chue,self.__ngoenduean)
    @ngoenduean.setter
    def ngoenduean(self,ngoenduean):
        print('{ท่านไม่มีสิทธิ์แก้ไขเงินเดือน}')

k = Phanakngan('นาย ก.',12930.1293)
print(k.ngoenduean) # ได้ {นาย ก. เงินเดือน: 12930.129300 บาท}
k.ngoenduean = 99999 # ได้ {ท่านไม่มีสิทธิ์แก้ไขเงินเดือน}
print(k.ngoenduean) # ได้ {นาย ก. เงินเดือน: 12930.129300 บาท}

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



ตัวอย่างการใช้อีกอย่างหนึ่งก็คือ การสร้างให้เมธอดกลายเป็นเหมือนแอตทริบิวต์

เช่นว่าปกติแล้วถ้าเราต้องการให้ออบเจ็กต์นี้แสดงค่าอะไรบางอย่างที่เกิดจากการคำนวณเราจะใช้เมธอดเพื่อให้ทำการคำนวณแล้วคืนค่า

แต่โดยปกติแล้วเมธอดจะต้องใส่วงเล็บต่อท้าย แม้ว่าจะไม่ต้องใส่อาร์กิวเมนต์อะไรเลยก็ตาม กรณีแบบนี้ถ้าเปลี่ยนเมธอดให้เป็นแอตทริบิวต์ก็ไม่ต้องใส่วงเล็บต่อท้ายแล้ว

ตัวอย่าง สร้างออบเจ็กต์ทรงสี่เหลี่ยมขึ้นมา โดยมีการเก็บค่าความกว้าง, ยาว และสูงไว้ จากนั้นต้องการให้มันสามารถแสดงค่าปริมาตร, พื้นที่ผิว และความยาวเส้นขอบ
class Songsiliam:
    def __init__(self,kwang,yao,sung):
        self.kwang = kwang
        self.yao = yao
        self.sung = sung
    @property
    def parimat(self):
        return self.kwang*self.yao*self.sung
    @property
    def phuenthiphiu(self):
        return 2*(self.kwang*self.yao+self.kwang*self.sung+self.yao*self.sung)
    @property
    def khwamyaosenkhop(self):
        return 4*(self.kwang+self.yao+self.sung)

na = Songsiliam(2,2,3)
print(na.parimat) # ได้ 12
print(na.phuenthiphiu) # ได้ 32
print(na.khwamyaosenkhop) # ได้ 28

จะเห็นว่าเรานิยามเมธอด parimat, phuenthiphiu และ khwamyaosenkhop ขึ้นมาโดยใช้เดคอเรเตอร์  @property ใส่ไว้ด้านบนด้วย ผลก็คือแทนที่มันจะเป็นเมธอด มันกลับกลายเป็นเหมือนแอตทริบิวต์ไปแทน คือเรียกใช้ได้โดยไม่ต้องใส่วงเล็บด้านหลัง

อย่างไรก็ตาม ถ้าหากเราป้อนค่าทับให้มัน ค่านั้นก็จะถูกทับไป พอถูกเรียกหาค่ามันก็จะกลายเป็นค่านั้นออกมา และไม่สะท้อนค่าตามที่เราต้องการอีกต่อไป
na.parimat = 0
print(na.parimat) # ได้ 0

นั่นเป็นเพราะเรายังไม่ได้กำหนด setter ให้นั่นเอง ถ้าลองตั้งดูดีๆก็อาจสามารถทำอะไรๆแปลกๆได้หลายอย่าง

เช่น เราสามารถตั้ง setter ให้พอเวลามีการแก้ค่าแล้วจะไปแก้ค่าอะไรบางอย่างที่อยู่ภายในออบเจ็กต์ได้

เช่น ลองสร้างวัตถุทรงสี่เหลี่ยมจากตัวอย่างที่แล้วใหม่โดยตั้ง setter ให้กับปริมาตร ว่าถ้าหากมีการแก้ปริมาตรเมื่อไหร่ให้มันไปแก้ค่าส่วนสูงให้เป็นสัดส่วนกับ ค่าที่ถูกแก้
class Songsiliam:
    def __init__(self,kwang,yao,sung):
        self.kwang = kwang
        self.yao = yao
        self.sung = sung
    @property
    def parimat(self):
        return self.kwang*self.yao*self.sung
    @parimat.setter
    def parimat(self,p):
        self.sung *= p/self.parimat

na = Songsiliam(2,3,4)
print(na.sung) # ได้ 4
print(na.parimat) # ได้ 24
na.parimat = 72
print(na.sung) # ได้ 12.0
print(na.parimat) # ได้ 72.0
na.parimat = 30
print(na.sung) # ได้ 5.0
print(na.parimat) # ได้ 30.0

จะเห็นว่าค่า sung เปลี่ยนแปลงไปทั้งๆที่เราไม่ได้ป้อนค่าอะไรให้มันโดยตรงเลย ซึ่งก็ทำค่า parimat เปลี่ยนแสดงผลตรงกับค่าที่ป้อนเข้าไปด้วยโดยอ้อม (ค่า 72 หรือ 30 ไม่ได้ถูกเก็บในตัวแปรไหนจริงๆ เพราะ parimat ไม่ใช่แอตทริบิวต์ แต่ที่มันสะท้อนค่าตามที่ป้อนเพราะ sung เปลี่ยนค่าไป ค่าที่คำนวณได้จึงเปลี่ยนตาม)



ทั้งหมดนี้ก็เป็นเรื่องของการใช้พรอเพอร์ตี ซึ่งก็อาจจะถือว่าเข้าใจยากอยู่ไม่น้อย แต่พอเข้าใจแล้วก็จะสามารถนำไปประยุกต์ใช้ทำอะไรต่อไปได้



อ้างอิง


-----------------------------------------

囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧囧

ดูสถิติของหน้านี้

หมวดหมู่

-- คอมพิวเตอร์ >> เขียนโปรแกรม >> python

ไม่อนุญาตให้นำเนื้อหาของบทความไปลงที่อื่นโดยไม่ได้ขออนุญาตโดยเด็ดขาด หากต้องการนำบางส่วนไปลงสามารถทำได้โดยต้องไม่ใช่การก๊อปแปะแต่ให้เปลี่ยนคำพูดเป็นของตัวเอง หรือไม่ก็เขียนในลักษณะการยกข้อความอ้างอิง และไม่ว่ากรณีไหนก็ตาม ต้องให้เครดิตพร้อมใส่ลิงก์ของทุกบทความที่มีการใช้เนื้อหาเสมอ

สารบัญ

รวมคำแปลวลีเด็ดจากญี่ปุ่น
มอดูลต่างๆ
-- numpy
-- matplotlib

-- pandas
-- manim
-- opencv
-- pyqt
-- pytorch
การเรียนรู้ของเครื่อง
-- โครงข่าย
     ประสาทเทียม
ภาษา javascript
ภาษา mongol
ภาษาศาสตร์
maya
ความน่าจะเป็น
บันทึกในญี่ปุ่น
บันทึกในจีน
-- บันทึกในปักกิ่ง
-- บันทึกในฮ่องกง
-- บันทึกในมาเก๊า
บันทึกในไต้หวัน
บันทึกในยุโรปเหนือ
บันทึกในประเทศอื่นๆ
qiita
บทความอื่นๆ

บทความแบ่งตามหมวด



ติดตามอัปเดตของบล็อกได้ที่แฟนเพจ

  ค้นหาบทความ

  บทความแนะนำ

ตัวอักษรกรีกและเปรียบเทียบการใช้งานในภาษากรีกโบราณและกรีกสมัยใหม่
ที่มาของอักษรไทยและความเกี่ยวพันกับอักษรอื่นๆในตระกูลอักษรพราหมี
การสร้างแบบจำลองสามมิติเป็นไฟล์ .obj วิธีการอย่างง่ายที่ไม่ว่าใครก็ลองทำได้ทันที
รวมรายชื่อนักร้องเพลงกวางตุ้ง
ภาษาจีนแบ่งเป็นสำเนียงอะไรบ้าง มีความแตกต่างกันมากแค่ไหน
ทำความเข้าใจระบอบประชาธิปไตยจากประวัติศาสตร์ความเป็นมา
เรียนรู้วิธีการใช้ regular expression (regex)
การใช้ unix shell เบื้องต้น ใน linux และ mac
g ในภาษาญี่ปุ่นออกเสียง "ก" หรือ "ง" กันแน่
ทำความรู้จักกับปัญญาประดิษฐ์และการเรียนรู้ของเครื่อง
ค้นพบระบบดาวเคราะห์ ๘ ดวง เบื้องหลังความสำเร็จคือปัญญาประดิษฐ์ (AI)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

ไทย

日本語

中文