永远使用关键字参数有什么不好的理由吗?

72
在学习Python之前,我先从一些Objective-C / Cocoa书籍开始入手。据我回忆,大多数函数需要明确声明关键字参数。直到最近,我才忘记了这一点,只是在Python中使用位置参数。但最近,我遇到了一些错误,由于使用不当的位置而导致了这些错误——它们是一些难以察觉的小问题。
让我思考-一般来说,除非有特定需要使用非关键字参数,否则是否没有使用关键字参数的好理由?对于简单的函数,总是使用它们被认为是不好的风格吗?
我感觉随着我的50行程序经常扩展到500行或更多行,如果我习惯于始终使用关键字参数,那么代码将更容易阅读和维护。有什么理由不这样做呢?
更新:
我得到的普遍印象是这是一种风格偏好,有很多好的论据表明它们通常不应该用于非常简单的参数,但与良好的风格一致。在接受之前,我想澄清一下-是否存在任何特定的非风格问题-例如,重大的性能问题?

3
我看过一个建议,在使用__init__时应始终使用关键字参数。请参见http://fuhm.net/super-harmful/。尽管这不如您在此处提出的问题普遍,但这是支持您建议的一个论点。 - Joe White
我认为你会问为什么Python这样做,因为他是从Objective-C转到Python的。如果你走相反的路,你就会问为什么Objective-C要求我们在任何地方都使用关键字参数!(虽然这不是一个不好或非理性的选择; 它只是一种不同的设计。然而,整个语言都受到了影响,所以我建议你不要尝试将这种设计复制到不同的语言中) - brandizzi
11个回答

69
除了代码的清晰度和可读性外,没有理由不使用关键字参数。决定是否使用关键字应基于在阅读代码时,该关键字是否添加了额外有用的信息。
我遵循以下一般规则:
1. 如果从函数名难以推断参数的函数(名称),则通过关键字传递它(例如,我不希望在我的代码中使用text.splitlines(True))。 2. 如果很难推断参数的顺序,例如当你有太多参数或者独立的可选参数时,则通过关键字传递它(例如:funkyplot(x, y, None, None, None, None, None, None, 'red')看起来不太好)。 3. 如果参数的目的明显,就不要通过关键字传递前几个参数。你看,sin(2*pi)比sin(value=2*pi)更好,对于plot(x, y, z)也是如此。
在大多数情况下,稳定的必填参数将是位置参数,可选参数将是关键字参数。
此外,可能会存在性能差异,因为在每个实现中,关键字参数都会稍微慢一些。但考虑到这通常是过早地优化,而且结果并不重要,我认为这不是决策的关键因素。
更新:非样式方面的问题
关键字参数可以胜任所有位置参数的工作。如果您正在定义新的API,则除了可能存在的性能问题之外,没有技术上的劣势。但是,如果您将代码与现有元素结合使用,则可能会遇到一些小问题。
考虑以下情况:如果让函数接受关键字参数,那么这将成为您的接口的一部分。如果要使用具有类似签名但不同关键字的其他函数替换该函数,则可能会出现问题。
  • 你可能想在函数上使用一个装饰器或其他工具,假设你的函数需要一个位置参数。未绑定方法就是这样一种工具的例子,因为它们总是将第一个参数作为位置参数传递,并在读取后将其作为位置参数传递,所以即使定义中有一个参数self,cls.method(self=cls_instance)也不起作用。
  • 如果您设计良好的API并记录关键字参数的使用情况,尤其是如果您不设计应该与已存在的东西可互换的内容,那么这些都不会是真正的问题。


    12
    完美的回答。我想补充一下,Python 的方式类似于程序命令行选项的传递方式:必需的应该很少且位置固定,可选的应该作为一个选项传递(这在某种意义上是一个关键字)。 - brandizzi
    关键字参数真的会导致任何重大的性能损失吗?我想这只是多解析一两个额外的标记,但除此之外还有更深层次的原因吗? - Gravity
    1
    @Rosh:严格来说,“关键字参数可以做到位置参数所能做到的一切”这个说法并不正确。例如,元组解包在关键字参数中不起作用,而 def f((x, y)): … 是一个有效的定义。当然,它既不被广泛使用也不是推荐的风格。 - Denis Otkidach

    19

    如果你的考虑是为了提高函数调用的可读性,为什么不直接像正常情况下一样声明函数,例如:

    def test(x, y):
        print "x:", x
        print "y:", y
    

    只需明确声明函数名称,就可以通过调用函数来执行。

    test(y=4, x=1)
    

    这显然会给你输出:

    x: 1
    y: 4
    
    否则这个练习就没有意义了。
    这样避免了参数变成可选项并需要默认值(除非你希望它们是这样,如果是这样,可以直接使用关键字参数!:)),为你提供了所有命名参数的灵活性和更好的可读性,而不受顺序限制。

    这是乔治所问的完美答案。使args作为kwargs。在Salty Crane Blog中,他展示了如何在函数调用中混合使用kwargs和args。 - j_syk
    是的!这提供了大部分帖子所询问的好处,而不会产生更改函数参数规范所带来的所有缺点。 - wim

    10

    好的,我不会这么做有几个原因。

    如果所有的参数都是关键字参数,会增加代码的噪音,并且可能会使必需的参数和可选参数之间的清晰度降低。

    另外,如果我必须使用你的代码,我可能想杀了你!!(开玩笑),但每次都要输入所有参数的名称...并不好玩。


    3
    我看过一些Smalltalk代码(Obj-C就是基于它的)。如果写得好,读起来非常自然。你不会看到参数列表,而是看到一个完整的句子。你需要以不同的方式命名参数,但一个有Objective-C背景的人可能已经具备这种思维方式。 - Joe White
    1
    我能看到Noise参数。对于它们是否是可选的,您能举个例子吗?就我现在所想的来说,您不管如何都需要了解函数运作方式才能知道一个参数是否是可选的 - 如果参数命名得好的话,命名参数会加速理解,不是吗? - chris
    @Joe White,确实在SmallTalk/Objective-C中编写params的方式感觉很好,但这是因为这些语言整体设计时就考虑到了使用它,并且这些语言的开发人员非常注重以“正确的方式”来实现它。例如,请注意,ObjC params既基于关键字又基于位置,因此强制出现句子外观。问题在于,在Python中,“正确的方式”是不同的,而遵循Objective-C的模式会导致奇怪的代码。 - brandizzi
    @brandizzi 说得好。如果这个应用程序将由熟悉Objective-C的人编写和维护,那么它可能会很好地工作,但它不会是Pythonic的。 - Joe White

    8

    仅提供一个不同的论点,我认为在某些情况下,命名参数可能会提高可读性。例如,想象一下一个在您的系统中创建用户的函数:

    create_user("George", "Martin", "g.m@example.com", "payments@example.com", "1", "Radius Circle")
    

    从这个定义来看,即使所有这些值都是必需的,但并不清楚它们可能意味着什么。然而,使用命名参数就总是很明显:

    create_user(
        first_name="George",
        last_name="Martin",
        contact_email="g.m@example.com",
        billing_email="payments@example.com",
        street_number="1",
        street_name="Radius Circle")
    

    这就是我喜欢学习Cocoa/Obc的原因。第一次看到一个函数并不那么神秘,通常你可以很好地理解它的作用,我觉得这很棒。只是我经验不够,不知道大型项目的代码是否足够简洁,以至于在长期运行中不会因为输入参数的繁琐而成为麻烦。 - chris
    3
    我认为这取决于函数的作用。如果像 def validate_email(email) 这样的函数,当你查看函数实例时,很可能会明确它在做什么。 - Brent Newey
    1
    的确,在将文本传递给函数时,关键字参数非常有用。但是,如果参数是带有足够清晰名称的变量,则会变得非常嘈杂。考虑以下示例:create_user(first_name=first_name, last_name=last_name, contact_email=contact_email, ...) - André Caron
    这是一个很好的论点,当传递参数的含义不清楚时,代码很快变得难以阅读。例如,通常将布尔值传递给函数是一个坏主意,例如:do_some_stuff(True)。这里的True到底完成了什么?这会直接影响代码的可读性,因为现在你必须查找所述函数的定义才能找出答案。Python的命名参数提供了一种改善代码可读性的好方法,只要确保不过度使用,因为有一个转折点,它会产生相反的效果。 - 303

    5
    我记得在UNIX程序中读到了一个非常好的“选项”解释:“选项应该是可选的,一个程序应该能够在没有任何选项的情况下运行”。
    同样的原则也可以应用于Python中的关键字参数。这种类型的参数应该允许用户“自定义”函数调用,但函数应该能够在没有任何隐式关键字-值参数对的情况下被调用。

    那么,为什么Python 3添加了一种必需关键字参数的方式呢? - agf
    5
    def some_func(x, y, *, method, error='strict') -- methoderror只能通过关键字参数指定,而且method必须被指定,因为它没有默认值。 - Ethan Furman

    4
    当Python内置的compile()__import__()函数获得关键字参数支持时,同样的论点是为了清晰明了。似乎没有任何显著的性能损失,如果有的话。
    现在,如果你让你的函数接受关键字参数(而不是在调用函数时使用关键字传递位置参数,这是允许的),那么是很麻烦的。

    4
    有时候,事情应该保持简单,因为它们本来就很简单。
    如果你总是强制在每个函数调用中使用关键字参数,很快你的代码将变得难以阅读。

    3
    我认为当参数的含义明显时,使用关键字参数没有意义。

    2

    我能看到的一个缺点是,你需要为每个参数都设定一个合理的默认值,而在许多情况下可能没有任何合理的默认值(包括None)。然后你会觉得有责任为应该是位置参数的关键字参数写很多错误处理代码,以防这些参数未被指定。

    想象一下每次编写类似于下面这样的内容..

    def logarithm(x=None):
        if x is None:
            raise TypeError("You can't do log(None), sorry!")
    

    为什么需要检查x的值,当无效值会自动引发ValueError?在这种情况下引发“BadArgsException”是极其冗余的。 - cowbert
    @cowbert “ValueError” 不会“自动”引发。无论如何,关键是没有明智的默认值可用于“x” - 有时位置参数更好。 - wim
    在您发布的示例中,签名使用位置参数还是kwarg并不重要,因为使用位置参数,您可以轻松地将x分配为None并调用logarithm(x),您仍然必须处理NoneType。我不明白为什么有人会感到“有义务编写大量错误处理代码”,使用默认值,因为只有两个代码路径:返回实际值或引发异常(或某些内部运算符/函数将执行此操作)。默认情况下,您将引发异常,无需额外的代码... - cowbert
    但是使用关键字意味着参数是可选的。因此,这是有区别的!如果用户明确传递了None,那么这是他们自己传递了错误的参数的错。 - wim
    但是使用关键字表示参数是可选的。不是 - Géry Ogam
    @Maggyero 这个回答已经超过10年了,当时我可能在使用Python 2.7 :) - wim

    2
    关键字参数适用于具有无明确定序的长参数列表(您无法轻松想出一个清晰的方案来记住它们)的情况;然而,在许多情况下,使用它们会过度复杂化程序或使程序不够清晰。首先,有时候记住关键字的顺序比记住关键字参数的名称更容易,并且指定参数名称可能会使其不太清晰。以来自scipy.randomrandint为例。
    randint(low, high=None, size=None)    
    Return random integers x such that low <= x < high.
    If high is None, then 0 <= x < low.
    

    当想要生成 [0,10) 范围内的随机整数时,我认为写 randint(10) 比写 randint(low=10) 更清晰明了。如果你需要在 [0,10) 范围内生成一个包含 100 个数字的数组,你可能能够记住参数顺序并写出 randint(0, 10, 100)。但是,你可能不记得变量名称(例如第一个参数是 low、lower、start、min 还是 minimum),一旦你不得不查找参数名称,你可能就不会使用它们(因为你刚刚查找了正确的顺序)。
    此外,考虑可变参数函数(具有匿名变量数量的函数)。例如,你可能想写类似以下的内容:
    def square_sum(*params):
        sq_sum = 0
        for p in params:
            sq_sum += p*p
        return sq_sum
    

    可以应用一堆裸参数的函数(square_sum(1,2,3,4,5) # 结果为55)。当然,你也可以编写一个接受命名关键字可迭代对象的函数 def square_sum(params): 并像这样调用它 square_sum([1,2,3,4,5]) 但这可能不太直观,特别是当参数名称或其内容没有任何潜在混淆时。


    关于空间记忆(位置参数)与语言记忆(关键字参数)的有趣评论。 - Géry Ogam

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