Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 324 – subprocess - Neues Prozessmodul

Autor:
Peter Astrand <astrand at lysator.liu.se>
Status:
Final
Typ:
Standards Track
Erstellt:
19-Nov-2003
Python-Version:
2.4
Post-History:


Inhaltsverzeichnis

Zusammenfassung

Diese PEP beschreibt ein neues Modul zum Starten und Kommunizieren mit Prozessen.

Motivation

Das Starten neuer Prozesse ist eine gängige Aufgabe in jeder Programmiersprache und besonders häufig in einer Hochsprache wie Python. Gute Unterstützung für diese Aufgabe ist notwendig, denn

  • Ungeeignete Funktionen zum Starten von Prozessen können ein Sicherheitsrisiko darstellen: Wenn das Programm über die Shell gestartet wird und die Argumente Shell-Metazeichen enthalten, kann das Ergebnis katastrophal sein. [1]
  • Dadurch wird Python zu einer noch besseren Ersatzsprache für übermäßig komplizierte Shell-Skripte.

Derzeit verfügt Python über eine große Anzahl unterschiedlicher Funktionen zur Prozesserstellung. Dies macht es für Entwickler schwierig, die richtige Wahl zu treffen.

Das subprocess-Modul bietet folgende Verbesserungen gegenüber früheren Funktionen:

  • Ein „einheitliches“ Modul bietet die gesamte Funktionalität der früheren Funktionen.
  • Prozessübergreifende Ausnahmen: Ausnahmen, die im Kindprozess auftreten, bevor der neue Prozess mit der Ausführung begonnen hat, werden im Elternprozess erneut ausgelöst. Dies bedeutet, dass z.B. `exec()`-Fehler leicht behandelt werden können. Mit popen2 ist es beispielsweise unmöglich zu erkennen, ob die Ausführung fehlgeschlagen ist.
  • Ein Hook zum Ausführen von benutzerdefiniertem Code zwischen fork und exec. Dies kann beispielsweise zum Ändern der UID verwendet werden.
  • Kein impliziter Aufruf von /bin/sh. Das bedeutet, dass keine gefährlichen Shell-Metazeichen maskiert werden müssen.
  • Alle Kombinationen der Dateideskriptor-Umleitung sind möglich. Zum Beispiel muss die „python-dialog“-Bibliothek [2] einen Prozess starten und stderr umleiten, aber nicht stdout. Dies ist mit den aktuellen Funktionen nicht möglich, ohne temporäre Dateien zu verwenden.
  • Mit dem subprocess-Modul ist es möglich zu steuern, ob alle offenen Dateideskriptoren vor der Ausführung des neuen Programms geschlossen werden sollen.
  • Unterstützung für die Verbindung mehrerer Subprozesse (Shell-„Pipe“).
  • Universelle Zeilenende-Unterstützung.
  • Eine `communicate()`-Methode, die es einfach macht, stdin-Daten zu senden und stdout- und stderr-Daten zu lesen, ohne Blockaden zu riskieren. Die meisten Menschen sind sich der Flusskontrollprobleme bewusst, die mit der Kommunikation mit Kindprozessen verbunden sind, aber nicht alle haben die Geduld oder die Fähigkeiten, eine vollständig korrekte und blockierungsfreie Select-Schleife zu schreiben. Dies bedeutet, dass viele Python-Anwendungen Wettlaufsituationen enthalten. Eine `communicate()`-Methode in der Standardbibliothek löst dieses Problem.

Begründung

