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)
คลาสในไพธอนถูกออกแบบมาให้ใช้งานง่าย มันเลยไม่ได้ป้องกันแน่นหนาแบบภาษาอื่น
อย่างไรก็ตามมันก็ยังมีคุณสมบัติของคลาสอย่างที่ควรเป็น
เพื่อให้เกิดความสุขสวัสดีทั้งผู้เขียนและผู้อ่าน บทนี้จะใช้ทับศัพท์ให้มากที่สุดเท่าที่เป็นไปได้ :)
ศัพท์สนุก ๆ
เนมสเปซต่าง ๆ จะถูกสร้างขึ้นเมื่อออปเจคต์ถูกสร้าง และมีอายุตามออปเจคต์นั้น ๆ เช่น
__main__
__buildin__
อธิบายตามภาษาชาวบ้าน เนม(Names) ก็คืออะไรที่เราต้องตั้งชื่อให้มัน เวลาเรียกก็เรียกจากชื่อ เช่นฟังก์ชั่น ตัวแปร เป็นต้น ส่วนเนมสเปซ(Namespace) ก็คือห้องบรรจุเนมนั่นเอง
z.real
เราเรียก real
ว่าเป็นแอตทริบิวต์ของออปเจคต์ z
อะไรก็ตามที่อยู่ในระดับเดียวกับ real
คืออ้างถึงด้วย z.XXX
เราจะเรียกว่าอยู่ภายใต้เนมสเปซเดียวกัน (ถ้าชื่อซ้ำก็ตีกัน)
แอตทรีบิวต์ อาจเป็นได้ทั้งอ่านอย่างเดียวและเขียนได้ด้วย ถ้าเป็นแบบเขียนได้ เราก็สามารถเปลี่ยนแปลงค่าได้ และใช้ประโยค del
ในการลบแอตทริบิวต์นั้นได้
เรื่องสนุก ๆ
--รอแปล--
โครงคือ
class ClassName: <statement-1> . . . <statement-N>
--รอแปล--
จะใช้งานหรือเข้าถึงคลาสออปเจคต์ได้สองแบบ คือ
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'
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)
นิยามว่ามันมีสองแอตทริบิวต์เนม คือ แอตทริบิวต์ที่เป็นข้อมูล และ เมธอด
x.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print x.counter del x.counter
x.f
คือเมธอดของอินสแตนซ์ ส่วน MyClass.f
คือฟังก์ชั่นของคลาสจากตัวอย่างคือ x.f()
เรียกว่าเป็นเมธอด
เราอาจอ้างถึงเมธอดผ่านตัวแปรได้
xf = x.f while True: print xf()
ตัวอย่างนี้จะพิมพ์ "hello world" ไปเรื่อย จนกว่าจะกดขัดจังหวะ
เมธอดสามารถใช้งานพารามิเตอร์ได้เหมือนฟังก์ชั่นปกติ เวลาเรียกใช้งานก็ต้องใส่พารามิเตอร์ให้ครบเช่นกัน ไม่งั้นไพธอนจะยกข้อผิดพลาดขึ้นแสดง
แต่พารามิเตอร์ (arguments) ของเมธอดจะต่างไปจากฟังก์ชั่นปกติเล็กน้อย เพราะเมธอดเป็นฟังก์ชั่นของอินสแตนซ์ของคลาส ดังนั้นพฤติกรรมของเมธอดคือ เมื่อเราเรียกใช้เมธอดว่า x.f()
จริง ๆ แล้วมันคือการที่เราเรียกว่า MyClass.f(x)
นี่คือเหตุที่ต้องกำหนดตัวแปรพิเศษ self
ในตอนนิยามคลาส
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)
ถ้าสืบทอดไม่ได้ก็ไม่ใช่คลาส รูปแบบโครงสร้างของการสืบทอดคือ
class DerivedClassName(BaseClassName): <statement-1> . . . <statement-N>
ถ้าคลาสฐานถูกกำหนดไว้ที่มอดูลอื่น รูปแบบจะเป็น
class DerivedClassName(modname.BaseClassName):
คลาสใหม่ที่แตกออกมานี้ สามารถสร้างเมธอดเพื่อครอบงำเมธอดเดิมได้อย่างไม่มีข้อจำกัด โดยที่ถ้าสร้างขึ้นมาแล้วโค๊ดภายใต้เมธอดเดิมจะไม่ถูกเรียก แต่หากยังต้องการเรียกโค๊ดจากเมธอดเดิมอยู่ เราต้องเรียกใช้เองในรูปแบบ BaseClassName.methodname(self, arguments)
รูปแบบโครงสร้างคือ
class DerivedClassName(Base1, Base2, Base3): <statement-1> . . . <statement-N>
ลำดับการทำงานก็คือ ถ้าพบคลาส Base1
ตรง ๆ ก็จะสืบทอดจาก Base1
เลย แต่ถ้าไม่พบก็จะควานหาคลาสฐานของ Base1
ลึกลงไปจนสุด และถ้ายังไม่พบจึงมาเริ่มต้นค้นจาก Base2
ต่อไปเรื่อย ๆ จนหมด
(ลำดับการทำงานแบบนี้จะขัดธรรมชาติสักหน่อย คือจริง ๆ แล้วน่าจะไล่ไป Base1 - Base2 - Base3 แล้วจึงย้อนมาหาฐานของ Base1 อีกที ซึ่งดูจะมีประโยชน์กว่า ด้วยเหตุผลดังกล่าวนี้เอง จึงควรใช้ด้วยความระมัดระวัง เพราะมีโอกาสผิดพลาดมาก)
ใช้หลักแค่ว่านำหน้าชื่อตัวแปรต้วย 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']
อาจเติมแอตทริบิวต์ข้อมูลให้กับคลาสอินสแตนซ์ได้ทุกเมื่อ
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
ดังนั้น เราสามารถสร้างตัวยกข้อผิดพลาดแบบซ้อนลึกลงไปเรื่อย ๆ
เขียนได้สองรูปแบบคือ
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)
ลองดูตัวอย่าง 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
เป็นอีกตัวนึงที่ใช้สร้างตัวกระทำซ้ำ โดยใช้ประโยค 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
เอง
รูปแบบเหมือน ลิสต์คอมพรีเฮนชั่น (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']