Évaluer les expressions de manière dynamique – Real Python

By | mai 11, 2020

trouver un expert Python

Python eval () vous permet d'évaluer Python arbitraire expressions à partir d'une entrée basée sur une chaîne ou basée sur un code compilé. Cette fonction peut être utile lorsque vous essayez d'évaluer dynamiquement des expressions Python à partir de n'importe quelle entrée fournie sous forme de chaîne ou d'objet de code compilé.

Bien que Python eval () est un outil incroyablement utile, la fonction a des implications de sécurité importantes que vous devez considérer avant de l'utiliser. Dans ce didacticiel, vous apprendrez comment eval () fonctionne et comment l'utiliser en toute sécurité et efficacement dans vos programmes Python.

Dans ce didacticiel, vous apprendrez:

  • Comment Python eval () travaux
  • Comment utiliser eval () à évaluer dynamiquement entrée arbitraire basée sur une chaîne ou basée sur un code compilé
  • Comment eval () peut rendre votre code non sécurisé et comment minimiser les associés risques de sécurité

De plus, vous apprendrez à utiliser les eval () pour coder une application qui évalue de manière interactive les expressions mathématiques. Avec cet exemple, vous appliquerez tout ce que vous avez appris sur eval () à un problème du monde réel. Si vous souhaitez obtenir le code de cette application, vous pouvez cliquer sur la case ci-dessous:

Comprendre Python eval ()

Vous pouvez utiliser le Python intégré eval () pour évaluer dynamiquement des expressions à partir d'une entrée basée sur une chaîne ou basée sur un code compilé. Si vous passez une chaîne à eval (), puis la fonction l'analyse, le compile en bytecode et l'évalue en tant qu'expression Python. Mais si tu appelles eval () avec un objet de code compilé, la fonction effectue uniquement l'étape d'évaluation, ce qui est très pratique si vous appelez eval () plusieurs fois avec la même entrée.

La signature de Python eval () est défini comme suit:

