วิธีคิดแบบนักวิทยาการคอมพิวเตอร์/Classes and methods

บทที่ 14 Classes and methods

แก้ไข

14.1 Object-oriented features

แก้ไข

Python คือ object-oriented programming language ซึ่งหมายถึงว่ามันสนับสนุน features ของ object-oriented programming. มันไม่ง่ายนักที่จะจำกัดความของ object-oriented programming แต่เราสามารถดูลักษณะเฉพาะตัวบางอย่างได้

• โปรแกรมที่สร้างโดยใช้ definitions แบบ object, function และ แบบอื่นๆ เป็นการแสดงความคิดที่อยู่บนพื้นฐานของ object

• Object definition แต่ละอันจะมีลักษณะเช่นเดียวกับวัตถุบางอย่าง หรือแนวคิดในโลกแห่งความเป็นจริงและฟังก์ชันที่มีผลบน object นั้นก็จะมีลักษณะเช่นเดียวกับวัตถุในโลกของความเป็นจริง

ตัวอย่างเช่นคลาส Time ที่เรา defined ไว้ในบทที่ 13 มีลักษณะเช่นเดียวกับเวลาที่ผู้คน ทั่วไปใช้ในแต่ละวัน และฟังก์ชันที่เรา defined จะมีลักษณะเป็นความคิดของผู้คนที่มีต่อเวลา เหมือนกับกับคลาส Point และ Rectangle มีลักษณะเหมือนกับแนวคิดในทางคณิตศาสตร์ในเรื่องของจุดและสี่เหลี่ยมผืนผ้า

ที่ผ่านมาเราไม่ได้ใช้ข้อดีของ features ใน Python ที่สนับสนุน object-oriented programming เหตุเพราะ features นี้ยังไม่จำเป็นสำหรับที่ผ่านมา

เป็นต้นว่าในโปรแกรม Time มันไม่ชัดเจนที่จะเชื่อมระหว่าง class definition และ function definition ในบางการทดสอบแสดงให้เห็นว่าจะต้องใส่ Time object อย่างน้อยหนึ่งตัวเป็นค่าparameter

การสังเกตนี้เป็นที่มาของ methods เราได้เห็น method บางอย่างไปแล้ว อย่างเช่น keys และ values ที่เราเรียกใช้ใน dictionaries แต่ละ method จะขึ้นอยู่กับคลาสและเรียกใช้โดยตรงจากคลาสเท่านั้น

Methods เหมือนกับฟังก์ชัน แต่มี 2 จุดที่ต่างกันคือ

• Methods ถูก define ภายในคลาส

• Syntax ในการเรียกใช้ method ต่างจาก syntax ในการเรียกใช้ฟังก์ชัน

ในอีก 2-3 section ถัดไป เราจะนำเอาฟังก์ชันจาก 2 บท ก่อนหน้านี้มาแปลงให้เป็น Method คุณสามารถทำตามขึ้นตอนง่ายๆ ในการแปลงอย่างเป็นลำดับขั้นตอน หรือถ้าคุณสะดวกกว่าที่จะใช้วิธีอื่นๆ ก็สามารถทำได้ตามความต้องการ

14.2 printTime

แก้ไข

ในบทที่ 13 เรากำหนดคลาสที่ชื่อว่า Time และเขียนฟังก์ชันที่ชื่อว่า printTime ซึ่งมีลักษณะดังนี้

        	class Time:
  	pass
        	def printTime(time):
   	print str(time.hours) + ":" + \

str(time.minutes) + ":" + \

str(time.seconds)

เมื่อเรียกฟังก์ชันเราจะส่งค่า Time object เป็น parameter

>>> currentTime = Time()

>>> currentTime.hours = 9

>>> currentTime.minutes = 14

>>> currentTime.seconds = 30

>>> printTime(currentTime)

ในการสร้าง printTime method เราก็แค่ย้าย function definition มาอยู่ภายใน class definition

        	class Time:

def printTime(time):

print str(time.hours) + ":" + \

str(time.minutes) + ":" + \

 		str(time.seconds)

ในตอนนี้เราเรียกใช้ printTime โดยใช้ dot notation

>>> currentTime.printTime()

มันเป็นรูปแบบที่แน่นอนว่า object ที่มีการเรียกใช้ method จะอยู่หน้าจุด และชื่อ method จะอยู่หลังจุด

