在`x = xy()`之后,为什么`x`变成了`None`而不是被修改(可能导致“AttributeError:'NoneType'对象没有属性”)?

Kar*_*tel 7 python attributeerror command-query-separation nonetype

如果您的问题作为此问题的重复项而被关闭,那是因为您有一些通用形式的代码

x = X()
# later...
x = x.y()
# or:
x.y().z()
Run Code Online (Sandbox Code Playgroud)

其中X是某种类型,它提供了y旨在z变异修改)对象(X类型的实例)的方法。这可以适用于:

  • 可变的内置类型,例如listdictsetbytearray
  • 标准库(尤其是 Tkinter 小部件)或第三方库提供的类。

这种形式的代码很常见,但并不总是错误的。问题的明显迹象是:

  • x.y().z()一样,会引发异常AttributeError: 'NoneType' object has no attribute 'z'

  • 有了x = x.y(),x就变成None, 而不是被修改的对象。这可能会被后来的错误结果发现,或者被像上面这样的异常(x.z()稍后尝试时)发现。

Stack Overflow 上有大量关于这个问题的现有问题,所有这些问题实际上都是同一个问题。之前甚至有多次尝试在特定上下文中涵盖同一问题的规范。然而,理解问题并不需要上下文,因此这里尝试一般性地回答:

代码有什么问题吗?为什么这些方法会这样,我们如何解决这个问题?


另请注意,当尝试使用 alambda或列表理解)来产生副作用时,会出现类似的问题。

同样明显的问题可能是由因其他原因返回的方法引起的None- 例如,BeautifulSoup 使用None返回值来指示在 HTML 中未找到标记。然而,一旦当前的问题——期望一个方法更新对象并返回相同的对象——被识别出来,那么在所有上下文中都是同样的问题。

请不要使用此问题来关闭有关使用.append 循环重复附加到列表的其他问题。在这些情况下,仅仅了解用法出了什么问题.append并没有多大帮助,提出这些问题的人还应该看到其他构建列表的技术。请使用如何收集列表、字典等中重复计算的结果(或制作修改每个元素的列表的副本)?反而。

更具体的问答版本:

Kar*_*tel 3

概括

相关方法返回特殊值,它是该NoneNoneType类型的唯一实例。作为副作用,它更新对象,并且不返回该对象。由于x.y()returns Nonex = x.y()导致x变为None,并且x.y().z()由于None没有指定的z方法而失败(事实上,它只有不应该直接调用的辅助方法)。

这种情况在 Python 的许多地方都会发生,并且是一个经过深思熟虑的设计决定。它允许代码的读者x.y().z()正确地假设代码没有副作用;它在更新可变对象的代码和替换不可变对象的代码之间做出了清晰的视觉区分。

对于简单的情况,不使用x = x.y(),只需编写x.y()。不要尝试像 那样链接调用x.y().z(),而是分别进行每个调用:x.y()然后x.z()。然而,通常有必要(或者更好的主意)制作 的修改副本x而不是就地更新它。正确的方法将根据具体情况而定,需要更仔细的理解。

类似的代码x = x.y()可以在类违反 Python 约定的情况下工作,并在更新对象后x执行类似操作。return self然而,通常只写仍然会更好x.y()

x.y().z()如果该y方法通过计算结果而不是更新来工作,则类似的代码可能是正确的x。在这些情况下,z对该计算结果调用的,而不是 x,因此结果需要支持该方法。

特别是如果目标是对列表进行多次更改,通常最好使用列表理解或类似工具来创建包含所有更改的单独列表。然而,这超出了本次问答的范围。

了解问题

首先,一些更具体的例子。

  • 在列表上使用变异方法:

    >>> mylist = []
    >>> mylist.append(1).append(2) # try to append two values
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'NoneType' object has no attribute 'append'
    
    Run Code Online (Sandbox Code Playgroud)
  • 在列表上使用“外部”算法,例如对其进行洗牌

    >>> import random
    >>> mylist = [1, 2, 3]
    >>> print(random.shuffle(mylist))
    None
    >>> print(mylist) # 6 different possible results, of course
    [3, 1, 2]
    
    Run Code Online (Sandbox Code Playgroud)
  • 在 Tkinter 小部件上使用等方法gridpackplace(从其他问题复制并注释的示例):

    from tkinter import *
    
    root = Tk()
    
    def grabText(event):
        print(entryBox.get())    
    
    entryBox = Entry(root, width=60).grid(row=2, column=1, sticky=W)
    
    # This button is supposed to print the contents of the Entry when clicked,
    # but instead `entryBox` will be `None` when the button is clicked,
    # so an exception is raised.
    grabBtn = Button(root, text="Grab")
    grabBtn.grid(row=8, column=1)
    grabBtn.bind('<Button-1>', grabText)
    
    root.mainloop()
    
    Run Code Online (Sandbox Code Playgroud)
  • 使用 Pandas 第三方库,使用inplace=True某些方法

    >>> df = pd.DataFrame({'name': ['Alice', 'Bob']}, employee_id=[2, 1])
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: __init__() got an unexpected keyword argument 'employee_id'
    >>> df = pd.DataFrame({'name': ['Alice', 'Bob'], 'employee_id': [2, 1]})
    >>> df
        name  employee_id
    0  Alice            2
    1    Bob            1
    >>> print(df.set_index('employee_id', inplace=True))
    None
    >>> df
                  name
    employee_id       
    2            Alice
    1              Bob
    
    Run Code Online (Sandbox Code Playgroud)

