为啥使用 Python 的 os 模块方法而不是直接执行 shell 命令?

Posted

技术标签:

【中文标题】为啥使用 Python 的 os 模块方法而不是直接执行 shell 命令?【英文标题】:Why use Python's os module methods instead of executing shell commands directly?为什么使用 Python 的 os 模块方法而不是直接执行 shell 命令? 【发布时间】:2015-04-18 19:37:36 【问题描述】:

我试图了解使用 Python 的库函数执行特定于操作系统的任务(例如创建文件/目录、更改文件属性等)的动机,而不是仅仅通过 os.system()subprocess.call() 执行这些命令?

例如,我为什么要使用os.chmod 而不是os.system("chmod...")

我知道尽可能地使用 Python 的可用库方法而不是直接执行 shell 命令更“pythonic”。但是,从功能的角度来看,这样做还有其他动机吗?

我在这里只讨论执行简单的单行 shell 命令。例如,当我们需要对任务的执行进行更多控制时,我知道使用subprocess 模块更有意义。

【问题讨论】:

你基本上一针见血。您所指的操作系统级别的任务很常见,它们保证了它们自己的功能,而不是仅仅被降级为通过 os.system 调用。 顺便说一句,您是否尝试过计算执行时间 - os.chmodos.system("chmod...")。我会冒险猜测它会回答你的部分问题。 既然可以os.system("echo Hello world!"),为什么还要print 出于同样的原因,您应该使用os.path 来处理路径而不是手动处理它们:它适用于运行它的每个操作系统。 “直接执行shell命令”其实是less直接的。 shell 不是系统的低级接口,os.chmod 不会调用 shell 会调用的chmod 程序。使用os.system('chmod ...') 启动一个shell 来解释一个字符串以调用另一个可执行文件来调用C chmod 函数,而os.chmod(...) 更直接地调用C chmod 【参考方案1】:

    更快os.systemsubprocess.call 创建新进程,这对于这么简单的事情是不必要的。事实上,os.systemsubprocess.call 带有 shell 参数通常会创建至少两个新进程:第一个是 shell,第二个是你正在运行的命令(如果它不是内置的 shell - 像test)。

    某些命令在单独的进程中无用。例如,如果您运行os.spawn("cd dir/"),它将更改子进程的当前工作目录,但不会更改 Python 进程的当前工作目录。您需要为此使用os.chdir

    您不必担心 shell 会解释特殊的字符。无论文件名是什么,os.chmod(path, mode) 都可以使用,而如果文件名类似于 ; rm -rf ~os.spawn("chmod 777 " + path) 将严重失败。 (请注意,如果您使用不带 shell 参数的 subprocess.call,则可以解决此问题。)

    您不必担心以破折号开头的文件名os.chmod("--quiet", mode) 将更改名为 --quiet 的文件的权限,但 os.spawn("chmod 777 --quiet") 将失败,因为 --quiet 被解释为参数。即使subprocess.call(["chmod", "777", "--quiet"])也是如此。

    您的跨平台 和跨shell 问题较少,因为Python 的标准库应该为您处理这些问题。你的系统有chmod 命令吗?安装了吗?它是否支持您期望它支持的参数? os 模块将尝试尽可能跨平台,并在不可能的情况下记录。

    如果您正在运行的命令具有您关心的 输出,则需要对其进行解析,这比听起来更棘手,因为您可能会忘记极端情况(文件名包含空格、制表符和换行符),即使您不关心可移植性。

【讨论】:

