[Python][이게 왜 안돼?] * (애스터리스크), map, zip
[Python][이게 왜 안돼?] yield 키워드와 Generator
예전에 회사에서 만들었던 프로젝트를 재구성하다가 몰랐던걸 발견해서 기록함.OpenCV로 캠영상을 읽은 뒤 웹(Flask)으로 스트리밍하는 모듈을 만드는 중이었다.캠을 읽는 단위는 클래스 단위로
mhd329.tistory.com
위 포스팅이랑 이어지는 글임.
멀티쓰레드 작업을 하려고 함수 하나 만들었다.
처음에는 이렇게 작성했음.
def multi_thread(method: str, cams_number: int, test: bool) -> None:
def run(args: tuple):
method, thread_number, test = args
cam = Cam(thread_number)
cam.run(method, test)
with ThreadPoolExecutor(max_workers=cams_number) as excutor:
[*excutor.map(run, ((method, i, test) for i in range(0, cams_number)))]
cv2.destroyAllWindows()
왜냐하면 `excutor.map`의 첫 번째로 들어가는 함수 인자는 하나의 인자만 받을 수 있는걸로 알고있었기 때문이다.
근데 여러 인자를 동시에 쓰고 싶었기 때문에 해당 인자들을 튜플로 묶어서 전달해줬다.
잘 작동했고 아무 문제 없었다.
근데 갑자기 이런 호기심이 들었다. 인자가 tuple[tuple] 형태로 전달되는데, 그걸 언패킹하면 어떻게 되지?
그러니까 원래 tuple[tuple] 구조였고 tuple 내부에서 각 tuple을 선택해서 줘야 되는건데, 그게 아니라 껍데기가 까진 상태에서 어떻게 되는지가 갑자기 궁금해졌다.
우선 아래와 같이 껍데기를 씌우지 않은 상태를 가정하고 그냥 줘봤다.
from concurrent.futures import ThreadPoolExecutor
def add(a, b):
return a + b
list1 = [1, 2, 3, 4]
list2 = [10, 20, 30, 40]
with ThreadPoolExecutor() as executor:
results = executor.map(add, list1, list2)
print(list(results))
>>> [11, 22, 33, 44]
이 경우는 잘 되고 있었다.
내 경우는 `((method, i, test), (method, i, test))` 같은 모습이니까, `*args` 하면 `(method, i, test), (method, i, test)` 처럼 겉껍데기가 없어진 모습일 것이다. 그러면 `*args` 했을 때,
[*excutor.map(run, (method, 0, test), (method, 1, test)] 같은 모습이 되겠다고 예상할 수 있다.
그리고 위의 예시 코드의 결과를 바탕으로, 주어진 이터러블 객체들의 같은 인덱스에 있는것 끼리 묶어서 연산할 것이라고 예측해볼 수 있다. 위에서는 (1, 10), (2, 20) ... 과 같이 연산되었으니까...
그것을 염두해두고 아래 코드를 실행해봤다.
def multi_thread(method: str, cams_number: int, test: bool) -> None:
def run(args: tuple[str, int, bool]) -> None:
method, thread_number, test = args
cam = Cam(thread_number)
cam.run(method, test)
with ThreadPoolExecutor(max_workers=cams_number) as excutor:
args = ((method, i, test) for i in range(0, cams_number))
[*excutor.map(run, *args)]
cv2.destroyAllWindows()
>>> TypeError: multi_thread.<locals>.run() takes 1 positional argument but 2 were given
뭔가 예상한대로 에러가 나긴 했다. 함수에서는 tuple 타입의 매개변수 하나만 받기로 했는데 두 개가 주어졌다고 한다.
출력해보면 다음과 같다.
def multi_thread(method: str, cams_number: int, test: bool) -> None:
def run(*args) -> None:
print(args)
cam = Cam(thread_number)
cam.run(method, test)
with ThreadPoolExecutor(max_workers=cams_number) as excutor:
args = ((method, i, test) for i in range(0, cams_number))
[*excutor.map(run, *args)]
cv2.destroyAllWindows()
>>> ('mt', 'mt')
>>> (0, 1)
>>> Traceback (most recent call last):
...
이것으로 map의 동작 방식을 알 수 있다.
`[*excutor.map(run, (method, 0, test), (method, 1, test)]` 처럼 동작하는데, 내부적으로는 zip()을 호출하여 각 요소의 같은 위치끼리 묶어서 함수에 전달한다.
바꿔서 쓰면 아래와 같다.
for args in zip(("mt", 0, True), ("mt", 1, True)):
run(*args)
즉, zip(("mt", 0, True), ("mt", 1, True))의 결과는 아래와 같이 되고,
[
("mt", "mt"),
(0, 1),
(True, True)
]
저것들이 run 함수에 전달되면서 아래와 같은 에러가 발생했던 것이다.
>>> ('mt', 'mt')
>>> (0, 1)
>>> Traceback (most recent call last):
...