(Python) 데이터를 입력받는 방법을 유연하게 생각해보기

컴퓨터 프로그램은 사실상 하나의 함수와 같다. (그래서 많은 프로그램은 실행자체가 main() 과 같은 함수의 호출이기도 하다.) 프로그램은 입력장치로부터 데이터를 읽어들이고, 이 소스데이터를 가공하여 결과를 만들고, 이를 출력한다. 마치 어떤 프로그램의 실행과정은 일종의 쥬스메이커와 같다고 볼 수 있다. 과일(입력될 데이터)을 투입구에 밀어넣고 동작 버튼을 누르면 기계속으로 들어간 과일이 잘리고 눌려서 과즙이 되고, 그것이 노즐을 통해서 퐁퐁 흘러나와 컵에 담기는데, 맛있는 쥬스 대신에 데이터가 흘러나온다는 점만 다를 뿐 둘의 흐름은 똑같다.

파이썬에서 프로그램이 사용자로부터 입력을 받는 방법 중에 가장 대표적인 것은 input() 함수이다. 이 함수는 키보드로부터 한 줄의 문자열을 입력받아, 그 내용을 리턴한다는 동작을 수행한다. 예를 들면 다음과 같이 쓸 수 있다.

name = input('what is your name?: ')
print('hello, %s' % name)

그런데 왜 이 함수의 이름이 keyboard 따위가 아니라 input일까? 이 함수의 동작의 의미를 보다 엄밀하게 따져보도록 하자. 실제로 이 함수의 뜻은 다음과 같다.


input(prompt=None, /)
  Read a string from standard input. The trailing newline is stripped.

먼저 “키보드”가 아니라 표준입력으로부터 문자열을 입력받는다고 한다. 그리고 문자열 끝의 개행은 제거된다는 이야기다. 그럼 표준입력이 뭘까? 그냥 콘솔에서 키보드로 입력받는 걸 표준입력이라고 하는건가? 마우스나 조이스틱, 바코드 리더기는 비표준입력이고?

표준입력이란