Die folgenden Punkte fassen das Design zusammen:

  • subprocess basierte auf popen2, was sich bewährt hat.
  • Die Factory-Funktionen in popen2 wurden entfernt, da ich den Klassenkonstruktor für ebenso einfach zu handhaben halte.
  • popen2 enthält mehrere Factory-Funktionen und Klassen für verschiedene Umleitungskombinationen. subprocess hingegen enthält eine einzige Klasse. Da das subprocess-Modul 12 verschiedene Umleitungskombinationen unterstützt, wäre die Bereitstellung einer Klasse oder Funktion für jede einzelne davon umständlich und nicht sehr intuitiv. Selbst bei popen2 ist dies ein Lesbarkeitsproblem. Zum Beispiel können viele Leute ohne die Dokumentation den Unterschied zwischen popen2.popen2 und popen2.popen4 nicht erkennen.
  • Eine kleine Hilfsfunktion wird bereitgestellt: `subprocess.call()`. Sie zielt darauf ab, eine Verbesserung gegenüber `os.system()` zu sein, während sie dennoch sehr einfach zu bedienen ist.
    • Sie verwendet nicht die Standard-C-Funktion system(), die Einschränkungen hat.
    • Sie ruft die Shell nicht implizit auf.
    • Keine Notwendigkeit für Anführungszeichen; Verwendung einer Argumentenliste.
    • Der Rückgabewert ist einfacher zu handhaben.

    Die Hilfsfunktion `call()` akzeptiert ein 'args'-Argument, genau wie der Konstruktor der `Popen`-Klasse. Sie wartet, bis der Befehl abgeschlossen ist, und gibt dann das `returncode`-Attribut zurück. Die Implementierung ist sehr einfach.

    def call(*args, **kwargs):
        return Popen(*args, **kwargs).wait()
    

    Die Motivation hinter der `call()`-Funktion ist einfach: Das Starten eines Prozesses und das Warten auf dessen Beendigung ist eine häufige Aufgabe.

    Während `Popen` eine breite Palette von Optionen unterstützt, haben viele Benutzer einfache Bedürfnisse. Viele Leute verwenden derzeit `os.system()`, hauptsächlich weil es eine einfache Schnittstelle bietet. Betrachten Sie dieses Beispiel:

    os.system("stty sane -F " + device)
    

    Mit `subprocess.call()` würde dies so aussehen:

    subprocess.call(["stty", "sane", "-F", device])
    

    Oder, wenn die Ausführung über die Shell erfolgt:

    subprocess.call("stty sane -F " + device, shell=True)
    
  • Die „preexec“-Funktionalität ermöglicht die Ausführung beliebigen Codes zwischen fork und exec. Man könnte sich fragen, warum es spezielle Argumente für die Umgebung und das aktuelle Verzeichnis gibt, aber nicht beispielsweise für das Setzen der UID. Die Antwort ist:
    • Das Ändern der Umgebung und des Arbeitsverzeichnisses wird als recht üblich angesehen.
    • Ältere Funktionen wie `spawn()` haben Unterstützung für ein „env“-Argument.
    • env und cwd gelten als ziemlich plattformübergreifend: Sie sind auch unter Windows sinnvoll.
  • Auf POSIX-Plattformen ist kein Erweiterungsmodul erforderlich: Das Modul verwendet `os.fork()`, `os.execvp()` usw.
  • Auf Windows-Plattformen benötigt das Modul entweder Mark Hammonds Windows-Erweiterungen [5] oder ein kleines Erweiterungsmodul namens _subprocess.

Spezifikation

Dieses Modul definiert eine Klasse namens Popen.

class Popen(args, bufsize=0, executable=None,
            stdin=None, stdout=None, stderr=None,
            preexec_fn=None, close_fds=False, shell=False,
            cwd=None, env=None, universal_newlines=False,
            startupinfo=None, creationflags=0):

