web-dev-qa-db-fra.com

Pourquoi utiliser 'eval' est-il une mauvaise pratique?

J'utilise la classe suivante pour stocker facilement les données de mes chansons.

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()

Je pense que ceci est juste beaucoup plus extensible que d’écrire un bloc if/else. Cependant, eval semble être considéré comme une mauvaise pratique et une utilisation peu sûre. Si oui, quelqu'un peut-il m'expliquer pourquoi et me montrer un meilleur moyen de définir la classe ci-dessus?

105
Nikwin

Oui, utiliser eval est une mauvaise pratique. Juste pour nommer quelques raisons:

  1. Il y a presque toujours une meilleure façon de le faire
  2. Très dangereux et peu sûr
  3. Rend le débogage difficile
  4. Lent

Dans votre cas, vous pouvez utiliser setattr à la place:

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)

MODIFIER:

Dans certains cas, vous devez utiliser eval ou exec. Mais ils sont rares. Utiliser eval dans votre cas est certainement une mauvaise pratique. J'insiste sur les mauvaises pratiques, car eval et exec sont fréquemment utilisés au mauvais endroit.

EDIT 2:

Il semblerait que certains ne soient pas d’accord sur le fait que eval est «très dangereux et peu sûr» dans le cas des PO. Cela pourrait être vrai pour ce cas particulier, mais pas en général. La question était générale et les raisons que j'ai énumérées sont également valables pour le cas général.

EDIT 3: Réordonné points 1 et 4

164
Nadia Alramli

Utiliser eval est faible, pas clairement mauvais pratique.

  1. Cela viole le "principe fondamental du logiciel". Votre source n'est pas la somme totale de ce qui est exécutable. En plus de votre source, il existe les arguments de eval, qui doivent être clairement compris. Pour cette raison, c'est l'outil de dernier recours.

  2. C'est généralement un signe de conception irréfléchie. Il y a rarement une bonne raison pour le code source dynamique, construit à la volée. Presque tout peut être fait avec la délégation et d'autres techniques de conception OO.

  3. Cela conduit à une compilation à la volée relativement lente de petits morceaux de code. Une surcharge qui peut être évitée en utilisant de meilleurs modèles de conception.

En note de bas de page, entre les mains de sociopathes dérangés, cela risque de ne pas bien fonctionner. Cependant, lorsque vous êtes confronté à des utilisateurs ou à des administrateurs sociopathes dérangés, il est préférable de ne pas leur donner interprété Python en premier lieu. Dans les mains du vrai mal, Python peut constituer un handicap; eval n'augmente pas le risque du tout. 

27
S.Lott

Dans ce cas, oui. Au lieu de

exec 'self.Foo=val'

vous devriez utiliser la fonction builtinsetattr:

setattr(self, 'Foo', val)
22
Josh Lee

Oui, ça l'est:

Pirater en utilisant Python:

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

Le code ci-dessous répertorie toutes les tâches exécutées sur un ordinateur Windows.

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

Sous Linux:

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

Il convient de noter que pour le problème spécifique en question, il existe plusieurs alternatives à l'utilisation de eval:

Le plus simple, comme indiqué, utilise setattr:

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

Une approche moins évidente consiste à mettre à jour directement l'objet __dict__ de l'objet. Si tout ce que vous voulez faire, c'est initialiser les attributs à None, c'est moins simple que ce qui est décrit ci-dessus. Mais considérez ceci:

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

Cela vous permet de transmettre des arguments de mots clés au constructeur, par exemple:

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

Cela vous permet également de rendre votre utilisation de locals() plus explicite, par exemple:

s = Song(**locals())

... et si vous voulez vraiment affecter None aux attributs dont les noms se trouvent dans locals():

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

Une autre approche pour fournir à un objet des valeurs par défaut pour une liste d'attributs consiste à définir la méthode __getattr__ de la classe:

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

Cette méthode est appelée lorsque l'attribut nommé n'est pas trouvé normalement. Cette approche est un peu moins simple que de simplement définir les attributs dans le constructeur ou de mettre à jour le __dict__, mais elle a le mérite de ne pas créer l'attribut s'il n'existe pas, ce qui peut considérablement réduire l'utilisation de la mémoire par la classe.

L'intérêt de tout ceci: il y a beaucoup de raisons, en général, d'éviter eval - le problème de sécurité lié à l'exécution de code que vous ne contrôlez pas, le problème pratique de code que vous ne pouvez pas déboguer, etc. La raison est que généralement, vous n'avez pas besoin de l'utiliser. Python expose tellement de ses mécanismes internes au programmeur qu'il est rarement nécessaire d'écrire du code qui écrit du code.

6
Robert Rossney

D'autres utilisateurs ont souligné comment votre code peut être modifié pour ne pas dépendre de eval; Je vais vous proposer un cas d'utilisation légitime pour utiliser eval, qui se trouve même dans CPython: testing.

Voici un exemple que j'ai trouvé dans test_unary.py où un test permettant de savoir si (+|-|~)b'a' génère un TypeError:

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

L'utilisation n'est clairement pas une mauvaise pratique ici; vous définissez l'entrée et observez simplement le comportement. eval est pratique pour les tests.

Jetez un coup d'œil à cette recherche pour eval, effectuée sur le référentiel CPython git; les tests avec eval sont très utilisés.

5

Lorsque eval() est utilisé pour traiter une entrée fournie par l'utilisateur, vous autorisez l'utilisateur à Drop-to-REPL en fournissant quelque chose comme ceci:

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

Vous pouvez vous en tirer, mais normalement vous ne voulez pas de vecteurs pour exécution de code arbitraire dans vos applications.

2
moooeeeep

En plus de @Nadia Alramli, étant donné que je suis nouveau sur Python et désireux de vérifier comment utiliser eval affectera les timings , j’ai essayé un petit programme dont voici les observations: 

#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
0
Lokeshwar Tailor