为什么要使用Python的os模块方法而不是直接执行Shell命令?

160

我正在尝试理解为什么使用Python库函数来执行操作系统特定任务,例如创建文件/目录、更改文件属性等等,而不是通过os.system()subprocess.call()执行那些命令?

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

我知道尽可能使用Python的可用库方法比直接执行shell命令更符合"pythonic"的原则。但是,从功能的角度来看,是否还有其他动机呢?

我只是在谈论执行简单的一行shell命令。当需要更多对任务执行的控制时,我理解使用subprocess模块更有意义, 例如。


6
你的理解基本上很到位。你所提到的操作系统级别的任务很常见,因此它们有了自己的函数,而不仅仅是通过os.system调用。 - deweyredman
7
顺便问一下,你是否尝试过计时执行时间 - os.chmod vs. _os.system("chmod...")_。我敢猜想这会回答你问题的一部分。 - volcano
63
为什么要用print,而不使用os.system("echo Hello world!") - user253751
25
为了同样的原因,您应该使用os.path来处理路径,而不是手动处理它们:它可以在运行的每个操作系统上工作。 - Bakuriu
52
直接执行 shell 命令实际上比较“间接”。Shell 不是系统的低级界面,而 os.chmod 不会调用 shell 执行 chmod 程序。使用 os.system('chmod ...') 会启动一个 shell 来解释一个字符串并调用另一个可执行文件来调用 C 的 chmod 函数,而 os.chmod(...) 直接调用 C 的 chmod - user2357112
3
严格来说,它不一定会进入C的chmod函数,而是进入chmod系统调用。在CPython的情况下,它可能不会重新实现本地系统调用约定,而是只是重用C库的系统调用约定实现,但其他实现可能会做出不同的决定。 - Lie Ryan
6个回答