eval(expression[[[[ globals[[[[ des locaux]])

La fonction prend un premier argument, appelé expression, qui contient l'expression que vous devez évaluer. eval () prend également deux arguments facultatifs:

  1. globals
  2. des locaux

Dans les trois sections suivantes, vous apprendrez quels sont ces arguments et comment eval () les utilise pour évaluer les expressions Python à la volée.

Le premier argument: expression

Le premier argument pour eval () est appelé expression. C’est un argument obligatoire qui basé sur une chaîne ou basé sur le code compilé entrée à la fonction. Quand vous appelez eval (), le contenu de expression est évalué comme une expression Python. Consultez les exemples suivants qui utilisent une entrée basée sur des chaînes:

>>>

>>> eval("2 ** 8")
256
>>> eval("1024 + 1024")
2048
>>> eval("somme([8, 16, 32]) ")
56
>>> X = 100
>>> eval("x * 2")
200

Quand vous appelez eval () avec une chaîne en argument, la fonction renvoie la valeur résultant de l'évaluation de la chaîne d'entrée. Par défaut, eval () a accès à des noms globaux comme X dans l'exemple ci-dessus.

Pour évaluer une chaîne expression, Python eval () exécute les étapes suivantes:

  1. Analyser expression
  2. Compiler le bytecode
  3. Évaluer comme une expression Python
  4. Revenir le résultat de l'évaluation

Le nom expression pour le premier argument eval () souligne que la fonction ne fonctionne qu'avec des expressions et non avec des instructions composées. La documentation Python définit expression comme suit:

expression

Un morceau de syntaxe qui peut être évalué à une certaine valeur. En d'autres termes, une expression est une accumulation d'éléments d'expression comme des littéraux, des noms, des accès aux attributs, des opérateurs ou des appels de fonction qui renvoient tous une valeur. Contrairement à de nombreux autres langages, toutes les constructions de langage ne sont pas des expressions. Il existe également des instructions qui ne peuvent pas être utilisées comme expressions, telles que tandis que. Les affectations sont également des instructions et non des expressions. (La source)

D'un autre côté, un Python déclaration a la définition suivante:

déclaration

Une instruction fait partie d'une suite (un «bloc» de code). Une instruction est soit une expression, soit l'une de plusieurs constructions avec un mot-clé, comme si, tandis que ou pour. (La source)

Si vous essayez de passer une instruction composée à eval (), vous obtiendrez un Erreur de syntaxe. Jetez un œil à l'exemple suivant dans lequel vous essayez d'exécuter un si instruction utilisant eval ():

>>>

>>> X = 100
>>> eval("si x: print (x)")
  Fichier "", ligne 1
    si X: impression(X)
    ^
Erreur de syntaxe: Syntaxe invalide

Si vous essayez d'évaluer une instruction composée à l'aide de Python eval (), vous obtiendrez un Erreur de syntaxe comme dans le traceback ci-dessus. C'est parce que eval () accepte uniquement les expressions. Toute autre déclaration, telle que si, pour, tandis que, importation, def, ou classe, générera une erreur.

Les opérations d'affectation ne sont pas autorisées avec eval () Soit:

>>>

>>> eval("pi = 3,1416")
  Fichier "", ligne 1
    pi = 3.1416
       ^
Erreur de syntaxe: Syntaxe invalide

Si vous essayez de passer une opération d'affectation en tant qu'argument à Python eval (), vous obtiendrez un Erreur de syntaxe. Les opérations d'affectation sont des instructions plutôt que des expressions, et les instructions ne sont pas autorisées avec eval ().

Vous obtiendrez également un Erreur de syntaxe chaque fois que l'analyseur ne comprend pas l'expression d'entrée. Jetez un œil à l'exemple suivant dans lequel vous essayez d'évaluer une expression qui viole la syntaxe Python:

>>>

>>> # Expression incomplète
>>> eval("5 + 7 *")
  Fichier "", ligne 1
    5 + 7 *
          ^
Erreur de syntaxe: EOF inattendu lors de l'analyse

Vous ne pouvez pas transmettre une expression à eval () qui viole la syntaxe Python. Dans l'exemple ci-dessus, vous essayez d'évaluer une expression incomplète ("5 + 7 *") et obtenez un Erreur de syntaxe car l'analyseur ne comprend pas la syntaxe de l'expression.

Vous pouvez également transmettre des objets de code compilés à Python eval (). Pour compiler le code que vous allez transmettre eval (), vous pouvez utiliser compiler(). Il s'agit d'une fonction intégrée qui peut compiler une chaîne d'entrée dans un objet de code ou un objet AST afin que vous puissiez l'évaluer avec eval ().

Les détails de l'utilisation compiler() dépassent le cadre de ce didacticiel, mais voici un aperçu de ses trois premiers arguments requis:

  1. la source contient le code source que vous souhaitez compiler. Cet argument accepte les chaînes normales, les chaînes d'octets et les objets AST.
  2. nom de fichier donne le fichier à partir duquel le code a été lu. Si vous allez utiliser une entrée basée sur une chaîne, la valeur de cet argument doit être "".
  3. mode spécifie le type de code compilé que vous souhaitez obtenir. Si vous souhaitez traiter le code compilé avec eval (), cet argument doit être défini sur "eval".

Vous pouvez utiliser compiler() pour fournir des objets de code à eval () au lieu de chaînes normales. Découvrez les exemples suivants:

>>>

>>> # Opérations arithmétiques
>>> code = compiler("5 + 4", "", "eval")
>>> eval(code)
9
>>> code = compiler("(5 + 7) * 2", "", "eval")
>>> eval(code)
24
>>> importation math
>>> # Volume d'une sphère
>>> code = compiler("4/3 * math.pi * math.pow (25, 3)", "", "eval")
>>> eval(code)
65449.84694978735

Si tu utilises compiler() pour compiler les expressions que vous allez transmettre à eval (), puis eval () passe par les étapes suivantes:

  1. Évaluer le code compilé
  2. Revenir le résultat de l'évaluation

Si vous appelez Python eval () à l'aide d'une entrée basée sur le code compilé, la fonction effectue ensuite l'étape d'évaluation et renvoie immédiatement le résultat. Cela peut être utile lorsque vous devez évaluer plusieurs fois la même expression. Dans ce cas, il est préférable de précompiler l'expression et de réutiliser le bytecode résultant lors des appels suivants à eval ().

Si vous compilez au préalable l'expression d'entrée, les appels successifs à eval () s'exécutera plus rapidement car vous ne répéterez pas analyse et compilation pas. Les répétitions inutiles peuvent entraîner des temps CPU élevés et une consommation de mémoire excessive si vous évaluez des expressions complexes.

Le deuxième argument: globals

Le deuxième argument de eval () est appelé globals. Il est facultatif et contient un dictionnaire qui fournit un espace de noms global pour eval (). Avec globals, tu peux dire eval () quels noms globaux utiliser lors de l'évaluation expression.

Les noms globaux sont tous les noms disponibles dans votre portée ou espace de noms global actuel. Vous pouvez y accéder de n'importe où dans votre code.

Tous les noms passés à globals dans un dictionnaire sera disponible pour eval () au moment de l'exécution. Consultez l'exemple suivant, qui montre comment utiliser un dictionnaire personnalisé pour fournir un espace de noms global à eval ():

>>>

>>> X = 100  # Une variable globale
>>> eval("x + 100", "X": X)
200
>>> y = 200  # Une autre variable globale
>>> eval("x + y", "X": X)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 1, dans 
NameError: le nom «y» n'est pas défini

Si vous fournissez un dictionnaire personnalisé au globals argument de eval (), puis eval () prendra uniquement ces noms comme globaux. Les noms globaux définis en dehors de ce dictionnaire personnalisé ne seront pas accessibles de l'intérieur eval (). C’est pourquoi Python soulève un NameError lorsque vous essayez d'accéder y dans le code ci-dessus: Le dictionnaire est passé à globals n'inclut pas y.

Vous pouvez insérer des noms dans globals en les répertoriant dans votre dictionnaire, puis ces noms seront disponibles pendant le processus d'évaluation. Par exemple, si vous insérez y dans globals, puis l'évaluation de "x + y" dans l'exemple ci-dessus fonctionnera comme prévu:

>>>

>>> eval("x + y", "X": X, "y": y)
300

Depuis que vous ajoutez y à votre convenance globals dictionnaire, l’évaluation des "x + y" réussit et vous obtenez la valeur de retour attendue de 300.

Vous pouvez également fournir des noms qui n'existent pas dans votre portée globale actuelle. Pour que cela fonctionne, vous devez fournir une valeur concrète pour chaque nom. eval () interprétera ces noms comme des noms globaux lors de l'exécution:

>>>

>>> eval("x + y + z", "X": X, "y": y, "z": 300)
600
>>> z
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
NameError: le nom 'z' n'est pas défini

Même si z n'est pas défini dans votre portée globale actuelle, la variable est présente dans globals avec une valeur de 300. Dans ce cas, eval () a accès à z comme s'il s'agissait d'une variable globale.

Le mécanisme derrière globals est assez flexible. Vous pouvez transmettre n'importe quelle variable visible (globale, locale ou non locale) à globals. Vous pouvez également transmettre des paires clé-valeur personnalisées comme "z": 300 dans l'exemple ci-dessus. eval () les traitera tous comme des variables globales.

Un point important concernant globals est que si vous lui fournissez un dictionnaire personnalisé qui ne contient pas de valeur pour la clé "__builtins__", puis une référence au dictionnaire de intégrés sera automatiquement inséré sous "__builtins__" avant expression obtient analysé. Cela garantit que eval () aura un accès complet à tous les noms intégrés de Python lors de l'évaluation expression.

Les exemples suivants montrent que même si vous fournissez un dictionnaire vide à globals, l'appel à eval () aura toujours accès aux noms intégrés de Python:

>>>

>>> eval("somme([2, 2, 2]) ", )
6
>>> eval("min ([1, 2, 3]) ", )
1
>>> eval("pow (10, 2)", )
100

Dans le code ci-dessus, vous fournissez un dictionnaire vide () à globals. Étant donné que ce dictionnaire ne contient pas de clé appelée "__builtins__", Python en insère automatiquement un avec une référence aux noms intégrés. Par ici, eval () a un accès complet à tous les noms intégrés de Python lors de son analyse expression.

Si vous appelez eval () sans passer un dictionnaire personnalisé à globals, l'argument sera par défaut le dictionnaire renvoyé par globaux () dans l'environnement où eval () est appelé:

>>>

>>> X = 100  # Une variable globale
>>> y = 200  # Une autre variable globale
>>> eval("x + y")  # Accéder aux deux variables globales
300

Quand vous appelez eval () sans fournir un globals , la fonction évalue expression en utilisant le dictionnaire renvoyé par globaux () comme son espace de noms global. Ainsi, dans l'exemple ci-dessus, vous pouvez accéder librement X et y car ce sont des variables globales incluses dans votre étendue globale actuelle.

Le troisième argument: des locaux

Python eval () prend un troisième argument appelé des locaux. Ceci est un autre argument facultatif qui contient un dictionnaire. Dans ce cas, le dictionnaire contient les variables qui eval () utilise comme noms locaux lors de l'évaluation expression.

Les noms locaux sont les noms (variables, fonctions, classes, etc.) que vous définissez dans une fonction donnée. Les noms locaux ne sont visibles que depuis l'intérieur de la fonction englobante. Vous définissez ces types de noms lorsque vous écrivez une fonction.

Depuis eval () est déjà écrit, vous ne pouvez pas ajouter de noms locaux à son code ou à sa portée locale. Cependant, vous pouvez transmettre un dictionnaire à des locaux, et eval () traitera ces noms comme des noms locaux:

>>>

>>> eval("x + 100", , "X": 100)
200
>>> eval("x + y", , "X": 100)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 1, dans 
NameError: le nom «y» n'est pas défini

Le deuxième dictionnaire du premier appel à eval () détient la variable X. Cette variable est interprétée par eval () comme variable locale. En d'autres termes, il est considéré comme une variable définie dans le corps de eval ().

Vous pouvez utiliser X dans expression, et eval () y aura accès. En revanche, si vous essayez d'utiliser y, vous obtiendrez un NameError parce que y n’est défini ni dans le globals espace de noms ou des locaux espace de noms.

Comme avec globals, vous pouvez transmettre n'importe quelle variable visible (globale, locale ou non locale) à des locaux. Vous pouvez également transmettre des paires clé-valeur personnalisées comme "x": 100 dans l'exemple ci-dessus. eval () les traitera tous comme des variables locales.

Notez que pour fournir un dictionnaire à des locaux, vous devez d'abord fournir un dictionnaire à globals. Il n'est pas possible d'utiliser des arguments de mots clés avec eval ():

>>>

>>> eval("x + 100", des locaux="X": 100)
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
Erreur-type: eval () ne prend aucun argument de mot clé

Si vous essayez d'utiliser des arguments de mots clés lors de l'appel eval (), vous obtiendrez un Erreur-type expliquant que eval () ne prend aucun argument de mot clé. Donc, vous devez fournir un globals dictionnaire avant de pouvoir fournir un des locaux dictionnaire.

Si vous ne transmettez pas de dictionnaire à des locaux, puis par défaut, le dictionnaire est passé à globals. Voici un exemple dans lequel vous passez un dictionnaire vide à globals et rien à des locaux:

>>>

>>> X = 100
>>> eval("x + 100", )
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 1, dans 
NameError: le nom 'x' n'est pas défini

Étant donné que vous ne fournissez pas de dictionnaire personnalisé à des locaux, l'argument par défaut est le dictionnaire passé à globals. Dans ce cas, eval () n'a pas accès à X parce que globals contient un dictionnaire vide.

La principale différence pratique entre globals et des locaux est que Python insérera automatiquement un "__builtins__" clé en globals si cette clé n'existe pas déjà. Cela se produit, que vous fournissiez ou non un dictionnaire personnalisé à globals. En revanche, si vous fournissez un dictionnaire personnalisé à des locaux, ce dictionnaire restera inchangé pendant l'exécution de eval ().

Évaluation des expressions avec Python eval ()

Vous pouvez utiliser Python eval () pour évaluer tout type d'expression Python mais pas les instructions Python telles que les instructions composées basées sur des mots clés ou les instructions d'affectation.

eval () peut être pratique lorsque vous avez besoin d'évaluer dynamiquement des expressions et que l'utilisation d'autres techniques ou outils Python augmenterait considérablement votre temps et vos efforts de développement. Dans cette section, vous apprendrez comment utiliser Python eval () pour évaluer les expressions booléennes, mathématiques et Python à usage général.

Expressions booléennes

Expressions booléennes sont des expressions Python qui renvoient une valeur de vérité (Vrai ou Faux) lorsque l'interprète les évalue. Ils sont couramment utilisés dans si pour vérifier si une condition est vraie ou fausse. Étant donné que les expressions booléennes ne sont pas des instructions composées, vous pouvez utiliser eval () pour les évaluer:

>>>

>>> X = 100
>>> y = 100
>>> eval("x! = y")
Faux
>>> eval("X < 200 and y > 100 ")
Faux
>>> eval("x est y")
Vrai
>>> eval("x in 50, 100, 150, 200")
Vrai

Vous pouvez utiliser eval () avec des expressions booléennes qui utilisent l'un des opérateurs Python suivants:

Dans tous les cas, la fonction renvoie la valeur de vérité de l'expression que vous évaluez.

Maintenant, vous pensez peut-être, pourquoi devrais-je utiliser eval () au lieu d'utiliser l'expression booléenne directement? Supposons que vous ayez besoin d'implémenter une instruction conditionnelle, mais que vous souhaitez modifier la condition à la volée:

>>>

>>> def func(une, b, état):
...     si eval(état):
...         revenir une + b
...     revenir une - b
...
>>> func(2, 4, "a> b")
-2
>>> func(2, 4, "a <b")
6
>>> func(2, 2, "a is b")
4

À l'intérieur func (), tu utilises eval () pour évaluer le état et retourner soit a + b ou un B selon le résultat de l'évaluation. Vous utilisez seulement quelques conditions différentes dans l'exemple ci-dessus, mais vous pouvez utiliser n'importe quel nombre d'autres à condition que vous respectiez les noms une et b que vous avez défini dans func ().

Imaginez maintenant comment vous implémenteriez quelque chose comme ça sans utiliser Python. eval (). Cela prendrait-il moins de code et de temps? En aucune façon!

Expressions mathématiques

Un cas d'utilisation courant de Python eval () est d'évaluer les expressions mathématiques à partir d'une entrée basée sur une chaîne. Par exemple, si vous souhaitez créer une calculatrice Python, vous pouvez utiliser eval () pour évaluer l'entrée de l'utilisateur et renvoyer le résultat des calculs.

Les exemples suivants montrent comment vous pouvez utiliser eval () de même que math pour effectuer des opérations mathématiques:

>>>

>>> # Opérations arithmétiques
>>> eval("5 + 7")
12
>>> eval("5 * 7")
35
>>> eval("5 ** 7")
78125
>>> eval("(5 + 7) / 2")
6.0
>>> importation math
>>> # Aire d'un cercle
>>> eval("math.pi * pow (25, 2)")
1963.4954084936207
>>> # Volume d'une sphère
>>> eval("4/3 * math.pi * math.pow (25, 3)")
65449.84694978735
>>> # Hypoténuse d'un triangle rectangle
>>> eval("math.sqrt (math.pow (10, 2) + math.pow (15, 2))")
18.027756377319946

Lorsque vous utilisez eval () pour évaluer les expressions mathématiques, vous pouvez passer des expressions de tout type ou de toute complexité. eval () les analysera, les évaluera et, si tout va bien, vous donnera le résultat attendu.

Expressions à usage général

Jusqu'à présent, vous avez appris à utiliser eval () avec des expressions booléennes et mathématiques. Cependant, vous pouvez utiliser eval () avec des expressions Python plus complexes qui intègrent des appels de fonction, la création d'objets, l'accès aux attributs, les compréhensions, etc.

Par exemple, vous pouvez appeler une fonction intégrée ou une fonction que vous avez importée avec un module standard ou tiers:

>>>

>>> # Exécutez la commande echo
>>> importation sous-processus
>>> eval("subprocess.getoutput ('echo Hello, World')")
'Bonjour le monde'
>>> # Lancez Firefox (si disponible)
>>> eval("subprocess.getoutput ('firefox')")
''

Dans cet exemple, vous utilisez Python eval () pour exécuter quelques commandes système. Comme vous pouvez l’imaginer, vous pouvez tonne de choses utiles avec cette fonctionnalité. cependant, eval () peut également vous exposer à de sérieux risques de sécurité, comme autoriser un utilisateur malveillant à exécuter des commandes système ou tout morceau de code arbitraire sur votre machine.

Dans la section suivante, vous examinerez les moyens de résoudre certains des risques de sécurité associés à eval ().

Minimiser les problèmes de sécurité de eval ()

Bien qu'il ait un nombre presque illimité d'utilisations, Python eval () a également important implications pour la sécurité. eval () est considéré comme non sécurisé car il vous permet (ou à vos utilisateurs) d'exécuter dynamiquement du code Python arbitraire.

Ceci est considéré comme une mauvaise pratique de programmation car le code que vous lisez (ou écrivez) est ne pas le code que vous exécuterez. Si vous prévoyez d'utiliser eval () pour évaluer les entrées d'un utilisateur ou de toute autre source externe, vous ne saurez pas avec certitude quel code va être exécuté. C'est un grave risque pour la sécurité si votre application s'exécute entre de mauvaises mains.

Pour cette raison, les bonnes pratiques de programmation recommandent généralement de ne pas utiliser eval (). Mais si vous choisissez d'utiliser la fonction de toute façon, la règle générale est de plus jamais l'utiliser avec entrée non fiable. La partie délicate de cette règle consiste à déterminer à quels types d'entrées vous pouvez faire confiance.

Comme exemple d'utilisation de eval () irresponsable peut rendre votre code non sécurisé, supposons que vous souhaitiez créer un service en ligne pour évaluer des expressions Python arbitraires. Votre utilisateur introduira des expressions, puis cliquez sur le Courir bouton. L’application obtiendra l’entrée de l’utilisateur et la transmettra à eval () pour évaluation.

Cette application s'exécutera sur votre serveur personnel. Oui, le même serveur où vous avez tous ces précieux fichiers. Si vous exécutez une boîte Linux et que le processus de l'application dispose des autorisations appropriées, un utilisateur malveillant pourrait introduire une chaîne dangereuse comme celle-ci:

"__import __ ('sous-processus'). getoutput ('rm –rf *')"

Le code ci-dessus supprimerait tous les fichiers du répertoire actuel de l'application. Ce serait affreux, non?

Lorsque l’entrée n’est pas fiable, il n’existe aucun moyen complètement efficace d’éviter les risques de sécurité eval (). Cependant, vous pouvez minimiser votre risque en limitant l'environnement d'exécution de eval (). Vous apprendrez quelques techniques pour le faire dans les sections suivantes.

Restreindre globals et des locaux

Vous pouvez restreindre l'environnement d'exécution de eval () en passant des dictionnaires personnalisés au globals et des locaux arguments. Par exemple, vous pouvez transmettre des dictionnaires vides aux deux arguments pour empêcher eval () d'accéder aux noms dans la portée ou l'espace de noms actuel de l'appelant:

>>>

>>> # Évitez l'accès aux noms dans la portée actuelle de l'appelant
>>> X = 100
>>> eval("x * 5", , )
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 1, dans 
NameError: le nom 'x' n'est pas défini

Si vous passez des dictionnaires vides () à globals et des locaux, puis eval () ne trouvera pas le nom X dans son espace de noms global ou son espace de noms local lors de l'évaluation de la chaîne "x * 5". Par conséquent, eval () jettera un NameError.

Malheureusement, restreindre la globals et des locaux de tels arguments n’éliminent pas tous les risques de sécurité associés à l’utilisation de Python eval (), car vous pouvez toujours accéder à tous les noms intégrés de Python.

Restriction de l'utilisation des noms intégrés

Comme vous l’avez vu précédemment, Python eval () insère automatiquement une référence au dictionnaire de intégrés dans globals avant d'analyser expression. Un utilisateur malveillant pourrait exploiter ce comportement en utilisant la fonction intégrée __importation__() pour accéder à la bibliothèque standard et à tout module tiers que vous avez installé sur votre système.

Les exemples suivants montrent que vous pouvez utiliser n'importe quelle fonction intégrée et n'importe quel module standard comme math ou sous-processus même après avoir restreint globals et des locaux:

>>>

>>> eval("somme([5, 5, 5]) ", , )
15
>>> eval("__import __ ('math'). sqrt (25)", , )
5,0
>>> eval("__import __ ('sous-processus'). getoutput ('echo Hello, World')", , )
'Bonjour le monde'

Même si vous limitez globals et des locaux en utilisant des dictionnaires vides, vous pouvez toujours utiliser n'importe quelle fonction intégrée comme vous l'avez fait avec somme() et __importation__() dans le code ci-dessus.

Vous pouvez utiliser __importation__() d'importer n'importe quel module standard ou tiers comme vous l'avez fait ci-dessus avec math et sous-processus. Avec cette technique, vous pouvez accéder à n'importe quelle fonction ou classe définie dans math, sous-processusou tout autre module. Imaginez maintenant ce qu'un utilisateur malveillant pourrait faire à votre système en utilisant sous-processus ou tout autre module puissant de la bibliothèque standard.

Pour minimiser ce risque, vous pouvez restreindre l'accès aux fonctions intégrées de Python en remplaçant le "__builtins__" entrer globals. Les bonnes pratiques recommandent d'utiliser un dictionnaire personnalisé contenant la paire clé-valeur "__builtins__": . Découvrez l'exemple suivant:

>>>

>>> eval("__import __ ('math'). sqrt (25)", "__builtins__": , )
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 1, dans 
NameError: le nom '__import__' n'est pas défini

Si vous passez un dictionnaire contenant la paire clé-valeur "__builtins__": à globals, puis eval () n'aura pas d'accès direct aux fonctions intégrées de Python comme __importation__(). Cependant, comme vous le verrez dans la section suivante, cette approche ne fait toujours pas eval () complètement sécurisé.

Restriction des noms dans l'entrée

Même si vous pouvez restreindre l'environnement d'exécution de Python eval () en utilisant la coutume globals et des locaux dictionnaires, la fonction sera toujours vulnérable à quelques astuces fantaisistes. Par exemple, vous pouvez accéder à la classe objet utilisant un type littéral comme "", [], , ou () avec quelques attributs spéciaux:

>>>

>>> "".__classe__.__base__

>>> [].__classe__.__base__

>>> .__classe__.__base__

>>> ().__classe__.__base__

Une fois que vous avez accès à objet, vous pouvez utiliser la méthode spéciale .__ sous-classes __ () pour accéder à toutes les classes qui héritent de objet. Voici comment ça fonctionne:

>>>

>>> pour sous_classe dans ().__classe__.__base__.__sous-classes__():
...     impression(sous_classe.__Nom__)
...
type
faiblesse
proxy faible appelable
faible proxy
int
...

Ce code imprimera une grande liste de classes sur votre écran. Certaines de ces classes sont assez puissantes et peuvent être extrêmement dangereuses entre de mauvaises mains. Cela ouvre un autre trou de sécurité important que vous ne pouvez pas fermer en restreignant simplement l'environnement d'exécution de eval ():

>>>

>>> chaîne_entrée = "" "[[[[
...                 c pour c dans () .__ classe __.__ base __.__ sous-classes __ ()
...                 si c .__ nom__ == "plage"
... ][0](dix)"""
>>> liste(eval(chaîne_entrée, "__builtins__": , ))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

La compréhension de la liste dans le code ci-dessus filtre les classes qui héritent de objet retourner un liste contenant la classe gamme. Le premier indice ([0]) renvoie la classe gamme. Une fois que vous avez accès à gamme, vous l'appelez pour générer un gamme objet. Ensuite, vous appelez liste() sur le gamme pour générer une liste de dix entiers.

Dans cet exemple, vous utilisez gamme pour illustrer une faille de sécurité eval (). Imaginez maintenant ce qu'un utilisateur malveillant pourrait faire si votre système exposait des classes comme subprocess.Popen.

Une solution possible à cette vulnérabilité consiste à restreindre l'utilisation de noms dans l'entrée, soit à un groupe de sûr noms ou à non noms du tout. Pour implémenter cette technique, vous devez suivre les étapes suivantes:

  1. Créer un dictionnaire contenant les noms que vous souhaitez utiliser avec eval ().
  2. Compiler la chaîne d'entrée à bytecode en utilisant compiler() en mode "eval".
  3. Vérifier .co_names sur l'objet bytecode pour vous assurer qu'il ne contient que des noms autorisés.
  4. Élever une NameError si l'utilisateur essaie d'entrer un nom qui n'est pas autorisé.

Jetez un œil à la fonction suivante dans laquelle vous implémentez toutes ces étapes:

>>>

>>> def eval_expression(chaîne_entrée):
...     # Étape 1
...     noms_autorisés = "somme": somme
...     # Étape 2
...     code = compiler(chaîne_entrée, "", "eval")
...     # Étape 3
...     pour Nom dans code.co_names:
...         si Nom ne pas dans noms_autorisés:
...             # Étape 4
...             élever NameError(F"Utilisation de Nom    interdit")
...     revenir eval(code, "__builtins__": , noms_autorisés)

Dans eval_expression (), vous implémentez toutes les étapes que vous avez vues précédemment. Cette fonction restreint les noms que vous pouvez utiliser avec eval () à seulement ces noms dans le dictionnaire noms_autorisés. Pour ce faire, la fonction utilise .co_names, qui est un attribut d'un objet de code qui renvoie un tuple contenant les noms dans l'objet de code.

Les exemples suivants montrent comment eval_expression () travaille en pratique:

>>>

>>> eval_expression("3 + 4 * 5 + 25/2")
35,5
>>> eval_expression("somme([1, 2, 3]) ")
6
>>> eval_expression("len ([1, 2, 3]) ")
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne dix, dans eval_expression
NameError: Utilisation de len non autorisée
>>> eval_expression("pow (10, 2)")
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne dix, dans eval_expression
NameError: Utilisation de pow non autorisée

Si vous appelez eval_expression () pour évaluer les opérations arithmétiques, ou si vous utilisez des expressions qui incluent des noms autorisés, vous obtiendrez le résultat attendu. Sinon, vous obtiendrez un NameError. Dans les exemples ci-dessus, le seul nom que vous avez autorisé est somme(). D'autres noms comme len () et pow () ne sont pas autorisés, donc la fonction soulève un NameError lorsque vous essayez de les utiliser.

Si vous souhaitez interdire complètement l'utilisation des noms, vous pouvez réécrire eval_expression () comme suit:

>>>

>>> def eval_expression(chaîne_entrée):
...     code = compiler(chaîne_entrée, "", "eval")
...     si code.co_names:
...         élever NameError(F"Utilisation de noms non autorisée")
...     revenir eval(code, "__builtins__": , )
...
>>> eval_expression("3 + 4 * 5 + 25/2")
35,5
>>> eval_expression("somme([1, 2, 3]) ")
Traceback (dernier appel le plus récent):
  Fichier "", ligne 1, dans 
  
  
  
  Fichier "", ligne 4, dans eval_expression
NameError: Utilisation de noms non autorisée

Maintenant, votre fonction ne permet pas tout noms dans la chaîne d'entrée. Pour ce faire, vous vérifiez les noms dans .co_names et élever un NameError si on en trouve. Sinon, vous évaluez chaîne_entrée et retourner le résultat de l'évaluation. Dans ce cas, vous utilisez un dictionnaire vide pour restreindre des locaux ainsi que.

Vous pouvez utiliser cette technique pour minimiser les problèmes de sécurité de eval () et renforcez votre armure contre les attaques malveillantes.

Restreindre l'entrée aux seuls littéraux

Un cas d'utilisation courant pour Python eval () est d'évaluer les chaînes qui contiennent des littéraux Python standard et de les transformer en objets concrets.

La bibliothèque standard fournit une fonction appelée literal_eval () qui peuvent aider à atteindre cet objectif. La fonction ne prend pas en charge les opérateurs, mais elle prend en charge les listes, les tuples, les nombres, les chaînes, etc.:

>>>

>>> de ast importation literal_eval
>>> # Évaluation des littéraux
>>> literal_eval("15.02")
15.02
>>> literal_eval("[1, 15]")
[1, 15]
>>> literal_eval("(1, 15)")
(1, 15)
>>> literal_eval("'un': 1, 'deux': 2")
'un': 1, 'deux': 2
>>> # Essayer d'évaluer une expression
>>> literal_eval("somme([1, 15]) + 5 + 8 * 2 ")
Traceback (dernier appel le plus récent):
  ...
ValueError: nœud ou chaîne mal formé: <_ast.BinOp object at 0x7faedecd7668>

Remarquerez que literal_eval () ne fonctionne qu'avec des littéraux de type standard. Il ne prend pas en charge l'utilisation d'opérateurs ou de noms. Si vous essayez d'alimenter une expression literal_eval (), vous obtiendrez un ValueError. Cette fonction peut également vous aider à minimiser les risques de sécurité associés à l’utilisation de Python eval ().

Utilisation de Python eval () Avec contribution()

Dans Python 3.x, la fonction intégrée contribution() lit l'entrée utilisateur sur la ligne de commande, la convertit en chaîne, supprime la nouvelle ligne de fin et renvoie le résultat à l'appelant. Depuis le résultat de contribution() est une chaîne, vous pouvez la nourrir eval () et l'évaluer comme une expression Python:

>>>

>>> eval(contribution("Entrez une expression mathématique:"))
Entrez une expression mathématique: 15 * 2
30
>>> eval(contribution("Entrez une expression mathématique:"))
Entrez une expression mathématique: 5 + 8
13

Vous pouvez envelopper Python eval () environ contribution() pour évaluer automatiquement l'entrée de l'utilisateur. Il s'agit d'un cas d'utilisation courant pour eval () car il émule le comportement de contribution() en Python 2.x, dans lequel contribution() évalue l'entrée de l'utilisateur en tant qu'expression Python et renvoie le résultat.

Ce comportement de contribution() dans Python 2.x a été modifié dans Python 3.x en raison de ses implications en matière de sécurité.

Construire un évaluateur d'expressions mathématiques

Jusqu'à présent, vous avez appris comment Python eval () fonctionne et comment l’utiliser dans la pratique. Vous avez également appris que eval () a des implications importantes pour la sécurité et qu’il est généralement considéré comme une bonne pratique d’éviter eval () dans votre code. Cependant, il existe certaines situations dans lesquelles Python eval () peut vous faire économiser beaucoup de temps et d'efforts.

Dans cette section, vous allez coder une application pour évaluer les expressions mathématiques à la volée. Si vous vouliez résoudre ce problème sans utiliser eval (), vous devez alors suivre les étapes suivantes:

  1. Analyser l'expression d'entrée.
  2. Changement les composants de l'expression en objets Python (nombres, opérateurs, fonctions, etc.).
  3. Combiner tout en une expression.
  4. Confirmer que l'expression est valide en Python.
  5. Évaluer l'expression finale et renvoyer le résultat.

Ce serait beaucoup de travail compte tenu de la grande variété d'expressions possibles que Python peut traiter et évaluer. Heureusement, vous pouvez utiliser eval () pour résoudre ce problème, et vous avez déjà appris plusieurs techniques pour réduire les risques de sécurité associés.

Vous pouvez obtenir le code source de l'application que vous allez créer dans cette section en cliquant sur la case ci-dessous:

Tout d'abord, lancez votre éditeur de code préféré. Créez un nouveau script Python appelé mathrepl.py, puis ajoutez le code suivant:

    1 importation math
    2 
    3 __version__ = "1.0"
    4 
    5 ALLOWED_NAMES = 
    6     k: v pour k, v dans math.__dict__.articles() si ne pas k.commence avec("__")
    7 
    8 
    9 PS1 = "mr >>"
dix 
11 BIENVENUE = F"""
12 MathREPL __version__, your Python math expressions evaluator!
13 Enter a valid math expression after the prompt "PS1".
14 Type "help" for more information.
15 Type "quit" or "exit" to exit.
16 """
17 
18 USAGE = F"""
19 Usage:
20 Build math expressions using numeric values and operators.
21 Use any of the following functions and constants:
22 
23 ', '.join(ALLOWED_NAMES.keys())
24 """

In this piece of code, you first import Python’s math module. This module will allow you to perform math operations using predefined functions and constants. The constant ALLOWED_NAMES holds a dictionary containing the non-special names in math. This way, you’ll be able to use them with eval().

You also define three more string constants. You’ll use them as the user interface to your script and you’ll print them to the screen as needed.

Now you’re ready to code the core functionality of your application. In this case, you want to code a function that receives math expressions as input and returns their result. To do this, you write a function called evaluate():

26 def evaluate(expression):
27     """Evaluate a math expression."""
28     # Compile the expression
29     code = compile(expression, "", "eval")
30 
31     # Validate allowed names
32     pour name dans code.co_names:
33         si name ne pas dans ALLOWED_NAMES:
34             élever NameError(F"The use of 'name' is not allowed")
35 
36     revenir eval(code, "__builtins__": , ALLOWED_NAMES)

Here’s how the function works:

  1. Dans line 26, you define evaluate(). This function takes the string expression as an argument and returns a float that represents the result of evaluating the string as a math expression.

  2. Dans line 29, you use compile() to turn the input string expression into compiled Python code. The compiling operation will raise a SyntaxError if the user enters an invalid expression.

  3. Dans line 32, you start a pour loop to inspect the names contained in expression and confirm that they can be used in the final expression. If the user provides a name that is not in the list of allowed names, then you raise a NameError.

  4. Dans line 36, you perform the actual evaluation of the math expression. Notice that you pass custom dictionaries to globals et locals as good practice recommends. ALLOWED_NAMES holds the functions and constants defined in math.

The use of custom values for the globals et locals parameters, along with the check of names in line 33, allows you to minimize the security risks associated with the use of eval().

Your math expression evaluator will be finished when you write its client code in main(). In this function, you’ll define the program’s main loop and close the cycle of reading and evaluating the expressions that your user enters in the command line.

For this example, the application will:

  1. Print a welcome message to the user
  2. Show a prompt ready to read the user’s input
  3. Provide options to get usage instructions and to terminate the application
  4. Lis the user’s math expression
  5. Evaluate the user’s math expression
  6. Print the result of the evaluation to the screen

Check out the following implementation of main():

38 def main():
39     """Main loop: Read and evaluate user's input."""
40     impression(BIENVENUE)
41     while True:
42         # Read user's input
43         try:
44             expression = input(F"PS1    ")
45         except (KeyboardInterrupt, EOFError):
46             élever SystemExit()
47 
48         # Handle special commands
49         si expression.lower() == "help":
50             impression(USAGE)
51             continuer
52         si expression.lower() dans "quit", "exit":
53             élever SystemExit()
54 
55         # Evaluate the expression and handle errors
56         try:
57             résultat = evaluate(expression)
58         except SyntaxError:
59             # If the user enters an invalid expression
60             impression("Invalid input expression syntax")
61             continuer
62         except (NameError, ValueError) comme err:
63             # If the user tries to use a name that isn't allowed
64             # or an invalid value for a given math function
65             impression(err)
66             continuer
67 
68         # Print the result if no error occurs
69         impression(F"The result is: result")
70 
71 si __name__ == "__main__":
72     main()

Inside main(), you first print the BIENVENUE message. Then you read the user’s input in a try statement to catch KeyboardInterrupt et EOFError. If either of these exceptions occur, then you terminate the application.

If the user enters the Aidez-moi option, then the application shows your USAGE guide. Likewise, if the user enters quit ou exit, then the application terminates.

Finally, you use evaluate() to evaluate the user’s math expression, and then you print the result to the screen. It’s important to note that a call to evaluate() can raise the following exceptions:

  • SyntaxError: This happens when the user enters an expression that doesn’t follow Python syntax.
  • NameError: This happens when the user tries to use a name (function, class, or attribute) that isn’t allowed.
  • ValueError: This happens when the user tries to use a value that isn’t allowed as an input to a given function in math.

Notice that in main(), you catch all of these exceptions and print messages to the user accordingly. This will allow the user to review the expression, fix the problem, and run the program again.

C'est ça! You’ve built a math expression evaluator in about seventy lines of code using Python’s eval(). To run the application, open your system’s command line and type the following command:

This command will launch the math expression evaluator’s command-line interface (CLI). You’ll see something like this on your screen:

MathREPL 1.0, your Python math expressions evaluator!
Enter a valid math expression after the prompt "mr>>".
Type "help" for more information.
Type "quit" or "exit" to exit.

mr>>

Once you’re there, you can enter and evaluate any math expression. For example, type the following expressions:

mr>> 25 * 2
The result is: 50
mr>> sqrt(25)
The result is: 5.0
mr>> pi
The result is: 3.141592653589793

If you enter a valid math expression, then the application evaluates it and prints the result to your screen. If there are any problems with your expressions, then the application will tell you:

mr>> 5 * (25 + 4
Invalid input expression syntax
mr>> sum([1, 2, 3, 4, 5])
The use of 'sum' is not allowed
mr>> sqrt(-15)
math domain error
mr>> factorial(-15)
factorial() not defined for negative values

In the first example, you miss the closing parentheses, so you get a message telling you that the syntax is incorrect. Then you call sum(), which isn’t allowed, and you get an explanatory error message. Finally, you call a math function with an invalid input value, and the application generates a message identifying the problem in your input.

There you have it—your math expressions evaluator is ready! Feel free to add some extra features. A few ideas to get you started include enlarging the dictionary of allowed names and adding more elaborate warning messages. Give it a shot and let us know in the comments how it goes.

Conclusion

You can use Python’s eval() to evaluate Python expressions from a string-based or code-based input. This built-in function can be useful when you’re trying to evaluate Python expressions on the fly and you want to avoid the hassle of creating your own expressions evaluator from scratch.

In this tutorial, you’ve learned how eval() works and how to use it safely and effectively to evaluate arbitrary Python expressions.

You’re now able to:

  • Use Python’s eval() to dynamically evaluate basic Python expressions
  • Run more complex statements like function calls, object creation, et attribute access en utilisant eval()
  • Minimize the security risks associated with the use of Python’s eval()

Additionally, you’ve coded an application that uses eval() to interactively evaluate math expressions using a command-line interface. You can download the application’s code by clicking on the link below: