写时复制(Copy-on-Write, CoW)#
备注
写时复制将在 pandas 3.0 中成为默认行为。我们建议 turning it on now 以便从中受益。
Copy-on-Write (写时复制) 最初在 1.5.0 版本中引入。从 2.0 版本开始,大部分通过 Copy-on-Write (CoW) 实现的优化都得到了支持。所有可能的优化都从 pandas 2.1 开始支持。
CoW 将在 3.0 版本中默认启用。
CoW 会带来更可预测的行为,因为它不允许用一个语句修改多个对象,例如,索引操作或方法调用将没有副作用。此外,通过尽可能延迟复制,平均性能和内存使用量将得到改善。
之前的行为#
pandas 的索引行为很难理解。一些操作返回视图 (views),而另一些则返回副本 (copies)。根据操作的结果,修改一个对象可能会意外地修改另一个对象:
修改 subset,例如更新它的值,也会更新 df。其具体行为难以预测。Copy-on-Write 解决了意外修改多个对象的问题,它明确禁止这样做。启用 CoW 后,df 不会改变:
以下章节将解释这意味着什么以及它如何影响现有应用程序。
迁移到 Copy-on-Write#
Copy-on-Write 将成为 pandas 3.0 中的默认且唯一模式。这意味着用户需要迁移他们的代码以遵守 CoW 规则。
pandas 中的默认模式将对某些会主动改变行为从而改变用户预期行为的情况发出警告。
我们添加了另一种模式,例如:
pd.options.mode.copy_on_write = "warn"
该模式将针对所有在 CoW 下会改变行为的操作发出警告。我们预计此模式会非常“啰嗦”,因为许多我们认为不会影响用户的场景也会发出警告。我们建议检查此模式并分析警告,但不必解决所有这些警告。以下列表的前两项是使现有代码能够与 CoW 一起工作的唯一需要解决的案例。
以下几项描述了用户可见的更改:
链式赋值将不再有效
应使用 loc 作为替代。有关更多详细信息,请参阅 chained assignment section 。
访问 pandas 对象的底层数组将返回一个只读视图
此示例返回一个 NumPy 数组,它是 Series 对象的视图。此视图可以被修改,从而也修改 pandas 对象。这不符合 CoW 规则。返回的数组被设置为不可写,以防止此行为。创建此数组的副本允许修改。如果您不再关心 pandas 对象,也可以再次使数组可写。
有关更多详细信息,请参阅 read-only NumPy arrays 部分。
一次只更新一个 pandas 对象
以下代码片段在没有 CoW 的情况下同时更新 df 和 subset:
使用 CoW 将不再可能,因为 CoW 规则明确禁止了这一点。这包括将单列作为 Series 进行更新,并依赖于更改传播回父 DataFrame 。如果需要此行为,可以使用 loc 或 iloc 将此语句重写为单个语句。对于这种情况,DataFrame.where() 是另一个合适的替代方案。
使用就地 (inplace) 方法更新从 DataFrame 中选择的列将不再有效。
这是链式赋值的另一种形式。通常可以将其重写为两种不同的形式:
另一种替代方法是不使用 inplace:
构造函数现在默认复制 NumPy 数组
Series 和 DataFrame 构造函数现在默认复制 NumPy 数组,除非另行指定。更改此设置是为了避免当 NumPy 数组在 pandas 外部被就地修改时修改(共享数据的)pandas 对象。您可以设置 copy=False 来避免此复制。
描述#
CoW 意味着任何以任何方式从另一个 DataFrame 或 Series 派生的 DataFrame 或 Series 都总是表现得像一个副本。因此,我们只能通过修改对象本身来更改其值。CoW 禁止就地修改与另一个 DataFrame 或 Series 对象共享数据的 DataFrame 或 Series。
这避免了修改值时的副作用,因此,大多数方法可以避免实际复制数据,而仅在需要时触发复制。
以下示例将在写时复制(CoW)下进行原地操作:
对象 df 不与任何其他对象共享任何数据,因此在更新值时不会触发复制。相比之下,以下操作在写时复制(CoW)下会触发数据复制:
reset_index 在写时复制(CoW)下返回一个惰性副本,而没有写时复制(CoW)时则会复制数据。由于 df 和 df2 这两个对象共享相同的数据,因此在修改 df2 时会触发数据复制。对象 df 仍然保持初始值,而 df2 被修改了。
如果在执行 reset_index 操作后不再需要对象 df,则可以通过将 reset_index 的输出重新分配给同一变量来模拟类似原地操作的行为:
当 reset_index 的结果被重新分配后,初始对象就会超出作用域,因此 df 不再与其他对象共享数据。修改对象时无需复制。这对于 Copy-on-Write optimizations 中列出的所有方法通常都是如此。
以前,在操作视图时,视图和父对象都会被修改:
写时复制(CoW)在修改 df 时会触发复制,以避免也修改 view:
链式赋值#
链式赋值是指通过后续两次索引操作来更新对象的技巧,例如:
当 bar 列大于 5 时,更新 foo 列。但这违反了写时复制(CoW)的原则,因为它需要在一步中同时修改视图 df["foo"] 和 df。因此,在启用写时复制(CoW)的情况下,链式赋值将始终无效并引发 ChainedAssignmentError 警告:
使用写时复制(CoW),可以通过使用 loc 来完成此操作。
只读 NumPy 数组#
如果数组与初始 DataFrame 共享数据,则访问 DataFrame 的底层 NumPy 数组将返回一个只读数组:
如果初始 DataFrame 由多个数组组成,则该数组是一个副本:
如果 DataFrame 仅由一个 NumPy 数组组成,则该数组与 DataFrame 共享数据:
此数组是只读的,这意味着它不能原地修改:
由于 Series 始终由单个数组组成,因此对于 Series 也是如此。
这里有两种可能的解决方案:
如果您想避免更新与您的数组共享内存的 DataFrame,请手动触发复制。
使数组可写。这是一个更具性能的解决方案,但会绕过写时复制(CoW)规则,因此应谨慎使用。
应避免的模式#
当两个对象共享相同数据时,在原地修改其中一个对象时不会执行防御性复制。
这会创建两个共享数据的对象,因此 setitem 操作会触发复制。如果不再需要初始对象 df,则这是不必要的。只需将引用重新分配给同一变量即可使对象所持有的引用无效。
在此示例中,无需进行复制。创建多个引用会使不必要的引用保持活动状态,从而在写时复制(CoW)下会影响性能。
写时复制(Copy-on-Write)优化#
一种新的惰性复制机制,它推迟复制直到相关对象被修改,并且仅当该对象与另一个对象共享数据时。此机制已添加到不需要复制底层数据的方法中。流行的例子包括 axis=1 的 DataFrame.drop() 和 DataFrame.rename() 。
当启用写时复制(CoW)时,这些方法会返回视图,与常规执行相比,这提供了显著的性能提升。
如何启用 CoW#
可以通过配置选项 copy_on_write 来启用写时复制(Copy-on-Write)。可以通过以下任一方式 __全局__ 打开该选项: