9. คลาส (Classes)

9.1 ศัพท์ (A Word About Terminology)
9.2 สโคปและเนมสเปซ (Python Scopes and Name Spaces)
9.3 แรกพบคลาส (A First Look at Classes)
9.4 หมายเหตุเรื่องคลาส (Random Remarks)
9.5 การสืบทอดคลาส (Inheritance)
9.6 ตัวแปรเฉพาะที่ (Private Variables)
9.7 ปกิณกะ (Odds and Ends)
9.8 ตัวยกข้อผิดพลาดก็เป็นคลาส (Exceptions Are Classes Too)
9.9 ตัวกระทำซ้ำ (Iterators)
9.10 เจนเนอเรเตอร์ (Generators)
9.11 เจนเนอเรเตอร์เอกซ์เพรสชั่น (Generator Expressions)

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


9.1 ศัพท์ (A Word About Terminology)

เพื่อให้เกิดความสุขสวัสดีทั้งผู้เขียนและผู้อ่าน บทนี้จะใช้ทับศัพท์ให้มากที่สุดเท่าที่เป็นไปได้ :)


9.2 สโคปและเนมสเปซ (Python Scopes and Name Spaces)

ศัพท์สนุก ๆ

เนมสเปซ (Namespace)
เป็นรายชื่อของออบเจกต์ทั้งหมดในขอบเขตหนึ่ง ๆ เนมสเปซมีหลายระดับ ตั้งแต่ของทั้งโปรแกรมมาจนถึงเนมสเปซภายในออบเจกต์ ในทางปฏิบัติแล้วไพธอนเก็บไว้ในรูปดิกชันนารี ตัวอย่างของเนมสเปซคือ กลุ่มของชื่อบิลต์อิน (Built-in Names = built-in function + built-in exception) จะอยู่ภายใต้เนมสเปซเดียวกัน หรือ ชื่อส่วนรวม (Global Names = global function + global exception + global variables) ต่าง ๆ ของมอดูล ก็จะอยู่ในเนมสเปซของมัน การมีเนมสเปซทำให้ออบเจกต์ต่างเนมสเปซกันไม่ตีกันถึงแม้จะชื่อเดียวกัน เวลาเราอ้างถึงออบเจกต์ที่อยู่ลึกลงไป เราจึงต้องอ้างตามรายแอตทริบิวต์

เนมสเปซต่าง ๆ จะถูกสร้างขึ้นเมื่อออบเจกต์ถูกสร้าง และมีอายุตามออบเจกต์นั้น ๆ เช่น

  • เนมสเปซที่บรรจุชื่อบิลต์อิน (Built-in Name = built-in function + built-in exception) จะถูกสร้างขึ้นตั้งแต่เราเริ่มเรียกใช้โปรแกรมและคงอยู่จนกว่าโปรแกรมจะจบ
  • เนมสเปซส่วนรวมของมอดูล (Module Global Namespace) จะถูกสร้างขึ้นตั้งแต่มอดูลถูกอิมพอร์ตจนกว่าจะจบโปรแกรมเช่นกัน
  • ประโยคทั้งหมดที่ถูกรันโดยไพธอน ไม่ว่าจะเป็นการรันสคริปต์ไฟล์หรือพิมพ์สดในโหมดโต้ตอบก็ดี จะอยู่ภายใต้เนมสเปซของมอดูล __main__
  • ชื่อบิลต์อิน (Built-in Names = built-in function + built-in exception) จะอยู่ภายใต้เนมสเปซของมอดูล __builtin__
  • เวลาฟังก์ชันถูกเรียกใช้งาน มันจะสร้างเนมสเปซของมันขึ้นมา แล้วถูกลบทิ้งไปเมื่อทำงานเสร็จ การสร้างเนมสเปซจะเกิดทุกครั้งที่เรียกฟังก์ชัน แม้ในการเรียกตัวเองของฟังก์ชัน

อธิบายตามภาษาชาวบ้าน ชื่อ (Names) ก็คืออะไรที่เราต้องตั้งชื่อให้มัน เวลาเรียกก็เรียกจากชื่อ เช่นฟังก์ชัน ตัวแปร เป็นต้น ส่วนเนมสเปซ (Namespace) ก็คือห้องบรรจุชื่อนั่นเอง