328
  1. 更快速os.systemsubprocess.call会创建新的进程,这对于这种简单的操作来说是不必要的。实际上,os.system和带有shell参数的subprocess.call通常会至少创建两个新进程:第一个是shell,第二个是你正在运行的命令(如果它不是shell内置命令,如test)。

  2. 某些命令在单独的进程中没有意义。例如,如果你运行os.spawn("cd dir/"),它会改变子进程的当前工作目录,但不会改变Python进程的当前工作目录。你需要使用os.chdir来改变Python进程的当前工作目录。

  3. 你不必担心由shell解释的特殊字符。无论文件名是什么,os.chmod(path, mode)都能正常工作,而os.spawn("chmod 777 " + path)如果文件名像; rm -rf ~这样,将会失败得很惨。(请注意,如果你使用没有shell参数的subprocess.call,你可以绕过这个问题。)

  4. 你不必担心以破折号开头的文件名os.chmod("--quiet", mode)将会改变文件名为--quiet的文件的权限,但是os.spawn("chmod 777 --quiet")将会失败,因为--quiet被解释为一个选项参数。这对于subprocess.call(["chmod", "777", "--quiet"])也是如此。

  • 由于Python的标准库可以帮助您处理跨平台和跨Shell的问题,因此您拥有更少的跨平台和跨Shell方面的担忧。您的系统是否有chmod命令?它是否已安装?它是否支持您期望的参数?os模块将尽可能考虑跨平台性,并在不可能时进行文档记录。

  • 如果您关心正在运行的命令的输出,则需要解析它,这比听起来要棘手得多,因为即使您不关心可移植性,您也可能会忘记一些边角情况(文件名中包含空格、制表符和换行符)。


  • 38
    补充一下“跨平台”的要点,列出目录在Linux上是“ls”,在Windows上是“dir”。获取目录内容是一个非常常见的低级任务。 - Cort Ammon
    1
    @CortAmmon:"低级"是相对的,对某些类型的开发人员来说,lsdir已经相当高级了,就像bashcmdksh或你喜欢的任何外壳一样。 - Sebastian Mach
    1
    @phresnel:我从未这样想过。对我来说,“直接调用操作系统内核API”非常低级。我假设有一种不同的视角,但由于我自己的偏见,我无法理解它。 - Cort Ammon
    5
    @CortAmmon说得对,ls 比那个更高级一些,因为它不是直接调用操作系统的内核API,而是一个(小)应用程序。 - Steve Jessop
    1
    @SteveJessop。我称“获取目录内容”为低级操作。我不是在想lsdir,而是opendir()/readdir()(Linux API)或FindFirstFile()/FindNextFile()(Windows API)或File.listFiles(Java API)或Directory.GetFiles()(C#)。所有这些都与直接调用操作系统紧密相关。有些可能只需将一个数字推入寄存器并调用int 13h以触发内核模式即可。 - Cort Ammon
    显示剩余2条评论

    134

    更安全。这里是一个示例脚本,供您参考。

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

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

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

    因此,您应该使用子进程而不是系统。


    26
    或者换个角度看,写Python程序容易还是写能够编写shell脚本的Python程序容易呢? :-) - Steve Jessop
    3
    我的同事SteveJessop惊讶于我帮他写的一小段Python脚本比shell脚本快了20倍(!)。我解释道,输出重定向看上去很不错,但实际上需要在每次迭代中打开和关闭文件。但有些人喜欢用更困难的方法 - :) - volcano
    1
    @SteveJessop,这是一个诡计问题 - 直到运行时你才会知道! :) - user1902824

    60

    使用os模块中的更具体方法而不是使用os.systemsubprocess模块执行命令,有四个充分的理由:

    • 冗余 - 产生另一个进程是多余的,浪费时间和资源。
    • 可移植性 - os模块中的许多方法适用于多个平台,而许多shell命令是特定于操作系统的。
    • 理解结果 - 引发进程以执行任意命令会强制你从输出中解析结果并理解命令何时以及为什么出错。
    • 安全性 - 进程可以潜在地执行它收到的任何命令。这是一个薄弱的设计,可以通过在os模块中使用特定方法来避免。

    冗余(参见冗余代码):

    实际上,在您执行最终的系统调用(例如您的chmod)之前,您正在执行一个多余的“中间人”。这个中间人是一个新进程或子shell。

    来自os.system

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

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

    您可以在不生成这些进程的情况下完成所需的操作。

    可移植性(参见源代码可移植性):

    os模块的目的是提供通用的操作系统服务,其描述以以下内容开头:

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

    你可以在Windows和Unix上都使用os.listdir。尝试使用os.system/subprocess实现此功能将强制您维护两个调用(用于ls/dir)并检查您所在的操作系统。这不是很便携,并且稍后会引起更多的挫败感(请参见处理输出)。

    理解命令结果:

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

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

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

    如果您没有权限列出文件怎么办?

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

    这些只是我脑海中的一些问题,虽然有解决这些问题的方法,但为什么要再次解决已经为您解决的问题呢?

    这是遵循不要重复自己原则的一个示例(通常称为“DRY”),通过重复已经存在并且可以免费使用的实现来实现。

    安全性:

    os.systemsubprocess非常强大。当你需要这种力量时,它是好的,但是当你不需要时,它是危险的。当你使用os.listdir时,你知道它只能列出文件或引发错误。当你使用os.systemsubprocess来实现相同的行为时,你可能最终会做一些你本不想做的事情。

    注入安全性(见shell注入示例:

    如果你将用户输入作为新命令使用,那么你基本上给了他一个Shell。这就像SQL注入为用户提供了数据库中的Shell。

    一个示例是形如:

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

    这可以轻松地被利用来使用输入运行任意代码:NASTY COMMAND;#,以创建最终结果:
    os.system("NASTY COMMAND; # some continuation")
    

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


    3
    我认为2是主要原因。 - jaredad7

    23

    有一个简单的原因 - 当你调用一个 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 实现的?我原以为在普通解释器中不能使用以“%”开头的特殊函数。 - iProgram
    @aPyDeveloper,没错,它是在Ubuntu上的iPython。"Magical" %timeit 是一种福音 - 尽管有些情况 - 大多数情况下是字符串格式化 - 它无法处理。 - volcano
    1
    或者您也可以编写一个Python脚本,然后在终端中键入“time <脚本路径>”,它将告诉您所花费的实际时间、用户时间和进程时间。这是在您没有iPython但可以访问Unix命令行的情况下。 - iProgram
    1
    @aPyDeveloper,我觉得没有必要辛苦工作——因为我在我的机器上有iPython。 - volcano
    真的!我说过如果你没有iPython的话。 :) - iProgram

    16

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


    1
    Python模块函数还会生成新的子进程来调用一个新的子shell。 - Koderok
    7
    理论上,模块函数是在进程内被调用的,@Koderok说的不是很准确。 - dwurf
    3
    @Koderok:os模块使用与shell命令相同的底层系统调用,但它不使用shell命令本身。这意味着os系统调用通常比shell命令更安全且更快(无需字符串解析,避免了分叉和执行操作,而只需进行内核调用)。请注意,在大多数情况下,shell调用和系统调用通常具有类似或相同的名称,但它们在文档中是分开记录的;shell调用在man第1节(默认的man节)中,而同名的系统调用在man第2节中(例如,man 2 chmod)。 - Lie Ryan
    1
    @dwurf,LieRyan:我的错!看来我有一个错误的概念。谢谢! - Koderok

    11

    它更加高效。"shell"只是另一个包含许多系统调用的操作系统二进制文件。为什么要为了单个系统调用而产生整个shell进程的开销呢?

    如果你使用os.system来执行不是shell内置的功能,情况会更糟。你启动一个shell进程,它再启动可执行文件,然后(通过两个进程)进行系统调用。至少subprocess会删除需要shell中介进程的需求。

    这并不是具体针对Python的。 systemd出于同样的原因成为Linux启动时间的改进:它自己进行必要的系统调用,而不是生成一千个shells。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接