Argumente sind:

  • `args` sollte ein String oder eine Sequenz von Programmargumenten sein. Das auszuführende Programm ist normalerweise das erste Element in der `args`-Sequenz oder im String, kann aber explizit durch die Verwendung des `executable`-Arguments festgelegt werden.

    Unter UNIX, mit `shell=False` (Standard): In diesem Fall verwendet die `Popen`-Klasse `os.execvp()`, um das Kindprogramm auszuführen. `args` sollte normalerweise eine Sequenz sein. Ein String wird als Sequenz mit dem String als einzigem Element (dem auszuführenden Programm) behandelt.

    Unter UNIX, mit `shell=True`: Wenn `args` ein String ist, gibt er die auszuführende Befehlszeichenkette über die Shell an. Wenn `args` eine Sequenz ist, gibt das erste Element die Befehlszeichenkette an, und alle nachfolgenden Elemente werden als zusätzliche Shell-Argumente behandelt.

    Unter Windows: Die `Popen`-Klasse verwendet `CreateProcess()`, um das Kindprogramm auszuführen, das mit Strings arbeitet. Wenn `args` eine Sequenz ist, wird es mit der Methode `list2cmdline` in einen String umgewandelt. Bitte beachten Sie, dass nicht alle MS Windows-Anwendungen die Befehlszeile auf die gleiche Weise interpretieren: `list2cmdline` ist für Anwendungen konzipiert, die die gleichen Regeln wie die MS C-Laufzeit verwenden.

  • `bufsize`, falls angegeben, hat die gleiche Bedeutung wie das entsprechende Argument für die eingebaute `open()`-Funktion: 0 bedeutet unbuffered, 1 bedeutet zeilenweise gepuffert, jeder andere positive Wert bedeutet die Verwendung eines Puffers von (ungefähr) dieser Größe. Ein negatives `bufsize` bedeutet die Verwendung des Systemstandards, was normalerweise eine vollständige Pufferung bedeutet. Der Standardwert für `bufsize` ist 0 (unbuffered).
  • `stdin`, `stdout` und `stderr` geben die Standard-Input-, Standard-Output- und Standard-Error-Dateihandles des ausgeführten Programms an. Gültige Werte sind `PIPE`, ein existierender Dateideskriptor (eine positive Ganzzahl), ein existierendes Datei-Objekt und `None`. `PIPE` zeigt an, dass eine neue Pipe zum Kindprozess erstellt werden soll. Mit `None` findet keine Umleitung statt; die Dateihandles des Kindes werden vom Elternprozess übernommen. Zusätzlich kann `stderr` `STDOUT` sein, was anzeigt, dass die stderr-Daten von den Anwendungen im selben Dateihandle wie für stdout erfasst werden sollen.
  • Wenn `preexec_fn` auf ein aufrufbares Objekt gesetzt ist, wird dieses Objekt im Kindprozess aufgerufen, kurz bevor der Kindprozess ausgeführt wird.
  • Wenn `close_fds` true ist, werden alle Dateideskriptoren außer 0, 1 und 2 geschlossen, bevor der Kindprozess ausgeführt wird.
  • Wenn `shell` true ist, wird der angegebene Befehl über die Shell ausgeführt.
  • Wenn `cwd` nicht `None` ist, wird das aktuelle Verzeichnis vor der Ausführung des Kindprozesses zu `cwd` geändert.
  • Wenn `env` nicht `None` ist, definiert es die Umgebungsvariablen für den neuen Prozess.
  • Wenn `universal_newlines` true ist, werden die Datei-Objekte stdout und stderr als Textdateien geöffnet, aber Zeilen können durch jedes der folgenden Zeichen beendet werden: `\n`, die Unix-Zeilenende-Konvention, `\r`, die Macintosh-Konvention oder `\r\n`, die Windows-Konvention. Alle diese externen Darstellungen werden vom Python-Programm als `\n` betrachtet. Hinweis: Diese Funktion ist nur verfügbar, wenn Python mit Unterstützung für universelle Zeilenenden kompiliert wurde (Standard). Außerdem werden die `newlines`-Attribute der Datei-Objekte stdout, stdin und stderr von der `communicate()`-Methode nicht aktualisiert.
  • `startupinfo` und `creationflags`, falls angegeben, werden an die zugrunde liegende `CreateProcess()`-Funktion übergeben. Sie können Dinge wie das Aussehen des Hauptfensters und die Priorität für den neuen Prozess angeben. (Nur Windows)

Dieses Modul definiert auch zwei Shortcut-Funktionen:

  • call(*args, **kwargs):
    Führt den Befehl mit den Argumenten aus. Wartet auf die Beendigung des Befehls und gibt dann das `returncode`-Attribut zurück.

    Die Argumente sind die gleichen wie für den Popen-Konstruktor. Beispiel:

    retcode = call(["ls", "-l"])
    

Ausnahmen

