บทที่ 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 อย่าง คือ

  1. ลักษณะประจำ (attribute)

  2. เมท็อด (method) ซึ่งเป็นโค้ดที่ใช้ประมวลผลลักษณะประจำคลาส

  3. สิ่งสืบทอด (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 อาจพิจารณาใช้วิธีการเขียนโปรแกรมแบบอื่นที่ไพทอนรองรับได้เช่นกัน