แอตทริบิวต์ (Attribute)
ก็คือชื่อที่อยู่ภายในเนมสเปซหนึ่ง ๆ อ้างด้วยเนมสเปซ ตามด้วยจุด (.) ตามด้วยชื่อแอตทริบิวต์ เช่น z.real เราเรียก real ว่าเป็นแอตทริบิวต์ของออบเจกต์ z อะไรก็ตามที่อยู่ในระดับเดียวกับ real คืออ้างถึงด้วย z.XXX เราจะเรียกว่าอยู่ภายใต้เนมสเปซเดียวกัน (ถ้าชื่อซ้ำก็ตีกัน)

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

สโคป (Scope)
หมายถึงช่วงที่เนมสเปซสามารถเข้าถึงได้โดยตรง โดยไม่ต้องอ้างอิงไล่แอตทริบิวต์ไปถึงออบเจกต์ที่อยู่ในเนมสเปซอื่น

ในทุก ๆ ขณะของการทำงาน จะมีอย่างน้อยสามสโคปเสมอ คือ

  • สโคปในสุด (ซึ่งจะถูกค้นก่อน) บรรจุตัวแปรท้องถิ่นของฟังก์ชัน และอาจมีเนมสเปซของฟังก์ชันที่ครอบอยู่ ในกรณีที่มีการกำหนดฟังก์ชันซ้อนในฟังก์ชัน
  • สโคปชั้นกลาง บรรจุชื่อตัวแปรและฟังก์ชันส่วนรวมของมอดูล
  • สโคปชั้นนอกสุด บรรจุพวกชื่อบิลติอิน (Built-in Names = built-in function + built-in exception)

เรื่องสนุก ๆ

  • ถ้าเรากำหนดชื่อ (Name) ของเราให้เป็นชื่อส่วนรวม (global) ทุกออบเจกต์จะเห็นในสโคปชั้นกลางเสมอ
  • ตัวแปรอื่น ๆ ที่อยู่นอกสโคปในสุดที่ไม่ได้ประกาศให้เป็นชื่อส่วนรวม จะเป็นตัวแปรที่อ่านได้อย่างเดียว (ถ้าพยายามจะเขียนทับ จะถือเป็นการสร้างตัวแปรใหม่ในเนมสเปซชั้นในสุด)
  • โดยปกติแล้ว สโคปท้องถิ่น (local scope) จะอ้างถึงชื่อต่าง ๆ ที่อยู่ในฟังก์ชันปัจจุบัน ถ้าอยู่นอกฟังก์ชันแล้ว สโคปท้องถิ่นจะอ้างเนมสเปซเดียวกับสโคปส่วนรวม (global scope) ส่วนการนิยามคลาส จะเป็นการสร้างเนมสเปซครอบทับสิ่งที่อยู่ในคลาสอีกชั้นหนึ่ง

--รอแปล--


9.3 แรกพบคลาส (A First Look at Classes)

9.3.1 โครงสร้างคลาส (Class Definition Syntax)

โครงคือ

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

--รอแปล--

9.3.2 ออบเจกต์ที่เป็นคลาส (Class Objects)

จะใช้งานหรือเข้าถึงคลาสออบเจกต์ได้สองแบบ คือ

เข้าถึงแบบอ้างตามแอตทริบิวต์ (Attribute references)
ดูโค้ด
class MyClass:
    "A simple example class"
    i = 12345
    def f(self):
        return 'hello world'

เราสามารถเข้าถึงคลาสนี้ดังนี้

>>> MyClass.i
12345

>>> MyClass.f
<unbound method MyClass.f>

>>> MyClass.__doc__
'A simple example class'

>>> MyClass.i = 2
>>> MyClass.i
2

>>> MyClass.__doc__ = "Modified docstring"
>>> MyClass.__doc__
'Modified docstring'
การสร้างอินสแตนซ์ (instantiation)
ใช้รูปแบบธรรมชาติเหมือนกับการสร้างฟังก์ชัน
x = MyClass()

เป็นการสร้างอินสแตนซ์ซึ่งเป็นออบเจกต์ที่ถูกบรรจุอยู่ในตัวแปร x

หากต้องการออบเจกต์ที่ต้องมีการถูกเตรียมการในครั้งแรก ต้องใส่เมธอดพิเศษชื่อ __init__() ลงในการนิยามคลาสด้วย

def __init__(self):
        self.data = []

พอสร้างออบเจกต์แล้ว เราจะได้ผลของการรันเมธอด __init__() มาด้วย เช่น

>>> x=MyClass()
>>> x.data
[]

ถ้าต้องการใส่พารามิเตอร์ให้กับคลาส ก็ต้องใส่ในเมธอด __init__() นี้เอง เช่น

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
... 
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
9.3.3 ออบเจกต์ที่เป็นอินสแตนซ์ (Instance Objects)

