인풋을 받았을 때 표준입력 버퍼의 상태 변화
알 수 있는 점
- input()과 표준 입력 버퍼의 상관관계
- input() 함수는 표준 입력 버퍼에서 데이터를 한 줄 단위로 읽어오며, 입력이 버퍼에서 처리된 이후에는 다음 읽기에서 이전 데이터를 사용할 수 없다.
- 입력이 input()에 의해 처리된 이후 남아있는 입력이 없다면, 후속 표준 입력 함수는 빈 데이터를 반환하거나 에러를 발생시킬 수 있다.
- open(0)과 표준 입력 처리
- open(0)을 통해 표준 입력을 파일 객체로 열면, 사용된 메소드(read()/readline())에 따라 데이터의 처리 범위와 버퍼 상태가 달라진다.
- 여러 번 표준 입력을 읽으려고 하면 에러(예: Bad file descriptor)가 발생한다.
- os.read()와 바이트 데이터 처리
- os.read(0, n)은 표준 입력을 바이트 단위로 읽는다.
- 바이트 데이터를 처리하기 때문에, 문자열이 아닌 바이트로 결과가 출력된다.
- sys.stdin.read()와의 차이
- sys.stdin.read(n)은 문자 데이터를 반환한다.
- 표준 입력을 파일처럼 다룰 수 있는 유사성을 보여주지만, input() 함수와의 상호작용에 따라 읽히는 데이터가 달라진다.
반복가능? | input() 뒤 가능? | input() 앞 가능? | 임포트 필요 | 바이트/문자열 입력 | |
open(0).read(n) | ⨉ | ⨉ | ◯ | ⨉ | 둘 다 가능 |
sys.stdin.read(n) | ◯ | ◯ | ◯ | ◯ | 문자열 |
os.read(0,n) | ◯ | ⨉ | ◯ | ◯ | 바이트 |
os.read()와 open(0)이 빈 값을 반환한 이유는 표준 입력 버퍼의 EOF(End of File) 상태가 발생했기 때문이다. 이를 좀 더 구체적으로 분석해보자.
1. 표준 입력과 같은 FD(파일 디스크립터) 0을 공유한다는 점
- 파이썬에서 input()은 내부적으로 sys.stdin(FD 0)을 사용해 한 줄을 읽는다.
- open(0) 역시 표준 입력과 같은 FD(파일 디스크립터) 0을 사용한다.
즉,
- a = input() 호출 시: FD 0에서 한 줄을 읽고, 커서를 그 다음 위치로 옮긴다.
- b = open(0).read() 호출 시: 이미 이동된 커서 위치부터 읽어야 한다.
만약 그 시점에 커서가 이미 EOF에 있으면 b는 ''(빈 문자열)이 된다.
2. 그런데 왜 EOF(End Of File)에 도달해 버리는가?
이 부분이 여러 모로 혼동을 준다. "분명 2줄을 준비해 두었는데, input()이 한 줄만 읽었을 텐데 나머지는 다 어디로 갔지?" 하는 의문이 생긴다. 이유는 크게 두 가지가 있다:
- 실행 환경(특히 온라인 컴파일러/실행기)에서 입력을 한 덩어리로만 주고 더 이상 데이터를 주지 않는 경우
- 예: TIO, Repl.it, 백준 채점 서버 등등.
- 사용자가 2~3줄을 입력해 놓았어도, 첫 줄을 input()이 읽으면서 내부 버퍼가 소진될 때, 해당 환경에서 스트림을 바로 닫아버리거나, 파이썬 쪽에서 “이만큼만 데이터가 왔다”며 EOF로 처리해 버릴 수 있다.
- input()이 실제로는 버퍼를 더 많이 가져와서(혹은 운영체제 수준에서) 이미 모든 입력을 소비했을 수도 있음
- input()은 “한 줄씩 읽는다”라고 알려져 있지만, 내부적으로 운영체제나 파이썬이 **버퍼링(buffering)**을 할 수 있다.
- 즉, OS 차원에서 “표준 입력 FD 0”으로 들어온 데이터를 한꺼번에 읽어 놓고, 파이썬 input() 레벨에서는 그중 필요한 한 줄만 반환하고 나머지를 내부 버퍼 어딘가에 쥐고 있다가, 이후 추가로 input()을 호출하면 그 내부 버퍼에서 꺼내 주는 식이다.
- 그런데 open(0).read()를 호출하면, 이미 OS 레벨에서 “모든 데이터를 읽었지만, 파이썬의 내부 버퍼에 있는 부분”을 open(0)이 다시는 볼 수 없게 되는 상황이 생길 수 있다.
- 왜냐하면 open(0)은 OS 수준 FD 0의 "남은" 데이터를 가져오는데, 파이썬 인터프리터(내부 버퍼) 쪽에서 이미 input()을 위해 가져가 버렸을 수 있기 때문이다.
정리하면, "input()이 단지 한 줄만 가져가고, 나머지는 놔두지 않느냐"라는 생각과는 달리,
- (a) 실제로는 운영체제나 파이썬 내부의 버퍼링 메커니즘으로 인해 이미 전부 소비되었을 수 있고,
- (b) 온라인 실행 환경 같은 곳에서는 첫 줄 읽은 뒤 바로 EOF가 되어버리는 상황이 종종 벌어진다.
이렇게 해서 os.read(0,n)이나 open(0).read(n) 입장에서는 “커서가 이미 끝까지 가 있네?”가 되어, 결과적으로 ''만 돌아오게 된다.
“커서가 둘째 줄 처음에 있을 것”이라고 순진하게 생각하지만, 이미 “파이썬 내부 버퍼”나 “환경”에서 그 뒤줄도 다 가져가 버린 상태가 되어 있을 수 있다는 것이다.
3. 같은 코드라도 환경마다 결과가 달라질 수 있음
위에서 언급한 대로,
- 로컬 환경(직접 터미널에서 파이썬을 실행)에서 여러 줄을 차례대로 입력하면 input()이 차례대로 정상 동작하고, open(0).read()로도 남은 줄을 읽을 수 있는 경우도 있다.
- 온라인 채점 환경이나 TIO 같은 샌드박스에서는 입력을 한 덩어리로 처리하거나, 첫 줄만 읽힌 뒤 EOF가 되어버리는 경우가 흔하다.
즉, "반드시 이론적으로는 3\n4\n5\n가 남았으니 open(0).read()가 4\n5\n를 읽을 것이다"라는 기대가 실행 환경에 따라 달라지는 것이다.
# test.py
import sys
print("=== First input() ===")
a = input() # 첫 줄 읽기
print("=== open(0).read() ===")
b = open(0).read()
print("a =", a)
print("b =", b)
print("=== Done ===")
# test.py
import os
print("=== First input() ===")
a = input() # 첫 줄 읽기
print("=== open(0).read() ===")
b = os.read(0,2024)
print("a =", a)
print("b =", b)
print("=== Done ===")
# test.py
print("=== First input() ===")
a = input() # 첫 줄 읽기
print("=== open(0).read() ===")
b = open(0).read()
print("a =", a)
print("b =", b)
print("=== Done ===")
4. readline()을 여러 번 호출할 때도 비슷한 문제가 생길 수 있음
아래 코드처럼 readline()을 여러 번 호출하면 에러가 날 수 있다:
a = open(0).readline()
b = open(0).readline().split() print(a, b)
- 어떤 환경에서는 open(0)을 여러 번 열었다 닫았다고 간주해 Bad file descriptor 오류가 날 수 있다.
- 혹은 두 번째로 열었을 때는 이미 EOF 상태여서 빈 결과가 나올 수도 있다.
6. 어떻게 하면 제대로 여러 줄을 읽을 수 있나?
- input()만 써서 줄바꿈마다 읽기
- 이렇게 하면 파이썬 내부 버퍼에서 그 줄들을 차례대로 잘 꺼내온다.
a = input() # 첫 줄 b = input() # 두 번째 줄 c = input() # 세 번째 줄
- 이렇게 하면 파이썬 내부 버퍼에서 그 줄들을 차례대로 잘 꺼내온다.
- sys.stdin.read()나 open(0).read()를 맨 처음에 한 번만 써서 전체 입력을 받아 놓고 파싱
import sys data = sys.stdin.read().strip().splitlines() # 이제 data 리스트에 모든 줄이 들어 있음 print(data) # data[0] -> 첫 줄, data[1] -> 두 번째 줄, ...
- sys.stdin을 한 번만 사용
- input()과 open(0).read() 같은 식으로 혼합하지 말고, 한 가지 방식을 일관되게 쓰면 이러한 문제를 피할 수 있다.
7. 최종 결론
- 표준 입력 FD 0은 단 하나이며, 한 번 읽으면 해당 부분(혹은 그 이상)이 **소비(consume)**된다.
- 파이썬의 input()과 open(0), sys.stdin은 모두 동일한 FD 0에서 데이터를 가져온다.
- 이때 “한 줄만 읽었는데 나머지 줄은 남아 있지 않느냐?”라는 순진한 기대와 달리,
- (a) 파이썬 내부 버퍼가 이미 모든 줄을 끌어와서 input()에 대비하고 있을 수 있고,
- (b) 온라인 환경 같은 곳에서는 한 줄 읽은 뒤 바로 EOF가 되어 다음 데이터를 주지 않을 수 있다.
- 따라서 input() 다음에 open(0).read()를 부르면 빈 문자열이 나오거나, 환경에 따라서는 잘 나오기도 하는 등, 일관성이 없어 보이나 사실 같은 원리에서 비롯된다.
- 여러 줄 입력을 제대로 처리하고 싶다면, 처음부터 input()만 사용하거나, 처음에 sys.stdin.read()로 한 번에 몽땅 읽은 뒤 필요한 만큼 파싱하는 방식으로 코드를 짜는 것이 안전하다.
1. 스트림의 주요 상태
스트림의 상태는 주로 다음과 같은 요소들로 구성된다.
1.1 현재 위치 (Stream Position)
- 스트림은 읽기/쓰기 포인터(cursor 또는 pointer)를 유지한다.
- 예를 들어:
- 파일을 읽을 때, 읽은 바이트나 줄 이후의 다음 읽기 위치를 포인터가 가리킨다.
- input()을 호출하면 현재 줄을 읽고 포인터는 다음 줄의 시작으로 이동한다.
- 표준 입력에서는 읽힌 데이터 이후의 위치를 유지하며, 모든 데이터가 소모되면 EOF(End of File) 상태가 된다.
1.2 상태 플래그 (Flags)
- 스트림이 특정 상태인지 확인하거나 제어하기 위해 상태 플래그를 제공한다.
- EOF (End of File):
- 스트림의 데이터를 모두 읽었을 때 설정된다.
- 이후 읽기 동작은 빈 값을 반환하거나 오류를 발생시킨다.
- 에러 상태:
- 읽기/쓰기 중 에러가 발생하면 스트림이 에러 상태로 전환된다.
- 예: 네트워크 연결이 끊겼거나 파일 접근 권한이 없을 때.
- EOF (End of File):
1.3 버퍼 상태
- 스트림은 데이터를 메모리 내의 버퍼에 임시 저장한 뒤 처리한다.
- 표준 입력 스트림의 경우, 사용자가 입력한 데이터는 운영 체제의 버퍼에 저장된 후 프로그램에 제공된다.
- 읽기 동작은 버퍼에서 데이터를 소모하며, 버퍼가 비면 EOF 상태로 전환된다.
2. 표준 입력 스트림의 상태
2.1 표준 입력 스트림의 동작
- 입력 과정:
- 사용자가 데이터를 입력하면, 운영 체제는 이를 표준 입력 버퍼에 저장한다.
- 프로그램이 input(), sys.stdin.read(), open(0) 등을 호출하면, 데이터는 버퍼에서 소비된다.
- 버퍼에 데이터가 더 이상 없으면 EOF 상태가 된다.
- 상태 변화 예시:
- 초기 상태: 데이터가 입력되면 버퍼에 저장되고, 읽기 포인터는 버퍼의 시작을 가리킨다.
- 읽기 후: 읽힌 데이터는 버퍼에서 제거되거나 포인터가 이동하며, 남은 데이터만 읽을 수 있다.
- 모든 데이터 소모 후: 읽기 포인터는 버퍼의 끝에 도달하며, EOF 상태가 된다.
3. 상태 확인과 관련 메서드
스트림의 상태를 확인하거나 조작하는 주요 메서드는 다음과 같다.
3.1 EOF 확인
- EOF 상태를 확인하는 방법은 스트림 구현에 따라 다르다.
- 파일 스트림:
with open('example.txt', 'r') as f: while True: line = f.readline() if not line: # EOF에 도달 break print(line)
- 표준 입력: 표준 입력에서는 데이터가 남아 있지 않을 경우 EOF로 간주된다.
import sys data = sys.stdin.read() print("EOF 상태:", data == "")
- 파일 스트림:
3.2 스트림 포인터 이동
- 파일 스트림에서는 읽기/쓰기 포인터를 수동으로 조정할 수 있다.
with open('example.txt', 'r') as f: f.seek(0) # 스트림의 시작으로 이동 print(f.read(5)) # 처음 5바이트 읽기
next를 쓰지 않고 이터레이터 쓰기
1바이트씩 읽어오기
바이트로 디짓 처리하기
중복된 숫자 찾기
1부터 N - 1까지의 정수가 하나씩 정렬되지 않은 채로 저장되어 있는 어떤 수열 A가 있다. 수열 A에 임의의 정수 M(1 ≤ M ≤ N – 1)을 넣어 크기가 N인 수열로 만들었을 때, 임의의 정수 M을 찾는 프로그램을 작성하라.
입력
- 첫째 줄에 수열의 크기 N(2 ≤ N ≤ 10,000,000)이 주어진다.
- 둘째 줄에 수열 A의 원소인 N개의 정수가 주어진다. 입력으로 주어지는 정수는 모두 1보다 크거나 같고, N-1보다 작거나 같은 정수이며 문제의 답인 M을 제외하고는 모두 서로 다른 정수이다.
출력
M을 출력하라.
제약 조건
시간 제한 | 메모리 제한 |
---|---|
2 초 | 256 MB |
예제
입력
10
1 2 2 5 6 4 3 7 8 9
출력
2
다음 코드는 os.read 함수를 이용해 표준 입력(파일 디스크립터 0) 으로부터 모든 바이트를 읽어 들인 뒤, 이를 직접 순회(iterate)하면서 필요한 정보를 추출한 뒤 결과를 출력한다.
import os
r = iter(os.read(0, os.fstat(0).st_size))
N = 0
for c in r:
if c == 10:
break
N = N * 10 + (c - 48)
ref = (N * (N - 1)) >> 1
while True:
num = 0
for c in r:
if c <= 47:
break
num = num * 10 + (c - 48)
ref -= num
if c == 10:
break
print(-ref)
1. 표준 입력을 한 번에 읽고, 이터레이터로 변환하기
import os
r = iter(os.read(0, os.fstat(0).st_size))
- os.read(0, os.fstat(0).st_size):
- os.read를 사용하여 **파일 디스크립터 0(표준 입력)**에서,
- os.fstat(0).st_size(표준 입력에 들어 있는 전체 바이트 수)만큼 데이터를 한 번에 읽는다.
- 그 결과로 얻은 바이트열(byte string)을 iter(...)로 감싸 이터레이터로 만든다.
- 이터레이터를 사용하면, 한 바이트(문자)씩 순회할 수 있다.
2. 첫 번째 줄에서 N 읽어들이기
N = 0
for c in r:
if c == 10:
break
N = N * 10 + (c - 48)
- N = 0으로 초기화하고, for c in r:를 통해 이터레이터 r에서 바이트를 하나씩 꺼낸다.
- if c == 10: 은 ASCII 코드 10(개행 문자)인지 확인한다. 만약 개행이라면 break로 반복을 종료한다.
- 즉, 첫 번째 줄이 끝날 때까지 숫자를 읽는다.
- N = N * 10 + (c - 48):
- c는 읽은 문자의 바이트값이다.
- '0' 문자의 ASCII 코드가 48이므로, (c - 48)를 통해 실제 숫자로 변환한다.
- N = N * 10 + ... 누적하면, 문자열 형태의 숫자를 정수로 계산할 수 있다.
- 예: 만약 입력 첫 줄이 "123"라면, c는 [49, 50, 51](ASCII)이고, 순서대로 (49 - 48), (50 - 48), (51 - 48)이 되어 1, 2, 3이 된다.
3. ref 계산
ref = (N * (N - 1)) >> 1
- (N * (N - 1)) >> 1는 오른쪽 비트 시프트 연산으로, 사실상 $\frac{N(N - 1)}{2}$ 와 같은 값을 얻는다.
- 예: N * (N-1)을 2로 나눈 것과 동일 (정수 나눗셈).
- 이는 흔히 1부터 N-1까지의 합(또는 조합 계산) 등에 자주 사용되는 공식이다.
- 구체적으로 $$
\binom{N}{2} = \frac{N(N - 1)}{2}
$$
값이 된다.
- 구체적으로 $$
4. 다음 줄부터 숫자를 계속 읽으며 ref 감소시키기
while True:
num = 0
for c in r:
if c <= 47:
break
num = num * 10 + (c - 48)
ref -= num
if c == 10:
break
- while True:로 무한 루프를 돌며 다음과 같은 과정을 반복한다.
- num = 0으로 초기화하고, for c in r:에서 바이트를 하나씩 꺼내 숫자로 변환한다.
- if c <= 47: break
- 공백(32), 개행(10), 구분자 등 '0'(48)보다 작은 ASCII 코드가 나오면 숫자 읽기를 멈춘다.
- 여기서는 대부분 개행(10) 또는 공백(32) 등이 구분자로 쓰일 것이므로, 이를 기준으로 하나의 숫자 읽기를 종료하는 로직이다.
- num = num * 10 + (c - 48)을 통해 숫자를 누적하여 정수로 만든다.
- if c <= 47: break
- ref -= num
- 읽어들인 숫자를 ref에서 뺀다.
- if c == 10: break
- 만약 현재 바이트가 개행(10)이라면 while 루프도 빠져나오도록 설정되어 있다.
- 즉, 한 줄 읽기를 종료하면 반복을 끝낸다.
정리하면, N 이후의 두 번째 줄부터는(혹은 뒤이어 오는 숫자 리스트) 각 숫자를 읽어 ref에서 감소시킨 뒤, 개행을 만나면 종료한다.
5. 최종 결과 출력
print(-ref)
- 마지막으로 ref에 대해 음수 부호를 붙인 -ref를 출력한다.
- 위에서 ref는 $\frac{N(N - 1)}{2}$ 에서 시작해 입력받은 숫자들을 모두 뺀 값이므로, 그 결과에 -1을 곱한 값을 출력한다.
요약
- 표준 입력으로부터 모든 바이트를 읽어 이터레이터로 만든다.
- 첫 번째 줄에서 숫자 N을 읽어 정수로 변환한다.
- ref = (N * (N - 1)) >> 1을 구한다 $\frac{N(N - 1)}{2}$.
- 이어서 읽은 숫자들을 모두 ref에서 빼준다.
- 마지막으로 -ref를 출력한다.
*ARRAY활용해서 조금 더 빠르게
import os
import array
r = array.array('B', os.read(0, os.fstat(0).st_size)) # Byte
idx = 0
N = 0
while idx < len(r):
c = r[idx]
idx += 1
if c == 10:
break
N = N * 10 + (c - 48)
ref = (N * (N - 1)) >> 1
while idx < len(r):
num = 0
while idx < len(r):
c = r[idx]
idx += 1
if c <= 47:
break
num = num * 10 + (c - 48)
ref -= num
if c == 10:
break
print(-ref)