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