นิยามว่ามันมีแอตทริบิวต์สองแบบ คือ แอตทริบิวต์ที่เป็นข้อมูล และ เมธอด

แอตทริบิวต์ที่เป็นข้อมูล (Data Attributes)
ก็คือตัวแปรของอินแสตนซ์ออบเจกต์นั่นเอง ดังนั้นจึงสามารถใช้งานแบบตัวแปร คือกำหนดค่าได้ทันที จากตัวอย่างข้างล่างนี้ จะได้ผลลัพธ์คือ 16
x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print x.counter
del x.counter
เมธอด (Method)
ก็คือฟังก์ชันของอินสแตนซ์ออบเจกต์นั่นเอง จากตัวอย่าง x.f คือเมธอดของอินสแตนซ์ ส่วน MyClass.f คือฟังก์ชันของคลาส
9.3.4 ออบเจกต์ที่เป็นเมธอด (Method Objects)

จากตัวอย่างคือ x.f() เรียกว่าเป็นเมธอด

เราอาจอ้างถึงเมธอดผ่านตัวแปรได้

xf = x.f
while True:
    print xf()

ตัวอย่างนี้จะพิมพ์ "hello world" ไปเรื่อย จนกว่าจะกดขัดจังหวะ

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

แต่พารามิเตอร์ (arguments) ของเมธอดจะต่างไปจากฟังก์ชันปกติเล็กน้อย เพราะเมธอดเป็นฟังก์ชันของอินสแตนซ์ของคลาส ดังนั้นพฤติกรรมของเมธอดคือ เมื่อเราเรียกใช้เมธอดว่า x.f() จริง ๆ แล้วมันคือการที่เราเรียกว่า MyClass.f(x) นี่คือเหตุที่ต้องกำหนดตัวแปรพิเศษ self ในตอนนิยามคลาส


9.4 หมายเหตุเรื่องคลาส (Random Remarks)

  • แอตทริบิวต์ข้อมูลจะสำคัญกว่าแอตทริบิวต์ที่เป็นเมธอดเสมอ แปลว่าถ้าตั้งชื่อตัวแปรกับฟังก์ชันซ้ำกัน ชื่อตัวแปรจะสำคัญกว่า ดังนั้นเราจึงควรตระหนักและหาทางป้องกันข้อผิดพลาดนี้ อาจจะด้วยการตั้งชื่อเมธอดให้เป็นอักษรตัวใหญ่นำหน้า หรือนำหน้าตัวแปรด้วยเครื่องหมาย "_" หรืออื่น ๆ ที่เคยชิน
  • ไพธอนไม่ได้ปกป้องตัวแปรในอินสแตนซ์อย่างสมบูรณ์แบบ ใคร ๆ ก็สามารถเปลี่ยนค่ามันได้ กล่าวคือ ไม่รองรับการซ่อนข้อมูล (data hiding) แต่อย่างใด หากต้องการคุณสมบัตินี้จริง ๆ อาจต้องเขียนส่วนขยายด้วยภาษาซี
  • เป็นหน้าที่ของเราเองที่ต้องใช้แอตทริบิวต์ข้อมูลด้วยความระมัดระวัง ควรตั้งข้อกำหนดให้แม่น ในเรื่องการตั้งชื่อ อาจลดความผิดพลาดได้
  • ไม่สามารถอ้างถึงแอตทริบิวต์ของออบเจกต์แบบสั้นจากภายในตัวเมธอดได้ ไม่ว่าจะเป็นแอตทริบิวต์ข้อมูลหรือแอตทริบิวต์ที่เป็นเมธอด ซึ่งปรากฏว่าทำให้อ่านโค้ดง่าย คือจะไม่เกิดความสับสนระหว่างแอตทริบิวต์ข้อมูลกับตัวแปรท้องถิ่น
  • อาร์กิวเมนต์ตัวแรกของเมธอดมักชื่อ self แต่ก็ไม่ใช่ข้อบังคับอะไร เป็นเพียงข้อตกลงเท่านั้น แต่ใช้ดีกว่าไม่ใช้ เพราะเพื่อให้เป็นนิสัยแห่งการทำตามมาตรฐาน มีผลให้คนอื่นอ่านโค้ดเราง่ายขึ้น
  • เนื่องจากไพธอนแทนค่าตัวแปรได้อย่างไม่มีข้อจำกัด ดังนั้นจึงสามารถเขียนโค้ดให้เอาฟังก์ชันจากภายนอก มาเป็นฟังก์ชันภายในคลาสได้ แต่อันนี้เป็นตัวอย่างที่ไม่ดีมาก ๆ เพราะอ่านโค้ดแล้วสับสน
    # Function defined outside the class
    def f1(self, x, y):
        return min(x, x+y)
    
    class C:
        f = f1
        def g(self):
            return 'hello world'
        h = g

    คลาส C สามารถเรียกใช้งานเมธอดแอตทริบิวต์ C.f(x,y) ได้

    >>> x = C()
    >>> x.f(1,2)
    1
  • เมธอดอาจเรียกใช้เมธอดอื่นในคลาสเดียวกันได้ด้วยการใช้ self นำหน้าเมธอดที่จะเรียก
    class Bag:
        def __init__(self):
            self.data = []
        def add(self, x):
            self.data.append(x)
        def addtwice(self, x):
            self.add(x)
            self.add(x)


