关于python:为什么使用”eval”是一个坏习惯?

Why is using 'eval' a bad practice?

我正在使用下面的类轻松存储我的歌曲的数据。

1
2
3
4
5
6
7
8
9
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似乎被认为是一种不好的做法,使用起来不安全。如果是这样的话,有人能向我解释为什么,并向我展示一种更好的定义上述类的方法吗?


是的,使用eval是一种不好的做法。举几个原因:

  • 几乎总是有更好的方法
  • 非常危险和不安全
  • 使调试困难
  • 缓慢的
  • 在您的情况下,您可以使用setattr代替:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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经常被用在错误的地方。

    编辑2:

    看起来有些人不同意评估是"非常危险和不安全"的行动。对于这个特定的情况,这可能是正确的,但一般来说不是。这个问题是一般性的,我列出的理由对一般情况也是正确的。

    编辑3:重新排序点1和4


    使用eval很弱,不是一个明显的坏做法。

  • 它违反了"软件的基本原理"。您的源代码不是可执行文件的总和。除了你的资料来源之外,还有一些关于eval的论点,必须清楚地理解。因此,它是最后的手段。

  • 这通常是设计不周密的标志。动态源代码几乎没有一个好的理由,它是动态构建的。几乎任何事情都可以通过授权和其他OO设计技术完成。

  • 它导致代码片段的动态编译相对缓慢。使用更好的设计模式可以避免的开销。

  • 作为脚注,在神经错乱的反社会者的手中,这可能行不通。然而,当面对神经错乱的反社会用户或管理员时,最好不要首先给他们解释python。在真正邪恶的手中,Python可以承担责任;eval根本不会增加风险。


    在这种情况下,是的。而不是

    1
    exec 'self.Foo=val'

    您应该使用内置函数setattr

    1
    setattr(self, 'Foo', val)

    是的,它是:

    使用python进行黑客攻击:

    1
    2
    3
    4
    5
    >>> eval(input())
    "__import__('os').listdir('.')"
    ...........
    ...........   #dir listing
    ...........

    下面的代码将列出在Windows计算机上运行的所有任务。

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

    在Linux:

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

    值得注意的是,对于所讨论的具体问题,使用eval有几种替代方法:

    如前所述,最简单的方法是使用setattr

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

    一种不太明显的方法是直接更新对象的__dict__对象。如果您只想初始化None的属性,那么这就不那么简单了。但是考虑一下:

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

    这允许您将关键字参数传递给构造函数,例如:

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

    它还允许您更明确地使用locals(),例如:

    1
    s = Song(**locals())

    …如果您真的想将None分配给在locals()中找到其名称的属性:

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

    为对象提供属性列表的默认值的另一种方法是定义类的__getattr__方法:

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

    当未以正常方式找到命名属性时,将调用此方法。这种方法比简单地在构造函数中设置属性或更新__dict__要简单一些,但它的优点是,除非存在属性,否则实际上不会创建属性,这可以大大减少类的内存使用。

    这一切的要点是:一般来说,有很多原因可以避免eval—执行不受控制的代码的安全问题,不能调试的代码的实际问题,等等。但更重要的原因是,一般来说,你不需要使用它。Python向程序员公开了它的许多内部机制,以至于几乎不需要编写编写代码的代码。


    其他用户指出了如何更改代码,使其不依赖于eval;我将为使用eval提供一个合法的用例,这个用例甚至可以在cpython中找到:测试。

    下面是我在test_unary.py中发现的一个例子,其中一个关于(+|-|~)b'a'是否引发TypeError的测试:

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

    这里的用法显然不是一个坏的实践;您定义输入并只观察行为。eval便于测试。

    看看在cpython-git存储库上执行的对eval的搜索;大量使用eval测试。


    当使用eval()来处理用户提供的输入时,您可以让用户通过如下方式进入repl:

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

    您可以摆脱它,但通常情况下,您不希望在应用程序中使用向量来执行任意代码。


    除了@nadia alramli answer之外,由于我对python不太熟悉,并且很想了解使用eval会如何影响计时,我尝试了一个小程序,下面是观察结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #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