อะไรนะ! แค่ for ก็ ทำ Pagination ได้แล้วเหรอ
หลายคนเรียน Python มาสักพักจะใช้ for ได้คล่อง
for x in range(5):
print(x)
แต่มีคำถามที่น่าสนใจมากคือ
Python รู้ได้ยังไงว่าต้องเอา 0, 1, 2, 3, 4 ออกมาทีละตัว?
มันเรียก
next()ให้เราเองหรือเปล่า?
คำตอบคือ ใช่ครับ!
จริง ๆ แล้ว for ... in เป็นเพียง "น้ำตาลไวยากรณ์" (syntactic sugar) ที่ซ่อนการเรียก iter() และ next() ไว้ให้เรา
range คืออะไรกันแน่
หลายคนเข้าใจว่า
range(5)
คือ
[0, 1, 2, 3, 4]
แต่จริง ๆ ไม่ใช่
ลองดู
r = range(5)
print(r)
ผลลัพธ์
range(0, 5)
จะเห็นว่า Python ไม่ได้สร้าง List
มันแค่จำข้อมูลว่า
start = 0
stop = 5
step = 1
เท่านั้น
Iterable คืออะไร
Iterable คือวัตถุที่สามารถถูกนำไปวนซ้ำได้
เช่น
list
tuple
dict
set
str
range
ทั้งหมดเป็น Iterable
เพราะสามารถใช้กับ
for x in something:
...
ได้
Iterator คืออะไร
Iterator คือวัตถุที่มีความสามารถพิเศษคือ
next(iterator)
และจะคืนค่าทีละตัว
ตัวอย่าง
r = range(5)
it = iter(r)
ตอนนี้
it
คือ Iterator
ลองเรียก
print(next(it))
print(next(it))
print(next(it))
ผลลัพธ์
0
1
2
จะเห็นว่า Iterator จำตำแหน่งปัจจุบันเอาไว้
Sequence Diagram ของ for ... in
เมื่อเขียน
for x in range(3):
print(x)
สิ่งที่ Python ทำจริง ๆ ใกล้เคียงกับ
_iterator = iter(range(3))
while True:
try:
x = next(_iterator)
print(x)
except StopIteration:
break
ถ้าวาดเป็น Sequence Diagram
+------+ +---------+ +-----------+
| for | | range | | iterator |
+------+ +---------+ +-----------+
| | |
| iter(range) | |
|--------------->| |
| | create iterator |
|<---------------------------------|
| | |
| next() | |
|--------------------------------->|
| | |
|<--------------- 0 --------------|
| print(0) | |
| | |
| next() | |
|--------------------------------->|
|<--------------- 1 --------------|
| print(1) | |
| | |
| next() | |
|--------------------------------->|
|<--------------- 2 --------------|
| print(2) | |
| | |
| next() | |
|--------------------------------->|
|<-------- StopIteration ----------|
| break |
ดังนั้น
for ไม่ได้อ่านข้อมูลเอง
แต่ใช้
iter()
next()
อยู่เบื้องหลังทั้งหมด
แล้ว range ใช้ yield หรือไม่
ถ้าจะเขียนเองแบบง่าย ๆ
อาจเขียนได้แบบนี้
def my_range(n):
i = 0
while i < n:
yield i
i += 1
ใช้งาน
for x in my_range(5):
print(x)
ผลลัพธ์เหมือน
range(5)
มาก
แต่ในความเป็นจริง
range() ของ Python ไม่ได้ใช้ yield
เพราะมันถูกเขียนด้วยภาษา C
และใช้สูตรคณิตศาสตร์แทน
เช่น
r = range(0, 100, 2)
print(r[20])
Python คำนวณได้ทันทีว่า
40
โดยไม่ต้อง generate ค่า 20 ตัวแรกก่อน
สร้าง Iterable เองได้ไหม
ได้ง่ายมากด้วย Generator
def countdown():
yield 3
yield 2
yield 1
ใช้งาน
for x in countdown():
print(x)
Python จะเรียก
next()
ให้อัตโนมัติ
list(range()) VS iter(range())
นี่คือจุดที่หลายคนเริ่มเข้าใจโลกของ Iterator จริง ๆ
แบบ List
nums = list(range(5))
ผลลัพธ์
[0, 1, 2, 3, 4]
Python สร้างข้อมูลทั้งหมดไว้ใน RAM
ข้อดี
nums[3]
เข้าถึงได้ทันที
for x in nums:
...
วนได้กี่รอบก็ได้
ข้อเสีย
list(range(100000000))
กิน RAM มหาศาล
แบบ Iterator
it = iter(range(5))
ยังไม่มีการดึงข้อมูลออกมา
จนกว่า
next(it)
จะถูกเรียก
ข้อดี
ใช้หน่วยความจำน้อยมาก
เหมาะกับข้อมูลจำนวนมหาศาล
for line in open("bigfile.txt"):
...
ไฟล์หลาย GB ก็อ่านได้
เพราะอ่านทีละบรรทัด
ข้อเสีย
อ่านแล้วหาย
it = iter(range(5))
print(next(it))
print(next(it))
ตำแหน่งจะเลื่อนไปเรื่อย ๆ
ย้อนกลับไม่ได้
ความเข้าใจสำคัญที่สุด
จริง ๆ แล้วเวลาคุณเขียน
for x in something:
...
Python ไม่ได้สนใจว่า something คืออะไร
มันสนใจเพียงว่า
iter(something)
ทำได้ไหม
และ
next(...)
ดึงค่าต่อไปได้ไหม
เท่านั้น
ดังนั้นไม่ว่าจะเป็น
list
tuple
dict
set
range
generator
file
database cursor
API stream
ถ้ามันสร้าง Iterator ได้
for ... in ก็ทำงานได้ทั้งหมด
นี่คือเหตุผลที่ระบบ Iterator ของ Python ทรงพลังมาก เพราะมันทำให้ for สามารถวนซ้ำกับอะไรก็ได้ ตั้งแต่ลิสต์เล็ก ๆ ไปจนถึงไฟล์หลายสิบ GB หรือข้อมูลที่ถูกสร้างขึ้นแบบ Lazy ผ่าน yield โดยใช้กลไกเดียวกันทั้งหมด นั่นคือ
iter(...)
next(...)
StopIteration
ซึ่งถือเป็นหนึ่งในแนวคิดที่สวยที่สุดของภาษา Python เหมือนกับการทำ Pagination ด้วยการเขียน for ง่ายๆ และแนวคิดของ pagination, infinite scroll, streaming data, lazy loading ล้วนอยู่บนพื้นฐานเดียวกับ Iterator นี่เอง
ตัวอย่างง่าย ๆ
for item in get_data():
print(item)
ถ้า get_data() เป็น generator
def get_data():
page = 1
while True:
results = api_call(page)
if not results:
break
yield from results
page += 1
โค้ดฝั่งผู้ใช้ยังคงเป็น
for item in get_data():
process(item)
แต่เบื้องหลังเกิด
for
└── next()
└── page 1
└── API call
└── next()
└── next()
└── ...
└── page 2
└── API call
└── next()
└── ...
ผู้ใช้ไม่รู้เลยว่ามีการเปลี่ยนหน้า (pagination) อยู่ข้างใน
ยิ่งไปกว่านั้น for ไม่ได้รองรับแค่ pagination
มันรองรับทุกอย่างที่ "ดึงข้อมูลทีละชิ้น"
เช่น
อ่านไฟล์ใหญ่
for line in open("huge.log"):
print(line)
ไม่ได้โหลดไฟล์ทั้งหมดเข้าหน่วยความจำ
อ่านฐานข้อมูล
for row in cursor:
print(row)
หลาย database driver จะ fetch ทีละ batch
อ่านข้อมูลจาก API
for record in api_stream():
print(record)
อาจมีการเรียก API หลายครั้งโดยที่คนใช้ไม่รู้
Infinite Scroll
for post in facebook_feed():
render(post)
เบื้องหลัง
page 1
page 2
page 3
...
แต่ฝั่ง UI มองเป็น stream เดียว
นี่จึงเป็นเหตุผลว่าทำไมใน Python เวลาคนออกแบบ library ดี ๆ เขาจะพยายามคืนค่าเป็น Iterable/Iterator แทน List
เพราะผู้ใช้จะได้เขียนแค่
for item in something:
...
โดยไม่ต้องสนใจว่า
- ข้อมูลมาจาก RAM
- มาจากไฟล์
- มาจากฐานข้อมูล
- มาจาก API
- มี pagination หรือไม่
- มีการ batch fetch หรือไม่
ทั้งหมดถูกซ่อนอยู่หลัง iter() และ next() ครับ
พูดอีกแบบหนึ่งได้ว่า
forของ Python เป็น abstraction สำหรับ "การดึงข้อมูลลำดับถัดไป" ไม่ใช่ abstraction สำหรับ "การวนลูปบน list"
นี่เป็นเหตุผลว่าทำไม generator, yield, pagination, infinite scroll, stream processing, log processing และ data pipeline ใน Python ถึงเข้ากันได้สวยมาก เพราะสุดท้ายแล้วทุกอย่างถูกทำให้มีหน้าตาเป็น Iterator ที่ for เข้าใจได้เหมือนกันหมดครับ
ความคิดเห็น
แสดงความคิดเห็น