写时复制 (CoW)#
注意
写时复制将在 pandas 3.0 中成为默认设置。我们建议立即启用它以从所有改进中受益。
写时复制最初在 1.5.0 版本中引入。从 2.0 版本开始,大多数通过 CoW 实现的优化已实现并得到支持。从 pandas 2.1 开始,所有可能的优化都得到支持。
CoW 将在 3.0 版本中默认启用。
CoW 将导致更可预测的行为,因为它不可能用一个语句更新多个对象,例如索引操作或方法不会产生副作用。此外,通过尽可能延迟复制,平均性能和内存使用量将得到改善。
先前行为#
pandas 索引行为很难理解。一些操作返回视图,而另一些操作返回副本。根据操作的结果,修改一个对象可能会意外地修改另一个对象。
In [1]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [2]: subset = df["foo"]
In [3]: subset.iloc[0] = 100
In [4]: df
Out[4]:
foo bar
0 100 4
1 2 5
2 3 6
修改 subset
,例如更新其值,也会更新 df
。确切的行为很难预测。写时复制解决了意外修改多个对象的问题,它明确禁止了这种行为。在启用 CoW 的情况下,df
保持不变。
In [5]: pd.options.mode.copy_on_write = True
In [6]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [7]: subset = df["foo"]
In [8]: subset.iloc[0] = 100
In [9]: df
Out[9]:
foo bar
0 1 4
1 2 5
2 3 6
以下部分将解释这意味着什么以及它如何影响现有应用程序。
迁移到写时复制#
写时复制将成为 pandas 3.0 中的默认模式和唯一模式。这意味着用户需要迁移他们的代码以符合 CoW 规则。
pandas 中的默认模式将针对某些情况发出警告,这些情况将积极改变行为,从而改变用户预期的行为。
我们添加了另一种模式,例如
pd.options.mode.copy_on_write = "warn"
这将针对每个将改变 CoW 行为的操作发出警告。我们预计这种模式会非常嘈杂,因为许多我们认为不会影响用户的案例也会发出警告。我们建议检查这种模式并分析警告,但没有必要解决所有这些警告。以下列表中的前两项是使现有代码与 CoW 一起工作的唯一需要解决的案例。
以下几项描述了用户可见的更改
链式赋值将永远无法工作
loc
应该用作替代方案。查看 链式赋值部分 以了解更多详细信息。
访问 pandas 对象的底层数组将返回一个只读视图
In [10]: ser = pd.Series([1, 2, 3])
In [11]: ser.to_numpy()
Out[11]: array([1, 2, 3])
此示例返回一个 NumPy 数组,它是 Series 对象的视图。此视图可以修改,从而也修改 pandas 对象。这与 CoW 规则不符。返回的数组被设置为不可写,以防止这种行为。创建此数组的副本允许修改。您也可以再次使数组可写,如果您不再关心 pandas 对象。
有关更多详细信息,请参阅关于 只读 NumPy 数组 的部分。
一次只更新一个 pandas 对象。
以下代码片段更新了 df
和 subset
,而没有使用 CoW。
In [12]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [13]: subset = df["foo"]
In [14]: subset.iloc[0] = 100
In [15]: df
Out[15]:
foo bar
0 1 4
1 2 5
2 3 6
使用 CoW 后,这将不再可能,因为 CoW 规则明确禁止这样做。这包括将单个列更新为 Series
并依赖更改传播回父 DataFrame
。如果需要此行为,可以将此语句重写为使用 loc
或 iloc
的单个语句。 DataFrame.where()
是此情况下的另一种合适替代方案。
使用就地方法更新从 DataFrame
中选择的列也不再起作用。
In [16]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [17]: df["foo"].replace(1, 5, inplace=True)
In [18]: df
Out[18]:
foo bar
0 1 4
1 2 5
2 3 6
这是另一种形式的链式赋值。这通常可以重写为两种不同的形式。
In [19]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [20]: df.replace({"foo": {1: 5}}, inplace=True)
In [21]: df
Out[21]:
foo bar
0 5 4
1 2 5
2 3 6
另一种选择是不使用 inplace
。
In [22]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [23]: df["foo"] = df["foo"].replace(1, 5)
In [24]: df
Out[24]:
foo bar
0 5 4
1 2 5
2 3 6
构造函数现在默认情况下复制 NumPy 数组。
Series 和 DataFrame 构造函数现在将在未指定的情况下默认复制 NumPy 数组。更改此行为是为了避免在 NumPy 数组在 pandas 之外就地更改时修改 pandas 对象。您可以设置 copy=False
来避免此复制。
描述#
CoW 意味着任何以任何方式从另一个 DataFrame 或 Series 派生的 DataFrame 或 Series 始终表现为副本。因此,我们只能通过修改对象本身来更改对象的值。CoW 不允许就地更新与另一个 DataFrame 或 Series 对象共享数据的 DataFrame 或 Series。
这避免了修改值时的副作用,因此,大多数方法可以避免实际复制数据,而只在必要时触发复制。
以下示例将使用 CoW 就地操作。
In [25]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [26]: df.iloc[0, 0] = 100
In [27]: df
Out[27]:
foo bar
0 100 4
1 2 5
2 3 6
对象 df
不与任何其他对象共享数据,因此在更新值时不会触发任何复制。相反,以下操作会在 CoW 下触发数据的复制。
In [28]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [29]: df2 = df.reset_index(drop=True)
In [30]: df2.iloc[0, 0] = 100
In [31]: df
Out[31]:
foo bar
0 1 4
1 2 5
2 3 6
In [32]: df2
Out[32]:
foo bar
0 100 4
1 2 5
2 3 6
reset_index
返回一个带有 CoW 的延迟副本,而它在没有 CoW 的情况下复制数据。由于两个对象 df
和 df2
共享相同的数据,因此在修改 df2
时会触发复制。对象 df
仍然具有与最初相同的数值,而 df2
被修改了。
如果在执行 reset_index
操作后不再需要对象 df
,可以通过将 reset_index
的输出分配给同一个变量来模拟类似于就地操作的操作。
In [33]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [34]: df = df.reset_index(drop=True)
In [35]: df.iloc[0, 0] = 100
In [36]: df
Out[36]:
foo bar
0 100 4
1 2 5
2 3 6
一旦 reset_index
的结果被重新分配,初始对象就会超出范围,因此 df
不与任何其他对象共享数据。修改对象时不需要复制。这通常适用于 Copy-on-Write 优化 中列出的所有方法。
以前,在对视图进行操作时,视图和父对象会被修改。
In [37]: with pd.option_context("mode.copy_on_write", False):
....: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
....: view = df[:]
....: df.iloc[0, 0] = 100
....:
In [38]: df
Out[38]:
foo bar
0 100 4
1 2 5
2 3 6
In [39]: view
Out[39]:
foo bar
0 100 4
1 2 5
2 3 6
当 df
被更改时,CoW 会触发复制,以避免同时修改 view
。
In [40]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [41]: view = df[:]
In [42]: df.iloc[0, 0] = 100
In [43]: df
Out[43]:
foo bar
0 100 4
1 2 5
2 3 6
In [44]: view
Out[44]:
foo bar
0 1 4
1 2 5
2 3 6
链式赋值#
链式赋值引用一种技术,其中对象通过两个后续索引操作进行更新,例如:
In [45]: with pd.option_context("mode.copy_on_write", False):
....: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
....: df["foo"][df["bar"] > 5] = 100
....: df
....:
当列 bar
大于 5 时,更新列 foo
。但这违反了 CoW 原则,因为它必须一步修改视图 df["foo"]
和 df
。因此,链式赋值始终无法正常工作,并在启用 CoW 时引发 ChainedAssignmentError
警告。
In [46]: df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
In [47]: df["foo"][df["bar"] > 5] = 100
使用 CoW,可以通过使用 loc
来完成此操作。
In [48]: df.loc[df["bar"] > 5, "foo"] = 100
只读 NumPy 数组#
如果数组与初始 DataFrame 共享数据,则访问 DataFrame 的底层 NumPy 数组将返回一个只读数组。
如果初始 DataFrame 包含多个数组,则该数组是一个副本。
In [49]: df = pd.DataFrame({"a": [1, 2], "b": [1.5, 2.5]})
In [50]: df.to_numpy()
Out[50]:
array([[1. , 1.5],
[2. , 2.5]])
如果 DataFrame 仅包含一个 NumPy 数组,则该数组与 DataFrame 共享数据。
In [51]: df = pd.DataFrame({"a": [1, 2], "b": [3, 4]})
In [52]: df.to_numpy()
Out[52]:
array([[1, 3],
[2, 4]])
此数组是只读的,这意味着它不能就地修改。
In [53]: arr = df.to_numpy()
In [54]: arr[0, 0] = 100
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[54], line 1
----> 1 arr[0, 0] = 100
ValueError: assignment destination is read-only
Series 也是如此,因为 Series 始终包含单个数组。
对此有两种可能的解决方案。
如果您想避免更新与您的数组共享内存的 DataFrame,请手动触发复制。
使数组可写。这是一个性能更高的解决方案,但会绕过写时复制规则,因此应谨慎使用。
In [55]: arr = df.to_numpy()
In [56]: arr.flags.writeable = True
In [57]: arr[0, 0] = 100
In [58]: arr
Out[58]:
array([[100, 3],
[ 2, 4]])
要避免的模式#
如果两个对象在您就地修改一个对象时共享相同的数据,则不会执行防御性复制。
In [59]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
In [60]: df2 = df.reset_index(drop=True)
In [61]: df2.iloc[0, 0] = 100
这将创建两个共享数据的对象,因此 setitem 操作将触发复制。如果初始对象 df
不再需要,则没有必要这样做。只需重新分配到同一个变量将使该对象持有的引用失效。
In [62]: df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
In [63]: df = df.reset_index(drop=True)
In [64]: df.iloc[0, 0] = 100
在本例中,不需要复制。创建多个引用会使不必要的引用保持活动状态,因此会影响写时复制的性能。
写时复制优化#
一种新的延迟复制机制,它会延迟复制,直到要修改的对象被修改,并且只有当该对象与另一个对象共享数据时才会进行复制。此机制已添加到不需要复制底层数据的那些方法中。常见的例子是 DataFrame.drop()
用于 axis=1
和 DataFrame.rename()
.
当启用写时复制时,这些方法会返回视图,与常规执行相比,这提供了显著的性能提升。
如何启用 CoW#
可以通过配置选项 copy_on_write
启用写时复制。该选项可以通过以下任一方式在 __全局__ 范围内启用
In [65]: pd.set_option("mode.copy_on_write", True)
In [66]: pd.options.mode.copy_on_write = True