什么是 Python 中的子进程? [5 Usage Examples]

深入理解Python中的子进程

子进程允许我们在全新的层面上与操作系统进行互动,拓展了 Python 的应用边界。

我们的计算机时刻都在执行着各种子进程。 事实上,即使您正在阅读这篇文章,也有很多进程在后台运行,例如网络管理程序或者您正在使用的浏览器。

有趣的是,我们在计算机上进行的几乎所有操作都涉及到子进程的调用。 即使是运行一个简单的 Python “hello world” 脚本,背后也离不开子进程的支持。

即使您已经有一定的编程经验,子进程的概念可能仍然有些模糊。 本文将深入探讨子进程的核心概念,以及如何在 Python 中使用 子进程标准库 来操控它们。

阅读完本教程后,您将:

  • 深刻理解子进程的概念
  • 掌握 Python 子进程库的基础知识
  • 通过实际案例练习 Python 编程技巧

让我们开始探索吧!

子进程概念解析

从广义上讲,子进程是指由另一个进程创建的计算机程序。 父进程可以启动多个子进程,共同完成任务。

我们可以把子进程想象成一棵树,其中每个父进程都拥有在其后运行的子进程。 这听起来可能有些复杂,但我们用简单的图示来解释一下。

我们可以通过多种方式来可视化计算机上运行的进程。 例如,在类 UNIX 系统(如 Linux 和 macOS)中,我们可以使用 htop,它是一个交互式的进程查看器。

树状视图是观察运行中子进程最实用的方式。 你可以使用F5键来启用它。

仔细观察命令部分,我们就可以清晰地看到计算机上运行的进程结构。

一切都始于 /sbin/init,它是计算机启动时运行的第一个进程,之后会启动其他进程,比如 xfce4-screenshoter 和 xfce4-terminal (这些进程又会产生更多的子进程)。

在 Windows 系统中,我们有功能强大的任务管理器,它可以帮助我们终止那些崩溃的程序。

现在,我们对子进程有了清晰的理解。接下来,让我们看看如何在 Python 中使用它们。

Python 中的子进程模块

在 Python 中,子进程指的是由 Python 脚本委托给操作系统(OS)执行的任务。

通过子进程库,我们可以在 Python 中直接启动和管理子进程,并能处理标准输入 (stdin)、标准输出 (stdout) 和返回代码。

子进程库是 Python 标准库 的一部分,无需额外安装。

因此,我们可以通过导入模块来开始在 Python 中使用子进程。

import subprocess

# 使用子进程模块...

注意:为了顺利阅读本文,您应使用 Python 3.5 或更高版本。

若要检查您的 Python 版本,请运行以下命令:

❯ python --version
Python 3.9.5 # 我的结果

如果您的 Python 版本为 2.x,请使用以下命令:

python3 --version

继续探讨,子进程库的核心思想在于,它能够允许我们通过 Python 解释器执行任何我们需要的命令,从而与操作系统进行交互。

这意味着,只要您的操作系统允许,我们可以执行任何想做的事情(当然,前提是您不会删除您的根文件系统)。

接下来,我们通过一个简单的脚本来演示如何使用子进程列出当前目录中的文件。

第一个子进程应用

首先,创建一个名为 `list_dir.py` 的文件,我们将在其中进行实验。

touch list_dir.py

现在打开文件并输入以下代码:

import subprocess

subprocess.run('ls')

首先,我们导入 `subprocess` 模块,然后使用 `run` 函数来执行命令,命令本身作为参数传递。

`run` 函数是 Python 3.5 版本引入的,它为 `subprocess.Popen` 提供了一个更友好的快捷方式。`subprocess.run` 函数允许我们运行一个命令并等待其执行完成,不同于 `Popen`,我们可以在之后调用 `communicate`。

关于代码输出,`ls` 是一个 UNIX 命令,它列出当前目录中的文件。因此,运行这个命令将返回当前目录中所有文件的列表。

❯ python list_dir.py
example.py  LICENSE  list_dir.py  README.md

注意:如果您使用的是 Windows,您需要使用不同的命令。 例如,您可以使用“dir”而不是“ls”。

这可能看起来太简单了,您说的没错。 为了充分利用 shell 的功能,我们来学习如何使用子进程向 shell 传递参数。

例如,要列出隐藏文件(以点开头的文件)和文件的所有元数据,我们编写以下代码:

import subprocess

# subprocess.run('ls')  # 简单命令

subprocess.run('ls -la', shell=True)

我们将命令作为字符串传递,并使用 `shell=True` 参数。 这表示在启动子进程时调用 shell,并且命令参数由 shell 直接解释。

然而,使用 `shell=True` 存在一些缺点,最重要的是潜在的安全风险。 您可以在 官方文档 中找到更多信息。

将命令传递给 `run` 函数的最佳方式是使用列表,其中 `lst[0]` 是要执行的命令(本例中为 `ls`),而 `lst[n]` 则是该命令的参数。

使用列表的方式,我们的代码如下:

import subprocess

# subprocess.run('ls')  # 简单命令

# subprocess.run('ls -la', shell=True) # 危险命令