Object ซึ่งเรียกใช้ method จะถูกกำหนดให้เป็น parameter ตัวแรกที่ส่งเข้าไปยัง method ดังนั้นในกรณี currentTime จะถูกกำหนดให้เป็น parameter time

เมื่อความสะดวก parameter ตัวแรกของ method จะเรียกว่า self แม้มันจะดูยุ่งยากแต่มันถือว่าเป็นการใช้การอุปมาได้ดีมาก

Syntax ในการเรียกฟังก์ชัน printTime (currentTime) ฟังก์ชันทำหน้าที่เหมือนตัวแทนมันจะพูดว่า “Hey printTime! นี่คือ object ที่คุณจะต้องพิมพ์ค่า”

ใน object-oriented programming ตัวอย่าง object จะเป็นตัวแทนทำหน้าที่ ขอร้อง เช่นใน currentTime.printTime ( ) มันจะพูดว่า “ Hey currentTime ! กรุณาพิมพ์ค่าของตัวเอง ”

การเปลี่ยนแปลงในทัศนคตินี้อาจสุภาพกว่า แต่มันไม่เห็นได้อย่างชัดเจนว่ามันมีประโยชน์เป็นต้นว่า สิ่งที่เราเห็นอาจไม่เป็นเช่นนี้ แต่บางครั้งการเปลี่ยนการใช้งานจากฟังก์ชั่นไปเป็น object ทำให้ต้องเขียนฟังก์ชันที่หลากหลายกว่า และต้องทำให้มันบำรุงรักษาและนำกลับมาใช้ได้ง่าย

14.3 Another example

แก้ไข

ลองเปลี่ยน increment ( จาก section 13.3 ) เป็น method เพื่อให้มันดูสั้นลง เราจึงตัดการ defined method ก่อนหน้านี้ แต่ใน version ที่คุณเขียนจะต้องมี

class Time:

   #previous method definitions here...
   def increment(self, seconds):
         self.seconds = seconds + self.seconds

while self.seconds >= 60:

         self.seconds = self.seconds - 60
                                    self.minutes = self.minutes + 1

while self.minutes >= 60:

                                    self.minutes = self.minutes - 60
                                    self.hours = self.hours + 1

ในการเปลี่ยนรูปนี้ เราต้องย้าย method definition มาใส่ใน class definition และเปลี่ยนชื่อของ parameter ตัวแรก ตอนนี้เราสามารถเรียกใช้ increment ที่เป็น method ได้แล้ว

currentTime.increment(500)

อีกครั้งที่ object ที่เรียกใช้ method จะถูกกำหนดให้เป็น parameter ตัวแรก ซึ่งก็คือ self และ parameter ตัวที่ 2 จะรับค่า 500

14.4 A more complicated example

แก้ไข

ฟังก์ชัน after มันซับซ้อนกว่าเล็กน้อย เพราะมันทำงานกับ Time object 2 ตัว ไม่ใช่หนึ่งตัว และเราสามารถเปลี่ยน parameter เป็น self ได้เพียงตัวเดียวเท่านั้น ส่วนอีกตัวก็ใส่ค่ามันเหมือนเดิม

class Time:

     #previous method definitions here...

def after(self, time2):

      if self.hour > time2.hour:

return 1

      if self.hour < time2.hour:

return 0

 		       if self.minute > time2.minute:

return 1

      if self.minute < time2.minute:

return 0

      if self.second > time2.second:

return 1

      return 0

เราเรียกใช้ method นี้ด้วย object ตัวเดียว และส่งผ่าน object อีกตัวเป็น argument

if doneTime.after(currentTime):

   print "The bread is not done yet."

คุณสามารพอ่านการร้องขอนี้เหมือนในภาษาอังกฤษ “If the done-time is after the current-time, then..."

14.5 Optional arguments

แก้ไข

เราได้เห็น built-in ของฟังก์ชัน ที่รับค่า arguments ได้หลายค่า เป็นต้นว่า string.find สามารถรับค่า arguments ได้ 2,3 หรือ 4 ตัว

มันเป็นไปได้ที่จะเขียนฟังก์ชันให้รับค่า optional argument lists เป็นต้นว่า เราสามารถยกระดับ find ของเราให้สามารถทำได้แบบ string.find

นี้คือ code จาก section 7.7

def find(str, ch):

   index = 0
   while index < len(str):
      if str[index] == ch:
          return index
      index = index + 1
  return -1

