φυβλαςのβλογ
บล็อกของ 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)
หอดูดาวโบราณปักกิ่ง ตอนที่ ๑: แท่นสังเกตการณ์และสวนดอกไม้
พิพิธภัณฑ์สถาปัตยกรรมโบราณปักกิ่ง
เที่ยวเมืองตานตง ล่องเรือในน่านน้ำเกาหลีเหนือ
ตระเวนเที่ยวตามรอยฉากของอนิเมะในญี่ปุ่น
เที่ยวชมหอดูดาวที่ฐานสังเกตการณ์ซิงหลง
ทำไมจึงไม่ควรเขียนวรรณยุกต์เวลาทับศัพท์ภาษาต่างประเทศ

บทความแต่ละเดือน

2024年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2023年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2022年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2021年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

2020年

1月 2月 3月 4月
5月 6月 7月 8月
9月 10月 11月 12月

ค้นบทความเก่ากว่านั้น

ไทย

日本語

中文