在所有这些情况下,问题如摘要中所述:被调用的方法 -append在列表上、grid在 Tkinter 上Entry以及在指定的set_indexPandas 上-DataFrame通过inplace=True就地更新listEntryDataFrame(分别)来工作,而不是通过计算并return得出结果。然后它们return是特殊值None,而不是原始实例,也不是同一类的新实例。在 Python 中,像这样修改对象的方法预计会以这种方式返回None

另一方面,返回新实例(在某些情况下,可能是新实例或原始实例)的方法预计不会修改调用它们的实例的内部状态。再次考虑 Tkinter 示例,该get方法以Entry字符串形式返回 GUI 中该文本输入框中包含的文本;这样做时它不会修改Entry(至少不会以可以从外部观察到的方式修改)。

设计理由

这种设计选择称为命令查询分离,它被认为是 Python 中的一个重要习惯用法,受到标准库和流行第三方库的尊重。

这个想法很简单:方法应该返回计算结果(关于对象“包含”或“知道”的内容的“查询”),或者更新对象的内部状态(对象的“命令” “做某事”) - 但不能两者兼而有之。从概念上讲,返回值是对可能取决于对象状态的问题的答案。如果重复问同一个问题,从逻辑上讲,答案应该保持不变;但如果我们允许更新对象的状态,则可能不会发生这种情况。

某些语言,如 C、C++、C# 和 Java,在“查询”和“命令”之间进行语法区分:方法和函数可以有返回类型void,在这种情况下,它们实际上并不返回值(并且调用那些方法和函数不能在更大的表达式中使用)。然而,Python却不是这样工作的;我们能做的最好的事情就是返回特殊值None,并期望调用者适当地处理它。

另一方面,Python确实在语法上区分了赋值和表达式;在Python中,赋值是语句,因此它们不能在更大的表达式中使用:

>>> a = b = 1 # support for this is built in to the assignment syntax.
>>> a = (b = 1) # `b = 1` isn't an expression, so the result can't be assigned to `a`.
  File "<stdin>", line 1
    a = (b = 1)
           ^
SyntaxError: invalid syntax
Run Code Online (Sandbox Code Playgroud)

使用方法的命令-查询分离类似于这种“赋值-表达式分离”。(在3.8中,添加了一个新的“walrus”运算符,以允许表达式执行赋值作为副作用。这在当时非常有争议,因为它是故意设计的东西中的一个漏洞。但是,有很好的用途它的情况,并且语法是有限且明确的。)

在其他一些语言中,例如 JavaScript,为了创建“流畅”接口,首选不遵守此原则,其中许多方法调用链接在同一对象上,并且该对象的状态可能会多次更新。例如,这可以被视为一种通过多个步骤构造对象的优雅方式。return方法通过在完成一些工作后调用当前对象来实现此策略。

return self然而,虽然在 Python 中使用方法本身是可以的,但 Python 代码通常不应在更新对象 state 后执行此操作。约定是返回None(或者只是return显式返回),以便客户端代码知道此方法是“命令”而不是“查询”。

特殊情况:“pop”方法

.pop内置list, setanddict和类型的方法很特殊(s的方法bytearray也是如此)。这些方法不遵循命令-查询分离 - 它们实现命令(从容器中删除元素)和查询(指示删除的内容)。这样做是因为“弹出”的概念在计算机科学中已得到很好的确立,因此实现了标准的、预先存在的设计。.popitemdict

请记住,返回值仍然不是原始对象,因此这些方法仍然不允许链接原始对象。方法(和其他表达式)仍然可以链接,但结果可能不符合预期:

>>> number_names = {1: 'one', 2: 'two', 3: 'three'}
>>> number_names.pop(2)[1]
'w'
Run Code Online (Sandbox Code Playgroud)

在这里,结果不是(弹出键后保留在字典中的键'one'的值),而是(弹出的字符串索引处的元素)。12'w'[1] 'two'

解决方法

首先,决定是否创建对象的(修改的)副本。上面的示例都是修改现有对象而不创建新对象。通常,更简单的方法是创建一个与原始对象类似的新对象,但进行了特定的更改。通常这并不重要,但制作副本的范围可能从必要到不可接受,具体取决于总体任务。例如,在构建列表列表时,可能需要单独的对象;但其他设计可能需要故意共享对象。