นี่เป็นตัวใหม่ที่ได้รับการเพิ่มเติมแล้ว

def find(str, ch, start=0):

   index = start
   while index < len(str):
       if str[index] == ch:
             return index
       index = index + 1
  return -1

parameter ตัวที่ 3 start คือ optional เพราะค่า default คือ 0 ตามที่กำหนดไว้ ถ้าเราเรียกใช้ find ด้วย arguments เพียง 2 ตัว มันก็จะเรียกใช้ค่า default และเริ่มต้นจากจุดเริ่มต้นของ string

>>> find("apple", "p")

1

ถ้าเรากำหนดค่า parameter ตัวที่ 3 มันก็จะทำการ overrides ตัว default

>>> find("apple", "p", 2)

2

>>> find("apple", "p", 3)

-1

14.6 The initialization method

แก้ไข

Initialization method คือ method พิเศษที่จะเรียกใช้เมื่อ object ถูกสร้างขึ้น ชื่อของ method นี้คือ __init__ ( underscore 2 ตัวตามด้วยตัว init และตามด้วย underscore 2 ตัว )

initialization method ของคลาส Time จะมีลักษณะดังนี้

     class Time:
                          def __init__(self, hours=0, minutes=0, seconds=0):
		      self.hours = hours
       		      self.minutes = minutes
     self.seconds = seconds

ตรงนี้จะไม่มีการขัดแย้งกันระหว่าง attribute self.hours และค่า parameter ที่ชื่อ hours โดย Dot notation ( จุด ) จะเป็นตัวระบุตัวแปรที่เรากำลังอ้างอิงถึง

เมื่อเราเรียกใช้ Time constructor ตัว arguments ที่เรากำหนดจะถูกส่งให้ init

>>> currentTime = Time(9, 14, 30)

>>> currentTime.printTime()

9:14:30

เพราะ parameters เป็นแบบ optional เราไม่ต้องกำหนดก็ได้

>>> currentTime = Time()

>>> currentTime.printTime()

0:0:0

หรือกำหนดเฉพาะ parameter แรก

>>> currentTime = Time (9)

>>> currentTime.printTime()

9:0:0

หรือกำหนด 2 ตัวแรก

>>> currentTime = Time (9, 14)

>>> currentTime.printTime()

9:14:0

สุดท้าย เราสามารถกำหนด subset ของ parameters โดยใส่ชื่อของมันให้ชัดเจน

>>> currentTime = Time(seconds = 30, hours = 9)

>>> currentTime.printTime()

9:0:30

14.7 Points revisited

แก้ไข

ลองเขียน Point คลาสอีกครั้ง จาก section 12.1 ในรูปของ object-oriented

class Point:

     def __init__(self, x=0, y=0):

self.x = x

self.y = y

     def __str__(self):

return '(' + str(self.x) + ', ' + str(self.y) + ')'

initialization method รับค่า x และ y เป็น optional parameters โดยมีค่า default เป็น 0 ทั้งคู่

method ต่อมาคือ _ _ str_ _ จะ returns ค่า string ที่แสดงถึงตัว Point object ถ้าคลาสมีการกำหนด method ที่ชื่อ _ _str_ _ มันจะ overrides ตัว default ของ python ที่สร้าง built-in ฟังก์ชัน str

>>> p = Point(3, 4)

>>> str(p)

'(3, 4)'

พิมพ์ค่า Point object โดยการเรียกใช้ __str__ ใน object ดังนั้นเมื่อกำหนด __ str__ มันจึงเปลี่ยนรูปแบบการพิมพ์ค่า >>> p = Point(3, 4)

>>> print p

(3, 4)

เมื่อเราเขียนคลาสใหม่ เราจะต้องเริ่มจากการเขียน __init__ และ __str__ ซึ่งมีประโยชน์ในการ debugging

14.8 Operator overloading

แก้ไข

ในบางภาษามันเป็นไปได้ที่จะเปลี่ยนแปลง built-in operators เมื่อมันถูกกำหนดใช้ใน User-defined types ซึ่งเรียก feature นี้ว่า operator overloading มันมีประโยชน์โดยเฉพาะเมื่อมีการกำหนดการคำนวณทางคณิตศาสตร์ใหม่

