Subprocess 모듈 사용법 – 파이썬에서 서브 프로세스를 생성하기

Subprocess 모듈 이란?

subprocess 모듈은 파이썬 프로그램 내에서 새로운 프로세스를 스폰하고 여기에 입출력 파이프를 연결하며 리턴코드를 획득할 수 있도록 하는 모듈로, 다른 언어로 만들어진 프로그램을 통합, 제어할 수 있게 만드는 모듈이다. 이 모듈은 기존에 오랜된 몇몇 모듈과 함수(os.system, os.spawn*)들을 대체하기 위해 만들어졌다. (혹은 os.popen 같은 함수도…)

사용방법

새로운 서브 프로세스를 만들기 위해 권장되는 방법은 다음의 편의 함수들을 사용하는 것이다. 보다 세부적인 제어를 위해서는 Popen 인터페이스를 직접 사용한다.

subprocess.call 함수 – 가장 간단한 서브 프로세스 실행

가장 기본적인 서브프로세스 호출명령이다. 인자값들을 사용하여 바로 명령을 실행한다. 이 함수는 블럭킹함수로 서브프로세스가 동작을 시작하면 이를 기다렸다가 서브 프로세스가 종료되면 해당 프로세스의 실행 리턴코드를 반환한다. 보통의 쉘에서 돌아가는 프로그램들은 정상 종료시 0을 리턴한다.

subprocess.call(args, *, stdin=None, stdout=None, stderr=None, shell=False, timeout=None)

call 함수는 Popen의 컨스트럭터와 비슷한 시그니쳐를 가지고 있다.(사실 subprocess 모듈의 거의 모든 함수들이 Popen의 인자중에서 일부를 갖는 형태로 디자인되어 있다.) 몇 가지 인자를 살펴보면 다음과 같다.

  • args는 명령행에 문자열 혹은 입력될 인자들을 공백으로 자른 리스트이다.
  • stdin, stdout, stderr 은 각각 표준입력, 표준출력, 표준에러의 표준입출력 리다이렉션에 대응한다. 파일디스크립터를 사용해서 파일의 내용을 표준입출력으로 대체할 수 있다. 보통은 생략되면 현재의 표준입출력이 그대로 적용된다.
  • shell=True 가 되면 서브 프로세스로 주어진 명령을 바로 실행하는 것이 아니라, 별도의 서브 쉘을 실행하고 해당 쉘 위에서 명령을 실행하도록 한다. 이 값이 True가 되는 경우에는 args가 리스트 보다는 하나의 문자열인 쪽이 좋다고 한다. 참고로 쉘을 사용하면 쉘이 제공하는 파이프, 리다이렉션과 쉘의 문자열 확장 (예를 들어 FILES* 라고 쓰면 FILES1 FILES2 FILES3 … 이렇게 만들어지는 것)을 함께 사용할 수 있게 된다.
  • 기본적으로 이 함수는 서브프로세스가 종료될 때까지 기다리는데 timeout 값을 설정하면 주어진 시간(초단위)만큼 대기한 후, 대기 시간을 초과하면 TimeoutExpired 예외를 일으키게 된다.
>>> subprocess.call(["ls", "-l"])
...파일 목록이 출력되고...
0  ## 성공했다면 0이 리턴값으로 나오게 될 것이다

subprocess.check_call – 결과값을 강제로 체크하기

call() 함수는 서브 프로세스를 통해서 주어진 명령을 실행하고 리턴한다. 이 과정에서 리턴 코드를 받아오긴 하지만, 해당 명령이 처리에 실패하고 비정상 종료를 하는 경우에도 이후의 코드가 무사히 실행이 될 것이다. (물론 부지러한 사람이라면 이 리턴코드가 0인지 비교해볼 것이다.)