9.5 การสืบทอดคลาส (Inheritance)

ถ้าสืบทอดไม่ได้ก็ไม่ใช่คลาส รูปแบบโครงสร้างของการสืบทอดคือ

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

ถ้าคลาสฐานถูกกำหนดไว้ที่มอดูลอื่น รูปแบบจะเป็น

class DerivedClassName(modname.BaseClassName):

คลาสใหม่ที่แตกออกมานี้ สามารถสร้างเมธอดเพื่อครอบงำเมธอดเดิมได้อย่างไม่มีข้อจำกัด โดยที่ถ้าสร้างขึ้นมาแล้ว เมื่อมีการเรียกเมธอดจะเรียกไปยังเมธอดใหม่แทน โค้ดภายใต้เมธอดเดิมจะไม่ถูกเรียก แต่หากยังต้องการเรียกโค้ดจากเมธอดเดิมอยู่ เราต้องเรียกใช้เองในรูปแบบ BaseClassName.methodname(self, arguments)

9.5.1 การสืบทอดหลายทาง (Multiple Inheritance)

รูปแบบโครงสร้างคือ

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

ลำดับการค้นหาแอตทริบิวต์ในอินสแตนซ์ของ DerivedClassName ก็คือ ถ้าพบแอตทริบิวต์ใน DerivedClassName ก็จะใช้เลย แต่ถ้าไม่พบก็จะหาใน Base1 และคลาสฐานทั้งหมดของ Base1 ลึกลงไปจนสุด และถ้ายังไม่พบจึงมาเริ่มต้นค้นจาก Base2 ต่อไปเรื่อย ๆ จนหมด

(บางคนอาจคิดว่าน่าจะไล่ไป Base1 - Base2 - Base3 แล้วจึงย้อนมาหาฐานของ Base1 อีกที แต่การไล่แบบนั้นจะทำให้คุณต้องแยกแยะก่อน ว่าแอตทริบิวต์เจ้าปัญหานั้นกำหนดไว้ใน Base1 หรือคลาสฐานของ Base1 ก่อนที่จะตรวจสอบว่าชนกับชื่อใน Base2 หรือไม่ ซึ่งเท่ากับเป็นการตัดทอนสายตระกูลตามปกติของคลาสออกเป็นส่วน ๆ ในขณะที่การค้นหาแบบลงลึกทีละสายจะไม่มีความแตกต่างตรงนี้)


9.6 ตัวแปรส่วนตัว (Private Variables)

ใช้หลักแค่ว่านำหน้าชื่อตัวแปรด้วย underscore สองตัว เช่น __spam ตัวแปรนั้นจะกลายเป็นตัวแปรส่วนตัวของคลาสนั้นเอง ไม่สามารถถูกเรียกจากที่อื่นในรูป X.__spam ได้

>>> class C:
...   __spam = 5
...   s = 6
... 
>>> a = C()
>>> a.s
6

>>> a.__spam
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
AttributeError: C instance has no attribute '__spam'

แต่ไพธอนก็ยังไม่ทำให้เป็นส่วนตัวจริง ๆ อยู่ดี เพราะอาจถูกเรียกในรูปของ X._classname__spam ได้

>>> a._C__spam
5

>>> dir(a)
['_C__spam', '__doc__', '__module__', 's']


9.7 ปกิณกะ (Odds and Ends)

อาจเติมแอตทริบิวต์ข้อมูลให้กับคลาสอินสแตนซ์ได้ทุกเมื่อ

class Employee:
    pass

john = Employee() # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000


9.8 ตัวยกข้อผิดพลาดก็เป็นคลาส (Exceptions Are Classes Too)