subprocess.run(['ls', '-la'])

如果我们想将子进程的标准输出存储到变量中,可以通过设置 `capture_output=True` 参数来实现。

list_of_files = subprocess.run(['ls', '-la'], capture_output=True)

print(list_of_files.stdout)

❯ python list_dir.py
b'total 36ndrwxr-xr-x 3 daniel daniel 4096 may 20 21:08 .ndrwx------ 30 daniel daniel 4096 may 20 18:03 ..n-rw-r--r-- 1 daniel daniel 55 may 20 20:18 example.pyndrwxr-xr-x 8 daniel daniel 4096 may 20 17:31 .gitn-rw-r--r-- 1 daniel daniel 2160 may 17 22:23 .gitignoren-rw-r--r-- 1 daniel daniel 271 may 20 19:53 internet_checker.pyn-rw-r--r-- 1 daniel daniel 1076 may 17 22:23 LICENSEn-rw-r--r-- 1 daniel daniel 216 may 20 22:12 list_dir.pyn-rw-r--r-- 1 daniel daniel 22 may 17 22:23 README.mdn'

要访问进程的输出,我们需要使用实例属性 `stdout`。

在这个例子中,我们希望将输出存储为字符串而不是字节,可以通过设置 `text=True` 参数来实现。

list_of_files = subprocess.run(['ls', '-la'], capture_output=True, text=True)

print(list_of_files.stdout)

❯ python list_dir.py
total 36
drwxr-xr-x  3 daniel daniel 4096 may 20 21:08 .
drwx------ 30 daniel daniel 4096 may 20 18:03 ..
-rw-r--r--  1 daniel daniel   55 may 20 20:18 example.py
drwxr-xr-x  8 daniel daniel 4096 may 20 17:31 .git
-rw-r--r--  1 daniel daniel 2160 may 17 22:23 .gitignore
-rw-r--r--  1 daniel daniel  271 may 20 19:53 internet_checker.py
-rw-r--r--  1 daniel daniel 1076 may 17 22:23 LICENSE
-rw-r--r--  1 daniel daniel  227 may 20 22:14 list_dir.py
-rw-r--r--  1 daniel daniel   22 may 17 22:23 README.md

太棒了!现在我们已经掌握了子进程库的基础知识,接下来,我们来看一些使用示例。

Python 子进程库的应用实例

本节中,我们将探讨子进程库的一些实际应用。 您可以在GitHub 仓库中找到所有代码。

程序检测器

子进程库的主要应用之一是实现简单的操作系统操作。

例如,我们可以编写一个简单的脚本来检查某个程序是否已安装。在 Linux 中,我们可以使用 `which` 命令来完成此操作。

'''使用子进程检测程序'''

import subprocess

program = 'git'

process = subprocess. run(['which', program], capture_output=True, text=True)

if process.returncode == 0:
    print(f'程序 "{program}" 已安装')

    print(f'二进制文件的位置是: {process.stdout}')
else:
    print(f'抱歉,程序 {program} 未安装')

    print(process.stderr)

注意:在类 UNIX 系统中,当命令成功执行时,其状态代码为 0。否则,在执行过程中会产生问题。

由于我们没有使用 `shell=True` 参数,可以安全地获取用户输入。 此外,我们还可以使用正则表达式模式验证输入是否为有效的程序。

import subprocess
import re

programs = input('请用空格分隔要检查的程序: ').split()

secure_pattern = '^[a-zA-Z0-9]+$'

for program in programs:

    if not re.match(secure_pattern, program):
        print("抱歉,我们无法检查该程序")

        continue

    process = subprocess. run(
        ['which', program], capture_output=True, text=True)

    if process.returncode == 0:
        print(f'程序 "{program}" 已安装')

        print(f'二进制文件的位置是: {process.stdout}')
    else:
        print(f'抱歉,程序 {program} 未安装')

        print(process.stderr)

    print('n')

在这个示例中,我们从用户处获取程序,然后使用正则表达式来验证程序字符串是否仅包含字母和数字。我们使用 `for` 循环来检查每个程序是否存在。

Python 中的简单 Grep

您的朋友 Tom 有一个包含模式的列表文件和一个大型文本文件。他想在大型文件中找出每个模式的匹配数量。如果他为每个模式运行 `grep` 命令,将会花费数小时。

幸运的是,您知道如何用 Python 来解决这个问题,您可以在几秒钟内帮助他完成这项任务。

import subprocess

patterns_file="patterns.txt"
readfile="romeo-full.txt"

with open(patterns_file, 'r') as f:
    for pattern in f:
        pattern = pattern.strip()

        process = subprocess.run(
            ['grep', '-c', f'{pattern}', readfile], capture_output=True, text=True)

        if int(process.stdout) == 0:
            print(
                f'模式 "{pattern}" 在 {readfile} 中没有匹配任何行')

            continue

        print(f'模式 "{pattern}" 匹配了 {process.stdout.strip()} 次')

在这个脚本中,我们定义了两个变量,分别表示我们要使用的文件名。然后,我们打开包含所有模式的文件,并对其进行迭代。接下来,我们调用一个子进程来执行带有 `-c` 标志(表示计数)的 `grep` 命令,并使用条件语句来确定匹配的输出。

