参数验证,Python中的最佳实践

10

让我们以API为例

def get_abs_directory(self, path):
    if os.path.isdir(path):
       return path
    else:
       return os.path.split(os.path.abspath(path))[0]

我的问题是,Python中验证参数的方式是什么?我应该忽略任何类型的验证(我观察到所有的Python代码都没有进行任何验证)。

  1. 我应该检查"path"是否为空且不为null吗?
  2. 我应该始终检查"path"的类型是否为字符串吗?
  3. 通常情况下,我应该检查参数的类型吗?(我想不用,因为Python是动态类型语言)

这个问题并不特定于文件IO,只是使用文件IO作为一个例子。

4个回答

16

正如文档所述,在此处,Python遵循一种EAFP方法。这意味着我们通常使用更多的trycatch块,而不是尝试验证参数。让我演示一下:

import os


def get_abs_directory(path):
    try:
        if os.path.isdir(path):
            return path
        else:
            return os.path.split(os.path.abspath(path))[0]
    except TypeError:
        print "You inserted the wrong type!"


if __name__ == '__main__':
    get_abs_directory(1)  # Using an int instead of a string, which is caught by TypeError

不过,你可能希望以LBYL (Look Before You Leap)方式编写代码,这将类似于:

import os


def get_abs_directory(path):

    if not isinstance(path, str):
        print "You gave us the wrong type, you big meany!"
        return None

    if os.path.isdir(path):
        return path
    else:
        return os.path.split(os.path.abspath(path))[0]

if __name__ == '__main__':
    get_abs_directory(1)

谢谢您的回答,不过我的问题更加通用。假设我的API只是返回一个数字的平方(square())。现在的问题是,我应该在方法中进行类型验证吗? - Bhushan
3
不,只需让它“尝试”进行计算。如果有什么失败了,异常会自动传递给调用者。除非是实际的用户输入,否则请假定人们正确使用你的API。 - poke
2
@Bhushan 我想当我谈到 EAFP 时已经回答了你的问题。这意味着你必须_尝试_一些东西,如果失败了,你就要处理它。这是 Python 编程的操作方式。因为,如果你考虑一下,通常不会得到错误的输入,所以每次调用时都尝试检查它是浪费资源的。 - Games Brainiac
非常感谢大家,这帮助我们得出了一个结论。 - Bhushan
我认为 EAFP 方法唯一的问题在于当异常被抛出时,它可能很难进行分类,因为异常是在较晚的阶段抛出的。而且它并不总是抛出 TypeError,这使得捕获异常变得困难。 - jay1648

7

EAFP 是 Python 中类似于此类情况的事实标准,同时,如果您喜欢,完全可以一路遵循LBYL

然而,在适用 EAFP 时有一些保留:

  • 当代码仍能够处理异常场景、在某一点中断或启用调用者验证可能出现的错误时,最好只遵循EAFP原则。

  • 当使用EAFP会导致静默错误时,显式检查/验证(LBYL)可能更好。

关于这一点,有一个 Python 模块parameters-validation,可在需要时简化函数参数验证:

@validate_parameters
def register(
    token: strongly_typed(AuthToken),
    name: non_blank(str),
    age: non_negative(int),
    nickname: no_whitespaces(non_empty(str)),
    bio: str,
):
    # do register

声明:我是该项目的维护者。


4
即使这个问题已经有答案了,但是评论区写不下这么长,所以我会再回答一遍。
通常来说,类型检查有两个目的:确保函数能够正确完成,以及避免因为错误输出导致难以调试的故障。对于第一个问题,答案总是合适的——EAFP是通常的方法,你不需要担心错误的输入。对于第二个问题,答案取决于你的正常使用情况,并且你需要担心错误的输入和错误。当坏输入总是生成异常时(其中“坏输入”可以限制在应用程序期望产生的坏输入类型上),EAFP仍然是合适的(而且更容易,更易于调试)。但是,如果坏输入可能会创建有效的输出,则LYBL可能会使您以后的生活更加轻松。
例如:假设您调用了square(),将该值放入字典中,然后(更)稍后从字典中提取该值并将其用作索引。索引必须是整数。
square(2) == 4,是有效的整数,因此是正确的。square('a')将始终失败,因为'a'*'a'是无效的,并且将始终抛出异常。如果这些是唯一的两种可能性,那么您可以安全地使用EAFP。如果您确实得到了错误的数据,它将引发异常、生成回溯,并且您可以通过pdb重新启动并获得有关问题的良好指示。
然而……假设您的应用程序使用一些FP。如果您意外调用square(1.43)(假设您有一个错误!当然不是正常操作),则这将返回一个有效值——大约为2.0449。您不会在此处收到异常,因此您的应用程序将快乐地接受这个2.0449并为您将其放入字典中。更久以后,你的应用程序会从字典中取回这个值,将它用作列表的索引,最终崩溃。您将收到一个回溯,您将通过pdb重新启动,并意识到这对您毫无帮助,因为该值早已计算出来,您不再具有输入,也不知道该数据是如何到达那里的。这些是不好调试的情况。
在这种情况下,您可以使用断言(LYBL的特殊形式)将此类bug的检测移动到较早的位置,或者您可以明确地进行检测。如果您从未发现调用该函数的错误,则任何一种方法都可以正常工作。但是,如果您遇到了这样的问题……那么您真的会很高兴在失败之前人为地检查输入,而不是自然地在应用程序的某个随机后期检查它。

1

这段代码会“捕获”错误,正如这个测试代码所展示的一样,如果传入None,则会引发异常。

import os.path
import os


class pathetic(unittest.TestCase):
    def setUp(self):
        if (not(os.path.exists("ABC"))):
            os.mkdir("ABC")
        else:
            self.assert_(False, "ABC exists, can't make test fixture")

    def tearDown(self):
        if (os.path.exists("ABC")):
            os.rmdir("ABC")

    def test1(self):
        mycwd = os.path.split(os.path.abspath(os.getcwd()))[0]
        self.assertEquals("/", self.get_abs_directory("/abc"))
        self.assertEquals(mycwd, self.get_abs_directory(""))
        self.assertEquals("/ABC", self.get_abs_directory("/ABC/DEF"))
        try:
            self.get_abs_directory(None)
            self.assert_(False, "should raise exception")
        except TypeError:
            self.assert_(True, "woo hoo, exception")

    def get_abs_directory(self, path):
        if os.path.isdir(path):
            return path
        else:
            return os.path.split(os.path.abspath(path))[0]

if __name__ == '__main__':
    unittest.main()

请看我的回复,我正在寻找有关处理方法参数的Pythonic方式的准则。 - Bhushan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接