Skip article metadata to content Skip navigation

For Statements – Python Iterables Part 1 of N

ก็ไม่รู้ว่ามันจะมีกี่ตอน

Cover image: A cluster of colorful glass bottles with glowing liquid sit on a dark table.
The bottles are different shapes and sizes.

ตอนแรกอยากเปิด blog ด้วยหัวข้อบันเทิงหน่อย ๆ แต่เผอิญได้ไอเดียเขียนเรื่องจริงจังระหว่างที่ทำงานอยู่ เลยได้เรื่องเขียนเกี่ยวกับของที่เจอบ่อยอย่าง for loop ในภาษา Python เสียหน่อยแล้วกัน

เคยสังเกตไหมว่า for statement ในภาษา Python จะใช้งานกับ iterable เพียงอย่างเดียวเท่านั้น ( ⁠ถ้างงกับประโยคนี้ อ่านข้ามไปก่อน ⁠)  ซึ่งจะแตกต่างจากหลาย ๆ ภาษาที่ for statement ที่มักใช้ประโยคเงื่อนไข ( ⁠conditional clause ⁠) เพื่อตรวจสอบเงื่อนไขของการซ้ำลูป

ตัวอย่างการใช้งาน for statement ในภาษา C ซึ่งมี i < n เป็น conditional clause

// Find a product of n integers in array A
int product(int A[], int n) {
int accm = 1;
for (int i = 0; i < n; ++i) {
accm *= A[i];
}
return accm;
}

มือใหม่หลาย ๆ คนที่พยายามจะแปลงโค้ดข้างต้นเป็นภาษา Python อาจจะเขียนโค้ดในลักษณะดังต่อไปนี้

example.py
def product(A):
# Find the product of all integers in the list A
accm = 1 # accumulated product
for i in range(len(A)):
accm *= A[i]
return accm
ข้อโต้แย้ง

“นี่ไง! ค่าของตัวแปร i จะวิ่งไล่จาก 0 จนถึง len( ⁠A ⁠) - 1 ซึ่งตรงตามเงื่อนไขเดียวกับโปรแกรมภาษา C++ ข้างบนเลย ( ⁠เมื่อกำหนดให้ n == len( ⁠A ⁠) ⁠)”

หากเรากำลังพูดถึงการเปลี่ยนแปลงค่าของตัวเแปร i ใน for statement เป็นหลักแล้ว คำกล่าวข้างต้นนี้ถูกต้องเลยทีเดียว  แต่ว่ากลไกของ for statement ของทั้งสองโปรแกรมนี้กลับแตกต่างกันโดยสิ้นเชิง

ก่อนที่เราจะพูดถึงกลไกการทำงานของ for statement ในภาษา Python เรามาปรับปรุงโค้ดข้างต้นเสียหน่อยดีกว่า ( ⁠ซึ่งผู้อ่านบางท่านอาจจะตั้งข้อสังเกตไว้แล้ว ⁠)

example_v2.py
def product(values):
# Find the product of all integers in the list values
accm = 1
for v in values:
accm *= v
return accm

สังเกตว่า for statement ของเราในโค้ดนี้ v จะแทนจำนวนแต่ละจำนวนในลิสต์ values  แต่ถ้าพิจารณาในละเอียดยิ่งขึ้น จะพบว่า values ไม่จำเป็นต้องเป็นลิสต์ก็ได้ เช่น

Python REPL
>>> product([2, 3, 5, 7]) # list
210
>>> product({2, 2, 3, 3, 3}) # set
6
>>> product((1, 4, 9)) # tuple
36
>>> data = {1: 3, 2: 3, 3: 5, 4: 4, 5: 4}
>>> product(data.keys()) # dict keys
120
>>> product(data.values()) # dict values
720
>>> product(data) # same as dict keys
120
>>> product(range(1, 10, 2)) # range
945
>>> product(
... i for i in range(10)
... if i % 2 == 1
... ) # generator expression
945
ข้อโต้แย้ง

“เดี๋ยวนะ! ชักจะไปกันใหญ่แล้วหละ! แล้วทำไมโค้ดพวกนี้ถึงใช้กับค่าพวกนี้ได้ทั้งหมดเลยหละ?” ( ⁠ซึ่งหากลองย้อนกลับไปดูย่อหน้าแรก ๆ ของบทความนี้จะมีคำใบ้อยู่ ⁠)

ใช่แล้ว! ค่า values เหล่านี้ล้วนแต่เป็น iterable ทั้งนั้น

หมายความว่าอย่างไร? เจตนารมณ์ของ iterable ในภาษา Python ก็คือวัตถุที่สามารถไล่เรียกดูสมาชิกแต่ละตัวได้ ไม่ว่าจะเป็น built-in data types อย่าง list, set, tuple, dict, etc. หรือแม้แต่วัตถุอื่น ๆ เช่น range หรือ generator expression และอื่น ๆ อีกมากมายเองก็เกิดเป็น iterable เช่นกัน

ในกรณีของ range( ⁠start, stop, step ⁠) ⁠) เองนั้นจะก่อเกิด iterable ที่มีค่าเริ่มจาก start ไปจนถึงก่อนค่า stop โดยนับกระโดดข้ามทีละ step นั่นเอง

Python Iterable Specification

ในเชิงสเปกของภาษา Python นั้น วัตถุทุกชิ้นที่มี method ชื่อ __iter__( ⁠self ⁠) สร้างไว้ล้วนแต่เป็น iterable ทั้งสิ้น และเมื่อพูดถึง for statement แล้ว สิ่งแรกที่เกิดขึ้นเมื่อเริ่มรัน for statement ก็คือการเรียก method __iter__( ⁠self ⁠) นี้นี่เอง