check_call()은 서브 프로세스에 의한 처리가 성공하는 것이 보장되어야 하는 경우에 쓰인다. 즉 서브 프로세스의 명령이 비정상 종료하였다면 그 리턴코드가 0이 아닐 것이다. 이 때 다른 리턴 코드 값을 내놓는게 아니라 그 즉시 CalledProcessError 예외를 일으킨다. (이 예외 객체내에 returncode라는 속성이 있고, 따라서 예외처리 구문에서 리턴 코드를 확인할 수 있다.)

subprocess.check_output – 출력되는 문자열이 필요할 때

서브프로세스를 실행하고 그 출력 문자열을 리턴한다. 따라서 실행되어 출력된 결과를 문자열로 받고 싶을 때 사용한다. 스폰된 프로세스의 리턴코드가 0이 아닌 경우 CalledProcessError예외를 일으킨다. (그래서 이름에 check가 포함된다.)  이 버전은 Popen 객체를 사용함에 있어서 표준입출력을 문자열 데이터로 사용하는 버전에 가깝다. 즉 프로세스가 실행되고 종료되면 표준출력으로 넘어온 데이터를 읽어서 그 값을 리턴해주는 함수라 보면 되겠다. 프로세스스가 사용하는 입력의 경우에도 input= 인자를 통해서 미리 지정해 줄 수 있다. 

universal_newlines= 파라미터는 사실 그 이름과 의미가 약간 모호한데, 서브 프로세스와의 파이프 통신을 문자열을 주고받는 것으로 처리한다. (기본적으로 False 값일 때에는 파이프를 통해서 바이트스트림이 오간다.) 다음은 사용예이다.

>>> subprocess.check_output(["echo", "Hello World!"])
b'Hello World!\n'

### universal_newline을 쓰면 바이트가 아닌 문자열로 받게된다.
>>> subprocess.check_output(["echo", "Hello World!"], universal_newlines=True)
'Hello World!\n'

## input을 쓰면 표준입력을 통해서 입력될 내용을 인자로 넘겨줄 수 있다. 
>>> subprocess.check_output(["sed", "-e", "s/foo/bar"],
...                input=b"when in the course of fooman eventsn")
b'when in the course of barman eventsn'

## check_...가 들어있기 때문에 정상종료하지 않으면 예외처리된다.
>>> subprocess.check_output("exit 1", shell=True) # 리턴코드가 1이다.
Traceback ...
...
subprocess.CalledProcessError: command 'exit 1' returned non-zero exit status 1

기본적으로 리턴되는 출력 데이터는 인코딩된 바이트 스트림이다. 인코딩은 서브프로세스 프로그램에 의존하며, 따라서 이를 텍스트로 변환하기 위해서는 디코딩이 필요하다. 이 동작은 universal_newline=True이면 오버라이드 된다.

에러 출력을 잡기 위해서는 stderr=subprocess.PIPE라는 인자를 정의한다. 서브프로세스 상에서 에러메시지로 출력된 내용을 받아오게 된다.

>>> subprocess.check_output(
...    "ls non_existent_file; exit 0;",
...    stderr=subprocess.STDOUT,
...    shell=True)
'ls: non_existent_file: No such file or directoryn'

 

Popen 클래스

프로세스 생성과 관리 모듈은 내부적으로 Popen 클래스를 통해서 관리한다. 실질적으로 call-*류의 함수들은 이 클래스를 기반으로 하고 있다. Popen은 많은 유연성을 제공하기 때문에 기본적으로 제공되는 편의 함수들만으로 다룰 수 없는 케이스를 커버할 때, 세밀한 옵션들에 대해서 직접 제어하고자 할 때 사용할 수 있다.

Popen

Popen 클래스는 인스턴스 초기화시에 여러가지 옵션들을 인자로 받을 수 있다.  물론 여기서는 이걸 다 설명할 수 없다. 전체 내용은 파이썬 공식 문서를 참고하도록 하자.

class subprocess.Popen(args, bufsize=-1, excutable=None, 
                       stdin=None, stdout=None, stderr=None, 
                       preexec_fn=None, close_fds=None, shell=False, 
                       cwd=None, env=None, universal_newlines=False, 
                       startupinfo=None, creationflags=0, restoreflags=0, 
                       restore_signals=True, start_new_session=False, pass_fds=()
                      )