当代码无论哪种方式都是正确的时,修改现有对象通常会更快;但在许多情况下,差异并不明显。

不需要副本时的语法解决方法

当然,对于像这样的代码x = x.y(),最简单的解决方法就是不分配;就写x.y()吧。这已经导致了x变化(这就是该方法的目的y)。

要解决链式方法调用的问题,例如x.y().z(),最简单的方法是打破链并单独进行每个调用

x.y() # x still means the same object, but it has been modified
x.z() # so now the z method can be called, and both changes apply
Run Code Online (Sandbox Code Playgroud)

一种解决方法允许链接。首先,解决方法本身:

>>> x = []
>>> y = x.append(1) or x
>>> y
[1]
Run Code Online (Sandbox Code Playgroud)

这个想法很简单,但很棘手。None修改方法(此处为list.append)返回的是 Falsy,因此or 将求值为右侧。右侧是同一个对象,因此现在y命名相同的对象x,并“看到” 所做的更改x.append(1)。因此,要进行链式调用,只需将该方法应用于使用 .创建的表达式or即可。当然,这需要括号:

>>> x = []
>>> (x.append(1) or x).append(2)
>>> x
[1, 2]
Run Code Online (Sandbox Code Playgroud)

然而,这种方法很快就会变得笨拙:

>>> x = []
>>> (((x.append(3) or x).extend([1, 'bad', 2]) or x).remove('bad') or x).sort()
>>> x
[1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

与直接方法相比,不尝试链接:

>>> x = []
>>> x.append(3)
>>> x.extend([1, 'bad', 2])
>>> x.remove('bad')
>>> x.sort()
>>> x
[1, 2, 3]
Run Code Online (Sandbox Code Playgroud)

首先显式复制

当可以接受(或需要)单独的副本时,请首先检查文档以了解是否有首选的副本方式。在许多情况下,所需的功能已经可以通过创建修改副本的单独方法获得(请参阅下一节)。在其他情况下,出于技术原因,该类实现自己的复制方法。请注意,大多数复制对象的方法都会提供浅复制,而不是深复制。在差异很重要的情况下要小心这一点。

复制列表的方法有很多种——这是不可避免的,因为列表非常灵活。原则上,内置方法.copy是自 Python 3.3(添加时)以来制作列表浅表副本的“正确方法”:它明确说明代码正在做什么,并进行更新以使用最快的已知技术对于副本。

如果所有其他方法都失败,请尝试使用标准库copy模块通过copy.copy(浅拷贝)或copy.deepcopy(深拷贝)克隆对象。然而,即使这样也不是完全普遍的。

制作修改副本的特定于上下文的方法

下面的表格列出了内置对象上各种就地方法的替换代码,以获取修改后的副本。在每种情况下,替换代码都是对修改后的副本求值的表达式;没有分配发生。

对于库提供的方法,请再次先检查文档。例如,对于 Pandas,获取修改后的副本通常就像使用inplace=True.

例子 替代品
列出方法(xylist
x.clear() []
x.append(1) x + [1]
x.extend(y) x + y
x.remove(1)
(仅删除第一个匹配项)
x[:x.index(1)] + x[x.index(1):]
while 1 in x: x.remove(1)
(删除所有匹配项)
[y for y in x if y != 1]
x.insert(y, z)
(在任意位置插入)
x[:y] + [z] + x[y:]
(但是复制修改速度更快)
x.sort() sorted(x)
x.reverse()
(稍后需要使用该对象)
list(reversed(x))或者x[::-1]
x.reverse()然后for y in x:
(只需要对象设置一个循环)
for y in reversed(x):
(与上面相比节省内存)
random.shuffle(x) random.sample(x, len(x))或者
sorted(x, key=lambda _: random.random())
设置方法 (x和)yset
x.clear() set()不是 {},这会产生一个字典)
x.update(y)
(实施|=
x.union(y)或者x | y
x.add(1) x.union({1})或者x | {1}
x.difference_update(y)
(实施-=
x - y或者x.difference(y)
x.discard(1) x - {1}或者x.difference({1})
x.remove(1) 与 一样discard,但如果集合不包含该元素,则首先显式引发异常
x.intersection_update(y)
(实施&=
x.intersection(y)或者x & y
x.symmetric_difference_update(y)(实施^= x.symmetric_difference(y)或者x ^ y
字典方法 (xyare dict)
x.clear() {}
x.update(y) 许多替代方案,具体取决于 Python 版本;请参阅/sf/ask/2729121/|

请注意,虽然bytearray提供了许多听起来像“命令”而不是“查询”的方法,但通常也存在的bytes方法实际上是“查询”,它们将返回一个新的bytearray值。对于那些确实修改了原始文件的人bytearray,请尝试上面 s 所示的方法list


归档时间:

查看次数:

577 次

最近记录:

2 年,6 月 前