표준 입력 그리고 표준 출력의 개념은 당장은 약간 이해하기 힘들 수 있다. 우선 표준입력에 대한 위키백과의 내용을 인용하겠다. (https://en.wikipedia.org/wiki/Standard_streams#Background)

유닉스 이전의 대부분의 운영 체제에서, 프로그램은 명시적으로 적절한 입력장치와 출력장치에 연결해 줄 필요가 있었다. 이 작업은 OS마다 처리 방식이 달랐기 때문에 매우 방대한 작업이었다.

수많은 시스템에서 환경 설정을 제어하거나, 파일 테이블에 접근하거나, 필요한 데이터 셋을 결정하기 위해 펀치 카드 리더기나 자기 테이프 드라이브, 라인 프린터, 카드 펀치, 대화식 터미널을 적절하게 제어할 필요가 있었다.

이런 상황에서, 유닉스의 획기적인 발전 중 하나는 장치의 추상화였다. 프로그램은 더 이상 어떤 장치와 연결되는지 알 필요가 없었다.

유닉스는 기존의 복잡성을 데이터 스트림이라는 개념으로 해소시켰다. 데이터 스트림은 순차적인 데이터 바이트들을 파일의 끝(EOF)까지 읽는다. 이런 방식으로, 프로그램은 쓸 데이터가 얼만큼 남았는지, 혹은 어떤 식으로 묶여있는지 알 필요 없이 필요한 데이터를 쓸 수 있었다.

또 다른 유닉스의 획기적인 발전은, 자동으로 연결되는 입출력 장치였다. 프로그래머나 프로그램에서 입출력을 사용해야 할 때, 입출력 장치를 연결하기 위한 그 어떤 추가 작업도 필요하지 않게 되었다. 이전의 운영 체제들에서 입출력 장치를 연결하기 위해 복잡한 작업 제어 언어가 필요하거나, 그와 동일한 역할을 하는 프로그램이 필요했던 것과는 대조적이다.

간단히 말해서 표준입력입력 장치를 추상화한 것이라 할 수 있다. 추상화라는 말이 생소하거나 어렵게 느껴질 수 있는데, “구체적으로 결정하다”의 반대말로 생각해본다면 “그게 뭔지 상관하지 않는다”라고 해석할 수 있다.

말 그대로 입력장치가 어떻게 생겼고, 어떤 기술적인 원리로 동작하든 간에 상관없이 “입력장치”라 불리는 모든 컴퓨터의 주변 기기는 컴퓨터 외부로부터 어떤 데이터를 받아서 그것을 컴퓨터 내부로 넘겨주는 역할을 한다. 따라서 “컴퓨터 내부로 데이터를 넘겨주는” 방법만 한 가지로 약속해놓는다면, 그 장치가 무엇이든 간에 프로그램 외부로부터 어떤 데이터들을 프로그램 속으로 전달해 줄 수 있다는 말이다.

그리고 이때 표준입력은 데이터가 흘러들어올 수도꼭지 같은 것이다.(표준입력은 표준 스트림(stream) 중 하나이다.) 그 너머에 연결된 입력 장치가 무엇이든 간에, 프로그램은 이 수도꼭지에서 물이 콸콸 흘러나오기를 기다렸다가 그 데이터를 이용할 수 있는 것이다.

프로그램 입장에서 표준입력의 반대편 끝은 외부세계이다. 키보드나 스캐너와 같은 외부 입력 장치, 네트워크 스트림일 수 있다. 혹은 이러한 물리적인 장치가 아닌 파일이나 다른 프로그램의 출력도 표준 입력이 될 수 있다. 기본적으로 콘솔환경에서 표준 입력은 콘솔의 키보드가 되겠지만, 가장 적절한 아이디어는 표준 입력이 무엇일지에 대해 아무런 가정을 하지 않는 것이다. 대신에 표준 입력으로부터 어떤 데이터가 들어올 것인지에 대해서만 생각하면 된다.

표준 출력

input()의 반대에 해당하는 print() 함수를 생각해보자. 프로그램에서 만든 문자열을 외부 세계로 내보낸다. 표준 출력 역시 콘솔 환경에서는 화면에 글자가 찍히는 것으로 동작한다.

이렇게 표준입력/표준출력의 개념을 알게되면 파일을 통한 입출력이 사실은 input(), print()와 다를바 없다는 사실을 알 수 있다. 표준입력이 키보드와 같은 물리 장치가 아니라 파일로 대체될 수 있다면 우리는 open(), f.readline() 같은 방법을 사용하지 않아도 파일의 내용을 받아서 처리할 수 있게 될 것이다. 반대로 표준 출력이 파일이라면 print()를 사용하여 텍스트 파일을 생성할 수 있게 된다.

문자열을 뒤집는 프로그램

간단한 예제를 통해 input(), print()와 표준 입출력의 관계를 살펴보도록하자. manipulate.py라는 이름의 모듈을 하나 작성한다. 이 모듈은 문자열을 뒤집은 역순의 문자열을 만드는 간단한 함수 reversed_string()을 포함하며, 파일로 실행되었을 때에는 input()print() 사이를 연결하는 역할을 한다.

# manipulate.py 


def reversed_string(s: str) -> str:
    return s[::-1]


def main():
    while True:
        line = input()
        if not line:
            break
        print(reversed_string(line))


if __name__ == '__main__':
    main()

이 프로그램의 실행 결과는 다음과 같이 보일 것이다. 여기까지는 간단한 프로그램의 동작을 보는 것이었으므로 특별할 것이 없다.


> python manipulate.py
apple
elppa
hello
olleh

> 

이렇게 실행된 상황에서는 프로그램이 실행되고, 표준입력으로부터 한줄의 텍스트를 입력받고 다시 이를 뒤집어서 출력하기를 빈줄이 입력될 때까지 반복하는 식으로 동작한다.

리다이렉트와 파이프

이 때 다음과 같은 내용의 텍스트 파일 text.txt 가 있다고 하자. 이 파일의 내용을 한 줄씩 거꾸로 출력하고 싶다. 어떻게해야할까? manipulate.py의 내용에 파일을 열어서 읽는 코드를 추가할 수도 있겠지만, 표준입력을 바꿔치기 하는 것으로 기존 코드를 손대지 않고 이 문제를 해결할 수 있다.

Hello, my name is apple.
I have two friends of mine.
Oragne is good.
Banana is tall.

# 빈줄을 한 줄 남겨준다.

리다이렉트는 콘솔의 명령줄에서 > 혹은 < 를 사용한다. 먼저 파일의 내용을 출력해보자. 콘솔에서 텍스트 파일의 내용을 바로 열어보고 싶다면 type 명령을 사용할 수 있다. (type은 윈도용 명령이고, 만약 macOS나 리눅스/유닉스를 사용한다면 cat 명령을 사용한다.)

### 윈도 명령 프롬프트에서 실행
### 원래 프롬프트가 C:> 와 같이 > 로 끝나지만
### 리다이렉트 기호 > 와의 구분을 위해 $로 표기합니다.
$ type text.txt
Hello, my name is apple.
I have two friends of mine.
Oragne is good.
Banana is tall.

$ 

원래 python manuplate.py로 실행하면 키보드로 입력하는 식으로 프로그램이 동작했는데 다음과 같이 실행해보자.

$ python manipulate.py < text.txt

이 명령의 모양은 python manuplate.py 라는 명령에 대해서 text.txt라는 파일을 넣어주는 모양으로 생겼다. 이것이 리다이렉트이고, 프로그램에 대한 표준입력을 text.txt 파일로 교체하는 것이다. 이제 소스코드 내의 input()은 키보드가 아닌 텍스트 파일로부터 한줄을 읽는 f.readline()과 비슷하게 행동한다.

$ python manipulate.py < text.txt
.elppa si eman ym ,olleH
.enim fo sdneirf owt evah I
.doog si engarO
.llat si ananaB

$

단순히 호출하는 방법을 바꿨을 뿐인데, 파일을 열어서 출력하는 것처럼 동작하게 했다. 우리는 < 연산자를 통해서 표준입력을 파일로 리다이렉트했다. > 연산자는 반대 방향을 가리키는 것 처럼 보이는데, 그 느낌대로 프로그램으로부터 출력되는 내용을 리다이렉트하는 것이다.

출력을 리다이렉트하는 예를 살펴보자.

$ python manipulate.py < text.txt > reversed.txt

$ type reversed.txt
.elppa si eman ym ,olleH
.enim fo sdneirf owt evah I
.doog si egnarO
.llat si ananaB

$

> reversed.txt를 붙이면서 표준출력이 파일로 대체되었기 때문에 화면에 출력되는 내용이 없어진다. 그렇다고 아무일도 일어나지 않은 것은 아니다. 화면 대신 파일로 출력된 것이기 때문에 type (맥, 리눅스는 cat) 명령으로 확인해보면 파일이 만들어진 것을 볼 수 있다. Wow

파고들기 – 표준입출력과 IO 객체

표준입력과 표준출력은 프로그램이 실행되는 환경의 구성이다. 기본적으로 프로그램이 외부세계와 데이터를 주고 받는 IO는 사실상 파일에 읽고 쓰는 것과 같은 방식으로 작동한다. (이게 다 앞에서 이야기한 입출력의 추상화 덕분이다.) 따라서 표준입력과 표준출력은 sys 모듈 내의 sys.stdin, sys.stdout으로 정의되어 있으며, 이들은 각각 io.TextIOWrapper 로 구현되어 있다. 우리가 open()함수를 이용해서 파일을 열었을 때 얻게되는 파일 핸들(혹은 파일 디스크립터) 역시 TextIOWrapper의 인스턴스이다. 그래서 input(),print()sys.stdin, sys.stdout의 관계는 다음과 같이 정리된다.

  • input() 함수는 sys.stdin.readline().rstrip()과 같다.
  • print() 함수는 sys.stdout.write();sys.stdout.flush()와 같다.
  • sys.stdinTextIOWrapper이며 ‘r’모드와 utf8인코딩을 사용한다.
  • sys.stdoutTextIOWrapper이며 ‘w’모드와 utf8 인코딩을 사용한다.

sys.stdin,sys.stdout은 텍스트 파일을 연 것과 똑같이 작동한다는 것을 알 수 있고, 그 ‘파일’이 기본적으로는 키보드와 콘솔 화면에 연결된 것으로 생각할 수 있는 것이다.

좀 더 개선해보기

앞서 말했듯이, 콘솔 환경에서의 기본적인 표준 입력은 키보드이다. 표준 입력은 텍스트 파일처럼 읽을 수 있긴 하지만, 텍스트 파일과 차이점이 한가지 있다. 끝까지 읽은 파일은 읽고 있는 위치를 되돌리지 않는 이상 더 이상 읽을 수 없지만, 키보드 입력은 프로그램이 돌아가는 한 얼마든지 읽을 수 있다는 말이다. 따라서 input() 함수는 ‘닫히지 않는’ 파일을 읽는 것을 기본적으로 상정하고 있다.

앞서 manipulate.py 예제에서는 입력의 완료를 알기 위해서 빈 라인을 입력받는 것을 정했다. 하지만 빈 라인이 없는 텍스트 파일을 리다이렉트하면 어떻게 될까? input()으로 읽으려고 하는데 파일이 끝나버려서 더 읽을 내용이 없는 것이다. 이 때에는 None이 리턴되는 것이 아니라 EOFError라는 특별한 종류의 예외가 발생한다. 또 파일 중간에 빈 줄이 있다면 파일 내용을 다 처리하지 못하고 중간에 동작이 중단될 수 있다. 따라서 input() 함수가 리다이렉트된 표준입력을 처리할 수 있게 하려면 EOFError를 처리할 수 있도록 구현하여 유연성을 높일 수 있다.

다음은 입력의 끝을 감지하여 처리할 수 있도록 개선한 manipulate 모듈이다. get_lines()를 제너레이터 함수로 만들어서 EOF를 만날 때 까지 계속해서 한 줄씩 읽어들이도록 할 수 있다. 따라서 main() 함수는 입력의 끝을 생각하지 않고 간단하게 for 루프를 통해서 처리할 수 있게 된다.

"""manipulate.py"""

from typing import Generator


def reversed_str(s: str) -> str:
    return s[::-1]


def get_lines() -> Generator[str, None, None]:
    try:
        while True:
            yield input()
    except EOFError:
        return

def main():
    for line in get_lines():
        print(reversed_str(line))


if __name__ == '__main__':
    main()

파이프 – 프로그램과 프로그램을 연결하기

표준입력과 출력에 대한 개념을 이해했다면, 이것은 상당히 강력한 무기처럼 활용될 수 있다. 간단한 리다이렉트 명령만으로 input(), print() 함수만으로 파일을 읽고, 파일에 기록하는 동작을 구현할 수 있었다. 뿐만 아니라 표준입출력은 프로그램과 프로그램을 연결하는데 사용될 수 있다.

좀 전에 예를 들었던 type (cat) 명령을 보자. 이 명령은 텍스트 파일을 읽어서 그 내용을 화면에 출력해주는 기능이라고 했다. 이 명령들이 무슨 언어로 작성되었는지는 알 필요도 없겠지만, 화면에 출력을 하는 것을 보면 ‘표준 출력’을 사용했다는 것을 알 수 있다. 당연히 > 연산자를 사용해서 출력되는 내용을 다른 파일에 저장하는 것이 가능할 것이다.

그렇다면 어떤 한 프로그램의 출력 내용을 다른 프로그램의 표준 입력으로 보내는 것도 가능하지 않겠느냐는 말이다. 이 때 사용하는 것이 파이프 연산자 ( | )이다. 명령 프롬프트에서 이 파이프 연산자를 이용해서 두 개의 명령을 연결하면, 앞 명령의 출력이 뒤 명령의 입력으로 연결된다. 마치 배관에서 파이프를 연결하는 것과 똑같다. 다음 명령을 보자.

$ type text.txt | python manipulate.py 
.elppa si eman ym ,olleH
.enim fo sdneirf owt evah I
.doog si engarO
.llat si ananaB
$

여기까지만 본다면 python manipulate.py < text.txt와 똑같은 동작이기 때문에 별 감흥이 없을 수도 있겠다. 하지만 파이프는 어떤 명령이든 그 명령이 콘솔에 찍어주는 내용들을 모두 입력으로 가져간다고 했다. pip list 같은 명령도 예외는 아니다.

$ pip list | python manipulate.py
noisreV                           egakcaP
--------- ---------------------------------
4.4.1                           sridppa
0.3.91                             srtta
0.1.0                          llackcab
0b01.91                             kcalb
5.1.3                            hcaelb
2.1.7                             kcilc
...

파이프와 리다이렉트는 언제든지 조합하여 사용할 수 있다. 이 결과를 파일에 쓰려면 뒤에 > result.txt 같은 걸 붙여주면 된다.

파이프는 생각보다 엄청나게 강력하다. 단순히 쉘 상에서 실행할 수 있는 명령들을 조합하여 처리 프로세스를 만들 수 있는 셈이다. 생각해보라, pip list의 내용을 거꾸로 만들어서 별도의 파일의 저장하는 전체 내용을 파이썬 소스코드에서 처리하려고 한다면 (뭐, 그것도 어렵지는 않지만) 여러 모로 귀찮은 부분들이 존재하게 된다. 하지만 아주 간단히 만들어놓은 모듈하나와 파이프, 리다이렉트만 있으면 모든 게 해결된다. manipulate 모듈의 get_lines() 만 잘 사용하면 문자열을 다른 문자열로 필터링하거나 변환하는 함수는 얼마든지 만들 수 있을 것이다. 어떤 함수를 만들 든 변환한 결과를 print()하기만 하면 우리는 그것을 화면에 출력할 수도, 파일에 기록 할 수도 있는 셈이다.

조금 더 깊이 파고들기

EOF를 처리할 수 있는 버전으로 개선하기 전의 manipulate 모듈을 사용해서 빈 줄이 없는 텍스트를 출력하려고 하면, EOFError가 나게된다. 이 때 출력을 output.txt 같은 파일로 리다이렉트했다면 정상출력부분은 화면에 표시되지 않고 오류 메시지만 화면에 출력되는 것을 볼 수 있다.

이것은 실제로 오류 메시지를 출력하는 채널이 별도로 존재하는 것을 암시한다. 실제로 표준 스트림은 3개의 채널로 구성된다. 표준입력(stdin), 표준출력(stdout), 표준오류(stderr)이 그것이다. 기본적으로 대부분의 시스템에서 이들은 각각 0, 1, 2 번의 번호를 부여받는다.

우리가 > 연산을 사용해서 출력을 파일에 저장하는 것은 표준출력을 파일로 리다이렉트하는 것이다. 표준 오류 채널은 2> 라는 연산자를 사용해서 다른 파일로 리다이렉트할 수 있다. 종종 표준오류채널을 통해 출력되는 내용까지 같이 캡쳐하고자 할 때, 2>&1 이라는 표현을 쓰는데, 이는 “표준오류를 표준출력(1)으로 리다이렉트한다”는 내용이다.