args는 프로그램 인자의 리스트이거나, 단일 문자열이다(만약 shell값이 True라면 단일 문자열이어도 된다.)

서브 프로세스의 입출력

stdin, stdout, stderr은 서브 프로세스의 표준 입출력 핸들러를 가리킨다. 이에 유효한 값은 PIPEDEVNULL 혹은 이미 존재하는 다른 파일 핸들러 및 파일 객체일 수 있다. PIPE를 넘기는 경우, 자식 프로세스에게 새로운 파이프가 생성되어 연결된다. 디폴트값은 None이며 이 경우 리다이렉션이 일어나지 않고, 부모 프로세스(파이썬 프로그램 자신)의 입출력을 상속받는다. 참고로 stderr에는 STDOUT을 줄 수 있는데, 이렇게 하여 에러 문구를 표준 출력으로 받을 수 있다.

문자열 출력에 대한 변환

universal_newlines는 라인엔딩 변환을 하느냐 하지 않느냐의 차이를 보이는데, 이 값이 True이면 문자열을 주고 받게 되므로, 파일 객체를 파이프연결한 경우 이 값은 False가 되어야 한다. 개행 문자는 플랫폼 마다 다른데, 이 값이 True로 넘어가는 경우, os.linesep을 사용하여 개행 문자를 변환하게 된다.

shell 파라미터 값이 True인 경우, 주어진 명령을 쉘을 통해서 처리한다. 이는 쉘 스크립트 대신 파이썬 스크립트를 통해서 배치 명령의 흐름을 제어하고자 할 때 유용하다. 이 기능을 사용하면 파일 이름 와일드카드나, 환경변수 확장 등의 쉘 기능을 이용할 수 있다. 단, 파이썬 역시 왠만한 쉘 관련 기능들을 제공한다. glob, fnmatch, os.walk(), os.path.expandvars(), os.path.expanduser(), shutil 등의 모듈, 함수를 참고할 것.

컨텍스트 매니저

Popen 객체는 컨텍스트 매니저 프로토콜을 구현하고 있어서 with 구문에 사용될 수 있다. 빠져나오는 경우 표준 파일 디스크립터는 닫히게 되고, 프로세스를 기다리게 된다.

with Popen(["ifconfig"], stdout=PIPE) as proc:
    log.write(proc.stdout.read())

 Popen 객체의 메소드들

다음 메소드들은 Popen 생성자를 이용해서 만든 객체가 제공하는 메소드들이다.

  • .poll(): 자식 프로세스가 종료되었는지를 확인한다.
  • .wait(timeout=None): 자식 프로세스가 종료되길 기다린다. 자식 프로세스의 리턴 코드를 돌려준다.
  • .communicate(input=None, timeout=None): 자식 프로세스의 표준 입력으로 데이터를 보낸다음, 표준 출력의 EOF르 만날 때까지 이를 읽어온다. (프로세스가 끝날 때까지 기다린다.) 이 함수는 (stdout_data, stderr_data)의 튜플을 리턴한다. 이 함수를 사용하려면 stdin=PIPE 옵션으로 자식 프로세스를 시작해야 한다.
  • Popen.send_signal(signal): 자식 프로세스에 시그널을 보낸다.
  • Popen.terminate(): 자식 프로세스에 종료 시그널을 보낸다.
  • Popen.kill(): 자식 프로세스를 강제로 죽인다.
  • Popen.args: 주어진 args 를 리턴한다.
  • Popen.stdin: 만약 PIPE라면 이 객체는 읽고 쓸 수 있는 스트림 객체이다.
  • Popen.stdout: PIPE라면 읽을 수 있는 객체이다.
  • Popen.stderr: PIPE라면 읽을 수 있는 객체이다.
  • Popen.pid: 자식 프로세스의 pid값
  • Popen.returncode: 자식 프로세스가 종료되었을 때 리턴코드 값을 가진다.