บทความนี้เป็นส่วนเสริมของเนื้อหาเรื่องการใช้มอดูล subprocess
https://phyblas.hinaboshi.com/20200306
โดยจะพูดถึงการใช้ subprocess.Popen ซึ่งจะซับซ้อนขึ้นมากว่าการใช้ subprocess.run แต่เหมาะจะใช้ในบางกรณีมากกว่า
เช่นเมื่อต้องมีการป้อนค่าเพิ่มเข้าไป หรือเมื่อต้องการสั่งหลายๆคำสั่งพร้อมกันคู่ขนาดกันไป
ว่าด้วยเรื่องคำสั่งที่ต้องมีการป้อนค่าเข้า
ขอเริ่มจากเกริ่นถึงลักษณะของโปรแกรมที่ควรใช้ subprocess.Popen
สมมุติว่ามีโปรแกรมไพธอนอันนึง
sanpo.py
เขียนไว้แบบนี้
a = float(input())
print(a/11.)
เนื่องจากมีคำสั่ง input ดังนั้นโปรแกรมนี้จะต้องรอรับค่าเพื่อที่จะไปต่อได้
ไม่เช่นนั้นโปรแกรมก็จะไม่มีทางทำงานต่อจนจบ
ในกรณีแบบนี้หากใช้ subprocess.run ละก็ จะไม่ได้
import subprocess
subprocess.run(['python','sanpo.py'])
ได้
Traceback (most recent call last):
File "sanpo.py", line 1, in <module>
a = float(input())
EOFError: EOF when reading a line
ในกรณีแบบนี้ก็จะต้องใส่ input เข้าไปใน run ด้วย แบบนี้ ก็จะสามารถรันออกมาได้
import subprocess
subprocess.run(['python','sanpo.py'],input='2',text=True)
ได้
0.18181818181818182
แต่ว่าถ้าหากโปรแกรมต้องการค่าป้อนเข้า ๒ ตัว เช่น
a = int(input())
b = int(input())
print('%d/%d = %s'%(a,b,a/b))
แบบนี้ก็ต้องใส่ค่าเข้าไป ๒ ตัว โดยใช้ \n คั่น
subprocess.run(['python','sanpo.py'],input='2\n3',text=True)
ได้
2/3 = 0.6666666666666666
ปัญหาคือการทำแบบนี้ทำให้ต้องเตรียมข้อมูลป้อนเข้าไว้ตั้งแต่แรกทั้งหมด
ถ้ายังมีข้อมูลป้อนเข้าที่ต้องการอีกแต่ไม่มีเตรียมค่าป้อนเข้าไปแล้วก็จะไปต่อไม่ได้ทันที
และปกติแทนที่จะเตรียมข้อมูลป้อนเข้าไว้ทั้งหมดตั้งแต่แรก ปกติจะค่อยๆใส่ลงไปทีละตัวตามลำดับมากกว่า ซึ่งถ้าใช้
subprocess.Popen ก็จะสามารถทำแบบนั้นได้
การใช้ .Popen โดยป้อนค่าผ่าน .stdin
subprocess.Popen นั้นวิธีการใช้จะคล้ายกับ subprocess.run ใช้โดยป้อนคำสั่งที่ต้องการเข้าไปเหมือนกัน แต่ที่ต่างกันก็คือ
subprocess.Popen จะคืนออบเจ็กต์ Popen ซึ่งเป็นตัวกลางที่สามารถใช้เพื่อทำการสื่อสารกับคอมมานด์ไลน์ต่อได้
วิธีการใช้มีความซับซ้อนเล็กน้อย ขอเริ่มจากยกตัวอย่างการใช้
สมมุติมีโปรแกรม
sanpo.py
ซึ่งมีการใช้ input เพื่อรับค่า ๒ ตัว
a = int(input())
b = int(input())
print('%d×%d = %s'%(a,b,a*b))
เราสามารถเขียน subprocess.Popen เพื่อควบคุมได้ดังนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po.stdin.write('11\n') # ป้อนค่าตัวแรก
po.stdin.flush() # สิ้นสุดการป้อนตัวแรก
po.stdin.write('12\n') # ป้อนค่าตัวที่สอง
po.stdin.flush() # สิ้นสุดการป้อนตัวที่สอง
po.wait() # รอจนสิ้นสุดโปรแกรม
ได้
11×12 = 132
ในที่นี้เมื่อใช้ subprocess.Popen ก็จะได้ออบเจ็กต์ Popen มาเก็บไว้ในตัวแปร po จากนั้นก็นำมันมาใช้
ในที่นี้จำเป็นต้องระบุ stdin=subprocess.PIPE ลงไปด้วย แล้วจะทำให้ในออบเจ็กต์นี้จะมีตัวกลางสำหรับป้อนค่าเข้า คือ
.stdin
วิธีการป้อนค่าจะคล้ายกับเวลาใช้ open เพื่อเปิดไฟล์มาในโหมดเขียน (เช่น
open('ชื่อไฟล์','w')
)
นั่นคือจะใช้เมธอด .write() เพื่อเขียนค่าลงไปได้
ข้อความที่ป้อนเข้าไปต้องปิดท้ายด้วย \n คือแทนการกด enter ด้วย และหลังป้อนข้อมูลไปก็ใช้เมธอด .flush()
เพื่อสิ้นสุดการป้อน
และสุดท้ายใช้ .wait() เพื่อให้รอจนสิ้นสุดการทำงานของคำสั่ง
อาจใช้โครงสร้าง with เพื่อให้คำสั่งรอจนสิ้นสุดเองโดยไม่ต้องใช้ .wait()
with subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True) as po:
po.stdin.write('11\n')
po.stdin.flush()
po.stdin.write('12\n')
po.stdin.flush()
แบบนี้ก็ได้ผลเหมือนกัน โดยส่วนที่อยู่ในโครงสร้าง with คือสิ่งที่จะทำในขณะที่คอมมานด์ไลน์นี้กำลังทำงานอยู่ พอสิ้นสุด
with ก็จะรอจนคำสั่งสิ้นสุดลงไป
การเอาผลลัพธ์จาก .stdout และ .stderr เมื่อใช้ .Popen
หากใส่คีย์เวิร์ด stdout=subprocess.PIPE แทนที่ผลลัพธ์จะแสดงออกมาทันทีก็จะเก็บไว้ในออบเจ็กต์ Popen โดยอยู่ใน
.stdout
ซึ่ง .stdout นี้จะเป็นตัวกลางสำหรับดึงข้อมูลออกมา การใช้จะเหมือนกับเวลาใช้ open แล้วเปิดโหมดอ่านข้อมูล คือใช้เมธอด
.read, .readline, readlines ได้
ตัวอย่างเช่น ถ้าใน
sanpo.py
เป็น
a = int(input())
print('2^%d = %s'%(a,2**a))
สั่งรันแล้วเอาผลลัพธ์มาแสดงได้โดยเขียนแบบนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po.stdin.write('13\n')
po.stdin.flush()
print(po.stdout.read()) # ได้ 2^13 = 8192
เมื่ออ่านค่าจาก .stdout นั่นหมายความว่าคำสั่งจะต้องถูกทำจนจบและให้ผลลัพธ์ออกมา ดังนั้นไม่จำเป็นต้องใช้ with หรือ
.wait()
ถ้าต้องการจะเอาผลลัพธ์ใส่ลงไฟล์ก็ใช้ open ได้เช่นเดียวกันเมื่อตอนใช้ subprocess.run
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=open('xxx.txt','w'),text=True)
po.stdin.write('15\n')
po.stdin.flush()
ผลลัพธ์ที่ผิดพลาดก็เช่นกัน เอาออกมาได้โดยใส่ stderr=subprocess.PIPE แล้วเอา .stderr
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
po.stdin.write('x\n')
po.stdin.flush()
print(po.stderr.read())
ก็จะได้ผลลัพธ์ความผิิดพลาดออกมา
Traceback (most recent call last):
File "sanpo.py", line 1, in
a = int(input())
ValueError: invalid literal for int() with base 10: 'x'
หรือเอาผลลัพธ์ที่ผิดพลาดออกมาใส่ในส่วนของ .stdout ได้โดยใส่ stderr=subprocess.STDOUT
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
po.stdin.write('x\n')
po.stdin.flush()
print(po.stdout.read())
การใช้ผลลัพธ์จากคำสั่งแรกมาป้อนให้อีกคำสั่งทันที
.stdout ที่ได้จากคำสั่งหนึ่งยังเอามาใช้เป็น stdin ของ subprocess.Popen ได้ด้วย
ตัวอย่างเช่น เตรียมโปรแกรม
sanpo.py
ซึ่งจะรับค่าตัวเลขมาแล้วเอามาคูณ 2
a = int(input())
print(a*2)
ลองสั่งคำสั่งต่อกัน ๒ คำสั่งด้วย subprocess.Popen โดยเอาคำตอบของคำสั่งแรกมาป้อนให้คำสั่งถัดไป
po1 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po1.stdin.write('7\n')
po1.stdin.flush()
po2 = subprocess.Popen(['python','sanpo.py'],stdin=po1.stdout,stdout=subprocess.PIPE,text=True)
print(po2.stdout.read()) # ได้ 28
ค่าที่ป้อนเข้าไปตอนแรกคือ 7 เมื่อเข้าไปในคำสั่ง po1. จะให้ค่า 7×2 เท่ากับ 14 ออกมาทาง .stdout
แล้วเมื่อใช้ค่านี้ป้อนให้ po2 ก็จะได้คำตอบออกมาเป็น 14×2 เท่ากับ 28
การป้อนค่าสุดท้ายโดยใช้ .communicate()
สามารถใช้เมธอด .communicate เพื่อทำการป้อนค่าครั้งสุดท้ายและเป็นการจบคำสั่งไปในตัว
สมมุติโค้ดใน
sanpo.py
เป็นแบบนี้
a = int(input())
b = int(input())
print('%d^%d = %s'%(a,b,a**b))
เนื่องจากมี input รับค่า ๒ ตัว ดังนั้นอาจใช้ .stdin.write ๒ ครั้ง หรืออาจใช้ .communicate
เขียนแทนตัวหลังแบบนี้ได้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po.stdin.write('5\n')
po.communicate('4')
ได้
5^4 = 625
เมื่อใช้ .communicate จะได้ค่าคืนกลับคือทูเพิลของค่า (ผลลัพธ์ปกติ, ผลลัพธ์ที่ผิดพลาด)
ลองให้ stdout หรือ stderr เป็น subprocess.PIPE ก็จะได้ค่าผลลัพธ์ปกติและผลลัพธ์ที่ผิดพลาดออกมา เช่น
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
po.stdin.write('5\n')
res = po.communicate('4')
print(res) # ได้ ('5^4 = 625\n', None)
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stderr=subprocess.PIPE,text=True)
po.stdin.write('\n')
res = po.communicate('x')
print(res) # ได้ (None, 'Traceback (most recent call last):\n File "sanpo.py", line 1, in \n a = int(input())\nValueError: invalid literal for int() with base 10: \'\'\n')
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True)
po.stdin.write('\n')
res = po.communicate('x')
print(res) # ได้ ('Traceback (most recent call last):\n File "sanpo.py", line 1, in \n a = int(input())\nValueError: invalid literal for int() with base 10: \'\'\n', None)
เช่นเดียวกับ .stdout การใช้ .communicate จะทำให้คำสั่งสิ้นสุดและปิดตัวลงอยู่แล้วจึงไม่จำเป็นต้องใช้คู่กับ with
หรือปิดท้ายด้วย
.wait()
การใช้ timeout เพื่อจำกัดเวลาใน .communicate()
.communicate สามารถกำหนดเวลาจำกัด timeout ได้เหมือนตอนใช้ subprocess.run
ถ้าหลังป้อนค่าไปแล้วคำสั่งเสร็จไม่ทันตามเวลาที่กำหนดก็จะทำให้เกิดข้อผิดพลาด
ตัวอย่างให้
sanpo.py
เป็นโปรแกรมที่ใช้ฟังก์ชัน time.sleep เผื่อถ่วงเวลาตามที่ระบุไป
import time
a = float(input())
time.sleep(a)
print('ผ่านไปแล้ว %f วินาที'%a)
ลองใช้ .communicate โดยใส่ timeout ได้ดังนี้
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
res,_ = po.communicate('2',timeout=2.2)
print(res) # ได้ ผ่านไปแล้ว 2.000000 วินาที
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,stdout=subprocess.PIPE,text=True)
res,_ = po.communicate('2.5',timeout=2.2)
print(res) # ได้ TimeoutExpired: Command '['python', 'sanpo.py']' timed out after 2.2 seconds
เมื่อใช้ .Popen หลายตัวพร้อมกัน
เมื่อใช้ subprocess.Popen สั่งคอมมานด์ไลน์ไปแล้ว
คำสั่งต่อไปในโค้ดก็จะทำงานทันทีโดยที่ไม่ได้สนว่าคำสั่งในคอมมานด์ไลน์ที่สั่งไปนั้นจะทำเสร็จหรือยัง ยกเว้นว่าจะใช้เมธอด
.communicate() หรือดึงเอาผลลัพธ์มาด้วย .stdout.read() ซึ่งจะเป็นการรอจนได้ผลออกมา
นั่นหมายความว่าถ้าใช้ subprocess.Popen ต่อๆกันหลายอันในโปรแกรมเดียว โปรแกรมไม่จำเป็นต้องรอให้ subprocess.Popen
ตัวแรกเสร็จจึงจะทำตัวต่อไป สามารถทำคู่ขนานไปพร้อมๆกันได้เลย และตัวที่สั่งไปก่อนก็อาจจะเสร็จทีหลังก็ได้
เช่น ลองเขียน
sanpo.py
ให้กินเวลาตามค่าที่ใส่เข้าไป แบบนี้
import time
a = int(input())
time.sleep(a)
print('เวลาล่วงไป %s วินาที'%a)
จากนั้นลองสั่งรันโปรแกรมนี้พร้อมกันหลายๆตัวดู
import subprocess,time
po3 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po3.stdin.write('3\n')
po3.stdin.flush()
po2 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po2.stdin.write('2\n')
po2.stdin.flush()
po1 = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po1.stdin.write('1\n')
po1.stdin.flush()
time.sleep(4) # รอไปสัก ๔ วินาที ให้ทำเสร็จทั้งหมดก่อน
แบบนี้ผลที่ได้จะออกมาตามลำดับดังนี้
เวลาล่วงไป 1 วินาที
เวลาล่วงไป 2 วินาที
เวลาล่วงไป 3 วินาที
จะเห็นว่าแม้ว่า po3 จะสั่งก่อน แต่มันต้องใช้เวลา ๓ วินาที จึงออกมาลำดับท้ายสุด ในขณะที่ po1 สั่งทีหลังสุด
แต่ใช้เวลาแค่ ๑ วินาที จึงเสร็จก่อน
อาจใช้ for วนซ้ำเพื่อรันคำสั่งที่ใช้เวลาต่างกัน
tt = [2,6,5,1]
for t in tt:
po = subprocess.Popen(['python','sanpo.py'],stdin=subprocess.PIPE,text=True)
po.stdin.write('%d\n'%t)
po.stdin.flush()
time.sleep(max(tt)+1) # รอจนทำเสร็จหมด
ก็จะได้ผลออกมาตามลำดับเวลาดังนี้
เวลาล่วงไป 1 วินาที
เวลาล่วงไป 2 วินาที
เวลาล่วงไป 5 วินาที
เวลาล่วงไป 6 วินาที
การตรวจสอบสถานะการรันโดยใช้ .poll()
จากหัวข้อที่แล้วจะเห็นว่าเมื่อใช้ subprocess.Popen ไปแล้วคำสั่งต่อไปจะทำงานโดยไม่รอให้คำสั่งนั้นทำเสร็จ
แบบนั้นแล้วหากคำสั่งต่อไปขึ้นกับคำสั่งที่สั่งก่อนหน้า
ผลของคำสั่งต่อไปที่ได้ก็อาจต่างกันไปโดยขึ้นอยู่กับว่าคำสั่งในคอมมานด์ไลน์ไปถึงไหนแล้ว
บางครั้งจึงจำเป็นต้องตรวจดูว่าคำสั่งนั้นทำเสร็จไปหรือยัง
ถ้าต้องการจะดูว่าคำสั่งนั้นทำเสร็จสิ้นหรือยังสามารถใช้เมธอด .poll() ถ้าหากว่ายังไม่เสร็จจะได้ค่า None
แต่ถ้าเสร็จแล้วก็จะคืนค่า returncode มา (ถ้าไม่เกิดข้อผิดพลาดจะได้ 0)
ตัวอย่าง ลองเตรียมไฟล์
sanpo.py
ให้มีการใช้เวลา ๒ วินาทีดังนี้
import time
time.sleep(2)
ลองใช้ subprocess.Popen แล้วใช้ .poll() แล้วดูค่าที่ได้ในจังหวะต่างๆ
import subprocess,time
po = subprocess.Popen(['python','sanpo.py'])
time.sleep(1) # รอสัก 1 วินาที
print(po.poll()) # ได้ None
time.sleep(3) # รอต่ออีก 3 วินาที
print(po.poll()) # ได้ 0
สำหรับ .poll() อันแรกนั้นเพิ่งผ่านไป ๑ วินาที ยังรันไม่เสร็จ จึงได้ None แต่ .poll() ตัวหลังเกินเวลามาแล้วจึงได้
returncode ออกมา ในที่นี้ไม่มีข้อผิดพลาดอะไรจึงให้ค่า 0
อาจใช้ .poll() เป็นเงื่อนไขใน if หรือ while เพื่อให้โปรแกรมเลือกทำโดยดูว่าคำสั่งรันเสร็จหรือยัง เช่น
po = subprocess.Popen(['python','sanpo.py'])
t0 = time.time()
while(po.poll()==None):
print(f'ผ่านไปแล้ว {time.time()-t0} วินาที')
time.sleep(0.3)
แบบนี้ก็จะวนซ้ำไปเรื่อยๆใช้เวลารอบละ ๐.๓ วินาที จนกว่าจะเกิน ๒ นาที ได้ผลออกมาดังนี้
ผ่านไปแล้ว 4.696846008300781e-05 วินาที
ผ่านไปแล้ว 0.30647993087768555 วินาที
ผ่านไปแล้ว 0.6083638668060303 วินาที
ผ่านไปแล้ว 0.912254810333252 วินาที
ผ่านไปแล้ว 1.215419054031372 วินาที
ผ่านไปแล้ว 1.5203897953033447 วินาที
ผ่านไปแล้ว 1.8238389492034912 วินาที
ทั้งหมดนี้เป็นวิธีการใช้ subprocess.Popen เพื่อสั่งคำสั่งไปในคอมมานด์ไลน์จากภายในโปรแกรมไพธอน อาจมีความยุ่งยากสักหน่อย ต้องตั้งใจทำความเข้าใจให้ดี