Im Kindprozess ausgelöste Ausnahmen, bevor das neue Programm mit der Ausführung begonnen hat, werden im Elternprozess erneut ausgelöst. Zusätzlich hat das Ausnahmeobjekt ein zusätzliches Attribut namens 'child_traceback', das eine Zeichenkette mit Traceback-Informationen aus der Sicht des Kindes enthält.

Die häufigste Ausnahme ist `OSError`. Dies tritt beispielsweise auf, wenn versucht wird, eine nicht existierende Datei auszuführen. Anwendungen sollten für `OSErrors` vorbereitet sein.

Ein `ValueError` wird ausgelöst, wenn Popen mit ungültigen Argumenten aufgerufen wird.

Sicherheit

Im Gegensatz zu einigen anderen popen-Funktionen ruft diese Implementierung niemals implizit /bin/sh auf. Das bedeutet, dass alle Zeichen, einschließlich Shell-Metazeichen, sicher an Kindprozesse übergeben werden können.

Popen-Objekte

Instanzen der Popen-Klasse haben die folgenden Methoden:

poll()
Prüft, ob der Kindprozess beendet wurde. Gibt das `returncode`-Attribut zurück.
wait()
Wartet auf die Beendigung des Kindprozesses. Gibt das `returncode`-Attribut zurück.
communicate(input=None)
Interagiert mit dem Prozess: Sendet Daten an stdin. Liest Daten von stdout und stderr, bis das Ende der Datei erreicht ist. Wartet auf die Beendigung des Prozesses. Das optionale stdin-Argument sollte eine Zeichenkette sein, die an den Kindprozess gesendet werden soll, oder `None`, wenn keine Daten an den Kindprozess gesendet werden sollen.

`communicate()` gibt ein Tupel `(stdout, stderr)` zurück.

Hinweis: Die gelesenen Daten werden im Speicher gepuffert. Verwenden Sie diese Methode daher nicht, wenn die Datengröße groß oder unbegrenzt ist.

Die folgenden Attribute sind ebenfalls verfügbar:

stdin
Wenn das `stdin`-Argument `PIPE` ist, ist dieses Attribut ein Datei-Objekt, das Eingaben an den Kindprozess liefert. Andernfalls ist es `None`.
stdout
Wenn das `stdout`-Argument `PIPE` ist, ist dieses Attribut ein Datei-Objekt, das Ausgaben vom Kindprozess liefert. Andernfalls ist es `None`.
stderr
Wenn das `stderr`-Argument `PIPE` ist, ist dieses Attribut ein Datei-Objekt, das Fehlerausgaben vom Kindprozess liefert. Andernfalls ist es `None`.
pid
Die Prozess-ID des Kindprozesses.
returncode
Der Rückgabecode des Kindes. Ein `None`-Wert zeigt an, dass der Prozess noch nicht beendet wurde. Ein negativer Wert -N zeigt an, dass das Kind durch Signal N beendet wurde (nur UNIX).

Ersetzen älterer Funktionen durch das subprocess-Modul

In diesem Abschnitt bedeutet „a ==> b“, dass b als Ersatz für a verwendet werden kann.

Hinweis: Alle Funktionen in diesem Abschnitt schlagen (mehr oder weniger) stillschweigend fehl, wenn das auszuführende Programm nicht gefunden werden kann. Dieses Modul löst eine OSError-Ausnahme aus.

In den folgenden Beispielen wird davon ausgegangen, dass das subprocess-Modul mit `from subprocess import *` importiert wurde.

Ersetzen der Shell-Backquote von /bin/sh

output=`mycmd myarg`
==>
output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]

Ersetzen von Shell-Pipelining

output=`dmesg | grep hda`
==>
p1 = Popen(["dmesg"], stdout=PIPE)
p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
output = p2.communicate()[0]

Ersetzen von os.system()

sts = os.system("mycmd" + " myarg")
==>
p = Popen("mycmd" + " myarg", shell=True)
sts = os.waitpid(p.pid, 0)

Hinweis

  • Das Ausführen von Programmen über die Shell ist normalerweise nicht erforderlich.
  • Es ist einfacher, das `returncode`-Attribut zu betrachten als den Exit-Status.

Ein realistischeres Beispiel würde so aussehen:

