Save my data

[Python][이게 왜 안돼?] * (애스터리스크), map, zip 본문

개인공부/Python

[Python][이게 왜 안돼?] * (애스터리스크), map, zip

양을 좋아하는 문씨 2025. 2. 25. 02:56

https://mhd329.tistory.com/63

 

[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):
...
Comments