บทที่ 6 การเขียนโปรแกรมเชิงอ็อบเจกต์ (Object-Oriented Programming)#
สมมติว่าเราต้องการเก็บข้อมูลของคำแต่ละคำที่อยู่ในย่อหน้าย่อหน้าหนึ่ง โครงสร้างข้อมูลที่เหมาะกับข้อมูลประเภทนี้ คือ ลิสต์ที่เก็บสตริง เช่น
paragraph = ['Apple', 'Inc.' 'is' 'an' 'American' 'multinational' 'technology' 'firm' 'headquartered', 'in' 'Cupertino', 'California' '.']
แต่สมมติอีกว่าเราต้องการเก็บข้อมูลของแต่ละคำอย่างละเอียดขึ้น เช่น ชนิดของคำ (part-of-speech) ประเภทของชื่อเฉพาะ (named entity) คำนี้เป็นภาษาอะไร คำนี้เป็นคำที่ขึ้นต้นประโยคหรือไม่ คำนี้มีรูปปกติ (lemma) หรือรูปก่อนการผันรูปเป็นอะไร โครงสร้างข้อมูลที่เหมาะกับข้อมูลประเภทนี้ คือ ลิสต์ของดิกชันนารี เช่น
[
{"word": "Apple",
"pos": "NNP",
"ner": "ORG",
"lang": "en",
"is_first": True,
"lemma": "apple" },
{"word": "Inc.",
"pos": "NNP",
"ner": "ORG",
"lang": "en",
"is_first": False,
"lemma": "inc."},
{"word": "is",
"pos": "VBZ",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "be"},
{"word": "an",
"pos": "DT",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "an"},
{"word": "American",
"pos": "JJ",
"ner": "MISC",
"lang": "en",
"is_first": False,
"lemma": "american"},
{"word": "multinational",
"pos": "JJ",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "multinational"},
{"word": "technology",
"pos": "NN",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "technology"},
{"word": "firm",
"pos": "NN",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "firm"},
{"word": "headquartered",
"pos": "VBN",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "headquarter"},
{"word": "in",
"pos": "IN",
"ner": "O",
"lang": "en",
"is_first": False,
"lemma": "in"},
{"word": "Cupertino",
"pos": "NNP",
"ner": "LOC",
"lang": "en",
"is_first": False,
"lemma": "cupertino"},
{"word": "California",
"pos": "NNP",
"ner": "LOC",
"lang": "en",
"is_first": false,
"lemma": "california"},
{"word": ".",
"pos": ".",
"ner": "O",
"lang": "en",
"is_first": false,
"lemma": "."}
]
อย่างแรกที่เราสังเกตได้คือ ดิกชันนารีที่เราใช้มีฟีลด์ถึง 6 ฟีลด์ การจะเขียนโปรแกรมขึ้นมาเพื่อประมวลผลวิเคราะห์ข้อมูลประเภทนี้ เราจะต้องระวังว่าดิกชันนารีแต่ละอันที่เก็บข้อมูลของคำจะต้องมีฟีลด์เท่ากันหมด และผู้ที่เขียนฟังก์ชันเพื่อประมวลผลดิกชันนารีจะต้องทราบด้วยว่าฟีลด์เหล่านี้มีอะไรบ้าง เก็บอะไรไว้บ้าง ถ้าหากมีการเปลี่ยนแปลงชื่อฟีลด์ หรือเพิ่มฟีลด์ใหม่ ฟังก์ชันที่เขียนขึ้นมาเพื่อประมวลผลดิกชันนารีจะต้องถูกแก้ไขใหม่ทั้งหมด ซึ่งอาจจะทำให้เกิดข้อผิดพลาดได้ เพราะว่าโค้ดที่ใช้ในการสร้างดิกชันนารีขึ้นมา กับโค้ดที่ใช้ในการประมวลผลอาจจะอยู่คนละที่ คนละไฟล์กัน ทำให้โค้ดที่เขียนขึ้นมานั้นยากต่อการบำรุงรักษา และยากต่อการเปลี่ยนแปลง
คลาส (class) และอ็อบเจกต์ (object)#
การเขียนโปรแกรมเชิงอ็อบเจกต์ (Object-Oriented Programming: OOP) คือหลักการที่เราจะใช้ในการออกแบบโครงสร้างข้อมูลเพื่อให้สะดวกกับกรณีการใช้งานดังตัวอย่างข้างต้น หลักการของ OOP คือการออกแบบโครงสร้างข้อมูลขี้นมาเป็นพิเศษ เจาะจงกับข้อมูลที่เราต้องการจะเก็บ โครงสร้างข้อมูลนี้เรียกว่าคลาส (class)
ไพทอนไม่ได้รองรับลักษณะเด่นอื่น ๆ ของการเขียนโปรแกรมเชิงอ็อบเจกต์ เช่น การกำหนดสาระสำคัญ (abstraction) การห่อหุ้ม (encapsulation) ภาวะพหุสัณฐาน (polymorphism) หรือการควบคุมการเข้าถึงข้อมูลที่อยู่ในอ็อบเจกต์ ผู้เขียนจึงขอเน้นเฉพาะลักษณะที่ภาษาไพทอนรองรับเท่านั้น คลาสในภาษาไพทอนประกอบลักษณะพิเศษหลัก ๆ 3 อย่าง คือ
ลักษณะประจำ (attribute)
เมท็อด (method) ซึ่งเป็นโค้ดที่ใช้ประมวลผลลักษณะประจำคลาส
สิ่งสืบทอด (inheritance) ซึ่งใช้ดึงเอาเมท็อดจากคลาสอื่นมาใช้
ลักษณะประจำ (attribute)#
ลักษณะประจำ เป็นตัวแปรที่เก็บซ้อนอยู่ในคลาส ใช้เก็บข้อมูลคล้ายคลึงกับตัวแปร เพื่อให้เห็นภาพมากขึ้น เราจะลองเขียนคลาสเพื่อใช้เก็บข้อมูลเกี่ยวกับคำในประโยคแทนที่ดิกชันนารี เราขอเรียกว่าโทเค็น (token) แทนที่คำว่า คำ เนื่องจากคำในความหมายที่อ้างอิงตามมโนทัศน์ทางภาษาศาสตร์มีคำนิยามเฉพาะซึ่งไม่เหมือนกับโทเค็น โทเค็น หมายถึง หน่วยหนึ่งหน่วยที่ผู้วิเคราะห์ใช้เป็นหน่วยที่เล็กที่สุดในการประมวลผล ตัวอย่างโค้ดในการกำหนดคลาสมีดังนี้
class Token:
word = ''
pos = ''
ner = ''
lang = ''
is_first = False
lemma = ''
บรรทัดแรก คือ การตั้งชื่อคลาส โดยการใช้คีย์เวิร์ด class
ตามด้วยชื่อคลาส ชื่อคลาสมักจะใช้การสะกดแบบหลังอูฐ (camel case) ถ้าหากชื่อคลาสประกอบด้วยคำหลายคำ ให้ตัวอักษรแรกของแต่ละคำเป็นตัวใหญ่เช่น (LanguageModel
, AutoTokenizer
, DefaultModelConfiguration
) จากนั้นให้ปิดด้วย :
บรรทัดต่อมา จะต้องเริ่มด้วยการเคาะย่อหน้าเพื่อบ่งบอกว่าโค้ดบล็อกที่อยู่ในย่อหน้านั้นเป็นของการกำหนดคลาส และจากนั้นก็เป็นการกำหนดลักษณะประจำคลาส (class attribute) ซึ่งจะเป็นการกำหนดตัวแปรที่คลาสนี้จำเป็นต้องมี ในตัวอย่างนี้เรากำหนดลักษณะประจำของคลาส Token
มีลักษณะประจำทั้งหมด 6 อย่าง คือ word
, pos
, ner
, lang
, is_first
, lemma
โดยเรากำหนดค่าเริ่มต้นให้กับลักษณะประจำทั้งหมด โดยใช้เครื่องหมาย =
คล้ายคลึงกับการกำหนดค่าตัวแปร ตามธรรมเนียมปฏิบัติของการเขียนคลาส โปรแกรมเมอร์มักจะนำลักษณะประจำคลาสทั้งหมดมาไว้ตามหลังชื่อคลาสเพื่อความเข้าใจตรงกัน และทำให้หาได้ง่าย ถึงแม้ว่าที่จริงแล้วเราจะนำไปไว้ในส่วนใดของคลาสก็ได้
คลาส เป็นเพียงแค่ต้นแบบ หรือแบบแปลนให้ทราบว่าหนึ่งโทเคนนั้นจะต้องประกอบไปด้วยตัวแปรย่อย ๆ ซึ่งก็คือลักษณะประจำคลาส เมื่อเราต้องการนำคลาสนี้ไปใช้เก็บข้อมูล จะต้องมีการสร้างอ็อบเจกต์ (object) ของคลาสนี้ กระบวนการนี้เรียกว่า การสร้างกรณีตัวอย่าง (instantiation) ในภาษาไพทอนมีวิธีการสร้างอ็อบเจกต์โดยการใช้ชื่อคลาสตามด้วย ()
เช่น
first_word = Token()
second_word = Token()
เราได้สร้างอ็อบเจกต์ของคลาสเก็บใส่ตัวแปร first_word
และ second_word
ในการกระบวนการนี้เราได้สร้างตัวแปรย่อย 6 ตัวเก็บไว้ใน first_word
และตัวแปรย่อยอีก 6 ตัวเก็บไว้ใน second_word
เป็นตัวแปรสองชุดแยกจากกันโดยสิ้นเชิง
ถ้าหากเราต้องการเข้าถึงหรือกำหนดค่าใหม่ให้ตัวแปรที่อยู่ในอ็อบเจกต์ ให้ใช้ .
ตามด้วยชื่อของตัวแปร
first_word.word = 'Apple'
first_word.pos = 'NNP'
first_word.ner = 'ORG'
first_word.lang = 'en'
first_word.is_first = True
first_word.lemma = 'apple'
second_word.word = 'Inc.'
second_word.pos = 'NNP'
second_word.ner = 'ORG'
second_word.lang = 'en'
second_word.is_first = False
second_word.lemma = 'inc.'
จะเห็นได้ว่าการใช้คลาสที่สร้างขึ้นมาพิเศษในการเก็บข้อมูลแทนที่ดิกชันนารีมีข้อได้เปรียบหลายประการ
ประการแรกคือรับประกันได้ว่า first_word
และ second_word
มีจำนวนข้อมูลเท่ากันเพราะว่าเป็นอ็อบเจกต์ที่สร้างมาจากคลาสเดียวกัน และชื่อของตัวแปรย่อยที่อยู่ในอ็อบเจกต์เหล่านี้ก็ต้องเหมือนกัน และตัวแปรเหล่านั้นควรจะเก็บข้อมูลอย่างเดียวกันอยู่
ประการที่สองคือการใช้ .
เพื่อเข้าถึงข้อมูลที่อยู่ในอ็อบเจกต์สะดวกกว่าการใช้ []
เพื่อเข้าถึงข้อมูลที่อยู่ในดิกชันนารี เช่น
first_word['word']
first_word.word
ประการที่สามคือ หากเราใช้โปรแกรมไอดีอี (IDE: Integrated Development Environment หรือโปรแกรมที่มีเครื่องมือการช่วยพัฒนาโปรแกรมแบบครบครัน) อย่าง VSCode หรือเครื่องมือปัญญาประดิษฐ์ที่ช่วยในการเขียนโค้ด เช่น GitHub Copilot เครื่องมือเหล่านี้จะช่วยเติมโค้ดให้สมบูรณ์โดยอัตโนมัติได้ เพราะว่าการกำหนดคลาสเป็นส่วนหนึ่งของโค้ดที่ไอดีอี ได้ทำความเข้าใจแล้ว เช่น หากเราพิมพ์ first_word.w
และ ไอดีอีสแกนและทำความเข้าใจเรียบร้อยแล้วว่า first_word
มาจากคลาส Token
ซึ่งมีลักษณะประจำชื่อว่า word
ซึ่งเป็นตัวแแปรเดียวที่ขึ้นต้นด้วย w ไอดีอี จึงมีความสามารถที่จะเติมโค้ดให้เราเป็น first_word.word
โดยอัตโนมัติ
นอกจากนั้นแล้วหากเราห่างจากโค้ดก้อนนี้ไปนาน ๆ แล้วต้องกลับมาเขียนโค้ดเพิ่ม หรือต้องร่วมงานกับคนอื่นที่ต้องใช้โค้ดร่วมกับเรา เราก็สามารถอ้างอิงไปยังโค้ดที่กำหนดคลาส เพื่อทำความเข้าใจว่าคลาสที่เห็นอยู่เก็บข้อมูลอะไรอยู่บ้าง ซึ่งต่างจากดิกชันนารีที่ไม่มีโค้ดที่กำหนดคีย์ทั้งหมดของดิกชันนารีอย่างเป็นหลักเป็นแหล่งใช้อ้างอิงได้
ภาษาโปรแกรมที่เป็นภาษาเชิงอ็อบเจกต์ที่เก่าแก่กว่าภาษาไพทอน เช่น จาวา (Java) หรือ ซีพลัสพลัส (C++) จะมีการแบ่งแยกระดับการเข้าถึงตัวแปรชัดเจนว่าคลาสอื่น ๆ สามารถเข้าถึงแปรที่อยู่ในอ็อบเจกต์นี้ได้หรือไม่ (เช่น การใช้คีย์เวิร์ด public
private
และ protected
) แต่ภาษาไพทอนตัวพื้นฐานแบบที่ไม่ได้ใช้ไลบรารีตัวอื่น ๆ ช่วย ไม่ได้มีการแบ่งแยกระดับการเข้าถึง ทุกคลาส ทุกโปรแกรมสามารถเข้าถึงตัวแปรในอ็อบเจกต์และในคลาสได้ นักเขียนโปรแกรมบางกลุ่มแนะนำว่าให้ใส่ _
ไปข้างหน้าชื่อตัวแปรเพื่อเป็นการบ่งบอกว่าโปรแกรมอื่น ๆ คลาสอื่น ๆ ไม่ควรจะเข้าถึงตัวแปรตัวนี้โดยตรง แต่ว่าภาษาไพทอนไม่มีการบังคับใช้กฎนี้แต่อย่างใด อย่างมากที่สุด คือ ไอดีอี อาจจะมีการเตือนผู้เขียนโปรแกรมอยู่บ้าง
เมท็อด (method)#
ก่อนถึงบทนี้เราได้มีการใช้เมท็อดโดยการใช้ .
มาหลายครั้งแล้ว เพราะว่าทั้งสตริง ทั้งลิสต์ ทั้งดิกชันนารีที่ใช้ในการเก็บข้อมูลเพื่อเขียนโปรแกรมที่ผ่านมาต่างก็เป็นอ็อบเจกต์ทั้งหมด เมท็อดที่เราได้ใช้ล้วนแต่เป็นฟังกืชันที่ใช้ประมวลผลข้อมูลที่อยู่ในอ็อบเจกต์ เพราะฉะนั้นเราจึงถือได้ว่า เมท็อดเป็นฟังก์ชันที่เป็นลักษณะเฉพาะเจาะจงของคลาส เช่น ลิสต์มีเมท็อดที่ชื่อว่า append
และ pop
ซึ่งเป็นเมท็อดที่ใช้ในการปฏิสัมพันธ์กับข้อมูลที่อยู่ในอ็อบเจกต์ที่เป็นลิสต์ ดิกชันนารี หรือเซตไม่มีเมท็อดเหล่านี้ เพราะว่าเป็นคนละคลาสกัน
เมท็อดของคลาส#
ในการกำหนดคลาส เราสามารถกำหนดเมท็อดให้คลาสได้ด้วย โดยการใช้คีย์เวิร์ด def
ตามด้วยชื่อเมท็อด ()
และ :
และอาร์กิวเมนต์แรกจะต้องเป็นคีย์เวิร์ด self
ตัวอย่างเช่น
class Token:
word = ''
pos = ''
ner = ''
lang = ''
is_first = False
lemma = ''
def to_plural_form(self):
if self.pos[0] == 'N':
self.word = self.word + 's'
self
คืออะไร และทำไมต้องใช้ self
ในการกำหนดเมท็อด ในภาษาไพทอน การใช้ self
ในการกำหนดเมท็อดเป็นการบอกว่าเมท็อดนี้เป็นเมท็อดของคลาส และเมท็อดนี้จะใช้ตัวแปร self
เข้าถึงข้อมูลที่อยู่ในอ็อบเจกต์ซึ่งก็คือตัวแปร (ลักษณะประจำ) ทั้งหมด ในตัวอย่างนี้เมท็อด to_plural_form
ใช้ self
เข้าถึงตัวแปร pos
และ word
และเปลี่ยนค่าของ word
ให้เป็นรูปพหุสัณฐานของ word
ถ้าหาก pos
ของคำนั้นเป็น N ซึ่งหมายถึงคำนั้นเป็นนาม และเมท็อดนี้จะเปลี่ยนค่าของ word
ในอ็อบเจกต์ที่เรียกเมท็อดนี้ ซึ่งเป็นการเปลี่ยนค่าของ word
ในอ็อบเจกต์ที่เรียกเมท็อดนี้เท่านั้น ไม่ได้เปลี่ยนค่าของ word
ในอ็อบเจกต์อื่น ๆ ที่เก็บอยู่ในตัวแปรอื่น ๆ
ตัวอย่างการใช้เมท็อด to_plural_form
ในการเปลี่ยนคำว่า firm
ให้เป็น firms
แสดงในโค้ดด้านล่าง
word = Token()
word.word = 'firm'
word.pos = 'NN'
word.ner = 'O'
word.lang = 'en'
word.is_first = False
word.lemma = 'firm'
word.to_plural_form()
print(word.word) # firms
สังเกตว่าเวลาเรียกใช้เมท็อด .to_plural_form
เราไม่ต้องใส่ self
ลงไป เวลาเราเรียกใช้เมท็อด ไพทอนทราบเองโดยอัตโนมัติว่าเป็นการใช้เมท็อดของอ็อบเจกต์ที่อยู่ใน word
และจะส่งอ็อบเจกต์ที่เรียกเมท็อดนี้เข้าไปในอาร์กิวเมนต์แรกของเมท็อด ซึ่งเป็นเหตุผลที่ทำให้เราไม่ต้องใส่ self
ลงไปในอาร์กิวเมนต์ของเมท็อดอีก
ตัวสร้าง (Constructor)#
ตัวสร้าง (constructor) คือเมท็อดที่ถูกเรียกทำงานทันทีที่อ็อบเจกต์ถูกสร้างขึ้นมาในคลาสโดยการเรียกชื่อคลาสตามด้วย ()
ในภาษาไพทอน เมท็อดตัวสร้างจะต้องมีชื่อเป็น __init__
ไม่สามารถเป็นชื่ออื่นได้ แต่ว่าเราสามารถกำหนดอาร์กิวเมนต์เองได้ ตัวอย่างเช่น
class Token:
word = ''
pos = ''
ner = ''
lang = ''
is_first = False
lemma = ''
def __init__(self, word):
self.word = word
self.lang = detect_lang(word)
def to_plural_form(self):
if self.pos[0] == 'N':
self.word = self.word + 's'
ตามธรรมเนียมปฏิบัติของการเขียนตัวสร้าง โปรแกรมเมอร์มักจะนำตัวสร้างมาตามหลังลักษณะประจำของคลาส และก่อนเมท็อดอื่น ๆ ที่จะตามมา เพื่อให้มีความเข้าใจตรงกัน
ในตัวอย่างด้านบนเรากำหนดเมท็อดตัวสร้างที่จำเป็นต้องใส่อาร์กิวเมนต์หนึ่งตัว นั่นก็คือรูปของคำ และเมท็อดนี้จะกำหนดค่าให้ word
ในอ็อบเจกต์ที่เรียกเมท็อดนี้ ให้สังเกตว่า word
ใน __init__
อ้างถึงค่าที่ผู้ใช้โปรแกรมป้อนเข้ามา แต่ว่า self.word
อ้างถึงตัวแปรที่อยู่ในอ็อบเจกต์ที่เรียกเมท็อดตัวสร้างนี้ กล่าวคือเราได้สร้างตัวแปรทั้งหมดที่อยู่ในคลาส Token
เรียบร้อยแล้วได้แก่ word
, pos
, ner
, lang
, is_first
, lemma
เราจึงอ้างถึงตัวแปร self.word
จากนั้นตัวสร้างจึงเรียกฟังก์ชัน detect_lang
(คิดไปก่อนว่าเรามีฟังก์ชันนี้ที่สามารถรับสตริงและคืนค่ารหัสภาษา) เพื่อหาภาษาของคำนั้น ๆ และกำหนดค่าให้ lang
ซึ่งเป็นตัวแปรในอ็อบเจกต์ที่เรียกเมท็อดนี้
เนื่องจาก word
นั้นมีการถูกกำหนดค่าในตัวสร้างอยู่แล้ว บางครั้งเราอาจจะไม่กำหนดตัวแปรไว้ล่วงหน้านอกตัวสร้าง โค้ดอาจจะปรับเป็นดังตัวอย่างข้างล่าง โดยที่ไม่กระทบต่อการนำไปใช้จริง
class Token:
# ตัดเอา word ออกไป
pos = ''
ner = ''
lang = ''
is_first = False
lemma = ''
def __init__(self, word):
self.word = word
self.lang = detect_lang(word)
ตัวอย่างการใช้เมท็อดตัวสร้างแสดงในโค้ดด้านล่าง
my_token = Token('firm')
print(my_token.lang) # en
สังเกตว่าเวลาเราสร้างอ็อบเจกต์ของคลาส Token
เราใส่อาร์กิวเมนต์ 1 ตัวเพื่อส่งให้กับเมท็อดตัวสร้าง ไม่ใช่สองตัว (self
และ word
)
ในทำนองเดียวกันกับตัวแปรประจำคลาส เมท็อดในภาษาไพทอนไม่มีการแบ่งแยกระดับการเข้าถึง ทุกคลาส ทุกโปรแกรมสามารถเรียกใช้เมท็อดของคลาสอื่น ๆ ได้ และเมท็อดที่เรากำหนดขึ้นมาเองก็สามารถเรียกใช้เมท็อดของคลาสอื่น ๆ ได้เช่นกัน
สิ่งสืบทอด (inheritance)#
ตอนนี้ทุกคนอาจจะยังไม่ได้เห็นประโยชน์ของการเขียนโปรแกรมเชิงอ็อบเจกต์เท่าไรนัก เพราะว่าเราสามารถใช้ดิกชันนารีแทนคลาสได้ และเขียนโปรแกรมอย่างระมัดระวังไม่ให้เกิดการเข้าถึงข้อมูลผ่านคีย์ที่ไม่ได้มีอยู่ในดิกชันนารี และเราสามารถเขียนฟังก์ชันแทนการเขียนเมท็อดได้ ถ้าหากเราจัดระเบียบโค้ดดี ๆ ก็ทำให้เราหลีกเลี่ยงข้อผิดพลาดในการเขียนโปรแกรมได้
การใช้สิ่งสืบทอด (inheritance) เป็นรูปแบบการใช้งาน (feature) ที่ทำให้การเขียนโปรแกรมสะดวกขึ้น สิ่งสืบทอดเป็นการรับทอดลักษณะประจำและเมท็อดจากคลาสหนึ่งไปยังอีกคลาสหนึ่ง ซึ่งทำให้เราสามารถใช้โค้ดที่เขียนไว้แล้วมาใช้ใหม่ได้ คลาสที่เป็นซับคลาส (subclass) รับทอด (inherit) เอาลักษณะประจำ และเมท็อดจากคลาสที่เป็นซูเปอร์คลาส (superclass) และซับคลาสสามารถลบล้าง (override) เมท็อดที่สืบทอดมาให้เป็นไปตามต้องการได้ด้วย ดังนั้นเราพูดได้ว่าซับคลาสทำทุกอย่างที่ซูเปอร์คลาสทำได้ และอาจจะมีเมท็อดหรือลักษณะประจำอื่น ๆ เพิ่มเติมขึ้นมาได้อีกด้วย
ตัวอย่าง เช่น คลาสที่ชื่อว่าเคาน์เตอร์ (Counter
) ที่มากับไพทอน เป็นซับคลาสของคลาสที่ชื่อว่าดิกชันนารี (dict
) หมายความว่าเคาน์เตอร์มีตัวแปรและเมท็อดของดิกชันนารีทั้งหมด แต่สามารถปรับแก้เมท็อดที่สืบทอดมา และเพิ่มเมท็อดอื่น ๆ เพิ่มเติมเข้าไปได้อีกด้วย เพราะฉะนั้นหากเราทราบแล้วว่า Counter
เป็นซับคลาสของ dict
เราคิดไปก่อนได้เลยว่า Counter
จะต้องมีเมท็อด items
keys
values
update
clear
และเมท็อดอื่น ๆ ที่ dict
มี เพราะว่า Counter
รับทอดเมท็อดเหล่านี้มาจากซูเปอร์คลาสซึ่งก็คือ dict
นอกจากนั้นแล้วเนื่องจาก Counter
เป็นซับคลาสจึงอาจจะมีเมท็อดพิเศษเพิ่มเข้ามาอย่าง most_common
การกำหนดให้ Counter
เป็นซับคลาสของ dict
มีข้อดีคือ ทำให้ Counter
ใช้กลไกในการเก็บข้อมูลในลักษณะประจำ และกลไกการเข้าถึงข้อมูลได้เหมือน dict
ทุกประการ โดยที่ไม่ต้องคัดลอกโค้ดของ dict
มาทั้งหมด และยังสามารถปรับแก้เมท็อดที่สืบทอดมาได้อีกด้วย เช่น ลบล้างตัวสร้างที่สืบทอดมาเพื่อเปลี่ยนให้ตัวสร้างให้สามารถสร้างจากลิสต์ได้ ลบล้างเมท็อดการค้นหาแวลูจากคีย์เพื่อเปลี่ยนให้คืนค่าเป็น 0 ถ้าหากเรียกใช้คีย์ที่ไม่เคยปรากฏมาก่อน เป็นต้น
การกำหนดคลาสให้เป็นซับคลาสของอีกคลาสหนึ่งสามารถทำได้ตอนที่เขียนโค้ดส่วนที่กำหนดคลาส โดยใช้คียเวิร์ด class
ตามด้วยชื่อคลาสและใส่ชื่อซุปเปอร์คลาสในวงเล็บ ตามด้วย :
เช่น
class Counter(dict):
รูปแบบสำหรับการกำหนดคลาสและซับคลาสคือ
class ชื่อซับคลาส(ชื่อซูเปอร์คลาส):
ชื่อลักษณะประจำ
ตัวสร้าง (หากไม่เหมือนกับซูเปอร์คลาส)
เมท็อดเพิ่มเติมจากซูเปอร์คลาส
เมท็อดที่ต้องการลบล้างจากซูเปอร์คลาส
ในมุมมองของผู้ใช้คลาส เมื่อผู้ใช้ทราบว่าคลาสที่ใช้เป็นซับคลาสของคลาสอะไร ผู้ใช้ก็จะทราบทันทีว่าคลาสที่ต้องการใช้มีไว้ทำอะไร มีเมท็อดอะไรบ้าง ยกตัวอย่างเช่น Counter
และ dict
หากเราทราบแล้วว่าเคาน์เตอร์เป็นซับคลาสของดิกชันนารี เราสามารถคิดไปก่อนเลยได้ว่าเคาน์เตอร์มีความสามารถทำทุกสิ่งที่ดิกชันนารีสามารถทำได้ แต่ว่าอาจมีความแตกต่างกัน เช่น ผู้ใช้คลาสเคาน์เตอร์ต้องรู้เพียงแต่ว่าเคาน์เตอร์มีแวลูเป็นตัวเลขเท่านั้น และคีย์ที่ไม่ได้ปรากฏในเคาน์เตอร์จะมีแวลูเป็น 0 เสมอ ลักษณะดังกล่าวเราเรียกว่าการลบล้างเมท็อด หรือการกำหนดพฤติกรรมของเมท็อดที่สืบทอดมาจากซุปเปอร์คลาสใหม่ โดยการเขียนเมท็อดที่มีชื่อและพารามิเตอร์เหมือนกันในคลาสย่อย เพื่อเปลี่ยนการทำงานของเมท็อดนั้นให้เป็นไปตามที่ต้องการ
ในมุมมองของนักเขียนโปรแกรม การใช้สิ่งสืบทอดทำให้เราสามารถใช้โค้ดที่เขียนไว้แล้วมาใช้ใหม่ได้ และปรับแก้เฉพาะส่วนที่ต้องการเท่านั้น
ตัวอย่างการใช้สิ่งสืบทอด#
การเปลี่ยนให้เป็นโทเค็น เป็นกระบวนการแรกในการประมวลผลภาษาธรรมชาติ คือการตัดสตริงของข้อมูลภาษาให้เป็นหน่วยที่ชื่อว่าโทเค็น ซึ่งแปลว่าหน่วยที่เหมาะในการประมวลผล โทเค็นส่วนใหญ่แล้วจะคล้ายคลึงกับคำเป็นส่วนใหญ่ โทเค็นจะเป็นอะไรนั้นขึ้นอยู่กับผู้ที่วิเคราะห์และประมวลผลข้อความที่จะปรับให้โทเค็นเป็นลักษณะอย่างไร
ตัวอย่างเช่น หากเราต้องการสร้างตัวตัดคำภาษาไทย โดยการใช้รายการคำศัพท์เป็นเกณฑ์ในการตัด และถ้าหากว่าเราได้รับประโยคที่เราเคยตัดคำมาแล้ว ให้คืนค่าเป็นผลลัพท์เดิม เพื่อความรวดเร็ว เราอาจจะออกแบบคลาส ThaiTokenizer"
ขึ้นมาดังนี้
class ThaiTokenizer:
vocab_list = [x.strip() for x in open('thai-vocab.txt').readlines()]
cache = {}
def __init__(self):
pass
def tokenize(self, sentence):
if sentence in self.cache:
return self.cache[sentence]
segmented = segment(self.vocab_list, sentence)
self.cache[sentence] = segmented
return segmented
def add_more_vocab(word_list):
self.vocab_list.extend(word_list)
คลาสที่ชื่อว่า ThaiTokenizer
มีลักษณะประจำทั้งหมด 2 ตัว คือ vocab_list
และ cache
และมีเมท็อดที่ชื่อว่า tokenize
และ add_more_vocab
โดย tokenize
จะใช้ฟังก์ชันที่ชื่อว่า segment
(ไม่ได้ให้โค้ดของฟังก์ชันนี้มา) ในการตัดคำ และ add_more_vocab
จะใช้ในการเพิ่มคำศัพท์เข้าไปใน vocab_list
และเมท็อดตัวสร้าง __init__
จะไม่ทำอะไรเลย
เมื่อผู้ใช้ต้องการตัดคำสามารถทำได้ดังนี้
tokenizer = ThaiTokenizer()
print(tokenizer.tokenize('สวัสดีครับ')) # ['สวัสดี', 'ครับ']
สมมติว่าเราต้องการสร้างคลาสใหม่ที่มีความสามารถเพิ่มเติม คือสามารถตัดคำที่มีศัพท์ทางการแพทย์ เช่น ชื่อโรค ชื่อยา อยู่ เพื่อนำไปใช้ประมวลผลเอกสารที่มาจากโรงพยาบาล เราสังเกตว่าคลาสใหม่ที่เราต้องการสร้างนั้นที่จริงจะต้องมีความสามารถทั้งหมดของ ThaiTokenizer
เพียงแต่ว่าต้องมีคำศัพท์เพิ่มเติมใน vocab_list
เมื่อสังเกตได้ดังนี้ เราควรจะเขียนให้คลาสใหม่นี้เป็นซับคลาสของ ThaiTokenizer
ดังนี้
class MedicalThaiTokenizer(ThaiTokenizer):
def __init__(self, drug_name_list, disease_name_list):
self.vocab_list.extend(drug_name_list)
self.vocab_list.extend(disease_name_list)
self.drug_name_list = drug_name_list
self.disease_name_list = disease_name_list
def contains_medical_terms(self, token_list):
for token in token_list:
if token in self.drug_name_list or token in self.disease_name_list:
return True
return False
ในตัวอย่างนี้เราทำการลบล้างเมท็อดตัวสร้างของ ThaiTokenizer
ซึ่งสืบทอดมาจากซุปเปอร์คลาส โดยที่เมท็อดตัวสร้างของ MedicalThaiTokenizer
จะบังคับให้ผู้ใช้ระบุคำศัพท์ที่เป็นชื่อยา และชื่อโรคเพิ่มเติม ก่อนที่ตัวสร้างจะเพิ่มเข้าไปใน vocab_list
และเพิ่มตัวแปรที่เป็นลักษณะประจำอีกสองตัว คือ drug_name_list
และ disease_name_list
นอกจากนั้นเรายังเพิ่มเมท็อดที่ชื่อว่า contains_medical_terms
ซึ่งจะใช้ในการตรวจสอบว่าประโยคที่เราต้องการตัดคำมีคำที่เป็นชื่อยาหรือชื่อโรคหรือไม่
เมื่อผู้ใช้ต้องการใช้คลาสใหม่นี้สามารถทำได้ดังนี้
tokenizer = MedicalThaiTokenizer(['เพนนิซิลิน', 'แอสไพริน', 'ไวโคดิน'], ['เล็บโตสไปโรสิส', 'ไมเกรน'])
print(tokenizer.tokenize('เพนนิซิลินเป็นยาที่ใช้ในการรักษาโรคเบาหวาน')) # ['เพนนิซิลิน', 'เป็น', 'ยา', 'ที่', 'ใช้', 'ใน', 'การ', 'รักษา', 'โรค', 'เบาหวาน']
สังเกตว่าในโค้ดที่กำหนดคลาสเราไม่ได้เขียนเลยว่ามีเมท็อดที่ชื่อว่า tokenize
เนื่องจาก MedicalThaiTokenizer
เป็นซับคลาสของ ThaiTokenizer
และ ThaiTokenizer
มีเมท็อดที่ชื่อว่า tokenize
อยู่แล้ว จึงได้รับการสืบทอดลงมาถึง MedicalThaiTokenizer
ด้วย
ถึงแม้สิ่งสืบทอดทำให้สะดวกในการจัดระเบียบและนำโค้ดเก่ามาใช้เป็นระบบ ข้อเสียคือทำให้โค้ดทั้งหมดไม่ได้อยู่ในที่เดียวกัน เช่น ในกรณีข้างต้น ถ้าหากเราต้องการทราบว่า MedicalThaiTokenizer
มีเมท็อดอะไรบ้าง เราต้องไปเปิดโค้ดของซุปเปอร์คลาส ThaiTokenizer
ทำให้โค้ดกระจัดกระจาย ยากต่อการทำความเข้าใจในบางครั้ง โดยเฉพาะอย่างยิ่งถ้าหากเราไม่ระมัดระวังและทำให้ตระกูลของคลาสมีความซับซ้อนเกินไป เช่น LegalMedicalThaiTokenizer
เป็นซับคลาสของ MedicalThaiTokenizer
ซึ่งเป็นซับคลาสของ ThaiTokenizer
อีกครั้งหนึ่ง เพราะฉะนั้นบางเมท็อดของ LegalMedicalThaiTokenizer
อาจจะมาจาก
MedicalThaiTokenizer
หรือ ThaiTokenizer
ได้
สรุป#
ผู้เขียนโปรแกรมต้องตัดสินใจออกแบบว่า OOP นั้นเหมาะสมกับโจทย์หรือไม่ เพราะไพทอน ไม่ได้บังคับให้ใช้ OOP ในทุกสถานการณ์ แต่ก็มีคุณสมบัติหลักๆ ของ OOP อยู่ ข้อดีของการใช้ OOP คือช่วยให้โค้ดมีความสะอาด และง่ายต่อการอ่าน การแบ่งโปรแกรมเป็นส่วนย่อย ๆ ช่วยให้สามารถจัดการกับความซับซ้อนของโปรแกรมได้ดีขึ้น นอกจากนี้ยังช่วยในเรื่องของการนำโค้ดไปใช้ซ้ำ และการปรับเปลี่ยนโค้ดในอนาคตได้ง่ายขึ้น เพราะการแก้ไขหรือต่อเติมเมท็อดในคลาสหนึ่ง ๆ ไม่จำเป็นต้องกระทบกับส่วนอื่น ๆ ของโปรแกรม
อย่างไรก็ตาม ข้อเสียของการใช้ OOP คืออาจทำให้โค้ดมีความซับซ้อนเกินความจำเป็นในบางสถานการณ์ การออกแบบที่ไม่ดีอาจนำไปสู่โค้ดที่ยุ่งเหยิง และการอ้างอิงที่ซับซ้อน ซึ่งสามารถทำให้การบำรุงรักษาโค้ดเป็นเรื่องยาก
การตัดสินใจใช้ OOP ในภาษาไพทอน ควรขึ้นอยู่กับลักษณะของโปรเจกต์และทีมพัฒนา หากโปรเจกต์ต้องอาศัยความยืดหยุ่นในการจัดการกับออบเจกต์และข้อมูล หรือต้องการทำงานร่วมกับโค้ดที่มีอยู่แล้วซึ่งออกแบบมาในแบบ OOP การใช้แนวทางนี้อาจเป็นทางเลือกที่ดี แต่สำหรับโปรเจกต์ที่เรียบง่ายหรือทีมที่ไม่คุ้นเคยกับ OOP อาจพิจารณาใช้วิธีการเขียนโปรแกรมแบบอื่นที่ไพทอนรองรับได้เช่นกัน