2.34. Kod dynamiczny

Trzy funkcje wbudowane przyjmują łańcuch ze źródłem Pythona i uruchamiają go: eval(), exec() oraz compile(). Razem pozwalają one kodowi konstruować i wykonywać więcej kodu w czasie działania – co czasami jest dokładnie właściwym narzędziem, a znacznie częściej źródłem błędów i luk bezpieczeństwa.

Ostrzeżenie

Te funkcje wykonują dowolny kod Pythona. Przekazanie danych wejściowych użytkownika do eval lub exec – łańcucha z pliku, który użytkownik może edytować, ładunku odebranego przez sieć, wartości wpisanej w wierszu poleceń REPL – pozwala tym danym zrobić wszystko, co mógłby zrobić wywołujący skrypt, aż po usunięcie każdego pliku na urządzeniu włącznie. Używaj ich rozważnie, nigdy wewnątrz kodu uruchamianego automatycznie i nigdy na danych, których nie kontrolujesz.

2.34.1. eval

eval() uruchamia pojedyncze wyrażenie i zwraca jego wartość:

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

Wyrażenie domyślnie widzi zmienne globalne i lokalne wywołującego, dlatego name jest rozwiązywane w drugim przykładzie. Przekazanie jawnych słowników pozwala odizolować ewaluację w piaskownicy:

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

Nawet w piaskownicy eval jest niebezpieczne. Istnieją dobrze znane techniki ucieczki z takich piaskownic; nie polegaj na samej sztuczce z pustym __builtins__ w przypadku niezaufanych danych wejściowych.

2.34.2. exec

exec() uruchamia blok kodu zamiast pojedynczego wyrażenia – instrukcje, definicje funkcji, pętle – i zwraca None:

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

Wynik:

0
1
2

Blok może definiować nazwy, które stają się dostępne później, z pewnymi zastrzeżeniami dotyczącymi zakresu lokalnego i globalnego. exec wewnątrz funkcji rzadko zachowuje się tak, jak oczekuje piszący; jeśli go potrzebujesz, uruchamiaj go na poziomie modułu.

2.34.3. compile

compile() zamienia łańcuch źródłowy w obiekt kodu, który można później przekazać do eval() lub exec(). Użyj go, gdy to samo źródło ma być uruchamiane wiele razy – parsowanie odbywa się raz, a wykonanie jest szybsze:

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

Wynik:

0
1
4
9
16

Środkowy argument to etykieta, która pojawia się w śladach stosu (tracebackach), jeśli kod zgłosi wyjątek. Trzeci argument to "eval" dla pojedynczego wyrażenia, "exec" dla bloku lub "single" dla instrukcji w stylu interaktywnym, która wypisuje swój wynik.

2.34.4. Kiedy sięgać po te narzędzia

Prawie nigdy. Większość zastosowań, jakie wyobrażają sobie początkujący, ma bezpieczniejsze alternatywy:

  • Odczyt pliku konfiguracyjnego. Użyj json – dane strukturalne, bez wykonywania kodu.

  • Ewaluacja wartości liczbowej wpisanej przez użytkownika. Użyj int() / float() do parsowania, a następnie wykonaj działania arytmetyczne. Jeśli użytkownik naprawdę musi wprowadzić formułę, użyj małego parsera wyrażeń, a nie eval.

Gdy naprawdę potrzebujesz eval / exec / compile, odizoluj miejsce wywołania, rejestruj dokładny łańcuch, który ma zostać wykonany, i traktuj to źródło jako najbardziej podejrzaną rzecz w swoim kodzie.