Чому використання 'eval' є поганою практикою?


138

Я використовую наступний клас, щоб легко зберігати дані своїх пісень.

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            exec 'self.%s=None'%(att.lower()) in locals()
    def setDetail(self, key, val):
        if key in self.attsToStore:
            exec 'self.%s=val'%(key.lower()) in locals()

Я відчуваю, що це просто набагато розширніше, ніж виписувати if/elseблок. Однак, evalздається, це вважається поганою практикою та небезпечною у використанні. Якщо так, чи може хто-небудь пояснити мені, чому, і показати мені кращий спосіб визначення вищевказаного класу?


40
як ти дізнався exec/evalі досі не знав setattr?
u0b34a0f6ae

3
Я вважаю, що це було зі статті, в якій порівнювали python та lisp, ніж я дізнався про eval.
Ніквін

Відповіді:


194

Так, використання eval - це погана практика. Просто назвіть кілька причин:

  1. Майже завжди є кращий спосіб зробити це
  2. Дуже небезпечно і небезпечно
  3. Складає налагодження складним
  4. Повільно

У вашому випадку ви можете використовувати setattr замість цього:

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            setattr(self, att.lower(), None)
    def setDetail(self, key, val):
        if key in self.attsToStore:
            setattr(self, key.lower(), val)

Редагувати:

Є деякі випадки, коли вам доведеться використовувати eval або exec. Але вони рідкісні. Використання eval у вашому випадку - це погана практика напевно. Я наголошую на поганій практиці, оскільки eval та exec часто використовуються в неправильному місці.

EDIT 2:

Схоже, деякі не погоджуються з тим, що eval є "дуже небезпечним та незахищеним" у випадку з ОП. Це може бути правдою для цього конкретного випадку, але не загалом. Питання було загальним, і причини, які я перерахував, вірні і для загального випадку.

EDIT 3: упорядковані пункти 1 і 4


22
-1: "Дуже небезпечно і небезпечно" - помилково. Інші три є чітко зрозумілими. Перестановіть їх так, щоб 2 і 4 були першими двома. Це небезпечно лише в тому випадку, якщо вас оточують злі соціопати, які шукають способи підриву вашої заявки.
S.Lott

51
@ S.Lott, Небезпека - це дуже важлива причина уникати eval / exec взагалі. Багато додатків, таких як веб-сайти, повинні бути особливо обережні. Візьмемо приклад OP на веб-сайті, який очікує, що користувачі введуть назву пісні. Це неодмінно буде експлуатуватися рано чи пізно. Навіть невинний вклад типу: Давайте веселитися. викличе синтаксичну помилку і викриє вразливість.
Надія Алрамлі

17
@Nadia Alramli: Введення користувача і evalнічого спільного між собою. Додаток, який принципово неправильно розроблений, принципово неправильно розроблений. evalце не більше першопричини поганого дизайну, ніж поділ на нуль або спроба імпорту модуля, який, як відомо, не існує. evalне є небезпечним. Заявки небезпечні.
S.Lott

