Pandas 2 使用指南:写时复制(Copy-on-Write,CoW)

本文介绍了Pandas3.0中的写时复制模式,强调了其对代码行为的影响,包括链式赋值的弃用、只读NumPy数组和一次更新一个对象的规则。同时提供了如何迁移代码和解决常见问题的指导。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


注意

写时复制将在 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对象

以下代码片段在没有写时复制的情况下同时更新dfsubset

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。如果需要此行为,可以使用lociloc将此语句重写为一条语句。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在写时复制下返回一个惰性复制,而在不使用写时复制时复制数据。由于dfdf2两个对象共享相同的数据,因此在修改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]: 
  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数智笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值