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

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

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

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

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

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

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

เรื่องสนุก ๆ

  • ถ้าเรากำหนดเนม (Name) ของเราให้เป็นแบบกลอบอล (global) ทุกออปเจคต์จะเห็นเป็นสโคปชั้นกลางเสมอ
  • ถ้าไม่ได้เป็นตัวแปรที่อยู่ในระดับเดียวกันหรือระดับลูก จะเป็นตัวแปรที่อ่านได้อย่างเดียว (ถ้าพยายามจะเขียนทับ จะถือเป็นการสร้างตัวแปรใหม่ในเนมสเปซนั้น ๆ)
  • ถ้านับเฉพาะในฟังก์ชั่น เนม(Names)ทั้งหมดภายในจะถึอว่าเป็นสโคปท้องถิ่น (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)

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

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

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


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']