要添加到“跨平台”点,列出目录是 linux 上的“ls”,windows 上的“dir”。获取目录的内容是一项非常常见的低级任务。 @CortAmmon: "Low-Level" 是相对的,lsdir 对于某些类型的开发人员来说是相当高的水平,就像 bashcmdksh 或无论你喜欢什么外壳。 @phresnel :我从来没有这样想过。对我来说,“直接调用你的操作系统的内核 API”是非常低级的。我假设对此我有不同的看法,因为我(自然)带着自己的偏见接近它。 @CortAmmon:对,ls 比这更高,因为它不是对操作系统内核 API 的直接调用。这是一个(小)应用程序。 @SteveJessop。我将“获取目录的内容”称为低级。我想的不是lsdir,而是opendir()/readdir() (linux api) 或FindFirstFile()/FindNextFile() (windows api) 或File.listFiles (java API) 或Directory.GetFiles() (C#)。所有这些都与直接调用操作系统密切相关。有些可能就像将数字压入寄存器并调用int 13h 以触发内核模式一样简单。【参考方案2】:

这样更安全。这里给你一个想法是一个示例脚本

import os
file = raw_input("Please enter a file: ")
os.system("chmod 777 " + file)

如果用户的输入是test; rm -rf ~,这将删除主目录。

这就是使用内置函数更安全的原因。

因此你也应该使用 subprocess 而不是 system。

【讨论】:

或者换个角度来看,写 Python 程序还是写写 shell 脚本的 Python 程序哪个更容易正确? :-) @SteveJessop,我的一位同事对我帮助他编写的一个 Python 小脚本的运行速度比 tan shell 脚本快 20(!)倍感到惊讶。我解释说输出重定向可能看起来很性感——但它需要在每次迭代时打开和关闭文件。但是有些人喜欢用艰难的方式做事-:) @SteveJessop,这是一个棘手的问题——直到运行时你才会知道! :)【参考方案3】:

在执行命令时优先使用os 模块中的Python 更具体的方法而不是使用os.systemsubprocess 模块有四种强有力的案例:

冗余 - 生成另一个进程是多余的,而且会浪费时间和资源。 可移植性 - os 模块中的许多方法可用于多个平台,而许多 shell 命令是特定于操作系统的。 了解结果 - 生成一个执行任意命令的进程会强制您从输出中解析结果并了解 如果为什么 命令已完成出了点问题。 安全 - 一个进程可能会执行它给出的任何命令。这是一个弱设计,可以通过使用 os 模块中的特定方法来避免。

冗余(参见redundant code):

您实际上是在执行最终系统调用(在您的示例中为chmod)的途中执行了一个冗余的“中间人”。这个中间人是一个新的进程或子shell。

来自os.system

在子shell中执行命令(字符串)...

subprocess 只是一个生成新进程的模块。

您可以在不产生这些进程的情况下做您需要的事情。

可移植性(参见source code portability):

os 模块的目的是提供通用操作系统服务,它的描述以:

此模块提供了一种使用操作系统相关功能的可移植方式。

您可以在 windows 和 unix 上使用os.listdir。尝试使用 os.system / subprocess 来实现此功能将迫使您维持两个调用(对于 ls / dir)并检查您正在使用的操作系统。这不是那么便携,并且在以后造成更多的挫败感(请参阅处理输出)。

了解命令的结果:

假设您要列出目录中的文件。

如果你使用os.system("ls")/subprocess.call(['ls']),你只能取回进程的输出,这基本上是一个带有文件名的大字符串。

如何从两个文件中分辨出一个文件名中带有空格的文件?

如果您无权列出文件怎么办?

你应该如何将数据映射到 python 对象?

这些只是我的想法,虽然这些问题有解决方案 - 为什么要再次解决为您解决的问题?

这是一个遵循Don't Repeat Yourself 原则(通常称为“DRY”)的示例,重复已经存在且可供您免费使用的实现。

安全:

os.systemsubprocess 功能强大。当你需要这种力量时很好,但当你不需要时很危险。当您使用os.listdir 时,您知道除了列出文件或引发错误之外,它不能做任何其他事情。当您使用 os.systemsubprocess 来实现相同的行为时,您最终可能会做一些您不打算做的事情。

