Как использовать exec() в Python

Функция exec() позволяет нам выполнить блок кода Python из строки. Эта встроенная функция Python может пригодиться, когда нам нужно запустить динамически сгенерированный код. Но использовать ее нужно с умом, так как с ней связаны некоторые риски безопасности.

Содержание

Что собой представляет функция exec()

Функция exec() позволяет нам выполнить любой фрагмент кода Python, независимо от того, насколько он велик или мал. Ее помощью можно выполнять динамически генерируемый код.

Представьте интерпретатор Python, который берет кусок кода, обрабатывает его внутри и выполняет. Функция exec() делает то же самое. Она похожа на независимый интерпретатор Python.

Функция exec() способна выполнять как простые скрипты, так и полноценные программы на Python. Она может выполнять вызовы и определения функций, определение и инстанцирование классов, импорт и многое другое.

Синтаксис

exec(object [ , globals [ , locals]])
  • object должен быть строкой или объектом кода. Если это строка, то она будет разобрана как набор предложений Python, который будет выполнен, если не возникнет синтаксической ошибки. Если это объект кода, то он просто выполняется.
  • globals и locals позволяют предоставить словари, представляющие глобальное и локальное пространства имен.

Возвращаемое значение

Возвращаемое значение функции exec() — None. Это может быть связано с тем, что каждый отдельный фрагмент кода не имеет конечного результата.

Первое знакомство с exec()

Чтобы понять, как работает exec(), давайте рассмотрим пример кода:

obj = ["apple", "cherry", "melon", "strawberry"]
code = "print([sliced[:4] for sliced in obj if 'a' not in sliced])"

exec(code)

.........
['cher', 'melo']

И еще один пример:

# Этот код будет работать постоянно, и мы сможем запускать наш код так же,
# как делали бы это в интерпретаторе Python 
while True:
    exec(input(">>> "))
>>> print("Welcome to GeekPython")
Welcome to GeekPython

>>> import numpy as np
>>> print(np.random.randint(16, size=(2, 4)))
[[11 13  3 13]
 [ 7  6 15  5]]

>>> x = ["apple", "banana", "cherry", "mango"]
>>> print([fruit for fruit in x if "a" not in fruit])
['cherry']

Функция exec() работает точно так же, как интерпретатор Python, принимая наш код, обрабатывая его, выполняя и возвращая правильные результаты.

Мы запускаем бесконечный цикл, а внутри него принимаем ввод из командной строки и оборачиваем его в функцию exec() для выполнения.

Выполнение кода из строкового ввода

Мы можем использовать функцию exec() для выполнения кода в формате строки.

Создать строковый ввод можно несколькими способами. Для того можно использовать отдельные строки кода, а также код в несколько строк (с применением символов новой строки или тройных кавычкех с правильным форматированием).

Использование однострочников

В Python однострочный код, также известный как однострочник, — это код, написанный в одной строке, который может выполнять несколько задач одновременно.

Вот пример однострочника:

obj = ["apple", "cherry", "melon", "strawberry"]

print([sliced[:4] for sliced in obj if 'a' not in sliced])
['cher', 'melo']

Если мы запустим этот код с помощью exec(), выглядеть это будет так:

obj = ["apple", "cherry", "melon", "strawberry"]

exec("print([sliced[:4] for sliced in obj if 'a' not in sliced])")
#-----------------------------ИЛИ--------------------------------
exec("code = [sliced[:4] for sliced in obj if 'a' not in sliced]")

Второй вариант однострочника с использованием exec() ничего не выведет при выполнении. Вместо этого результат будет сохранен в переменной code для последующего доступа.

Выполнение нескольких строк кода, разделенных символами новой строки

Мы можем объединить несколько предложений в однострочную строку с помощью символов новой строки \n.

exec("square = int(input('Enter the number: '))\nprint(f'The square of {square}:', square**2)")
Enter the number: 30
The square of 30: 900

Символ новой строки (\n) используется для того, чтобы функция exec() воспринимала наш однострочный строковый код как многострочный набор предложений Python.

Использование строки в тройных кавычках

