【问题标题】:Class constructors and keyword arguments - How does Python determine which one is unexpected?类构造函数和关键字参数 - Python 如何确定哪一个是意外的?
【发布时间】:2013-09-03 13:16:20
【问题描述】:

假设我定义了以下类:

class MyClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y

通常,可以通过以下方式之一实例化此类:

>>> MyClass(1,2)
<__main__.MyClass object at 0x8acbf8c>
>>> MyClass(1, y=2)
<__main__.MyClass object at 0x8acbeac>
>>> MyClass(x=1, y=2)
<__main__.MyClass object at 0x8acbf8c>
>>> MyClass(y=2, x=1)
<__main__.MyClass object at 0x8acbeac>

这很好,花花公子。

现在,我们尝试使用无效的关键字参数,看看会发生什么:

>>> MyClass(x=1, j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'j'

Python 正确地引发类型错误并抱怨 unexpected keyword argument 'j'

现在,我们可以尝试使用两个无效的关键字参数:

>>> MyClass(i=1,j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

请注意,两个关键字参数无效,但 Python 只抱怨其中一个,在这种情况下为 'i'

让我们颠倒无效关键字参数的顺序:

>>> MyClass(j=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

这很有趣。我更改了无效关键字参数的顺序,但 Python 仍然决定抱怨 'i' 而不是 'j'。所以 Python 显然不会简单地选择第一个无效的键来抱怨。

让我们尝试更多:

>>> MyClass(c=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(q=2, i=1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'

按字母顺序,我在i 之前尝试了一个字母,在i 之后尝试了一个字母,所以 Python 不会按字母顺序抱怨。

这里还有一些,这次是i 在第一位:

>>> MyClass(i=1, j=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(i=1, b=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'i'
>>> MyClass(i=1, a=2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() got an unexpected keyword argument 'a'

啊哈!我得到它抱怨'a' 而不是'i'

我的问题是,当给类构造函数提供了无效的关键字参数时,Python 如何确定该抱怨哪一个?

【问题讨论】:

  • 附带说明,类构造函数调用的工作方式与调用函数(或方法)完全相同,因此您可以稍微简化测试。
  • @abarnert 是吗?我得到:def my_func(x, y): pass,然后my_func(i=2, a=1) 抱怨'i',而MyClass(i=2, a=1) 抱怨'a'
  • 如@MartijnPieters 的回答中所述,行为未定义。因此,实际测试的结果不一定有用或适用。
  • 是的。使用dis 模块查看字节码,您会发现两者都使用CALL_FUNCTION。(另外请注意,由于python 不是静态类型的,解释器无法区分一个类/callable/function 直到 开始执行调用)。另请注意,__init__ 方法有一个 self 参数。如果你用def __init__(self, x): pass 定义一个类,那么它会再次抱怨i。似乎函数的参数数量很重要。

标签: python


【解决方案1】:

关键字参数存储在字典中,并且字典顺序(例如任意,基于散列算法、散列冲突和插入历史)适用。

对于您的第一个示例,包含 ij 键的字典导致 i 被列在首位:

>>> dict(j=2, i=1)
{'i': 1, 'j': 2}

请注意,{...} 字面量字典表示法从右到左插入键,而关键字解析从左到右插入关键字(这是 CPython 实现细节);因此在上面的例子中使用了dict() 构造函数。

当两个键散列到同一个槽时,这很重要,例如 ia

>>> dict(i=1, a=2)
{'a': 2, 'i': 1}
>>> {'i': 1, 'a': 2}
{'i': 1, 'a': 2}

字典输出顺序高度依赖于插入和删除历史具体的Python实现;例如,Python 3.3 引入了随机哈希种子以防止出现严重的denial of service vector,这意味着即使在 Python 进程之间,字典顺序也会完全不同。

【讨论】:

  • 我也是这么想的,但是比较一下MyClass(**{'i': 1, 'a': 2})MyClass(i=1, a=2)。第一个报告i,而第二个报告a。不应该是一样的吗?
  • 不保证字典的顺序。没有结果可以一概而论...
  • @Claudiu:dict 的迭代顺序是任意的。所以不,你可以永远预测这将是同一件事。如果您想知道为什么它在某些特定情况下完全不同,而不仅仅是“允许不同,而且通常是这样”,您必须深入研究解释器和 dict 对象的较低级别。
  • @abarnert:我想这就是我很好奇的地方。如果您以相同的方式在同一个解释器中定义相同的字典,那么顺序将是相同的 - 那里没有随机化。我想答案是关键字 dict 在 2 种不同的方法中构造不同
  • 如果您想查看CALL_FUNCTION 和相关操作码的相关CPython 源代码,请在ceval.c 中查找_call_function_var_kw(上面的链接指向3.3 分支)和ext_do_call 在同一个文件。基本上,名称和值由调用者先从左侧压入堆栈,然后CALL_FUNCTION 将它们弹出。
猜你喜欢
  • 2017-07-06
  • 2019-04-05
  • 1970-01-01
  • 2019-12-19
  • 2021-12-04
  • 2012-07-03
  • 1970-01-01
  • 2020-03-08
  • 1970-01-01
相关资源
最近更新 更多