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____builtin__อธิบายตามภาษาชาวบ้าน ชื่อ (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.counterx.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>
ลำดับการค้นหาแอตทริบิวต์ในอินสแตนซ์ของ DerivedClassName ก็คือ ถ้าพบแอตทริบิวต์ใน DerivedClassName ก็จะใช้เลย แต่ถ้าไม่พบก็จะหาใน Base1 และคลาสฐานทั้งหมดของ Base1 ลึกลงไปจนสุด และถ้ายังไม่พบจึงมาเริ่มต้นค้นจาก Base2 ต่อไปเรื่อย ๆ จนหมด
(บางคนอาจคิดว่าน่าจะไล่ไป Base1 - Base2 - Base3 แล้วจึงย้อนมาหาฐานของ Base1 อีกที แต่การไล่แบบนั้นจะทำให้คุณต้องแยกแยะก่อน ว่าแอตทริบิวต์เจ้าปัญหานั้นกำหนดไว้ใน Base1 หรือคลาสฐานของ Base1 ก่อนที่จะตรวจสอบว่าชนกับชื่อใน Base2 หรือไม่ ซึ่งเท่ากับเป็นการตัดทอนสายตระกูลตามปกติของคลาสออกเป็นส่วน ๆ ในขณะที่การค้นหาแบบลงลึกทีละสายจะไม่มีความแตกต่างตรงนี้)
ใช้หลักแค่ว่านำหน้าชื่อตัวแปรด้วย 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']