ดังนั้น เราสามารถสร้างตัวยกข้อผิดพลาดแบบซ้อนลึกลงไปเรื่อย ๆ
เขียนได้สองรูปแบบคือ

  • raise Class, instance

    instance ในที่นี้ คืออินสแตนซ์ของ Class หรือคลาสลูก

  • อีกอันคือ
    raise instance

    ซึ่งถ้าเขียนแบบเต็ม ๆ ต้องเขียนว่า

    raise instance.__class__, instance

แต่ต้องระวังการดักตอนแตกลูกแตกหลานคลาส เพราะถ้าดักพบคลาสแม่ก่อน เขาจะถือว่าดักได้แล้ว และจะเอาขึ้นเลย ลองดู

>>> class B:
...     pass
... 
>>> class C(B):
...     pass
... 
>>> class D(C):
...     pass
... 
>>> for c in [B, C, D]:
...     try:
...         raise c()
...     except D:
...         print "D"
...     except C:
...         print "C"
...     except B:
...         print "B"
... 
B
C
D

>>> for c in [B, C, D]:
...     try:
...         raise c()
...     except C:
...         print "C"
...     except B:
...         print "B"
...     except D:
...         print "D"
... 
B
C
C

เวลายกข้อผิดพลาดขึ้นแสดง รูปแบบคือ

Exception_Class: str(instance)


9.9 ตัวกระทำซ้ำ (Iterators)

ลองดูตัวอย่าง for

for element in [1, 2, 3]:
    print element
for element in (1, 2, 3):
    print element
for key in {'one':1, 'two':2}:
    print key
for char in "123":
    print char
for line in open("myfile.txt"):
    print line

เบื้องหน้าก็ดูง่าย ๆ ดี เราลองมาดูเบื้องลึกบ้าง
ขั้นตอนคือ เมื่อไพธอนพบคำสั่ง for เขาจะไปเรียกเมธอด iter() ของออบเจกต์นั้น ซึ่งจะคืนค่าเป็นออบเจกต์ที่มีเมธอด next() ออกมา และจะเรียกซ้ำไปเรื่อย จนเมื่อหมดแล้ว เมธอด next() จะยกข้อผิดพลาดชื่อ StopIteration ขึ้นมาบอกให้รู้ว่าพอแล้ว ลองดูตัวอย่าง

>>> s = 'abc'
>>> it = iter(s)
>>> it
<iterator object at 0x00A1DB50>
>>> it.next()
'a'

>>> it.next()
'b'

>>> it.next()
'c'

>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
    it.next()
StopIteration

เมื่อรู้เบื้องลึกแล้ว เราก็สามารถแปลงพฤติกรรมของการทำซ้ำได้ ด้วยการนิยามเมธอด __iter__() และ next() ในคลาสของเราใหม่

class Reverse:
    "Iterator for looping over a sequence backwards"
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def next(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]

รันได้ว่า

>>> for char in Reverse('spam'):
...     print char
...
m
a
p
s


9.10 เจนเนอเรเตอร์ (Generators)

เป็นอีกตัวนึงที่ใช้สร้างตัวกระทำซ้ำ โดยใช้ประโยค yield ซึ่งพิเศษตรงที่ว่ามันสามารถจำข้อมูลและสถานะจากครั้งก่อนที่เคยรันได้ พอถูกเรียกจาก next() เมื่อไหร่ มันจะกลับไปทำงานด้วยสถานะจากครั้งก่อนทันที

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]

รันได้ว่า

>>> for char in reverse('golf'):
...     print char
...
f
l
o
g

ทำให้เขียนโค้ดได้สั้น แต่อาจอ่านยากนิดนึง แต่ถ้าใช้คล่องแล้วจะประหยัดโค้ดไปได้เยอะ เพราะไม่ต้องมานั่งเขียนพวก self.index และ self.data เอง


9.11 เจนเนอเรเตอร์เอกซ์เพรสชั่น (Generator Expressions)

รูปแบบเหมือน ลิสต์คอมพรีเฮนชั่น (list comprehension) แต่ใช้วงเล็บธรรมดาแทน เขียนและอ่านโค้ดง่าย และประหยัดหน่วยความจำ

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> from math import pi, sin
>>> sine_table = dict((x, sin(x*pi/180)) for x in range(0, 91))

>>> unique_words = set(word  for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1,-1,-1))
['f', 'l', 'o', 'g']
Taxonomy upgrade extras: 
Creative Commons License ลิขสิทธิ์ของบทความเป็นของเจ้าของบทความแต่ละชิ้น
ผลงานนี้ ใช้สัญญาอนุญาตของครีเอทีฟคอมมอนส์แบบ แสดงที่มา-อนุญาตแบบเดียวกัน 3.0 ที่ยังไม่ได้ปรับแก้