try:
    retcode = call("mycmd" + " myarg", shell=True)
    if retcode < 0:
        print >>sys.stderr, "Child was terminated by signal", -retcode
    else:
        print >>sys.stderr, "Child returned", retcode
except OSError, e:
    print >>sys.stderr, "Execution failed:", e

Ersetzen von os.spawn*

P_NOWAIT Beispiel

pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
==>
pid = Popen(["/bin/mycmd", "myarg"]).pid

P_WAIT Beispiel

retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
==>
retcode = call(["/bin/mycmd", "myarg"])

Vektor Beispiel

os.spawnvp(os.P_NOWAIT, path, args)
==>
Popen([path] + args[1:])

Umgebungsbeispiel

os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
==>
Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})

Ersetzen von os.popen*

pipe = os.popen(cmd, mode='r', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout

pipe = os.popen(cmd, mode='w', bufsize)
==>
pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin


(child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdin, child_stdout) = (p.stdin, p.stdout)


(child_stdin,
 child_stdout,
 child_stderr) = os.popen3(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
(child_stdin,
 child_stdout,
 child_stderr) = (p.stdin, p.stdout, p.stderr)


(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize)
==>
p = Popen(cmd, shell=True, bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)

Ersetzen von popen2.*

Hinweis: Wenn das `cmd`-Argument für `popen2`-Funktionen ein String ist, wird der Befehl über `/bin/sh` ausgeführt. Wenn es eine Liste ist, wird der Befehl direkt ausgeführt.

(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
==>
p = Popen(["somestring"], shell=True, bufsize=bufsize
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)


(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode)
==>
p = Popen(["mycmd", "myarg"], bufsize=bufsize,
          stdin=PIPE, stdout=PIPE, close_fds=True)
(child_stdout, child_stdin) = (p.stdout, p.stdin)

`popen2.Popen3` und `popen3.Popen4` funktionieren im Grunde wie `subprocess.Popen`, außer dass

  • `subprocess.Popen` eine Ausnahme auslöst, wenn die Ausführung fehlschlägt.
  • das Argument `capturestderr` durch das `stderr`-Argument ersetzt wird.
  • `stdin=PIPE` und `stdout=PIPE` angegeben werden müssen.
  • `popen2` schließt standardmäßig alle Dateideskriptoren, aber Sie müssen `close_fds=True` mit `subprocess.Popen` angeben.

Offene Fragen

Einige Funktionen wurden angefordert, sind aber noch nicht implementiert. Dazu gehören:

  • Unterstützung für die Verwaltung einer ganzen Herde von Subprozessen
  • Unterstützung für die Verwaltung von „Daemon“-Prozessen
  • Integrierte Methode zum Töten von Subprozessen

Obwohl dies nützliche Funktionen sind, wird erwartet, dass sie später ohne Probleme hinzugefügt werden können.

  • Funktionalität im Stil von „expect“, einschließlich pty-Unterstützung.

pty-Unterstützung ist stark plattformabhängig, was ein Problem darstellt. Außerdem gibt es bereits andere Module, die diese Art von Funktionalität bereitstellen [6].

Abwärtskompatibilität

Da dies ein neues Modul ist, werden keine größeren Probleme mit der Abwärtskompatibilität erwartet. Der Modulname „subprocess“ könnte mit anderen früheren Modulen [3] mit demselben Namen kollidieren, aber der Name „subprocess“ scheint bisher der am besten vorgeschlagene Name zu sein. Der erste Name dieses Moduls war „popen5“, aber dieser Name wurde als zu unintuitiv angesehen. Eine Zeit lang hieß das Modul „process“, aber dieser Name wird bereits von Trent Micks Modul [4] verwendet.

Die Funktionen und Module, die dieses neue Modul zu ersetzen versucht (`os.system`, `os.spawn*`, `os.popen*`, `popen2.*`, `commands.*`) werden voraussichtlich noch lange in zukünftigen Python-Versionen verfügbar sein, um die Abwärtskompatibilität zu gewährleisten.

Referenzimplementierung

Eine Referenzimplementierung ist verfügbar unter http://www.lysator.liu.se/~astrand/popen5/.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0324.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT