关于Python中的变量和对象的被忽视的事实:这都是关于指针的问题

223 阅读11分钟

在Python中,变量和数据结构不包含对象。 这个事实通常被忽视,也很难被内化。

你可以愉快地使用Python多年而不真正理解下面的概念,但这些知识肯定可以帮助减轻许多常见的Python问题。

目录。

术语

让我们从介绍一些术语开始。 最后几个定义很可能在我们后面更详细地定义它们时才会有意义。

对象(a.k.a.value):一个 "东西"。 列表、字典、字符串、数字、图元、函数和模块都是对象。"对象 "无法定义,因为在 Python 中所有东西都是对象

变量(a.k.a.name):用来指代一个对象的名字。

指针(a.k.a.reference):描述一个对象所在的位置 (通常以箭头的形式直观显示)

平等性:两个对象是否代表相同的数据

同一性:两个指针是否指代同一个对象

这些术语最好通过它们之间的关系来理解,这就是本文的主要目的。

Python 的变量是指针,不是桶

Python 中的变量不是包含东西的桶;它们是指针(它们指向对象)。

指针 "这个词听起来很吓人,但是很多吓人的东西来自于相关的概念 (例如,取消引用),而这些概念在 Python 中并不相关。 在 Python 中,指针只是表示一个变量和一个对象之间的联系

想象一下,变量生活在变量地,而对象生活在对象地指针是一个小箭头,将每个变量和它所指向的对象连接起来。

上面这个图表示我们的Python进程在运行这段代码后的状态。

1
2
3
>>> numbers = [2, 1, 3, 4, 7]
>>> numbers2 = [11, 18, 29]
>>> name = "Trey"

如果指针这个词让你感到害怕,那就用引用这个词来代替。 每当你在这篇文章中看到基于指针的短语,就在心里把它翻译成基于引用的短语。

  • 指针引用
  • 指向引用
  • 指向引用
  • 将X指向Y使X指向Y

赋值把一个变量指向一个对象

赋值语句将一个变量指向一个对象。 就是这样。

如果我们运行这段代码。

1
2
3
>>> numbers = [2, 1, 3, 4, 7]
>>> numbers2 = numbers
>>> name = "Trey"

我们的变量和对象的状态会是这样的。

请注意,numbersnumbers2 指向同一个对象。如果我们改变这个对象,两个变量似乎都会 "看到 "这个变化。

1
2
3
4
5
6
>>> numbers.pop()
7
>>> numbers
[2, 1, 3, 4]
>>> numbers2
[2, 1, 3, 4]

这种奇怪的现象都是由于这个赋值语句造成的。

1
>>> numbers2 = numbers

赋值语句并不复制任何东西:它们只是将一个变量指向一个对象。 所以将一个变量赋值给另一个变量只是将两个变量指向同一个对象

Python 中的 2 种 "变化 "类型

Python 有 2 种不同的 "改变 "类型。

  1. 赋值改变一个变量 (它改变了它所指向对象)
  2. 变异改变一个对象 (任何数量的变量都可能指向这个对象)

"改变 "这个词通常是模糊的。短语 "我们改变了x"可能意味着 "我们重新赋值了x",也可能意味着 "我们突变了x 指向的对象"。

变异会改变对象,而不是变量。 但是变量会指向对象。 因此,如果另一个变量指向我们刚刚变异的对象,那么另一个变量也会反映同样的变化;不是因为变量改变了,而是因为它指向的对象改变了。

平等性比较对象,同一性比较指针

Python 的== 操作符检查两个对象是否代表相同的数据(又称平等)。

1
2
3
4
>>> my_numbers = [2, 1, 3, 4]
>>> your_numbers = [2, 1, 3, 4]
>>> my_numbers == your_numbers
True

Python的is 操作符检查两个对象是否是同一个对象(又称身份)。

1
2
>>> my_numbers is your_numbers
False

变量my_numbersyour_numbers 指向代表相同数据的对象,但它们指向的对象不是同一个对象

因此,改变一个对象并不改变另一个对象。

1
2
3
>>> my_numbers[0] = 7
>>> my_numbers == your_numbers
False

如果两个变量指向同一个对象。

1
2
3
>>> my_numbers_again = my_numbers
>>> my_numbers is my_numbers_again
True

改变一个变量指向的对象也会改变另一个变量指向的对象,因为它们都指向同一个对象。

1
2
3
4
5
>>> my_numbers_again.append(7)
>>> my_numbers_again
[2, 1, 3, 4, 7]
>>> my_numbers
[2, 1, 3, 4, 7]

== 运算符检查平等性is 运算符检查同一性。同一性和平等性之间存在这种区别,因为变量并不包含对象,它们指向对象

在 Python 中,平等性检查非常普遍,而同一性检查则非常罕见

对于不可变的对象也没有例外。

但是等等,修改一个数字并不会改变指向同一个数字的其他变量,对吗?

1
2
3
4
5
6
7
>>> n = 3
>>> m = n  # n and m point to the same number
>>> n += 2
>>> n  # n has changed
5
>>> m  # but m hasn't changed!
3

嗯,修改一个数字在 Python 中是不可能的。 数字和字符串都是不可变的,这意味着你不能改变它们。 你不能改变一个不可变的对象。

那么,上面那个+= 操作符呢?它不是修改了一个数字吗? (不是的。)

对于不可变的对象,这两个语句是等同的。

1
2
>>> n += 2
>>> n = n + 2

对于不可变的对象,增强的赋值 (+=,*=,%=, 等等) 执行一个操作(返回一个新的对象),然后做一个赋值(给这个新对象)。

任何你认为会改变字符串或数字的操作都会返回一个新的对象。 任何对不可变的对象的操作都会返回一个新的对象,而不是修改原来的对象。

数据结构包含指针

像变量一样,数据结构不包含对象,它们包含指向对象的指针

比方说,我们做了一个列表的列表。

1
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

然后我们做一个变量,指向我们列表中的第二个列表。

1
2
3
>>> row = matrix[1]
>>> row
[4, 5, 6]

现在我们的变量和对象的状态看起来是这样的。

我们的row 变量指向与我们的matrix 列表中的索引1 相同的对象

1
2
>>> row is matrix[1]
True

因此,如果我们改变了row 所指向的列表。

1
>>> row[0] = 1000

我们会看到这两个地方的变化。

1
2
3
4
>>> row
[1000, 5, 6]
>>> matrix
[[1, 2, 3], [1000, 5, 6], [7, 8, 9]]

人们常说数据结构 "包含 "对象,但实际上它们只包含指向对象的指针。

函数参数的作用类似于赋值语句

函数调用也执行赋值。

如果你改变了一个被传入函数的对象,你就改变了原来的对象。

1
2
3
4
5
6
7
8
9
>>> def smallest_n(items, n):
...     items.sort()  # This mutates the list (it sorts in-place)
...     return items[:n]
...
>>> numbers = [29, 7, 1, 4, 11, 18, 2]
>>> smallest_n(numbers, 4)
[1, 2, 4, 7]
>>> numbers
[1, 2, 4, 7, 11, 18, 29]

但是如果你把一个变量重新赋值给一个不同的对象,原来的对象就不会改变。

1
2
3
4
5
6
7
8
9
>>> def smallest_n(items, n):
...     items = sorted(items)  # this makes a new list (original is unchanged)
...     return items[:n]
...
>>> numbers = [29, 7, 1, 4, 11, 18, 2]
>>> smallest_n(numbers, 4)
[1, 2, 4, 7]
>>> numbers
[29, 7, 1, 4, 11, 18, 2]

我们在这里重新赋值items 变量。这种重新赋值改变了items 变量所指向对象,但它并没有改变原来的对象。

在第一种情况下我们改变了一个对象,在第二种情况下我们改变了一个变量

这是你有时会看到的另一个例子。

1
2
3
4
class Widget:
    def __init__(self, attrs=(), choices=()):
        self.attrs = list(attrs)
        self.choices = list(choices)

类的初始化方法经常通过把它们的项目做成一个新的列表来复制给它们的可迭代对象。 这使得类可以接受任何可迭代对象(不仅仅是列表),并且把原始的可迭代对象与类解耦(修改这些列表不会扰乱原始的调用者)。 上面的例子是从Django借来的