และเมื่อ method __iter__( ⁠self ⁠) ถูกเรียกใช้งาน มันจะรีเทิร์นค่าออกมาเป็นวัตถุอีกชิ้นหนึ่งซึ่งเรียกว่า iterator  และเจ้า iterator นี่เองก็จะมี method ชื่อ __next__( ⁠self ⁠) กำหนดไว้เช่นกัน ซึ่งทุก ๆ ครั้งที่เรียก method นี้ซ้ำ ๆ กันจึงจะคืนค่าสมาชิกแต่ละตัวตามลำดับ

หมายเหตุ: แนวคิดนี้มีความใกล้เคียงกับ java.lang.Iterable และ java.util.Iterator สำหรับคนที่ใช้ภาษา Java มาก่อน

อย่าเพิ่งตกใจไป! ทั้ง method __iter__ และ __next__ ที่กล่าวมาข้างต้นนั้น ผู้พัฒนาภาษา Python ได้นำไปสเปกเหล่านี้ใช้สร้าง data types ( ⁠อย่าง list, set, dict, etc. ⁠) หรือแม้แต่ range ที่เราใช้งานกันอยู่ทุกวัน เพื่ออำนวยความสะดวกผู้ใช้ภาษา Python นั่นเอง  โดยส่วนใหญ่แล้ว เรามักไม่มีเหตุผลที่จะต้องเขียน method __iter__ หรือ __next__ เอง เว้นเสียแต่เราจะประกาศสร้าง data type ขึ้นใช้งานเองเพิ่มเติม

นอกจากนี้ ยังมีวิธีต่าง ๆ มากมายที่ Python สร้างสรรค์ไว้ให้ผู้ใช้งานภาษา Python สามารถสร้าง iterable ขึ้นใช้งานเองได้ ( ⁠อย่างเช่น Generator expression ดังที่ปรากฏข้างต้น ⁠) โดยไม่ต้องไปยุ่มย่ามกับ method __iter__ หรือ __next__

ในเมื่อเราทราบแล้วว่าโค้ดของฟังก์ชัน product ข้างต้นนั้นสามารถใช้งานกับ iterable ของจำนวนเต็มในรูปแบบใด ๆ ก็ได้ เราจะปรับปรุงโค้ดนี้ให้ชัดเจนขึ้นว่า values สามารถเป็น iterable แบบใดก็ได้ ซึ่งได้ผลดังนี้

example_v3.py
from typing import Iterable
def product(values: Iterable[int]) -> int:
"""
Return the product of all integers of a given iterable.
"""
accm = 1
for v in values:
accm *= v
return accm

เมื่อคุ้นชินกับการใช้งาน iterable สักพักหนึ่งแล้ว เราอาจจะเขียนฟังก์ชันเดียวกันนี้โดยใช้ functools.reduce ซึ่ง consume iterable โดยตรง จึงได้เป็นโค้ดใหม่ดังนี้

example_v4.py
import functools
import operator
from typing import Iterable
def product(values: Iterable[int]) -> int:
"""
Return the product of all integers of a given iterable.
"""
# may also write 'lambda a, b: a * b' instead of operator.mul
return functools.reduce(operator.mul, values)

เรายังมีคำถามที่ยังค้างคาอยู่คำถามหนึ่ง เราลองกลับไปดูโค้ดภาษา Python อันแรกของเราอีกครั้ง

example.py
def product(A):
# Find the product of all integers in the list A
accm = 1 # accumulated product
for i in range(len(A)):
accm *= A[i]
return accm

จะพบว่า range ที่ใช้ในโค้ดนี้จะเกิดเป็นวัตถุ iterable ที่มีสมาชิกเป็น 0, 1,  ⁠…, len( ⁠A ⁠)-1 ตามลำดับ จากนั้นเราจึงใช้จำนวนเหล่านั้นเพื่อ index ค่าภายในลิสต์ A อีกทอดหนึ่ง ( ⁠สังเกตดี ๆ ว่าเรายังทำงานกับวัตถุที่เป็น iterable อย่างไม่มีข้อยกเว้น ⁠)

หวังว่าจะทำให้ผู้ใช้งาน Python หลายท่านมองเห็นภาพการทำงานของ for statement มากขึ้น  ในบทความถัดไปจะพูดถึงการสร้าง iterable ด้วยวิธีต่าง ๆ และการนำมาใช้อำนวยความสะดวกในการเขียนโปรแกรม ซึ่งจะทำให้โค้ดของเราเข้าใจง่ายขึ้นตามไปด้วย

หมายเหตุ: มีรายละเอียดหลายประการที่เกี่ยวข้องกับหัวข้อนี้ ซึ่งผู้เขียนไม่สามารถกล่าวถึงได้ทั้งหมดภายในบทความเดียว จึงขออภัยมา ณ ที่นี้

อัปเดตเรื่องอื่น ๆ

สัปดาห์หน้ามีแพลนจะไปเที่ยวประเทศญี่ปุ่น อีเวนท์สำคัญก็คือคอนเสิร์ต Yuki Kajiura LIVE vol.#15 〜Soundtrack Special at the Amphitheater〜 ทั้งวันที่ 15 และ 16 มิถุนายนเลย และสัปดาห์ถัดไปก็ยังมีคอนเสิร์ตคล้าย ๆ กันของไทยด้วย คงจะมีรีวิวเร็ว ๆ นี้แน่นอน