← 목록

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

#Python

https://mhd329.tistory.com/63

🔗 관련 글: [Python][이게 왜 안돼?] yield 키워드와 Generator

위 포스팅이랑 이어지는 글임.


멀티쓰레드 작업을 하려고 함수 하나 만들었다.

처음에는 이렇게 작성했음.

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]

지금 원래대로(맨 위에 예시처럼) 쓰려면 인자가 add, (list1, list2) 이 되야하는데 그게 아니라 껍데기를 씌우지 않고 그냥 준 것이다.

이렇게 해놓고 보면 결과는 위와 같이 잘 나오고 있었다.

첫 번째 쓴 예시를 보면(이 바로 위에 예시 말고) ((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 타입의 매개변수 하나만 받기로 했는데 두 개가 주어졌다고 한다.

method에 "mt", cams_number에는 2, test는 True를 주고 출력해보면 다음과 같다.

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):
...
← 목록으로
오늘 0·전체 0