注射安全(见shell injection examples

如果您使用来自用户的输入作为新命令,您基本上就给了他一个 shell。这很像 SQL 注入在数据库中为用户提供一个 shell。

一个例子是以下形式的命令:

# ... read some user input
os.system(user_input + " some continutation")

这很容易被利用来运行 any 任意代码,使用输入:NASTY COMMAND;# 创建最终:

os.system("NASTY COMMAND; # some continuation")

有许多这样的命令会使您的系统处于危险之中。

【讨论】:

我会说2.是主要原因。【参考方案4】:

出于一个简单的原因 - 当您调用 shell 函数时,它会创建一个子 shell,该子 shell 在您的命令存在后被销毁,因此如果您在 shell 中更改目录 - 它不会影响您在 Python 中的环境。

此外,创建子 shell 非常耗时,因此直接使用操作系统命令会影响您的性能

编辑

我运行了一些计时测试:

In [379]: %timeit os.chmod('Documents/recipes.txt', 0755)
10000 loops, best of 3: 215 us per loop

In [380]: %timeit os.system('chmod 0755 Documents/recipes.txt')
100 loops, best of 3: 2.47 ms per loop

In [382]: %timeit call(['chmod', '0755', 'Documents/recipes.txt'])
100 loops, best of 3: 2.93 ms per loop

内部函数运行速度快 10 倍以上

EDIT2

在某些情况下,调用外部可执行文件可能会产生比 Python 包更好的结果 - 我只记得我的一位同事发送的一封邮件,通过子进程调用 gzip 的性能远高于性能他使用的一个 Python 包。但当我们谈论模拟标准操作系统命令的标准操作系统包时,肯定不是

【讨论】:

有可能用 iPython 完成吗?没想到您可以使用普通解释器使用以% 开头的特殊功能。 @aPyDeveloper,是的,它是 iPython - 在 Ubuntu 上。 “神奇”%timeit 是一种祝福——尽管在某些情况下——主要是字符串格式——它无法处理 或者你也可以制作一个python脚本,然后在终端输入time <path to script>,它会告诉你真实的、用户和进程所花费的时间。也就是说,如果您没有 iPython 并且可以访问 Unix 命令行。 @aPyDeveloper,我认为没有理由努力工作——当我的机器上有 iPython 时 真的!我确实说过如果你没有 iPython。 :)【参考方案5】:

在大多数情况下,Shell 调用是特定于操作系统的,而 Python os 模块函数则不是。并且它避免产生子进程。

【讨论】:

Python 模块函数也会产生新的子进程来调用新的子shell。 @Koderok 废话,模块函数在进程中调用 @Koderok:os 模块使用 shell 命令使用的底层系统调用,它不使用 shell 命令。这意味着 os 系统调用通常比 shell 命令更安全、更快(没有字符串解析、boo fork、没有 exec,而只是一个内核调用)。请注意,在大多数情况下,shell 调用和系统调用通常具有相似或相同的名称,但分别记录在案; shell 调用位于 man 部分 1(默认的 man 部分),而等效命名的系统调用位于 man 部分 2(例如 man 2 chmod)。 @dwurf,LieRyan:我的错!我有一个错误的想法,似乎。谢谢!【参考方案6】:

它的效率要高得多。 “shell”只是另一个包含大量系统调用的操作系统二进制文件。为什么要为单个系统调用创建整个 shell 进程的开销?

当您将os.system 用于不是shell 内置的东西时,情况会更糟。您启动一个 shell 进程,该进程又启动一个可执行文件,然后(两个进程之外)进行系统调用。至少 subprocess 会消除对 shell 中间进程的需要。

这不是特定于 Python 的。 systemd 对 Linux 启动时间的改进是出于同样的原因:它自己进行必要的系统调用,而不是产生一千个 shell。

【讨论】:

以上是关于为啥使用 Python 的 os 模块方法而不是直接执行 shell 命令?的主要内容,如果未能解决你的问题,请参考以下文章

Python:使用 os 模块检查目录是不是存在

为啥 os.path 指的是项目路径而不是文件路径?

python os模块功能和方法总结

python中os模块的常用方法

[Python3]目录与文件操作

Python必知必会 os 模块详解