注意
写时复制将在 pandas 3.0 中成为默认设置。我们建议立即启用以享受所有改进。
写时复制首次引入是在版本1.5.0中。从版本2.0开始,大部分通过写时复制实现的优化都已经实施和支持。从pandas 2.1开始,支持所有可能的优化。
写时复制将在版本3.0中默认启用。
写时复制将导致更可预测的行为,因为不可能用一条语句更新多个对象,例如索引操作或方法不会产生副作用。此外,通过尽可能延迟复制,平均性能和内存使用将得到改善。
以前的行为
pandas的索引行为很难理解。某些操作返回视图,而其他操作返回副本。根据操作的结果,更改一个对象可能会意外地更改另一个对象:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[4]:
foo bar
0 100 4
1 2 5
2 3 6
更改subset
,例如更新其值,也会更新df
。确切的行为很难预测。写时复制解决了意外修改多个对象的问题,它明确禁止这样做。启用写时复制后,df
保持不变:
pd.options.mode.copy_on_write = True
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[9]:
foo bar
0 1 4
1 2 5
2 3 6
下面的部分将解释这意味着什么以及它如何影响现有应用程序。
迁移到写时复制
写时复制将成为pandas 3.0的默认和唯一模式。这意味着用户需要将他们的代码迁移到符合写时复制规则的代码。
pandas的默认模式将对某些情况引发警告,这些情况将主动更改行为,从而更改用户预期的行为。
我们添加了另一种模式,例如
pd.options.mode.copy_on_write = "warn"
它将对每个将改变写时复制行为的操作发出警告。我们预计这种模式会非常嘈杂,因为我们不希望它们会影响用户的许多情况也会发出警告。我们建议检查此模式并分析警告,但不需要解决所有这些警告。以下列表的前两个项目是需要解决的唯一情况,以使现有代码与写时复制一起工作。
以下几个项目描述了用户可见的更改:
链式赋值将永远不起作用
应该使用loc
作为替代。有关更多详细信息,请查看链式赋值部分。
访问pandas对象的底层数组将返回只读视图
ser = pd.Series([1, 2, 3])
ser.to_numpy()
Out[11]: array([1, 2, 3])
此示例返回一个NumPy数组,该数组是Series对象的视图。此视图可以修改,从而也可以修改pandas对象。这与写时复制规则不符。返回的数组设置为不可写,以防止此行为。如果不再关心pandas对象,可以创建此数组的副本进行修改。如果不再关心pandas对象,还可以将数组设置为可写。
有关更多详细信息,请参见只读NumPy数组部分。
一次只更新一个pandas对象
以下代码片段在没有写时复制的情况下同时更新df
和subset
:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
subset = df["foo"]
subset.iloc[0] = 100
df
Out[15]:
foo bar
0 1 4
1 2 5
2 3 6
在写时复制中,这将不再可能,因为写时复制规则明确禁止这样做。这包括将单个列作为Series
更新,并依赖于更改传播回父DataFrame
。如果需要此行为,可以使用loc
或iloc
将此语句重写为一条语句。DataFrame.where()
是此情况的另一种合适的替代方法。
使用就地方法更新从DataFrame
选择的列也将不再起作用。
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"].replace(1, 5, inplace=True)
df
Out[18]:
foo bar
0 1 4
1 2 5
2 3 6
这是链式赋值的另一种形式。通常可以用两种不同的形式重写此操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df.replace({"foo": {1: 5}}, inplace=True)
df
Out[21]:
foo bar
0 5 4
1 2 5
2 3 6
另一种替代方法是不使用inplace
:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df["foo"] = df["foo"].replace(1, 5)
df
Out[24]:
foo bar
0 5 4
1 2 5
2 3 6
构造函数现在默认复制NumPy数组
当未另行指定时,Series和DataFrame构造函数现在默认复制NumPy数组。这样更改是为了避免在pandas之外就地更改NumPy数组时更改pandas对象。您可以设置copy=False
以避免进行此复制。
描述
写时复制意味着以任何方式从另一个DataFrame或Series派生的任何DataFrame或Series始终表现为副本。因此,我们只能通过修改对象本身来更改对象的值。写时复制不允许就地更新与另一个DataFrame或Series对象共享数据的DataFrame或Series。
这样可以避免在修改值时产生副作用,因此,大多数方法可以避免实际复制数据,并且仅在必要时触发复制。
以下示例将在写时复制中就地操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df.iloc[0, 0] = 100
df
Out[27]:
foo bar
0 100 4
1 2 5
2 3 6
对象df
不与任何其他对象共享数据,因此在更新值时不会触发复制。相反,在写时复制下,以下操作将触发数据的复制:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df2 = df.reset_index(drop=True)
df2.iloc[0, 0] = 100
df
Out[31]:
foo bar
0 1 4
1 2 5
2 3 6
df2
Out[32]:
foo bar
0 100 4
1 2 5
2 3 6
reset_index
在写时复制下返回一个惰性复制,而在不使用写时复制时复制数据。由于df
和df2
两个对象共享相同的数据,因此在修改df2
时会触发复制。对象df
仍然具有最初的值,而df2
已被修改。
如果在执行reset_index
操作后不再需要对象df
,则可以通过将reset_index
的输出分配给同一变量来模拟类似就地操作:
df = pd.DataFrame({"foo": [1, 2, 3], "bar": [4, 5, 6]})
df = df.reset_index(drop=True)
df.iloc[0, 0] = 100
df
Out[36]: