【问题标题】:Warning about mutable default argument in PyCharm关于 PyCharm 中可变默认参数的警告
【发布时间】:2017-05-31 21:47:16
【问题描述】:

我正在使用 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={} 是黄色的。将鼠标移到它上面会显示一个警告。

默认参数值是可变的

此检查检测何时作为列表或字典的可变值 在参数的默认值中检测到。

默认参数值仅在函数定义时计算一次 time,表示修改参数的默认值 将影响该函数的所有后续调用。

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

【问题讨论】:

  • @juanpa.arrivillaga 用户询问 PyCharm 的检查是否给了他/她一个他/她没有预料到的编译器警告。
  • @Vincenzzzochi 是的,但那又如何呢?请问是什么意思?一个不可变的替代方案?如何习惯性地拥有在后续调用中不会保留的可变默认参数?问题不够详细。
  • @juanpa.arrivillaga 用户的问题是“我如何让这个警告消失?”我们都知道他/她的代码中可能存在错误,但提出的问题很明显,您正试图窥探。
  • @Vincenzzzochi 你认为我在“窥探”吗?问一个格式正确的问题是如何“窥探”的?如果有人不想就他们的问题提出问题,他们就不应该提出问题。无论如何,最好让 OP 澄清他们的问题到底是什么,而不是做出假设,即使在这种情况下,我相信你有一个合理的解释。
  • @juanpa.arrivillaga 你在窥探它是否是故意的。你要么 1:不理解他们明显的问题(重读),要么 2:要求他们为自己的问题提供答案,因为你宁愿对他们不理解你理解的基本 python 原理感到高傲和强大。你对我的回应是#2的语气。我们生活在一个基本的隐含解释为王的世界里,我的孩子,你应该在某个时候试一试

标签: python pycharm warnings


【解决方案1】:

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

在您的情况下,您解包(执行隐式复制)“可变默认参数” - 所以您是安全的。

如果您想“删除该警告消息”,您可以使用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 可能会显示此警告,因为它很容易意外出错(例如,请参阅 “Least Astonishment” and the Mutable Default Argument 和所有链接的问题)。但是,如果您是故意这样做的 (Good uses for mutable function argument default values?),警告可能会很烦人。

【讨论】:

  • 这会影响类创建时__init__ 中定义的变量吗?例如:ElementTree.Element(tag, attrib={}, **extra)
  • @StevenVascellaro 是的。然而他们做的第一件事是copying it。这样他们就可以使用副本,并且不会冒险改变默认参数。
  • 或更短的类型 attachment = attachment or {} 而不是 if attachment is None: attachment = {}
  • @GeorgiiOleinikov 这两者之间有一些细微的差别,例如is None 方法不会默默地将 false-y 值转换为空字典(例如,如果有人传入 False)。我也会采用or {} 方法,但添加一些文档或类型提示,以便清楚应该传入什么。请注意,我还在另一个答案中提出了这种方法(@987654325 @) - 但实际上两者是等价的。
【解决方案2】:

您可以用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

【讨论】:

  • 或更短的attachment = attachment or {}
  • @MSeifert 我从来没有发现短路非常可读,如果我使用它,我也不希望其他人理解我的代码。我认为来自 C++ 背景,我希望布尔表达式产生真/假。也许我需要训练自己不要被它排斥(c:
  • 两个版本都有问题。如果参数是空字符串(或空列表),该函数将用空字典替换它。可能或可能不是有意的。
  • @Matthias 该函数需要一个字典。如果您要传递其他内容,则会遇到更大的问题。
  • @PeterWood:根据功能,我可能可以传递其他内容(关键字:duck typing)。但你是对的:在这种特殊情况下,使用字符串或列表是错误的。
【解决方案3】:

这是来自解释器的警告,因为您的默认参数是可变的,如果您就地修改它,您最终可能会更改默认值,这在某些情况下可能会导致意外结果。默认参数实际上只是对您指示的对象的引用,就像您将列表别名为两个不同的标识符时一样,例如

>>> 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}

等等,什么?是的,因为参数引用的对象在调用之间不会改变,改变它的元素之一会改变默认值。如果您使用不可变类型,则不必担心这一点,因为在标准情况下,更改不可变数据是不可能的。我不知道这是否适用于用户定义的类,但这就是为什么通常只用“无”来解决这个问题(你只需要它作为占位符,仅此而已。为什么将额外的 RAM 用于更多的东西复杂吗?)

胶带问题...

在您的情况下,正如另一个答案所指出的那样,您是由隐式副本保存的,但是依赖隐式行为,尤其是意外的隐式行为绝不是一个好主意,因为它可能会改变。这就是为什么我们说"explicit is better than implicit"。除此之外,隐式行为往往会隐藏正在发生的事情,这可能会导致您或其他程序员移除胶带。

...使用简单(永久)解决方案

您可以完全避免这个错误磁铁并满足警告,正如其他人所建议的那样,使用不可变类型(如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)

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

(顺便说一句,当不确定一个对象是否与另一个对象相同时,将它们与is比较或检查id(object)。前者可以检查两个引用是否引用同一个对象,后者可以通过打印对象的唯一标识符(通常是内存位置)对调试很有用)

【讨论】:

    【解决方案4】:

    改写警告:每次调用此函数,如果它使用默认值,将使用相同的对象。只要您从不更改该对象,它是可变的这一事实就无关紧要。但是如果你更改它,那么后续调用将以modified值开始,这可能不是你想要的。

    避免此问题的一种解决方案是将默认值设置为不可变类型,如 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)
    

    【讨论】:

    • > ~那么后续调用将从修改后的值开始。~你能举个例子吗?
    • 当然:执行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}
    【解决方案5】:
    • 列表是可变的,并且在编译时用 def 声明默认值会为某个地址的变量分配一个可变列表

      def abc(a=[]):
          a.append(2)
          print(a)
      
      abc() #prints [2]
      abc() #prints [2, 2] as mutable thus changed the same assigned list at func delaration points to same address and append at the end
      abc([4]) #prints [4, 2] because new list is passed at a new address
      abc() #prints [2, 2, 2] took same assigned list and append at the end 
      

       

    • 要纠正这个问题:

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

       

      • 这适用于每次创建新列表并且不将旧列表作为值引用时始终为空,从而在新地址分配新列表

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-02-17
      • 2011-05-14
      • 1970-01-01
      • 2012-11-13
      • 1970-01-01
      • 2013-04-24
      • 2016-03-12
      相关资源
      最近更新 更多