В Python мы часто используем тройные кавычки для комментирования или документирования кода. Однако в данном случае мы будем использовать их для генерации строкового ввода, который выглядит и ведет себя точно так же, как обычный код Python.

Код, который мы пишем в тройных кавычках, должен иметь правильные отступы и форматирование, так же, как и обычный код Python. Пример:

sample_code = """

integers = [4, 7, 2, 9, 44]

def square(num):
    return num ** 2

def odd_num(num):
    return num % 2 == 1

square_if_even = [square(number) for number in integers if number % 2 == 0]

odd_number = [number for number in integers if odd_num(number)]

print("Original values:", integers)

print("Square of even number:", square_if_even)

print("Odd number:", odd_number)

"""
exec(sample_code)
Original values: [4, 7, 2, 9, 44]
Square of even number: [16, 4, 1936]
Odd number: [7, 9]

Приведенный код похож на обычный код Python. Он правильно отформатирован, имеет все нужные отступы. Но он также заключен в тройные кавычки и в итоге сохранен как строковый ввод в переменной sample_code, которая затем передается на выполнение в функцию exec().

Выполнение кода из файла Python

С помощью функции exec() можно выполнить код, сохраненный в файле с расширением .py. Для этого нужно прочитать содержимое файла, используя функцию open().

Рассмотрим пример. Файл sample.py содержит следующий код:

# sample.py
def anime(name):
    print(f"Favourite anime: {name}")

anime("One Piece")

list_of_anime = input("Enter your favourite anime: ").split(",")
print("Your Favourite anime:", list_of_anime)

Первый блок кода просто выводит название аниме. Следующий блок принимает в качестве входных данных названия ваших любимых аниме, разделенные запятыми, и выводит их в виде списка.

А теперь давайте выполним этот файл с помощью функции exec().

with open("sample.py", mode="r") as sample:
    file = sample.read()

exec(file)
Favourite anime: One Piece
Enter your favourite anime: One Piece, Naruto, Demon Slayer, Jujutsu kaisen
Your Favourite anime: ['One Piece', ' Naruto', ' Demon Slayer', ' Jujutsu kaisen']

Мы использовали функцию open() с помощью оператора with, чтобы открыть файл .py как обычный текстовый файл. Затем мы использовали .read() на объекте file, чтобы прочитать содержимое файла как строку в переменную file, которая передается в exec для выполнения кода.

Параметры globals и locals

Эти параметры опциональны. С их помощью можно ограничить использование функций, методов и переменных, которые не являются обязательными.

Поскольку эти параметры необязательны, их отсутствие заставляет функцию exec() выполнять входной код в текущей области видимости. Для лучшего понимания рассмотрим следующий пример.

code = """
out = str1 + str2
print(out)
"""
# Глобальные переменные
str1 = "Hel"
str2 = "lo"

exec(code)
Hello

Этот код успешно выполнился и выдал результат, объединяющий обе глобальные переменные. Поскольку параметры globals и locals не были указаны, функция exec() выполнила введенный код в текущей области видимости. В данном случае текущая область видимости является глобальной.

Давайте теперь немного изменим код:

code = """
out = str1 + str2
print(out)
"""
# Глобальные переменные
str1 = "Hel"
str2 = "lo"

print(out)
Traceback (most recent call last):
    .....
NameError: name 'out' is not defined. Did you mean: 'oct'?

В этом примере мы попытались получить доступ к значению переменной out перед вызовом exec() и получили ошибку.

Однако мы можем получить доступ к значению переменной out после вызова exec(), поскольку после этого вызова переменная, определенная в коде инпута, будет доступна в текущей области видимости.

exec(code)
print(out)
Hello
Hello

Использование параметров globals и locals

code = """
out = str1 + str2
print(out)
"""
# Глобальные переменные
str1 = "Hel"
str2 = "lo"

exec(code, {"str1": str1})
Traceback (most recent call last):
    ....
NameError: name 'str2' is not defined. Did you mean: 'str1'?

Этот код возвращает ошибку, так как мы не определили ключ, содержащий str2 в словаре, поэтому exec() не имеет к нему доступа.

code = """
out = str1 + str2
print(out)
"""
# Глобальные переменные
str1 = "Hel"
str2 = "lo"