17
@jeffjose: Насправді це принципово погано / зло, тому що він розглядає непараматеризовані дані як код (саме тому існують XSS, ін'єкції SQL та пачки стека). @ S.Lott: "Це небезпечно лише в тому випадку, якщо ви оточені злими соціопатами, які шукають способів підривати вашу заявку". Класно, тому скажіть, що ви робите програму calcі додаєте номери, які вона виконує print(eval("{} + {}".format(n1, n2)))та виходить. Тепер ви поширюєте цю програму з деякою ОС. Потім хтось робить сценарій bash, який бере деякі номери з фондового сайту та додає їх за допомогою calc. бум?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

57
Я не впевнений, чому твердження Надії настільки суперечливе. Мені здається простим: eval - вектор для введення коду, і небезпечний тим, що більшість інших функцій Python не є. Це не означає, що ви не повинні використовувати його взагалі, але я думаю, ви повинні використовувати його розумно.
Оуен С.

32

Використання evalє слабкою, не очевидно поганою практикою.

  1. Це порушує "Основний принцип програмного забезпечення". Ваше джерело - це не загальна сума того, що виконується. Окрім вашого джерела, є аргументи eval, які слід чітко розуміти. З цієї причини це інструмент останньої інстанції.

  2. Зазвичай це ознака бездумного дизайну. Рідко є привід для динамічного вихідного коду, побудованого на ходу. З делегуванням та іншими методами проектування ОО можна зробити майже все.

  3. Це призводить до порівняно повільного на ході складання невеликих фрагментів коду. Накладні витрати, яких можна уникнути, використовуючи кращі моделі дизайну.

Як виноска, в руках зневірених соціопатів це може не вийти добре. Однак, стикаючись із зневіреними соціопатичними користувачами чи адміністраторами, краще не давати їм інтерпретувати Python в першу чергу. В руках справді злого, Пітон може нести відповідальність; evalзовсім не збільшує ризик.


7
@Owen S. Справа в цьому. Люди скажуть вам, що evalце якась "вразливість безпеки". Ніби сам Python - це не лише купа інтерпретованого джерела, який кожен міг змінити. Якщо ви зіткнулися з "eval - це безпека, то ви можете лише припустити, що це захисна діра в руках соціопатів. Звичайні програмісти просто модифікують існуюче джерело Python і безпосередньо викликають їх проблеми. Не опосередковано через evalмагію.
S.Lott

14
Ну, я можу вам точно сказати, чому я б сказав, що eval - це вразливість безпеки, і це стосується надійності рядка, який йому надається як вхідний. Якщо цей рядок, повністю або частково, надходить із зовнішнього світу, є можливість нападу сценарію на вашу програму, якщо ви не будете обережні. Але це жахливість зовнішнього зловмисника, а не користувача або адміністратора.
Оуен С.

6
@OwenS: "Якщо цей рядок повністю або частково походить із зовнішнього світу" Часто помилковий. Це не "обережна" річ. Це чорно-біле. Якщо текст надходить від користувача, йому ніколи не можна довіряти. Догляд насправді не є його частиною, він абсолютно недовірливий. В іншому випадку текст надходить від розробника, інсталятора чи адміністратора, і йому можна довіряти.
S.Lott

8
@OwenS: Немає можливих уникнути рядків ненадійного коду Python, який би зробив це надійним. Я згоден з більшістю сказаного, окрім "обережної" частини. Це дуже чітка відмінність. Код із зовнішнього світу недовірливий. AFAIK, жодна кількість витік або фільтрування не може очистити його. Якщо у вас є якась функція пропуску, яка зробить код прийнятним, будь ласка, поділіться ними. Я не думав, що таке можливо. Наприклад while True: pass, важко було б прибирати якусь втечу.
S.Lott

2
@OwenS .: "призначений як рядок, а не довільний код". Це не пов'язано. Це просто значення рядка, яке ви ніколи не проходите eval(), оскільки це рядок. Кодекс із "зовнішнього світу" не може бути укладений. Струни із зовнішнього світу - це лише струни. Мені незрозуміло, про що ти говориш. Можливо, вам слід надати більш повну публікацію в блозі та посилання на неї тут.
S.Lott


16

Так:

Хак за допомогою Python:

>>> eval(input())
"__import__('os').listdir('.')"
...........
...........   #dir listing
...........

У наведеному нижче коді будуть перераховані всі завдання, які виконуються на машині Windows.

>>> eval(input())
"__import__('subprocess').Popen(['tasklist'],stdout=__import__('subprocess').PIPE).communicate()[0]"

У Linux:

>>> eval(input())
"__import__('subprocess').Popen(['ps', 'aux'],stdout=__import__('subprocess').PIPE).communicate()[0]"

7

Варто зазначити, що для конкретної проблеми, про яку йдеться, існує кілька альтернатив використання eval:

Найпростішим, як зазначалося, є використання setattr:

def __init__(self):
    for name in attsToStore:
        setattr(self, name, None)

Менш очевидний підхід - це оновлення __dict__об'єкта безпосередньо. Якщо все, що ви хочете зробити, це ініціалізувати атрибути None, то це менш просто, ніж вище. Але врахуйте це:

def __init__(self, **kwargs):
    for name in self.attsToStore:
       self.__dict__[name] = kwargs.get(name, None)

Це дозволяє передавати аргументи ключового слова конструктору, наприклад:

s = Song(name='History', artist='The Verve')

Це також дозволяє зробити ваше використання locals()більш явним, наприклад:

s = Song(**locals())

... і, якщо ви дійсно хочете призначити Noneатрибути, імена яких знайдено в locals():

s = Song(**dict([(k, None) for k in locals().keys()]))

Іншим підходом до надання об'єкту стандартних значень для списку атрибутів є визначення __getattr__методу класу :

def __getattr__(self, name):
    if name in self.attsToStore:
        return None
    raise NameError, name

Цей метод викликається, коли названий атрибут не знайдеться нормально. Цей підхід дещо менш простий, ніж просто встановлення атрибутів у конструкторі або оновлення __dict__, але він має заслугу в тому, що насправді не створюється атрибут, якщо він не існує, що може значно зменшити використання пам'яті класу.

Суть у всьому цьому: Є багато причин, що взагалі слід уникати eval- проблема безпеки виконання коду, який ви не контролюєте, практична проблема коду, яку ви не можете налагодити, і т. Д. Але ще важливіша причина що це, як правило, не потрібно ним користуватися. Python виставляє програмісту настільки багато своїх внутрішніх механізмів, що вам дуже рідко потрібно писати код, який пише код.


1
Ще один спосіб, що, мабуть, більше (або менше) Pythonic: замість того, щоб __dict__безпосередньо використовувати об'єкт , дайте об'єкту фактичний словниковий об’єкт, або через спадкування, або як атрибут.
Джош Лі

1
"Менш очевидним підходом є оновлення прямого об'єкта dict безпосередньо" => Зауважте, що це обійде будь-який дескриптор (властивість чи інше) або __setattr__переопрацювання, що може призвести до несподіваних результатів. setattr()не має цієї проблеми.
bruno desthuilliers

5

Інші користувачі вказали, як ваш код можна змінити, щоб він не залежав eval; Я запропоную законний випадок використання eval, який можна знайти навіть у CPython: тестування .

Ось один приклад, який я знайшов, test_unary.pyде тест на те, чи (+|-|~)b'a'піднімає TypeError:

def test_bad_types(self):
    for op in '+', '-', '~':
        self.assertRaises(TypeError, eval, op + "b'a'")
        self.assertRaises(TypeError, eval, op + "'a'")

Використання тут очевидно не погана практика; Ви визначаєте вхід і просто спостерігаєте за поведінкою. evalзручно для тестування.

Подивіться на цей пошуку для eval, виконаного на репозиторії CPython; тестування з eval широко використовується.


2

Коли eval()використовується для обробки даних, що надаються користувачем, ви дозволяєте користувачеві переходити до REPL, надаючи щось подібне:

"__import__('code').InteractiveConsole(locals=globals()).interact()"

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


1

На додаток до відповіді @Nadia Alramli, оскільки я новачок у Python і хотів перевірити, як використання evalвплине на таймінги , я спробував невелику програму, і нижче були спостереження:

#Difference while using print() with eval() and w/o eval() to print an int = 0.528969s per 100000 evals()

from datetime import datetime
def strOfNos():
    s = []
    for x in range(100000):
        s.append(str(x))
    return s

strOfNos()
print(datetime.now())
for x in strOfNos():
    print(x) #print(eval(x))
print(datetime.now())

#when using eval(int)
#2018-10-29 12:36:08.206022
#2018-10-29 12:36:10.407911
#diff = 2.201889 s

#when using int only
#2018-10-29 12:37:50.022753
#2018-10-29 12:37:51.090045
#diff = 1.67292
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.