เป็นต้นว่า เมื่อ override การบวก + เราจะต้องกำหนด method ชื่อ _ _add_ _

      class Point:
  1. previously defined methods here...

def __add__(self, other):

       return Point(self.x + other.x, self.y + other.y)

โดยทั่วไป parameter ตัวแรกคือ object ที่เรียกใช้ method ส่วน parameter ตัวที่ 2 จะชื่อว่า other มันแตกต่างจาก self เพื่อบวกค่า Points 2 ตัว เราสร้างและ return Point ตัวใหม่ที่เก็บค่าผลรวมของ x และผมรวมของ y

ตอนนี้ เมื่อเรานำ + มาใช้ใน Point object ตัว Python จะเรียกใช้ __add__

>>> p1 = Point(3, 4)

>>> p2 = Point(5, 7)

>>> p3 = p1 + p2

>>> print p3

(8, 11)

Expression p1 + p2 จะเท่ากับ p1. add (p2) แต่มันดูแล้วเรียบง่ายกว่า

       As an exercise, add a method sub (self, other) that overloads

the subtraction operator, and try it out.

ยังมีอีกหลายทางที่จะ override การคูณโดยกำหนด method ที่ชื่อ __mul__ หรือ __rmul__ หรือทั้งคู่

ถ้าด้านซ้าย operand ในการ * คือ Point ตัว Python จะเรียกใช้ _ _mul_ _ ซึ่งสมมติให้ operand ตัวอื่น เป็น Point ด้วย ถ้าจะคำนวณ dot product ของ 2 points จะต้องกำหนดร่วมกับกฎตามหลักของพีชคณิตเส้นตรง

def __mul__(self, other):

    return self.x * other.x + self.y * other.y

ถ้าด้านซ้ายของ operand ในการ * คือ primitive type และด้านขวาเป็น Point ตัว python จะเรียกใช้ __rmul__ ซึ่งแสดง scalar multiplication

def __rmul__(self, other):

     return Point(other * self.x, other * self.y)

ผลลัพธ์คือ Point ตัวใหม่ ที่มีค่าเป็นการคูณกันของค่าเดิมใน Point ถ้า other คือชนิดข้อมูลที่ไม่สามารถคูณกันได้โดย floating-point ตัว __rmul__ จะแสดงผลลัพธ์ error ออกมา

ตัวอย่างนี้แสดงให้เห็นถึงการคูณทั้ง 2 แบบ

>>> p1 = Point(3, 4)

>>> p2 = Point(5, 7)

>>> print p1 * p2

43

>>> print 2 * p2

(10, 14)

จะเกิดอะไรขึ้นถ้าเราลองคูณทั้ง p2 * 2 ตัว parameter แรกเป็น Point ตัว Python จะเรียกใช้ __mul__ กับตัวเลข 2 ที่เป็น argument ที่ 2 ใน __mul__ โปรแกรมจะพยายามเข้าถึงค่า x ของ other มันจะผิดพลาดเพราะ integer ไม่มี attributes

>>> print p2 * 2

AttributeError: 'int' object has no attribute 'x'

น่าเสียดายที่ error message เข้าใจยากเล็กน้อย ตัวอย่างนี้จะแสดงให้เห็นถึงความยากบางอย่างของ object-oriented programming ในบางครั้งมันยากเพียงพอที่จะต้องนึกภาพให้ออกว่า code อะไรที่ทำงานอยู่

สำหรับตัวอย่างที่สมบูรณ์ของการทำ operator overloading จะอยู่ใน appendix B

14.9 Polymorphism

แก้ไข

Method ที่เราเขียนมาส่วนมากจะทำงานกับชนิดข้อมูลที่เฉพาะเจาะจงเท่านั้น เมื่อเราสร้าง object ใหม่ คุณเขียน method ที่ทำงานกับมัน

แต่เพื่อให้แน่ใจว่า operations ที่คุณต้องการนำไปใช้กับชนิดของข้อมูลที่หลากหลาย เช่น operations ทางคณิตศาสตร์ ใน section ที่ผ่านมา ถ้าให้มันสนับสนุนชนิดข้อมูลหลายๆชนิด คุณสามารถเขียนฟังก์ชันที่ทำให้มันใช้งานได้กับชนิดข้อมูลนั้นๆ

ตัวอย่างเช่น multadd operation ใช้ว่า parameter 3 ตัว โดยจะคูณ 2 ตัวแรก แล้วบวกกับตัวที่ 3 เราเขียนมันใน Python ได้ดังนี้ def multadd (x, y, z):