exec(code, {"str1": str1, "str2": str2})

print(out)
Hello
Traceback (most recent call last):
     ....
NameError: name 'out' is not defined. Did you mean: 'oct'?

Теперь функция exec() имеет доступ к обеим глобальным переменным, и код возвращает вывод без ошибки. Но на этот раз у нас нет доступа к out после вызова exec(), потому что мы используем пользовательский словарь для предоставления области выполнения для exec().

Вот пример совместного использования locals и globals:

code = """
out = str1 + str2 + " " + x
print(out)
"""
# Глобальные переменные
str1 = "Hel"
str2 = "lo"

def local():
    # Локальная переменная
    x = "there"
    exec(code, {"str1": str1, "str2": str2}, {"x": x})

local()
Hello there

Здесь мы вызвали exec() из локальной функции. В глобальной области видимости у нас есть глобальные переменные, а в локальной области видимости (на уровне функции) — локальная. Параметр globals задает переменные str1 и str2, а параметр locals — переменную x.

Блокирование ненужных методов и переменных

Используя параметры globals и locals, мы можем контролировать, ограничивать или использовать какую-либо переменную или метод при вводе кода. В этом разделе мы ограничим некоторые функции модуля datetime в Python.

from datetime import *

code = """
curr_time = datetime.now()
print(curr_time)
"""
exec(code, {})
Traceback (most recent call last):
     ....
NameError: name 'datetime' is not defined

Мы ограничили использование метода datetime из модуля datetime, что привело к ошибке.

Использование необходимых методов и переменных

Мы можем использовать только те методы, которые требуются при выполнении exec().

from datetime import *

# Разрешить только два метода
allowed_param = {'datetime': datetime, 'timedelta': timedelta}

exec("print(datetime.now())", allowed_param)
exec("print(datetime.now() + timedelta(days=2))", allowed_param)

# Метод date() не разрешен
exec("print(date(2022, 9, 4))", allowed_param)
2022-10-15 18:40:44.290550
2022-10-17 18:40:44.290550

Traceback (most recent call last):
     ....
NameError: name 'date' is not defined

Ошибка возникла из-за того, что метод date не был разрешен. За исключением двух методов, datetime и timedelta, все методы в модуле datetime были запрещены.

Давайте посмотрим, чего еще можно добиться с помощью параметров globals и locals.

from datetime import *

# Установить параметр globals в __builtins__
globals_param = {'__builtins__': __builtins__}

# Установить параметр locals на прием только print(), slice() aиnd dir()
locals_param = {'print': print, 'dir': dir, 'slice': slice}

exec('print(slice(2))', globals_param, locals_param)
exec('print(dir())', globals_param, locals_param)
slice(None, 2, None)
['dir', 'print', 'slice']

Внутри функции exec() можно выполнить только метод slice() и все встроенные методы. Несмотря на то что метод slice() не из модуля datetime, здесь он отлично работает.

Мы также можем ограничить использование __builtins__, установив для него значение None.

from datetime import *

# Установить параметр globals в None
globals_param = {'__builtins__': None}

# Установить параметр locals на прием только print(), slice(), sum() и dir()
locals_param = {'print': print, 'dir': dir, 'slice': slice, 'sum': sum}

# Директория разрешенных методов
exec('print(dir())', globals_param, locals_param)
exec('print(f"Sum of numbers: {sum([4, 6, 7])}")', globals_param, locals_param)
['dir', 'print', 'slice', 'sum']
Sum of numbers: 17

Мы ограничили использование __builtins__, поэтому не можем использовать встроенные методы и можем выполнить только методы print, sum, slice и dir внутри exec().

Заключение

Из этой статьи вы узнали, как использовать встроенную функцию Python exec() для выполнения кода из строкового ввода. Она позволяет запускать динамически генерируемый код Python. Мы разобрали:

  • Что такое функция exec() и как с ней работать
  • Выполнение кода Python из строкового ввода и исходных файлов Python
  • Использование параметров globals и locals.

Кроме того, мы рассмотрели несколько примеров, которые помогли вам лучше понять функцию exec().

Перевод статьи “How to Use exec() in Python — Everything You Need to Know”.

Прокрутить вверх