หลังจากที่ใน
บทที่ ๒๖ และ
บทที่ ๒๗ ได้แนะนำถึงสิ่งที่เรียกว่าอิเทอเรเตอร์ไปแล้ว คราวนี้จะแนะนำฟังก์ชันที่ทำหน้าที่สร้างอิเทอเรเตอร์ที่น่าจะมีประโยชน์และได้ใช้บ่อย
มีฟังก์ชันที่ถูกทำไว้สำเร็จรูปเพื่อใช้สร้างอิเทอเรเตอร์ที่จำเป็น ส่วนใหญ่อยู่ในมอดูลชื่อ
itertools
เวลาใช้จึงต้องเรียกใช้
ก่อนอื่นให้ทำการ
import
เข้ามาก่อนเลย
import itertools
แต่นอกจากนี้แล้วก็มีบางส่วนที่อยู่ในกลุ่มฟังก์ชันหลักสามารถใช้งานได้ทันที เช่น
map
,
filter
และ
reversed
ซึ่งได้กล่าวถึงไปแล้ว และในบทนี้จะแนะนำเพิ่มอีก ๒ ตัว คือ
zip
และ
enumerate
ฟังก์ชันเหล่านี้จะสร้างอิเทอเรเตอร์ใหม่ขึ้นโดยใช้ชุดข้อมูลหรืออิเทอเรเตอร์ที่ใส่ เข้าไปเป็นอาร์กิวเมนต์เป็นพื้นฐาน สามารถสร้างอะไรออกมาได้มากมาย
จุดประสงค์ของบทนี้ไม่ใช่ต้องการจะแนะนำฟังก์ชันสร้างอิเทอเรเตอร์ทั้งหมด เพราะมีอยู่เยอะและบางตัวก็ไม่ค่อยได้ใช้
แต่ต้องการจะยกบางส่วนมาเป็นตัวอย่างเพื่อให้ฝึกความเข้าใจเรื่องของอิเทอเรเตอร์ให้คล่องขึ้น เพราะบทที่ผ่านมาแค่อธิบายหลักการแต่ยังไม่ได้เห็นตัวอย่างการประยุกต์ใช้ จริงๆมากนัก
enumerate เป็นฟังก์ชันที่สร้างอิเทอเรเตอร์ของทูเพิลที่นำชุดข้อมูลมาผูกเข้ากับตัวเลข
ตัวอย่าง
print(list(enumerate('กขคงจ'))) # ได้ [(0, 'ก'), (1, 'ข'), (2, 'ค'), (3, 'ง'), (4, 'จ')]
จะเห็นว่าสมาชิกจากสายอักขระที่ใส่เป็นอาร์กิวเมนต์ถูกนำมาผูกเข้ากับทูเพิลทีละตัว
ค่าที่ได้จาก
enumerate
นั้นเป็นอิเทอเรเตอร์ดังนั้นต้องใช้
list
เพื่อแปลงเป็นลิสต์อีกทีเพื่อแสดงสมาชิกทั้งหมด
หากใส่คีย์เวิร์ดไปอีกตัวคือ
start
จะกำหนดจุดเริ่มต้นของการนับได้ ถ้าไม่ใส่จะเริ่มจาก
0
print(list(enumerate('OBAFGKM',start=100))) # ได้ [(100, 'O'), (101, 'B'), (102, 'A'), (103, 'F'), (104, 'G'), (105, 'K'), (106, 'M')]
enumerate
มีประโยชน์เวลาที่เราต้องการให้เวลาที่ใช้
for
เพื่อวนซ้ำอยู่มีตัวเลขบางอย่างที่ช่วยนับจำนวนรอบ เวลาใช้
for
สามารถนำตัวแปรมารับ ๒ ตัวได้ โดยตัวแรกจะรับค่าตัวเลข และตัวหลังจะรับสมาชิกของข้อมูล
for i,e in enumerate(['H','He','Li','Be','B'],start=1):
print('%d:%s'%(i,e),end=', ')
# ได้ 1:H, 2:He, 3:Li, 4:Be, 5:B,
การทำงานของฟังก์ชันนี้เราสามารถสร้างเจเนอเรเตอร์ที่ทำงานเหมือนกันขึ้นมาได้ง่าย
def genenumerate(ชุดข้อมูล,start=0):
i = start
for c in ชุดข้อมูล:
yield (i,c)
i += 1
zip เป็นฟังก์ชันที่ทำการผูกข้อมูลหลายชุดเข้าด้วยกัน โดยนำสมาชิกของแต่ละชุดมาใส่ในทูเพิล ผลที่ได้คืออิเทอเรเตอร์ที่สร้างทูเพิลที่มีสมาชิกของแต่ละชุดไล่เรียงทีละตัว
ตัวอย่าง
print(list(zip('กขค','abc'))) # ได้ [('ก', 'a'), ('ข', 'b'), ('ค', 'c')]
สามารถใช้ประโยชน์เช่นในเวลาที่เรามีชุดข้อมูลที่ต้องการวนซ้ำใน
for
ตั้งแต่ ๒ ลิสต์ขึ้นไป
a = ['ไก่','จิก','เด็ก','ตาย','เด็ก','ตาย','บน','ปาก','โอ่ง']
b = 'กจฎฏดตบปอ'
for x,y in zip(a,b):
print(y+':'+x, end='|')
# ได้ ก:ไก่|จ:จิก|ฎ:เด็ก|ฏ:ตาย|ด:เด็ก|ต:ตาย|บ:บน|ป:ปาก|อ:โอ่ง|
หรือจะนำมาใช้สร้างดิกชันนารีก็ได้
print(dict(zip(b,a))) # ได้ {'จ': 'จิก', 'อ': 'โอ่ง', 'ต': 'ตาย', 'ฏ': 'ตาย', 'ด': 'เด็ก', 'ฎ': 'เด็ก', 'ป': 'ปาก', 'ก': 'ไก่', 'บ': 'บน'}
ลองเขียนเจเนอเรเตอร์ที่ทำงานเหมือนกับ
enumerate
ขึ้นมาดูได้
def genzip(*กลุ่มชุดข้อมูล):
s = tuple(map(iter,กลุ่มชุดข้อมูล))
while(1):
y = tuple(map(next,s))
if(len(y)<len(s)): break
yield y
***ในไพธอน 2 นั้น
zip
จะคืนค่าเป็นลิสต์ไม่ใช่อิเทอเรเตอร์ ดังนั้นไม่ต้องมาแปลงเป็นลิสต์อีกทีเพื่อแสดงผล
>>> รายละเอียด itertools.count เป็นอิเทอเรเตอร์ที่จะสร้างตัวเลขเป็นลำดับต่อเนื่องออกมาเรื่อยๆไม่จำกัด เหมาะสำหรับใช้เพื่อนับเลขไล่ในการวนซ้ำแต่ละครั้ง
อาร์กิวเมนต์ที่ต้องใส่มี ๒ ตัวคือ เลขเริ่มต้น และระยะห่าง จำนวนที่ใส่อาจไม่ต้องเป็นจำนวนเต็มก็ได้
for i in itertools.count(10,10):
if(i>=100): break
print(i,end='.')
# ได้ 10.20.30.40.50.60.70.80.90.
หากไม่ใส่อาร์กิวเมนต์การนับจะเริ่มจาก
0
และไล่ไปทีละ
1
ลองสร้างเจเนอเรเตอร์เลียนแบบได้ดังนี้
def count(เริ่มต้น=0,ระยะห่าง=1):
n = เริ่มต้น
while(1):
yield n
n += ระยะห่าง
itertools.cycle เป็นอิเทอเรเตอร์ที่นำข้อมูลจำนวนหนึ่งมาวนซ้ำไปเรื่อยๆอย่างไม่มีที่สิ้นสุด
cyc = itertools.cycle(range(2,7,2))
i = 0
while(i<30):
print(next(cyc),end=';')
i += 1
# ได้ 2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;2;4;6;
สร้างเจเนอเรเตอร์ที่ทำงานในลักษณะเดียวกันได้ตามนี้
def cycle(ชุดข้อมูล):
s = tuple(ชุดข้อมูล)
while(1):
for x in s:
yield x
itertools.repeat เป็นอิเทอเรเตอร์ที่เอาข้อมูลตัวเดียวมาปล่อยซ้ำไปเรื่อยๆ
อาร์กิวเมนต์มี ๒ ตัว ตัวแรกคือออบเจ็กต์ที่ต้องการซ้ำ ส่วนตัวหลังคือจำนวนครั้งที่ต้องการซ้ำ แต่ถ้าไม่ใส่อาร์กิวเมนต์ตัวหลังจะเป็นการวนซ้ำไม่มีที่สิ้นสุด
print([list(itertools.repeat('o',7))]) # ได้ [['o', 'o', 'o', 'o', 'o', 'o', 'o']]
ลองสร้างเจเนอเรเตอร์จำลองการทำงานดูได้
def repeat(ออบเจ็กต์,จำนวนครั้ง='อนันต์'):
if(จำนวนครั้ง != 'อนันต์'): # กรณีที่ใส่จำนวนครั้ง
for i in range(จำนวนครั้ง):
yield ออบเจ็กต์
else: # กรณีที่ไม่ใส่จำนวนครั้ง ให้วนอนันต์ครั้ง
while(1):
yield ออบเจ็กต์
itertools.chain เป็นอิเทอเรเตอร์ที่เอาชุดข้อมูลหลายๆชุดมาเชื่อมเรียงต่อกัน
อาร์กิวเมนต์ที่ต้องใส่คือข้อมูลชนิดกลุ่มหรืออิเทอเรเตอร์ จะใส่กี่ตัวก็ได้ ทั้งหมดจะถูกนำมาเรียงต่อกัน
print(list(itertools.chain([1,2],range(3,7),(7,8),{9,10}))) # ได้ [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
การสร้างเจเนอเรเตอร์แสดงการทำงานต้องใช้ดอกจันในการรับอาร์กิวเมนต์ที่ไม่รู้ว่าจะมีกี่ตัว
def chain(*ชุดข้อมูลทั้งหมด):
for ชุดข้อมูลแต่ละชุด in ชุดข้อมูลทั้งหมด:
for ข้อมูลในชุด in ชุดข้อมูลแต่ละชุด:
yield ข้อมูลในชุด
itertools.product เป็นอิเทอเรเตอร์ที่เอาไว้สร้างทูเพิลที่เกิดจากการนำชุดข้อมูลหลายชุดมาแจกแจง
อาร์กิวเมนต์ที่ต้องใส่คือชุดข้อมูลทั้งหมดที่จะนำมาใช้แจกแจง และจำนวนทูเพิลที่ได้จะเท่ากับจำนวนสมาชิกของแต่ละชุดคูณกันทั้งหมด
x = ['x=1','x=2','x=3']
y = ['y=1','y=2']
print(list(itertools.product(x,y))) # [('x=1', 'y=1'), ('x=1', 'y=2'), ('x=2', 'y=1'), ('x=2', 'y=2'), ('x=3', 'y=1'), ('x=3', 'y=2')]
print(len(list(itertools.product(range(7),range(6),range(2))))) # ได้ 84
แต่นอกจากอาร์กิวเมนต์แล้วยังมีคีย์เวิร์ดตัวหนึ่งที่สามารถใช้ได้ คือ
repeat
ซึ่งใช้ระบุว่าต้องการให้ข้อมูลชุดหนึ่งถูกหยิบมาแจกแจงซ้ำกี่ครั้ง ถ้าไม่ใส่จะมีค่าเป็น
1
หมายความว่าข้อมูลแต่ละชุดถูกหยิบมาครั้งเดียว
print(list(itertools.product('+-',repeat=3))) # ได้ [('+', '+', '+'), ('+', '+', '-'), ('+', '-', '+'), ('+', '-', '-'), ('-', '+', '+'), ('-', '+', '-'), ('-', '-', '+'), ('-', '-', '-')]
print(len(list(itertools.product('+-',repeat=10)))) # ได้ 1024
print(' + '.join(map(''.join,itertools.product('xyz',repeat=3)))) # ได้ yzz + zxx + zxy + zxz + zyx + zyy + zyz + zzx + zzy + zzz
การสร้างเจเนอเรเตอร์ในลักษณะนี้เพื่อมาใช้นั้นมีความซับซ้อนยุ่งยากอยู่สักหน่อย มาลองไล่คิดไปทีละขั้น
เริ่มจากถ้าหากมีข้อมูลอยู่แค่ ๒ ชุดแล้วต้องการมาแจกแจงก็แค่นำมาเข้าวังวน
for
๒ ชั้น
def product2(ชุดข้อมูล1,ชุดข้อมูล2):
s1 = tuple(ชุดข้อมูล1)
s2 = tuple(ชุดข้อมูล2)
for c1 in s1:
for c2 in s2:
yield (c1,c2)
แต่ว่าในความเป็นจริงแล้วอาจจะมีชุดข้อมูลกี่ชุดก็ได้ ไม่จำกัดแค่ ๒ ถ้าหาก ๓ ชั้นก็ใช้
for
ซ้อนกัน ๓ ชั้น แต่ปัญหาคือเราต้องทำให้รองรับได้หมดไม่ว่าจะมีข้อมูลกี่ชุด
ในสถานการณ์ที่ไม่อาจรู้ได้ว่าจะมีวังวนซ้อนกันกี่ชั้นแบบนี้ สิ่งที่ใช้การได้ดีก็คือการเขียนฟังก์ชันเวียนเกิด (กล่าวถึงไปใน
บทที่ ๒๐)
ลองเขียนฟังก์ชันขึ้นมาใหม่โดยใช้ดอกจันแทนอาร์กิวเมนต์ไม่จำกัดจำนวน และมีการวัดจำนวนอาร์กิวเมนต์ ถ้าหากยาว ๒ ก็ทำการวนซ้ำด้วยข้อมูล ๒ ชุดนั้น แต่ถ้ายาวกว่าก็ให้ทำการเรียกตัวฟังก์ชันนี้เอง
def productn(*กลุ่มชุดข้อมูล):
s = tuple(map(tuple,กลุ่มชุดข้อมูล))
if(len(s)==2):
for c1 in s[0]:
for c2 in s[1]:
yield (c1,c2)
else:
for c1 in productn(*s[:-1]): # เรียกใช้ตัวเอง
for c2 in s[-1]:
yield c1 + (c2,)
แต่ในตัวฟังก์ชันจริงๆต้องเพิ่มคีย์เวิร์ด
repeat
เข้าไปด้วย สุดท้ายแล้วก็เลยเขียนเป็นแบบนี้
def product(*กลุ่มชุดข้อมูล,repeat=1):
s = tuple(map(tuple,กลุ่มชุดข้อมูล))*repeat
if(len(s)==2):
for c1 in s[0]:
for c2 in s[1]:
yield (c1,c2)
else:
for c1 in product(*s[:-1]):
for c2 in s[-1]:
yield c1 + (c2,)
itertools.permutations เป็นอิเทอเรเตอร์ที่เอาไว้สร้างจำลองการเรียงสับเปลี่ยนวัตถุ โดยเลือกวัตถุที่มาจากกลุ่มออกมาจำนวนหนึ่งโดยที่มีการพิจารณาถึงลำดับในการ เลือก ผลที่ได้คืออิเทอเรเตอร์ของทูเพิลที่แสดงรูปแบบการเลือกทั้งหมด
อาร์กิวเมนต์มี ๒ ตัว ตัวแรกคือชุดข้อมูลที่ต้องการใช้ ส่วนตัวหลังคือจำนวนที่ต้องการเลือกจากในนั้น หากไม่ใส่ก็จะถือว่าเป็นการเลือกทั้งหมด ก็คือเลือกเป็นจำนวนเท่ากับจำนวนข้อมูลในชุด
print(list(itertools.permutations('xy'))) # ได้ [('x', 'y'), ('y', 'x')]
print(' + '.join(map(''.join,itertools.permutations('xyz')))) # ได้ xyz + xzy + yxz + yzx + zxy + zyx
print(' + '.join(map(''.join,itertools.permutations(['x','y','z'],2)))) # ได้ xy + xz + yx + yz + zx + zy
การสร้างเจเนอเรเตอร์แบบนี้อาจเลือกใช้
itertools.product
ให้เป็นประโยชน์ได้ โดยเอาชุดข้อมูลที่ป้อนเข้ามาเพียงอันเดียวนี้มาแจกแจงตามจำนวนที่ต้องการ เลือกด้วยคีย์เวิร์ด
repeat
เพียงแต่ว่าจะต้องตัดกรณีที่เลือกอันซ้ำ กันทิ้งไป ซึ่งอาจใช้เงื่อนไขที่ว่าหากกรณีไหนที่มีเลขลำดับซ้ำกันก็ตัดกรณีนั้นทิ้ง การทำแบบนี้สามารถใช้
set
ช่วยได้ เพราะเซ็ตมีคุณสมบัติที่ว่าสมาชิกห้ามซ้ำ หากตรวจสอบความยาวหลังแปลงเป็นเซ็ตแล้วพบว่าลดลงก็แสดงว่ามีตัวซ้ำ ก็ตัดกรณีนั้นทิ้ง
def permutations(ชุดข้อมูล,r='จำนวนข้อมูลทั้งหมด'):
s = tuple(ชุดข้อมูล)
n = len(s)
if(r=='จำนวนข้อมูลทั้งหมด'): r = len(n)
for ii in product(range(n),repeat=r):
if(len(set(ii)) == len(ii)):
yield tuple(s[i] for i in ii)
itertools.combinations เป็นอิเทอเรเตอร์ที่เอาไว้สร้างจำลองผลการจัดหมู่วัตถุ โดยเลือกวัตถุที่มาจากกลุ่มออกมาจำนวนหนึ่ง โดยที่ไม่สนใจลำดับในการเลือก ผลที่ได้คืออิเทอเรเตอร์ของทูเพิลที่แสดงรูปแบบการเลือกทั้งหมด
combinations
ต่างจาก
permutations
ตรงที่ไม่สนใจลำดับของการหยิบ ดังนั้นผลที่ได้จะมีจำนวนน้อยกว่า โดยตัดตัวที่มีสมาชิกซ้ำกันออกไป
อาร์กิวเมนต์มีสองตัวเช่นเดียวกับ
permutations
แต่อาร์กิวเมนต์ตัวหลังคือจำนวนที่เลือกนั้นจำเป็นต้องใส่ จะละไว้ไม่ได้
print(list(itertools.combinations('xyzt',4))) # ได้ [('x', 'y', 'z', 't')]
print(' '.join(map(''.join,itertools.combinations('xyzt',3)))) # ได้ xyz xyt xzt yzt
print(' '.join(map(''.join,itertools.combinations(['x','y','z','t'],2)))) # ได้ xy xz xt yz yt zt
การสร้างเจเนอเรเตอร์นั้นอาจใช้วิธีในทำนองเดียวกับ
permutations
แต่เพิ่มเงื่อนไขว่าจะต้องมีการจัดเรียงรูปแบบเดียว คือตามลำดับเดิมที่ถูกจัดวางในกลุ่ม ดังนั้นหมายความว่าหากใช้ฟังก์ชัน
sorted
เพื่อจัดเรียงแล้วจะต้องได้เท่าเดิม ตัวที่ไม่เหมือนเดิมแสดงว่าเรียงลำดับไม่ถูกต้องอยู่ก็คัดออกไป
def combinations(ชุดข้อมูล,r):
s = tuple(ชุดข้อมูล)
n = len(s)
for ii in product(range(n),repeat=r):
if(sorted(ii) == list(ii) and len(set(ii)) == len(ii)):
yield tuple(s[i] for i in ii)
itertools.combinations_with_replacement คล้ายกับ
combinations
แต่ว่าพิจารณาในกรณีที่สามารถเลือกซ้ำได้
ลองเปรียบเทียบระหว่าง
product
,
permutations
,
combinations
และ
combinations_with_replacement
ดูได้
print(list(itertools.product('กขค',repeat=2))) # ได้ [('ก', 'ก'), ('ก', 'ข'), ('ก', 'ค'), ('ข', 'ก'), ('ข', 'ข'), ('ข', 'ค'), ('ค', 'ก'), ('ค', 'ข'), ('ค', 'ค')]
print(list(itertools.permutations('กขค',2))) # ได้ [('ก', 'ข'), ('ก', 'ค'), ('ข', 'ก'), ('ข', 'ค'), ('ค', 'ก'), ('ค', 'ข')]
print(list(itertools.combinations('กขค',2))) # ได้ [('ก', 'ข'), ('ก', 'ค'), ('ข', 'ค')]
print(list(itertools.combinations_with_replacement(' กขค',2))) # ได้ [('ก', 'ก'), ('ก', 'ข'), ('ก', 'ค'), ('ข', 'ข'), ('ข', 'ค'), ('ค', 'ค')]
การสร้างก็คล้ายกับ
combinations
แต่ตัดเงื่อนไขที่ว่าต้องไม่เลือกซ้ำทิ้งไป
def combinations_with_replacement(ชุดข้อมูล,r):
s = tuple(ชุดข้อมูล)
n = len(s)
for ii in product(range(n),repeat=r):
if(sorted(ii) == list(ii)):
yield tuple(s[i] for i in ii)
สรุปเนื้อหา ในบทนี้ได้แนะนำอิเทอเรเตอร์ที่มีประโยชน์ไปเป็นจำนวนมาก อีกทั้งแนวคิดในการสร้างเจเนอเรเตอร์ที่เลียนแบบรูปแบบการทำงานเหล่านั้น
การจำแต่ฟังก์ชันสำเร็จรูปไปใช้อาจไม่ได้ทำให้เราเกิดความเข้าใจลึกซึ้ง แต่หากเราเข้าใจแนวคิดในการสร้างจากนั้นจึงค่อยนำมาใช้น่าจะทำให้ได้ ประโยชน์ยิ่งขึ้น
อ้างอิง