ในภาษาไพธอนนั้นปกติเวลาที่เราสร้างคลาสขึ้นมาเราสามารถป้อนค่าให้กับแอตทริบิวต์ให้ได้อย่างอิสระด้วยการใช้ = และสามารถดูค่า
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 เปลี่ยนค่าไป ค่าที่คำนวณได้จึงเปลี่ยนตาม)
ทั้งหมดนี้ก็เป็นเรื่องของการใช้พรอเพอร์ตี ซึ่งก็อาจจะถือว่าเข้าใจยากอยู่ไม่น้อย แต่พอเข้าใจแล้วก็จะสามารถนำไปประยุกต์ใช้ทำอะไรต่อไปได้
อ้างอิง