为什么PyCharm会警告可变的默认参数?我该如何解决这个问题?

154

我正在使用PyCharm(Python 3)编写一个Python函数,该函数接受一个字典作为参数,其中attachment = {}

def put_object(self, parent_object, connection_name, **data):
    ...

def put_wall_post(self, message, attachment={}, profile_id="me"):
    return self.put_object(profile_id, "feed", message=message, **attachment)
在IDE中,attachment={}以黄色标记。将鼠标移动到它上面会显示警告。

默认参数值是可变的

此检查会检测在参数的默认值中是否检测到了可变值,如列表或字典。

默认参数值仅在函数定义时计算一次,这意味着修改参数的默认值将影响到该函数后续所有的调用。

这是什么意思,我应该如何解决?

3
用户询问PyCharm的检查功能为何会给出编译器警告,而这种警告并非他/她所预期的。 - the_constant
4
用户的问题是“如何让这个警告消失?”我们都知道他/她的代码中可能存在错误,但是问题很明显,你试图去打探。 - the_constant
3
@Vincenzzzochi,你认为我在“刨根问底”吗?那么询问一个明确的问题怎么会是“刨根问底”呢?如果有人不想被问到关于他们提出的问题的问题,他们就不应该提出问题。在任何情况下,最好让提问者明确他们的问题,而不是做出假设,即使在这种情况下,我认为你的解释是有*合理性的。 - juanpa.arrivillaga
11
你在问是否有意这一点,你要么1:没有理解他们明显的问题(请重新阅读),要么2:要求他们自己回答问题,因为你宁愿觉得他们不理解一个基本的Python原则而让自己感觉高人一等。你对我的回应有#2的口气。我们生活在一个基本的隐含解释统治世界的时代,年轻人,你应该尝试一下。 - the_constant
1
@Vincenzzzochi 这个解释有开放性,或许你可以在 聊天室 里和 juanpa 友好地聊一下。 - Peter Wood
显示剩余3条评论
6个回答

169

如果您不更改“可变默认参数”,也不将其传递到可能被更改的任何位置,请忽略该消息,因为没有什么需要“修复”的。

在您的情况下,您展开(隐式复制)“可变默认参数” - 所以非常安全。

如果您想要“删除该警告消息”,您可以使用None作为默认值,并在其为None时将其设置为{}

def put_wall_post(self,message,attachment=None,profile_id="me"):
    if attachment is None:
        attachment = {}

    return self.put_object(profile_id,"feed",message = message,**attachment)

简单解释一下"什么意思":在Python中,有些类型是不可变的(如intstr等),而另一些类型是可变的(如dictsetlist等)。如果你想改变不可变对象,会创建另一个对象;但如果你改变可变对象,则对象仍然保持不变,只是其内容改变了。

棘手的部分是,类变量和默认参数是在函数加载时创建的(仅一次),这意味着对"可变默认参数"或"可变类变量"所做的任何更改都是永久性的:

def func(key, value, a={}):
    a[key] = value
    return a

>>> print(func('a', 10))  # that's expected
{'a': 10}
>>> print(func('b', 20))  # that could be unexpected
{'b': 20, 'a': 10}

PyCharm可能会显示此警告,因为意外犯错很容易(例如请参见Why do mutable default arguments remember mutations between function calls?以及所有相关的问题)。然而,如果您是故意这么做的(Good uses for mutable function argument default values?),那么该警告可能会让人感到烦恼。


