Python lambda binding to local variables problem

TK·2021년 6월 3일
0

TIL

목록 보기
55/55

lambda in a loop

async event loop 기반 framework 인 python tornado 를 사용하여 웹소켓 서버에 데이터를 보내던 중에 문제가 발생했다.

tornado 는 멀티스레딩 사용 시 main event loop 에서 실행할 기능을 콜백에 담아 처리한다.
그래서 for loop 을 돌며 client 에게 메시지를 전달하는 로직을 구현했다.

# 중략
for client in self.clients:
    data = json.dumps({self.stock_code: stock_price})

    self.loop.add_callback(lambda client:client.write_message(data))
    # time.sleep(0.0000001)

self.clients 는 웹소켓에 접속한 클라이언트이다.
다음과 같은 코드를 돌렸을 때 맨 마지막에 접속한 클라이언트에게만 메시지를 보내는 버그가 발생했다.
그런데 아주 임시방편으로 아주 짧은 시간동안 print() 함수를 실행했더니 또 작동하는 것이였다.

왜 그런지 고민끝에 이유를 발견했다.

다음 예제를 보자.

funcs = []
for i in range(5):
    funcs.append(lambda : print(i))

# i 값은 이 때 4이다.
for f in funcs:
    f()

결과는 4만 다섯번 출력된다. 이유가 뭘까?

  • 바로 lambda 가 evaluate 되서야 비로서 코드가 생성되기 때문에 for loop에서 마지막 값인 4가 출력되는 것이다.

  • 그리고 time.sleep 함수가 실행됐을 때 제대로 작동하는 것 처럼 보였던 이유는, add_callback() 에 함수가 담기고 나서 time.sleep() 이 실행되는 동안 add_callback() 에 들어있는 함수가 해당 for loop 안에서 실행이 될 준비가 되었기 때문이였다.

  • 하지만 time.sleep() 이 없었을 떈 for loop 가 너무 빨리 돌아버려서 loop 이 끝나버린 뒤 함수가 실행준비가 되었기 때문에 마지막 client 에게만 데이터가 간 것이였다.

그렇다면 이런 이슈를 해결하기 위해 우리는 어떻게 할 수 있을까?

functools.partial

함수가 생성될 때 evaluate 되는 고차함수 partial 을 쓰면 된다.

from functools import partial
funcs = []
for i in range(5):
    funcs.append(partial(print, i))

for f in funcs:
    f()

결과는 차례대로 0 1 2 3 4 가 출력된다.

하지만 여기서 방법이 끝이아니다.

lambda default argument

lambda 의 default argument 를 사용하는 방법이 있다.

Using a default argument works because default arguments are evaluated when the function is created, not when it is called.

https://stackoverflow.com/a/10452819

default args 를 사용하면 해당 args 는 함수가 실행될 때가 아니라 생성될 때 evaluate 되기 때문에 이 방법도 해답이 될 수 있다.

wrap-up

중요한 것은 lambda 의 변수가 evaluate 되는 시점이다.

우리가 이 문제를 해결하기 위한 최선의 선택은 lambda 에 들어가는 함수가 얼마나 복잡한지에 따라 결정되는 것 같다.

  • 복잡하다면 : functools.partial
  • 간단하다면 : lambda expression
profile
Backend Developer

0개의 댓글