除非函数调用者希望你这样做,否则不要对传入你的函数的对象进行变异

复制是浅层的,这通常是好的

需要在 Python 中复制一个列表吗?

1
>>> numbers = [2000, 1000, 3000]

你可以调用copy 方法 (如果你确定你的可迭代对象是一个 list)。

1
>>> my_numbers = numbers.copy()

或者你可以把它传递给list 构造函数 (这对任何可迭代的对象都有效)。

1
>>> my_numbers = list(numbers)

这两种方法都会产生一个新的列表,它指向的对象与原来的列表相同

这两个列表是不同的,但其中的对象是相同的。

1
2
3
4
>>> numbers is my_numbers
False
>>> numbers[0] is my_numbers[0]
True

由于整数 (和所有的数字) 在 Python 中是不可改变的,我们并不关心每个列表包含相同的对象,因为我们无论如何都不能改变这些对象。

对于可变的对象,这种区别是很重要的。 这使得两个列表中的每个列表都包含指向相同的三个列表的指针。

1
2
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> new_matrix = list(matrix)

这两个列表并不一样,但其中的每一项都是一样的。

1
2
3
4
>>> matrix is new_matrix
False
>>> matrix[0] is new_matrix[0]
True

下面是这两个对象和它们所包含的指针的一个相当复杂的可视化表示。

因此,如果我们改变一个列表中的第一个项目,就会改变另一个列表中的同一个项目。

1
2
3
4
5
>>> matrix[0].append(100)
>>> matrix
[[1, 2, 3, 100], [4, 5, 6], [7, 8, 9]]
>>> new_matrix
[[1, 2, 3, 100], [4, 5, 6], [7, 8, 9]]

当你在 Python 中复制一个对象时,如果该对象指向其它对象,你就会复制这些其它对象的指针,而不是复制对象本身。

新的 Python 程序员通过在他们的代码中洒上copy.deepcopy 来应对这种行为。deepcopy 函数试图递归地复制一个对象以及它所指向的所有对象。

有时新的 Python 程序员会使用deepcopy 来递归复制数据结构。

1
2
3
4
5
6
7
8
9
from copy import deepcopy
from datetime import datetime

tweet_data = [{"date": "Feb 04 2014", "text": "Hi Twitter"}, {"date": "Apr 16 2014", "text": "At #pycon2014"}]

# Parse date strings into datetime objects
processed_data = deepcopy(tweet_data)
for tweet in processed_data:
    tweet["date"] = datetime.strptime(tweet["date"], "%b %d %Y")

但是在 Python 中,我们通常更喜欢制作新的对象,而不是突变现有的对象。 所以我们可以通过制作一个新的字典列表,而不是深度拷贝我们的旧字典列表,来完全删除上面那个deepcopy 的用法。

1
2
3
4
5
# Parse date strings into datetime objects
processed_data = [
    {**tweet, "date": datetime.strptime(tweet["date"], "%b %d %Y")}
    for tweet in tweet_data
]

在 Python 中我们倾向于浅层拷贝。 如果你**不改变不属于你的对象,**你通常就不需要deepcopy

deepcopy 函数当然有它的用途,但它通常是不必要的。"如何避免使用deepcopy" 值得在以后的文章中单独讨论。

总结

Python 中的变量不是包含东西的桶;它们是指针(它们指向对象)。

Python 的变量和对象模型可以归结为两个主要规则。

  1. 变异会改变一个对象
  2. 赋值将一个变量指向一个对象

以及这些推论性的规则。

  1. 重新赋值一个变量,把它指向一个不同的对象,使原来的对象保持不变。
  2. 赋值不会复制任何东西,所以你要根据需要复制对象。

此外,数据结构的工作方式是一样的:列表和字典是指向对象的容器指针,而不是对象本身。 属性的工作方式也是一样的:属性指向对象(就像任何变量指向一个对象一样)。 所以在 Python 中对象不能包含对象(它们只能指向对象)。

注意,虽然突变会改变对象(而不是变量),但多个变量可以指向同一个对象。 如果两个变量指向同一个对象,那么在访问任何一个变量时都会看到该对象的变化 (因为它们都指向同一个对象)。