return x * y + z

method นี้จะทำงานได้กับค่าใดๆ ของ x และ y ที่สามารถคูณกันได้ และค่าใดๆของ z ที่สามารถบวกกันได้

เราสามารถเรียกใช้กับค่าตัวเลขได้ดังนี้

>>> multadd (3, 2, 1)

7

หรือใช้กับ Point

>>> p1 = Point(3, 4)

>>> p2 = Point(5, 7)

>>> print multadd (2, p1, p2)

(11, 15)

>>> print multadd (p1, p2, 1)

44

ในกรณีแรก Point จะคูณโดย scalar และบวกโดย Point อีกตัวในกรณีที่ 2 ผลลัพธ์การคูณจะอยู่ในรูปของตัวเลข ดังนั้น parameter ตัวที่ 3 จึงต้องเป็นตัวเลข

ฟังก์ชันลักษณะนี้สามารถรับ parameters ด้วยชนิดข้อมูลใดๆ เราเรียกว่า polymorphic

อีกตัวอย่างหนึ่ง ลองพิจารณา method frontAndBack ที่พิมพ์ค่า list 2 ครั้ง แบบเรียงไปข้างหน้า และย้อนจากข้างหลัง

      def frontAndBack(front):

import copy

back = copy.copy(front)

back.reverse()

print str(front) + str(back)

เพราะ reverse method คือ Modifier เราสร้าง copy ของ list ก่อนจะย้อนมันกลับ เช่นนั้น method นี้จึงไม่แก้ไข list ที่มันรับมาเป็น parameter

นี่คือตัวอย่างในการนำ frontAndBack ไปใช้กับ list

>>> myList = [1, 2, 3, 4]

>>> frontAndBack(myList)

[1, 2, 3, 4][4, 3, 2, 1]

แน่นอนว่า เราตั้งใจจะนำฟังก์ชันนี้ไปใช้กับ lists ดังนั้นจึงไม่น่าแปลกใจที่มันจะทำงานได้ดี สิ่งที่จะทำให้เราประหลาดใจ คือเมื่อนำมันไปใช้กับ Point เพื่อตัดสินว่าฟังกชันสามารถนำไปใช้กับชนิดข้อมูลใหม่ได้ ไม่ว่าอย่างไรก็ตามเรานำไปใช้โดยอยู่บนพื้นฐานของกฎของ polymorphism

ถ้า operations ทั้งหมดในฟังก์ชันสามารถนำไปใช้กับชนิดข้อมูลใดได้ ฟังก์ชันนั้นก็สามารถใช้งานกับชนิดข้อมูลนั้นได้

Operations ใน method รวมถึง copy, reverse และ print

Copy ทำงานได้กับ object ใดๆ และที่เราเขียน __str__ method ของ Point ดังนั้นเราก็แค่เขียน reverse เพิ่มในคลาส Point

     def reverse(self):

self.x , self.y = self.y, self.x

เราสามารถเปลี่ยน Points เป็น frontAndBack

>>> p = Point(3, 4)

>>> frontAndBack(p)

(3, 4)(4, 3)

ชนิดที่ดีของ polymorphism คือ the unintentional kind ที่คุณค้นพบว่าฟังก์ชันที่คุณเขียนไปนำไปใช้ได้กับชนิดข้อมูลที่คุณไม่ได้วางแผนไว้

14.10 Glossary

แก้ไข

object-oriented language: A language that provides features, such as userde fined classes and inheritance, that facilitate object-oriented programming.

object-oriented programming: A style of programming in which data and the operations that manipulate it are organized into classes and methods.

method: A function that is defined inside a class definition and is invoked on instances of that class.

override: To replace a default. Examples include replacing a default parameter with a particular argument and replacing a default method by providing a new method with the same name.

initialization method: A special method that is invoked automatically when a new object is created and that initializes the object's attributes.

operator overloading: Extending built-in operators (+, -, *, >, <, etc.) so that they work with user-defined types.

dot product: An operation defined in linear algebra that multiplies two Points and yields a numeric value.

scalar multiplication: An operation defined in linear algebra that multiplies each of the coordinates of a Point by a numeric value.

polymorphic: A function that can operate on more than one type. If all the operations in a function can be applied to a type, then the function can be applied to a type.