这会影响在类创建时定义在 __init__ 中的变量吗?例如:ElementTree.Element(tag, attrib={}, **extra) - Stevoisiak
@StevenVascellaro 是的。然而,他们做的第一件事是copy。这样他们就可以使用副本,而不会冒着改变默认参数的风险。 - MSeifert
或者更简短的方式是使用 attachment = attachment or {} 代替 if attachment is None: attachment = {} - Georgii Oleinikov
@GeorgiiOleinikov 这两种方法之间存在一些微妙的差异,例如 is None 方法不会将 false-y 值静默地转换为空字典(例如如果有人传入 False)。我也会选择 or {} 方法,但是同时添加一些文档或类型提示,以便明确应该传入什么。请注意,我还在另一个答案中提出了这种方法(https://dev59.com/VlgR5IYBdhLWcg3wH6ds#41686973?noredirect=1#comment70571249_41686977),但在那里两者实际上是等价的。 - MSeifert

15

您可以使用None替换可变默认参数。然后在函数内部进行检查并分配默认值:

def put_wall_post(self, message, attachment=None, profile_id="me"):
    attachment = attachment if attachment else {}

    return self.put_object(profile_id, "feed", message=message, **attachment)
这是因为None会被视为False,所以我们将其赋值为空字典。
通常您可能希望明确检查None,因为其他值也可能被视为False,例如0''set()[]等都是False-y。如果您的默认值不是0而是5,那么您就不会想覆盖传递作为有效参数的0
def function(param=None):
    param = 5 if param is None else param

22
"attachment = attachment or {}" 的意思是如果变量 attachment 未定义或为假值,则将其赋值为空字典。 - MSeifert
3
@MSeifert,我从来没有觉得那个短路很易读,如果我使用它,我也不会期望其他人能理解我的代码。因为我来自C++背景,所以我期待布尔表达式产生true/false的结果。也许我需要训练自己不被它所排斥。(c: - Peter Wood
1
两个版本都有问题。如果参数是空字符串(或空列表),函数将用空字典替换它。这可能是有意的,也可能不是。 - Matthias
2
@Matthias 这个函数需要一个字典作为参数。如果你传递了其他类型的参数,那么你可能会遇到更大的问题。 - Peter Wood
2
@PeterWood:根据函数的不同,我可以通过传递其他内容来实现(关键字:_鸭子类型_)。但你是对的:在这种特殊情况下,使用字符串或列表将是错误的。 - Matthias
@Matthias更新了更通用的解释,谢谢。 - Peter Wood

13

这是解释器发出的警告,因为你的默认参数是可变的,如果你在原地修改它,可能会改变默认值,从而导致某些情况下产生意外结果。默认参数实际上只是一个指向你指定对象的引用,就像当你将列表别名到两个不同的标识符时一样,例如,

>>> a={}
>>> b=a
>>> b['foo']='bar'
>>> a
{'foo': 'bar'}

如果对象通过任何引用进行更改,无论是在函数调用期间、单独调用还是在函数外部,都会影响将来调用该函数的行为。如果您不希望函数的行为在运行时发生变化,这可能会导致错误。每次调用函数时,都会将相同的名称绑定到相同的对象上。(实际上,我不确定它是否每次都会经过整个名称绑定过程?我认为它只是获得另一个引用。)

(可能不想要的)行为

您可以通过声明以下内容并调用几次来查看其效果:

>>> def mutable_default_arg (something = {'foo':1}):
    something['foo'] += 1
    print (something)


>>> mutable_default_arg()
{'foo': 2}
>>> mutable_default_arg()
{'foo': 3}

等等,什么?是的,因为参数引用的对象在调用之间不会改变,更改其元素之一会更改默认值。如果使用不可变类型,则不必担心这一点,因为在标准情况下不应该更改不可变数据。我不知道这是否适用于用户定义的类,但这通常只需要用“None”来解决(那样做的原因是你只需要它作为占位符,无需更复杂的内容。为什么要在额外的RAM上花费更多呢?)

用胶带解决问题...

在你的情况下,正如另一个答案所指出的那样,你被隐式复制所拯救,但依赖于隐式行为从来都不是一个好主意,特别是意外的隐式行为,因为它可能会发生变化。这就是为什么我们说"显式优于隐式"。除此之外,隐式行为往往会隐藏正在发生的事情,这可能会导致您或另一个程序员删除胶带。

...具有简单(永久)的解决方案

您可以完全避免这个错误,并满足警告,如其他人建议的那样,使用不可变类型,例如None,在函数开头检查它,如果找到,立即替换它,然后开始您的函数:

def put_wall_post(self, message, attachment=None, profile_id="me"):
    if attachment is None:
        attachment = {}
    return self.put_object(profile_id, "feed", message=message, **attachment)

由于不可变类型强制你替换它们(从技术上讲,你是将一个新对象绑定到相同的名称上。在上面的例子中,当attachment重新绑定到新的空字典时,对None的引用被覆盖),而不是更新它们,因此你知道attachment除非在调用参数中指定,否则始终会以None开始,从而避免出现意外更改默认值的风险。

(顺便说一句,如果不确定一个对象是否与另一个对象相同,请使用is进行比较或检查id(object)。前者可以检查两个引用是否引用同一个对象,后者可以通过打印唯一标识符(通常是内存位置)来帮助调试。)


3
重新表述警告:每次调用此函数,如果使用默认值,则将使用相同的对象。只要您从未更改该对象,它是可变的事实就不会有影响。但是,如果您确实更改了它,则随后的调用将以修改后的值开始,这可能不是您想要的。
避免此问题的一种解决方案是将默认值设置为不可变类型,如None,并在使用默认值时将参数设置为 {}。
def put_wall_post(self,message,attachment=None,profile_id="me"):
    if attachment==None:
        attachment={}
    return self.put_object(profile_id,"feed",message = message,**attachment)

那么随后的调用将以修改后的值作为起点。您能举个例子吗? - Protect children of Donbas2014
1
当执行 def ptest( n, arg = {} ): print(n, arg); arg[n] = len(arg) ptest('a'); ptest('b'); ptest('c') 时,结果为 a {} b {'a':0} c {'a':0, 'b':1} - Scott Hunter

1

PyCharm警告默认参数是可变的,这可能看起来很晦涩,但它意味着使用默认参数创建的对象都共享对该默认参数的同一引用。

下面是一个演示问题的代码片段:

class foo:
def __init__(self, key, stuff: list = []):
    self.key = key
    self.stuff = stuff

def __str__(self):
    return f"{self.key} :: {self.stuff}"

def add_item(self, item):
    self.stuff.append(item)

如果我接下来创建了一些 foo 类的实例,并且没有为每个实例提供一个新的 stuff 列表,那么每个实例都将共享对同一个默认列表的引用!
a = foo('a')
b = foo('b')
print(a, b)
a.add_item(1)
a.add_item(2)
print(a, b)

>>> a :: [] b :: []
>>> a :: [1, 2] b :: [1, 2]

你可以看到我已经向stuff列表中添加了一些项目,但是当我第二次打印这两个实例时,b的stuff也有两个项目...事实上,它们是相同的两个项目!
解决这个问题的最好方法是稍微改变你的代码,并将None作为默认值提供,然后使用or运算符将其与构造函数内的一个新列表合并起来。
class foo:
def __init__(self, key, stuff: list = None):
    self.key = key
    self.stuff = stuff or []  # this will be a new list[]

现在,如果我们重复构建a和b,并且像之前一样向a添加内容,我们会得到一个不同的结果。
>>> a :: [] b :: []     # before adding stuff
>>> a :: [1, 2] b :: [] # after adding stuff to a,  b is still empty!

这是因为实例a和实例b不再共享对同一(默认)列表的引用,而是在初始化实例时使用新构建的列表。虽然Python隐藏了大部分指针和引用的丑陋细节,但它们仍然存在于幕后,有时我们仍然需要意识到它们的存在。顺便说一下,如果您将值(原始类型)作为默认值,它们就不会有这个问题,因为值本身被放置在实例中,而不是引用(例如stuff=1而不是stuff=[])。

0
  • 列表是可变的,并且在编译时使用 def 声明默认值会将可变列表分配给某个地址的变量

    def abc(a=[]):
        a.append(2)
        print(a)
    
    abc() #输出 [2]
    abc() #输出 [2, 2],因为它是可变的,所以更改了相同分配的函数声明处的列表,指向同一地址并在末尾添加
    abc([4]) #输出 [4, 2],因为在新地址处传递了新列表
    abc() #输出 [2, 2, 2],获取相同的分配列表并在末尾添加
    

     

  • 要纠正这个问题:

    def abc(a=None):
         if not a:
             a=[]
         a.append(2)
         print(a)
    

     

    • 每次都创建新列表,并且不引用旧列表作为值,始终为 null,因此将新列表分配给新地址

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