Để phần nào mở rộng các câu trả lời trước đây ở đây, có một số chi tiết thường bị bỏ qua.
- Thích
subprocess.run()
hơn subprocess.check_call()
và bạn bè trên subprocess.call()
trên subprocess.Popen()
trên os.system()
trênos.popen()
- Hiểu và có thể sử dụng
text=True
, aka universal_newlines=True
.
- Hiểu ý nghĩa của
shell=True
hoặc shell=False
và cách nó thay đổi trích dẫn và sự sẵn có của tiện ích vỏ.
- Hiểu sự khác biệt giữa
sh
và Bash
- Hiểu cách một quy trình con tách biệt với cha mẹ của nó và thường không thể thay đổi cha mẹ.
- Tránh chạy trình thông dịch Python như một quy trình con của Python.
Những chủ đề này được đề cập trong một số chi tiết dưới đây.
Thích subprocess.run()
hoặcsubprocess.check_call()
Các subprocess.Popen()
chức năng là một Workhorse ở mức độ thấp nhưng nó là khó khăn để sử dụng một cách chính xác và bạn kết thúc sao chép / dán nhiều dòng mã ... mà thuận tiện đã tồn tại trong thư viện chuẩn như một tập hợp các cấp cao hơn wrapper chức năng cho các mục đích khác nhau, được trình bày chi tiết hơn trong phần sau.
Đây là một đoạn trong tài liệu :
Cách tiếp cận được đề xuất để gọi các quy trình con là sử dụng run()
hàm cho tất cả các trường hợp sử dụng mà nó có thể xử lý. Đối với các trường hợp sử dụng nâng cao hơn, Popen
giao diện bên dưới có thể được sử dụng trực tiếp.
Thật không may, tính khả dụng của các hàm bao bọc này khác nhau giữa các phiên bản Python.
subprocess.run()
đã được giới thiệu chính thức trong Python 3.5. Nó có nghĩa là để thay thế tất cả những điều sau đây.
subprocess.check_output()
đã được giới thiệu trong Python 2.7 / 3.1. Về cơ bản là tương đương vớisubprocess.run(..., check=True, stdout=subprocess.PIPE).stdout
subprocess.check_call()
đã được giới thiệu trong Python 2.5. Về cơ bản là tương đương vớisubprocess.run(..., check=True)
subprocess.call()
đã được giới thiệu trong Python 2.4 trong subprocess
mô-đun gốc ( PEP-324 ). Về cơ bản là tương đương vớisubprocess.run(...).returncode
API cấp cao so với subprocess.Popen()
Các cấu trúc lại và mở rộng subprocess.run()
là hợp lý và linh hoạt hơn so với các chức năng cũ hơn mà nó thay thế. Nó trả về một CompletedProcess
đối tượng có các phương thức khác nhau cho phép bạn truy xuất trạng thái thoát, đầu ra tiêu chuẩn và một vài kết quả và chỉ báo trạng thái khác từ quy trình con đã hoàn thành.
subprocess.run()
là cách để đi nếu bạn chỉ cần một chương trình để chạy và trả lại quyền điều khiển cho Python. Đối với các kịch bản có liên quan nhiều hơn (các quy trình nền, có lẽ với I / O tương tác với chương trình mẹ Python), bạn vẫn cần phải sử dụng subprocess.Popen()
và tự mình chăm sóc tất cả các hệ thống ống nước. Điều này đòi hỏi một sự hiểu biết khá phức tạp về tất cả các bộ phận chuyển động và không nên được thực hiện nhẹ. Popen
Đối tượng đơn giản hơn đại diện cho quá trình (có thể vẫn đang chạy) cần được quản lý từ mã của bạn trong phần còn lại của vòng đời của quy trình con.
Có lẽ nên nhấn mạnh rằng chỉ subprocess.Popen()
đơn thuần là tạo ra một quá trình. Nếu bạn để nó ở đó, bạn có một quy trình con chạy đồng thời cùng với Python, vì vậy quá trình "nền". Nếu nó không cần thực hiện đầu vào hoặc đầu ra hoặc phối hợp với bạn, nó có thể thực hiện công việc hữu ích song song với chương trình Python của bạn.
Tránh os.system()
vàos.popen()
Kể từ thời gian vĩnh cửu (tốt, kể từ Python 2.5), os
tài liệu mô-đun đã chứa khuyến nghị thích subprocess
hơn os.system()
:
Các subprocess
mô-đun cung cấp cơ sở vật chất mạnh mẽ hơn để đẻ trứng quy trình mới và lấy kết quả của họ; sử dụng mô-đun đó là tốt hơn để sử dụng chức năng này.
Vấn đề system()
là nó rõ ràng phụ thuộc vào hệ thống và không cung cấp cách để tương tác với quy trình con. Nó chỉ đơn giản là chạy, với đầu ra tiêu chuẩn và lỗi tiêu chuẩn nằm ngoài tầm với của Python. Thông tin duy nhất Python nhận được là trạng thái thoát của lệnh (zero có nghĩa là thành công, mặc dù ý nghĩa của các giá trị khác không cũng phụ thuộc vào hệ thống).
PEP-324 (đã được đề cập ở trên) chứa một lý do chi tiết hơn về lý do tại sao os.system
có vấn đề và cách subprocess
cố gắng giải quyết các vấn đề đó.
os.popen()
đã từng được khuyến khích mạnh mẽ hơn nữa :
Không dùng nữa kể từ phiên bản 2.6: Chức năng này đã lỗi thời. Sử dụng subprocess
mô-đun.
Tuy nhiên, vì đôi khi trong Python 3, nó đã được thực hiện lại chỉ đơn giản là sử dụng subprocess
và chuyển hướng đến subprocess.Popen()
tài liệu để biết chi tiết.
Hiểu và thường sử dụng check=True
Bạn cũng sẽ nhận thấy rằng subprocess.call()
có nhiều hạn chế giống như os.system()
. Trong sử dụng thường xuyên, bạn thường nên kiểm tra xem quy trình đã kết thúc thành công chưa, subprocess.check_call()
và subprocess.check_output()
thực hiện (trong đó quy trình sau cũng trả về đầu ra tiêu chuẩn của quy trình con đã hoàn thành). Tương tự, bạn thường nên sử dụng check=True
với subprocess.run()
trừ khi bạn đặc biệt cần cho phép quy trình con trả về trạng thái lỗi.
Trong thực tế, với check=True
hoặc subprocess.check_*
, Python sẽ đưa ra một CalledProcessError
ngoại lệ nếu quy trình con trả về trạng thái thoát khác.
Một lỗi phổ biến subprocess.run()
là bỏ sót check=True
và ngạc nhiên khi mã nguồn bị lỗi nếu quá trình con thất bại.
Mặt khác, một vấn đề phổ biến check_call()
và check_output()
là người dùng sử dụng các chức năng này một cách mù quáng đã rất ngạc nhiên khi ngoại lệ được nêu ra, ví dụ như khi grep
không tìm thấy kết quả khớp. ( grep
Dù sao thì bạn cũng nên thay thế bằng mã Python nguyên gốc, như được nêu dưới đây.)
Tất cả mọi thứ được tính, bạn cần hiểu cách các lệnh shell trả về mã thoát và trong những điều kiện nào chúng sẽ trả về mã thoát không (lỗi) và đưa ra quyết định có ý thức về cách xử lý chính xác.
Hiểu và có thể sử dụng text=True
akauniversal_newlines=True
Vì Python 3, các chuỗi bên trong Python là các chuỗi Unicode. Nhưng không có gì đảm bảo rằng một quy trình con tạo ra đầu ra Unicode hoặc các chuỗi.
(Nếu sự khác biệt không rõ ràng ngay lập tức, Unicode thực dụng của Ned Batchelder được khuyến nghị, nếu không hoàn toàn bắt buộc, hãy đọc. Có một bài thuyết trình video dài 36 phút đằng sau liên kết nếu bạn thích, mặc dù việc tự đọc trang có thể sẽ mất ít thời gian hơn. )
Sâu xa hơn, Python phải tìm nạp một bytes
bộ đệm và giải thích nó bằng cách nào đó. Nếu nó chứa một đốm dữ liệu nhị phân, thì nó không nên được giải mã thành chuỗi Unicode, bởi vì đó là hành vi dễ bị lỗi và gây ra lỗi - chính xác là loại hành vi phiền phức đã đánh đố nhiều tập lệnh Python 2, trước khi có cách phân biệt đúng giữa văn bản được mã hóa và dữ liệu nhị phân.
Với text=True
, bạn nói với Python rằng trên thực tế, bạn mong đợi dữ liệu văn bản trở lại trong mã hóa mặc định của hệ thống và nó sẽ được giải mã thành chuỗi Python (Unicode) theo khả năng tốt nhất của Python (thường là UTF-8 trên bất kỳ mức độ vừa phải nào hệ thống ngày tháng, ngoại trừ Windows?)
Nếu đó không phải là những gì bạn yêu cầu trở lại, Python sẽ chỉ cung cấp cho bạn các bytes
chuỗi trong stdout
và stderr
chuỗi. Có lẽ tại một số sau đó chỉ cho bạn làm biết rằng họ là chuỗi văn bản sau khi tất cả, và bạn biết mã hóa của họ. Sau đó, bạn có thể giải mã chúng.
normal = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True,
text=True)
print(normal.stdout)
convoluted = subprocess.run([external, arg],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
check=True)
# You have to know (or guess) the encoding
print(convoluted.stdout.decode('utf-8'))
Python 3.7 đã giới thiệu bí danh ngắn hơn và mô tả và dễ hiểu hơn text
cho đối số từ khóa mà trước đây được gọi một cách sai lệch universal_newlines
.
Hiểu shell=True
vsshell=False
Với shell=True
bạn truyền một chuỗi duy nhất vào vỏ của bạn và vỏ sẽ lấy nó từ đó.
Với việc shell=False
bạn chuyển một danh sách các đối số cho HĐH, bỏ qua trình bao.
Khi bạn không có vỏ, bạn lưu một quy trình và loại bỏ một lượng phức tạp tiềm ẩn khá lớn, có thể hoặc không thể chứa lỗi hoặc thậm chí là các vấn đề bảo mật.
Mặt khác, khi bạn không có vỏ, bạn không có chuyển hướng, mở rộng ký tự đại diện, kiểm soát công việc và một số lượng lớn các tính năng vỏ khác.
Một lỗi phổ biến là sử dụng shell=True
và sau đó vẫn chuyển cho Python một danh sách mã thông báo hoặc ngược lại. Điều này xảy ra để làm việc trong một số trường hợp, nhưng thực sự không rõ ràng và có thể phá vỡ theo những cách thú vị.
# XXX AVOID THIS BUG
buggy = subprocess.run('dig +short stackoverflow.com')
# XXX AVOID THIS BUG TOO
broken = subprocess.run(['dig', '+short', 'stackoverflow.com'],
shell=True)
# XXX DEFINITELY AVOID THIS
pathological = subprocess.run(['dig +short stackoverflow.com'],
shell=True)
correct = subprocess.run(['dig', '+short', 'stackoverflow.com'],
# Probably don't forget these, too
check=True, text=True)
# XXX Probably better avoid shell=True
# but this is nominally correct
fixed_but_fugly = subprocess.run('dig +short stackoverflow.com',
shell=True,
# Probably don't forget these, too
check=True, text=True)
Câu trả lời chung "nhưng nó hoạt động với tôi" không phải là phản bác hữu ích trừ khi bạn hiểu chính xác trong trường hợp nào nó có thể ngừng hoạt động.
Ví dụ tái cấu trúc
Rất thường xuyên, các tính năng của shell có thể được thay thế bằng mã Python gốc. sed
Thay vào đó, Awk hoặc script đơn giản có lẽ nên được dịch sang Python.
Để minh họa một phần điều này, đây là một ví dụ điển hình nhưng hơi ngớ ngẩn bao gồm nhiều tính năng vỏ.
cmd = '''while read -r x;
do ping -c 3 "$x" | grep 'round-trip min/avg/max'
done <hosts.txt'''
# Trivial but horrible
results = subprocess.run(
cmd, shell=True, universal_newlines=True, check=True)
print(results.stdout)
# Reimplement with shell=False
with open('hosts.txt') as hosts:
for host in hosts:
host = host.rstrip('\n') # drop newline
ping = subprocess.run(
['ping', '-c', '3', host],
text=True,
stdout=subprocess.PIPE,
check=True)
for line in ping.stdout.split('\n'):
if 'round-trip min/avg/max' in line:
print('{}: {}'.format(host, line))
Một số điều cần lưu ý ở đây:
- Với
shell=False
bạn không cần trích dẫn rằng shell yêu cầu xung quanh chuỗi. Đặt dấu ngoặc kép có lẽ là một lỗi.
- Nó thường có ý nghĩa để chạy càng ít mã càng tốt trong một quy trình con. Điều này cho phép bạn kiểm soát nhiều hơn việc thực thi từ bên trong mã Python của mình.
- Phải nói rằng, các đường ống vỏ phức tạp rất tẻ nhạt và đôi khi rất khó để thực hiện lại trong Python.
Mã được cấu trúc lại cũng minh họa mức độ thực sự của vỏ đối với bạn với cú pháp rất ngắn gọn - tốt hơn hoặc xấu hơn. Python nói rõ ràng là tốt hơn so với tiềm ẩn nhưng mã Python là khá dài dòng và có lẽ trông phức tạp hơn điều này thực sự là. Mặt khác, nó cung cấp một số điểm mà bạn có thể giành quyền kiểm soát ở giữa một thứ khác, như được minh họa một cách tầm thường bởi sự tăng cường mà chúng ta có thể dễ dàng bao gồm tên máy chủ cùng với đầu ra lệnh shell. (Điều này cũng không phải là thách thức để thực hiện trong vỏ, nhưng với chi phí của một sự chuyển hướng khác và có lẽ là một quá trình khác.)
Cấu trúc vỏ chung
Để đầy đủ, đây là những giải thích ngắn gọn về một số tính năng vỏ này và một số lưu ý về cách chúng có thể được thay thế bằng các phương tiện Python nguyên gốc.
- Globbing aka mở rộng ký tự đại diện có thể được thay thế bằng
glob.glob()
hoặc rất thường xuyên bằng các so sánh chuỗi Python đơn giản như thế nào for file in os.listdir('.'): if not file.endswith('.png'): continue
. Bash có nhiều phương tiện mở rộng khác như .{png,jpg}
mở rộng dấu ngoặc và mở rộng {1..100}
dấu ngã ( ~
mở rộng sang thư mục chính của bạn và nói chung ~account
là thư mục chính của người dùng khác)
- Các biến Shell như
$SHELL
hoặc $my_exported_var
đôi khi có thể chỉ đơn giản được thay thế bằng các biến Python. Biến vỏ xuất khẩu có sẵn như là ví dụ os.environ['SHELL']
(ý nghĩa của export
là làm cho biến sẵn cho subprocesses -. Một biến mà không phải là có sẵn cho subprocesses sẽ rõ ràng là không có sẵn cho Python chạy như một tiến trình con của vỏ, hoặc ngược lại Các env=
từ khóa đối số cho subprocess
các phương thức cho phép bạn xác định môi trường của quy trình con là một từ điển, vì vậy đó là một cách để làm cho biến Python hiển thị với quy trình con). Với shell=False
bạn sẽ cần phải hiểu làm thế nào để loại bỏ bất kỳ trích dẫn; ví dụ, cd "$HOME"
tương đương với việc os.chdir(os.environ['HOME'])
không có dấu ngoặc kép quanh tên thư mục. (Rất thường xuyêncd
Dù sao cũng không hữu ích hoặc cần thiết, và nhiều người mới bắt đầu bỏ qua các dấu ngoặc kép xung quanh biến và thoát khỏi nó cho đến một ngày ... )
- Chuyển hướng cho phép bạn đọc từ một tệp dưới dạng đầu vào tiêu chuẩn của bạn và ghi đầu ra tiêu chuẩn của bạn vào một tệp.
grep 'foo' <inputfile >outputfile
mở outputfile
để viết và inputfile
để đọc, và chuyển nội dung của nó làm đầu vào tiêu chuẩn grep
, đầu ra tiêu chuẩn sau đó rơi vào outputfile
. Điều này thường không khó để thay thế bằng mã Python gốc.
- Đường ống là một hình thức chuyển hướng.
echo foo | nl
chạy hai quy trình con, trong đó đầu ra tiêu chuẩn echo
là đầu vào tiêu chuẩn của nl
(ở cấp độ HĐH, trong các hệ thống giống như Unix, đây là một xử lý tệp duy nhất). Nếu bạn không thể thay thế một hoặc cả hai đầu của đường ống bằng mã Python nguyên gốc, có lẽ bạn nên nghĩ đến việc sử dụng shell, đặc biệt là nếu đường ống có nhiều hơn hai hoặc ba quy trình (mặc dù nhìn vào pipes
mô-đun trong thư viện chuẩn Python hoặc một số của các đối thủ bên thứ ba hiện đại và linh hoạt hơn).
- Kiểm soát công việc cho phép bạn làm gián đoạn công việc, chạy chúng trong nền, đưa chúng trở lại nền trước, v.v ... Các tín hiệu Unix cơ bản để dừng và tiếp tục một quá trình tất nhiên cũng có sẵn từ Python. Nhưng các công việc là một sự trừu tượng hóa ở cấp độ cao hơn trong trình bao gồm các nhóm quy trình, v.v. mà bạn phải hiểu nếu bạn muốn làm một cái gì đó như thế này từ Python.
- Trích dẫn trong shell có khả năng gây nhầm lẫn cho đến khi bạn hiểu rằng mọi thứ về cơ bản là một chuỗi. Vì vậy,
ls -l /
tương đương với 'ls' '-l' '/'
nhưng trích dẫn xung quanh nghĩa đen là hoàn toàn tùy chọn. Các chuỗi không được trích dẫn có chứa các siêu ký tự shell trải qua quá trình mở rộng tham số, mã thông báo khoảng trắng và mở rộng ký tự đại diện; dấu ngoặc kép ngăn chặn mã thông báo khoảng trắng và mở rộng ký tự đại diện nhưng cho phép mở rộng tham số (thay thế biến, thay thế lệnh và xử lý dấu gạch chéo ngược). Điều này là đơn giản trong lý thuyết nhưng có thể gây hoang mang, đặc biệt là khi có một số lớp giải thích (ví dụ một lệnh shell từ xa).
Hiểu sự khác biệt giữa sh
và Bash
subprocess
chạy các lệnh shell của bạn /bin/sh
trừ khi bạn yêu cầu cụ thể khác (tất nhiên ngoại trừ trên Windows, nơi nó sử dụng giá trị của COMSPEC
biến). Điều này có nghĩa là các tính năng khác nhau chỉ có Bash như mảng, [[
v.v. không có sẵn.
Nếu bạn cần sử dụng cú pháp chỉ Bash, bạn có thể chuyển đường dẫn tới shell dưới dạng executable='/bin/bash'
(tất nhiên nếu Bash của bạn được cài đặt ở một nơi khác, bạn cần điều chỉnh đường dẫn).
subprocess.run('''
# This for loop syntax is Bash only
for((i=1;i<=$#;i++)); do
# Arrays are Bash-only
array[i]+=123
done''',
shell=True, check=True,
executable='/bin/bash')
A subprocess
tách biệt với cha mẹ của nó và không thể thay đổi nó
Một lỗi khá phổ biến là làm một cái gì đó như
subprocess.run('foo=bar', shell=True)
subprocess.run('echo "$foo"', shell=True) # Doesn't work
mà bên cạnh sự thiếu thanh lịch cũng phản bội sự thiếu hiểu biết cơ bản về phần "phụ" của tên "quy trình con".
Một tiến trình con chạy hoàn toàn tách biệt với Python và khi nó kết thúc, Python không biết nó đã làm gì (ngoài các chỉ số mơ hồ mà nó có thể suy ra từ trạng thái thoát và đầu ra từ tiến trình con). Một đứa trẻ thường không thể thay đổi môi trường của cha mẹ; nó không thể thiết lập một biến, thay đổi thư mục làm việc hoặc, trong rất nhiều từ, giao tiếp với cha mẹ của nó mà không có sự hợp tác từ cha mẹ.
Khắc phục ngay lập tức trong trường hợp cụ thể này là chạy cả hai lệnh trong một quy trình con;
subprocess.run('foo=bar; echo "$foo"', shell=True)
mặc dù rõ ràng trường hợp sử dụng cụ thể này không yêu cầu vỏ. Hãy nhớ rằng, bạn có thể thao túng môi trường của quy trình hiện tại (và cả con của nó) thông qua
os.environ['foo'] = 'bar'
hoặc chuyển một thiết lập môi trường cho một tiến trình con với
subprocess.run('echo "$foo"', shell=True, env={'foo': 'bar'})
(không đề cập đến việc tái cấu trúc rõ ràng subprocess.run(['echo', 'bar'])
; nhưng echo
là một ví dụ nghèo nàn về một cái gì đó để chạy trong một quy trình con ở nơi đầu tiên, tất nhiên).
Đừng chạy Python từ Python
Đây là lời khuyên hơi mơ hồ; chắc chắn có những tình huống có ý nghĩa hoặc thậm chí là một yêu cầu tuyệt đối để chạy trình thông dịch Python như một quy trình con từ tập lệnh Python. Nhưng rất thường xuyên, cách tiếp cận đúng chỉ đơn giản là import
mô-đun Python khác vào tập lệnh gọi của bạn và gọi trực tiếp các chức năng của nó.
Nếu tập lệnh Python khác nằm dưới sự kiểm soát của bạn và nó không phải là một mô-đun, hãy xem xét biến nó thành một . (Câu trả lời này đã quá dài rồi nên tôi sẽ không đi sâu vào chi tiết ở đây.)
Nếu bạn cần song song, bạn có thể chạy các hàm Python trong các quy trình con với multiprocessing
mô-đun. Ngoài ra còn threading
có nhiều nhiệm vụ chạy trong một quy trình (nhẹ hơn và cho bạn nhiều quyền kiểm soát hơn, nhưng cũng bị ràng buộc nhiều hơn trong các luồng trong một quy trình được liên kết chặt chẽ và ràng buộc với một GIL duy nhất .)
cwm
. Có lẽ bạn có một số cấu hình trong.bashrc
đó thiết lập môi trường để sử dụng bash tương tác?