如果您运行这个文件(请记住,您可以在GitHub 仓库中找到它)。

使用子进程设置虚拟环境

使用 Python 可以实现的酷炫的事情之一是流程自动化。编写脚本每周可以为您节省大量时间。

例如,我们将创建一个安装脚本,它将为我们创建一个虚拟环境,并在当前目录中查找 `requirements.txt` 文件,以安装所有依赖项。

import subprocess
from pathlib import Path

VENV_NAME = '.venv'
REQUIREMENTS = 'requirements.txt'

process1 = subprocess.run(['which', 'python3'], capture_output=True, text=True)

if process1.returncode != 0:
    raise OSError('抱歉,python3 未安装')

python_bin = process1.stdout.strip()

print(f'在以下位置找到 Python: {python_bin}')

process2 = subprocess.run('echo "$SHELL"', shell=True, capture_output=True, text=True)

shell_bin = process2.stdout.split('/')[-1]

create_venv = subprocess.run([python_bin, '-m', 'venv', VENV_NAME], check=True)

if create_venv.returncode == 0:
    print(f'您的虚拟环境 {VENV_NAME} 已创建')

pip_bin = f'{VENV_NAME}/bin/pip3'

if Path(REQUIREMENTS).exists():
    print(f'找到 Requirements 文件 "{REQUIREMENTS}"')
    print('正在安装依赖项')
    subprocess.run([pip_bin, 'install', '-r', REQUIREMENTS])

    print('流程完成!现在使用 "source .venv/bin/activate" 来激活您的虚拟环境')

else:
    print("未指定任何依赖项...")

在这个脚本中,我们使用了多个进程,并在 Python 脚本中解析我们需要的数据。 我们还使用了 pathlib 库来检测 `requirements.txt` 文件是否存在。

如果运行 Python 文件,您将获得关于操作系统正在执行的操作的有用信息。

❯ python setup.py
在以下位置找到 Python: /usr/bin/python3
您的虚拟环境 .venv 已创建
找到 Requirements 文件 "requirements.txt"
正在安装依赖项
Collecting asgiref==3.3.4 .......
流程完成!现在使用 "source .venv/bin/activate" 来激活您的虚拟环境

请注意,我们从安装过程中获取了输出,因为我们没有将标准输出重定向到变量。

运行其他编程语言

我们可以使用 Python 来运行其他编程语言,并获取这些文件的输出。 之所以能够实现这一点,是因为子进程直接与操作系统交互。

例如,让我们用 C++ 和 Java 创建一个 “hello world” 程序。要执行以下文件,您需要安装 C++Java 编译器。

文件 `helloworld.cpp`

#include <iostream>

int main(){
    std::cout << "这是一个 C++ 版本的 hello world" << std::endl;
    return 0;
}

文件 `helloworld.java`

class HelloWorld{
    public static void main(String args[]){
     System.out.println("这是一个 Java 版本的 hello world");
    }
}

我知道这与简单的 Python 单行代码相比,似乎有很多代码,但这仅仅用于测试目的。

我们将创建一个 Python 脚本,用于运行目录中所有 C++ 和 Java 文件。为此,我们首先要根据文件扩展名获取文件列表,使用 glob 可以轻松实现!

from glob import glob

# 获取指定扩展名的文件
java_files = glob('*.java')

cpp_files = glob('*.cpp')

之后,我们就可以开始使用子进程来执行每种类型的文件了。

for file in cpp_files:
    process = subprocess.run(f'g++ {file} -o out; ./out', shell=True, capture_output=True, text=True)

    output = process.stdout.strip() + ' 顺便说一句,这是由 Python 运行的'

    print(output)

for file in java_files:
    without_ext = file.strip('.java')
    process = subprocess.run(f'java {file}; java {without_ext}',shell=True, capture_output=True, text=True)

    output = process.stdout.strip() + ' 一个 Python 子进程运行了这个 :)'
    print(output)

一个小技巧是使用字符串的 `strip` 函数来修改输出,只保留我们需要的部分。

注意:请注意,运行大型 Java 或 C++ 文件可能会导致内存泄漏,因为我们会将它们的输出加载到内存中。

打开外部程序

我们可以使用子进程来调用其他程序的二进制文件位置来运行它们。

让我们尝试打开我最喜欢的网络浏览器 Brave。

import subprocess

subprocess.run('brave')

这会打开一个浏览器实例,或者如果您已经运行了浏览器,则只会打开另一个标签页。

与任何其他接受标志的程序一样,我们可以使用它们来产生所需的效果。

import subprocess

subprocess.run(['brave', '--incognito'])

总结

子进程是指由另一个进程创建的计算机进程。我们可以使用 `htop` 和任务管理器等工具来检查计算机正在运行的进程。

Python 拥有自己的库来处理子进程。目前,`run` 函数为我们提供了一个简单的接口来创建和管理子进程。

我们可以使用子进程来创建任何类型的应用程序,因为它允许我们直接与操作系统进行交互。

最后,请记住,最好的学习方式是创建一些您喜欢使用的东西。