2.34. 동적 코드#

세 가지 내장 함수가 Python 소스 문자열을 받아 실행합니다: eval(), exec(), compile(). 이들을 함께 사용하면 코드가 런타임에 더 많은 코드를 구성하고 실행할 수 있습니다 – 이는 가끔은 정확히 알맞은 도구이지만, 그보다 훨씬 더 자주 버그와 보안 구멍의 원천이 됩니다.

경고

이 함수들은 임의의 Python을 실행합니다. 사용자 입력 – 사용자가 편집할 수 있는 파일의 문자열, 네트워크로 받은 페이로드, REPL 프롬프트에 입력된 값 – 을 eval 이나 exec 에 전달하면, 그 입력이 호출하는 스크립트가 할 수 있는 모든 일을 할 수 있게 됩니다. 장치의 모든 파일을 삭제하는 것까지 포함해서 말입니다. 신중하게 사용하고, 자동으로 실행되는 코드 안에서는 절대 사용하지 말며, 통제할 수 없는 데이터에는 절대 사용하지 마세요.

2.34.1. eval#

eval() 은 단일 식(expression) 을 실행하고 그 값을 반환합니다:

>>> eval("3 * 7")
21
>>> name = "OpenMV"
>>> eval("name.lower()")
'openmv'

식은 기본적으로 호출자의 전역과 지역을 참조하므로, 두 번째 예제에서 name 이 해석됩니다. 명시적인 딕셔너리를 전달하면 평가를 샌드박스화할 수 있습니다:

eval("a + b", {"__builtins__": None}, {"a": 1, "b": 2})

샌드박스 처리를 하더라도 eval 은 위험합니다. 그러한 샌드박스를 벗어나는 잘 알려진 기법들이 있으므로, 신뢰할 수 없는 입력에 대해서는 빈 __builtins__ 트릭만으로 의존하지 마세요.

2.34.2. exec#

exec() 는 단일 식이 아니라 코드 블록(block) – 문, 함수 정의, 루프 – 을 실행하고 None 을 반환합니다:

exec("for i in range(3): print(i)")

출력:

0
1
2

블록은 이후에 사용할 수 있는 이름을 정의할 수 있지만, 지역 대 전역 범위와 관련해 몇 가지 주의 사항이 있습니다. 함수 내부에서의 exec 는 작성자가 기대하는 대로 동작하는 경우가 드뭅니다; 꼭 필요하다면 모듈 수준에서 실행하세요.

2.34.3. compile#

compile() 은 소스 문자열을 나중에 eval() 이나 exec() 에 전달할 수 있는 코드 객체(code object) 로 바꿉니다. 동일한 소스가 여러 번 실행될 때 사용하세요 – 파싱은 한 번만 일어나고, 실행은 더 빠릅니다:

expr = compile("x * x", "<expr>", "eval")
for x in range(5):
    print(eval(expr))

출력:

0
1
4
9
16

가운데 인자는 코드가 예외를 일으킬 때 트레이스백에 나타나는 레이블입니다. 세 번째 인자는 단일 식의 경우 "eval", 블록의 경우 "exec", 결과를 출력하는 대화형 스타일의 문의 경우 "single" 입니다.

2.34.4. 언제 이것들에 손을 대야 하는가#

거의 절대 사용하지 마세요. 초보자가 떠올리는 대부분의 사용 사례에는 더 안전한 대안이 있습니다:

  • 설정 파일 읽기. json 을 사용하세요 – 구조화된 데이터이며, 실행이 없습니다.

  • 사용자가 입력한 숫자 값 평가하기. int() / float() 로 파싱한 다음 산술 연산을 하세요. 사용자가 정말로 수식을 입력해야 한다면, eval 이 아니라 작은 식 파서를 사용하세요.

정말로 eval / exec / compile 이 필요하다면, 호출 지점을 격리하고, 실행되려는 정확한 문자열을 로그로 남기며, 그 소스를 코드에서 가장 의심스러운 대상으로 취급하세요.