python itertools의 tee 함수는
- 순회 가능한 iterable 한 것에 대해
- 여러 독립적인 iterator 들을 만들어 주는 함수입니다.
이 문서를 추가로 보면, tee 이터레이터가 thread safe 하지 않다는 내용이 있어요. 이에 대한 내용은 추후에 다시 언급하도록 하겠습니다.
tee 함수
함수의 설명부터 보도록 하겠습니다.
설명을 보면, iterble 한 대상을 받아서 n개의 독립적인 이터레이터들을 반환하는 것으로 되어 있습니다. 예제를 간단하게 보도록 하겠습니다.
2번째 줄에 리스트 li를 선언했습니다. 이것은 2, 3, 5를 담고 있습니다. 다음, 3번째 줄에 tee 함수로 순회 가능한 li에 대해, 3개의 독립적인 iterator를 선언합니다. iterator? 이 객체는 next라는 중요한 메서드가 있어요.
iterator가 2를 가리키고 있어요. 여기서 next를 호출하면, 다음 원소를 가리키게 됩니다.
여기까지 별로 어렵지 않지요? 그러면 4 ~ 6번째 줄을 이해해 보겠습니다. 독립적인 이터레이터 3개 a, b, c를 선언했어요. 그러면, 아래와 같이 도식화가 되겠네요.
a, b, c가 1을 가리키고 있어요. 정확히 말하면 next를 호출할 때 셋 다 다음 원소로 이동하게 됩니다. 이제, next(a)를 호출하면 어떻게 될까요?
a는 다음 원소인 3을 가리키게 됩니다. 이제 next(b)를 호출하면 어떻게 될까요?
b 또한 다음 원소로 이동하게 됩니다. 그리고 다시 next(a)를 호출하면 2에서 3으로 이동하게 됩니다. 실행 결과는 아래와 같습니다.
2, 2, 3이 출력되게 됩니다. 보통은 여기까지만 알아도 충분합니다. 그런데, thread safe 말고도 조심해야 할 게 더 있습니다. 아래에 공식 문서에 나온 자세한 동작 방식을 후술하겠습니다.
문제가 있는 프로그램 1
아래 프로그램의 문제점은 무엇일까요?
먼저, 3번째 줄에서, li의 iterator인 it을 얻어왔습니다. 그리고, tee로 it을 기반으로 a, b, c 이터레이터가 분할되었습니다. 그리고 나서, next(it)을 호출하였는데요. 실행 결과가 어떻게 나올까요?
2, 3, 2가 아니라 3, 5, 3이 나왔습니다. 어떻게 된 일일까요? 이것도 자세히 봅시다. 문서에 비슷하게 동작하는 코드가 있으니, 이를 토대로 분석해 보겠습니다. 먼저, gen의 deque는 튜플에 있는 각각의 deque를 나타냅니다.
gen (generator)이 호출될 때 마다, 이 iter를 소모하게 됩니다. 먼저, next(it)을 호출했습니다. 그러면, 어떻게 될까요?
하나의 원소 2를 소모하고, 3을 가리키게 됩니다. 이후에, next(b)를 호출하면
- gen generator가 호출되는데
- 이 함수에서 next(it)을 호출합니다.
따라서, 2가 아닌 3이 리턴되게 됩니다. 그리고, 모든 tuple에 있는 deque에 대해, 3을 추가하게 됩니다.
그러면 소모된 iterator는 무엇을 가리키게 될까요? 5를 가리키게 되겠지요. 이제, mydequeue b는 함수 popleft로 인해, 3이 제거됩니다.
여기까지 상황을 정리해 봅시다.
- python tee 로 분할된 것들은 인자로 넘겨진 iterable한 객체를 공유한다.
- 하지만, 별개로 도는 것 처럼 보이는 것은 각각은 deque와 비슷한 무언가로 관리되기 때문.
- 문서에서 significant auxiliary storage가 필요하다는 이유도 이 때문일 것.
이제 조금 더 심화된 프로그램을 봅시다.
문제가 있는 프로그램 2
예제 프로그램 3번을 보겠습니다.
어떤 결과가 나올까요? 하나씩 보도록 합시다. 먼저, tee에 의해 a, b, c로 분할되긴 합니다. 그 다음이 문제인데요. next(b)를 호출했다면, it을 하나 소모하면서, 내부에서 gen 함수가 호출이 됩니다. 이 경우, 그림과 같은 상황이 됩니다.
a, b, c를 나타내는 deque에 2가 들어가는데, next(b)로 인해, b에 있는 2가 제거됩니다.
다음 7번째 줄에서 next(it)을 호출했습니다. 이 때, 3을 소모하고 5를 가리키게 됩니다. 이제 next(a)와 next(b)를 호출해 볼까요?
next(a)를 호출한 경우, 이미 deque에 있기 때문에, 2가 리턴되고 끝납니다.
next(b)가 호출될 때, b에는 아무 것도 없으므로 5가 들어갑니다.
그리고 b가 소모되기 때문에, 맨 앞에 있는 5가 제거됩니다.
실행 결과는 위와 같습니다. 여기까지 python tee 함수의 동작을 다시 정리해 봅시다.
- tee의 대상 iterator는 공통으로 쓴다.
- tee로 넘겨진 iterator는 공유됩니다.
- 다만, 분할되는 iterator 마다 별도의 자료구조로 소모된 원소를 저장한다.
- 이로 인해 깊은 복사처럼 보이게 되는 것입니다.
따라서, 원본 iterable은 절대 변형되면 안 됩니다. 이터레이터를 tee의 인자로 넘긴 경우, 절대 다른 곳에서 순회해서도 안 됩니다. 만약, 다른 곳에서 순회가 된다면 이상한 동작을 하게 됩니다.