管理搜索路径冲突



从 R 3.6.0 开始,library()require() 函数允许在附加软件包时更好地控制搜索路径冲突的处理。此策略由新的 conflicts.policy 选项控制。此博文提供了一些此新功能的背景和详细信息。

背景

在加载软件包并将其附加到搜索路径时,新软件包中定义的对象与搜索路径上其他软件包已提供对象之间可能会发生冲突。有时,这些冲突是软件包作者有意为之的;例如,dplyr 提供了 basestats 中多个函数的自身版本

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

在其他情况下,冲突并非有意为之,并且可能导致问题。我班上的学生经常在 dplyr 之后加载 MASS 软件包,然后得到

library(MASS)
## 
## Attaching package: 'MASS'
## The following object is masked from 'package:dplyr':
## 
##     select

如果此时使用 select,将使用 MASS 中的 select;如果目的是使用 dplyr 中的 select,则会导致难以理解的错误消息。

默认情况下,library()require() 确实会显示有关冲突的消息,但这些消息很容易被忽略。并且,如果调用出现在根本不显示其结果或使用 message = FALSE 块选项的代码块中,则在 Rmarkdown 文档中根本不会看到这些消息。

新机制旨在提供对冲突处理方式的更多控制,特别是允许在需要时进行更严格的检查。不同级别的严格性可能适用于不同的上下文。一些上下文,按可能合理的严格性级别排序

  1. 控制台中进行交互式工作。
  2. 笔记本中进行交互式工作。
  3. RmarkdownSweave 文档中的代码。
  4. 脚本。
  5. 软件包。

软件包中的冲突已由 NAMESPACE 机制处理。其他冲突可能需要一些帮助。特别是脚本可能会受益于能够准确指定应从所用软件包中提供哪些对象,就像在软件包 NAMESPACE 文件中指定显式导入一样。

另一个有用的区别是预期冲突和意外冲突

  • 预期冲突发生在附加单个软件包时,而该软件包又可能导致附加其他软件包。软件包作者将看到消息,并应解决任何非预期消息。这些冲突通常不需要用户干预。

  • 意外冲突发生在用户请求附加两个可能未设计为一起使用的软件包时。在这些情况下,冲突通常需要用户做出适当的选择。

在初始示例中,加载 dplyr 导致的冲突将是预期冲突,但随后加载 MASS 导致的冲突将是意外冲突。对于交互式工作以及 Rmarkdown 文档中的代码,能够使用允许预期冲突但对未明确解决的意外冲突发出错误信号的设置将非常有用。

指定策略

可以通过 conflicts.policy 选项自定义冲突处理过程的多个方面。此选项的值可以是指定标准策略的字符串,或指定策略详细信息的具有命名元素的列表。支持的命名策略为 "strict""depends.ok"。支持的策略元素

  • error:如果为 TRUE,则冲突会产生错误。
  • generics.ok:如果设置,则此选项将确定是否将函数屏蔽为 S4 通用版本会被视为冲突。严格检查的默认值为 FALSE,否则为 TRUE
  • can.mask:一个字符向量,其中包含允许屏蔽而不产生错误的包的名称。指定基本包可以减少所需的显式屏蔽批准数量。
  • depends.ok:如果为 TRUE,则允许在单个包加载中产生的所有冲突。
  • warn:将 warn.conflicts 参数的默认值设置为 libraryrequire

适用于希望防止意外冲突的大多数用户的规范

options(conflicts.policy =
            list(error = TRUE,
                 generics.ok = TRUE,
                 can.mask = c("base", "methods", "utils",
                              "grDevices", "graphics",
                              "stats"),
                 depends.ok = TRUE))

此规范假设包作者知道自己在做什么,并且从包本身加载的所有冲突都是有意的且可以接受的。通过此规范,所有 CRANBIOC 包都应该可以单独加载而不会出错。指定此策略的快捷方式是名称 "depends.ok"

options(conflicts.policy = "depends.ok")

严格的策略规范将是

options(conflicts.policy = list(error = TRUE, warn = FALSE))

也可以指定为命名策略 "strict"

options(conflicts.policy = "strict")

解决冲突

使用 "strict" 策略,尝试加载冲突包会产生错误

options(conflicts.policy = "strict")
library(dplyr)
## Error: Conflicts attaching package ‘dplyr’:
##
## The following objects are masked from ‘package:stats’:
##
##     filter, lag
##
## The following objects are masked from ‘package:base’:
##
##     intersect, setdiff, setequal, union

使用严格检查发出的错误属于 packageConflictsError 类,具有字段 packageconflicts

明确指定允许屏蔽已在搜索路径上的变量的名称可以避免错误

library(dplyr,
        mask.ok = c("filter", "lag",
                    "intersect", "setdiff", "setequal",
                    "union"))

使用 "depends.ok" 策略,调用 library(dplyr) 将成功并生成有关冲突的通常消息。

使用 "strict""depends.ok" 策略,在 dplyr 之后加载 MASS 将产生错误

library(MASS)
## Error: Conflicts attaching package ‘MASS’:
##
## The following object is masked from ‘package:dplyr’:
##
##     select

消除此错误的一种方法是在附加 MASS 导出时排除 select

library(MASS, exclude = "select")

仍可以使用 MASS 中的 select 函数,方法是使用 MASS::select

为了更轻松地对常用的包和隐式附加的包使用严格策略,conflictRules 提供了一种指定包的默认 mask.okexclude 值的方法,例如,使用

conflictRules("dplyr",
              mask.ok = c("filter", "lag",
                          "intersect", "setdiff",
                          "setequal", "union"))
conflictRules("MASS", exclude = "select")

仅允许屏蔽特定包中的变量

library(dplyr, mask.ok = list(stats = c("filter", "lag"),
                              base = c("intersect", "setdiff",
                                       "setequal", "union")))

允许屏蔽特定包中的所有变量

library(dplyr, mask.ok = list(base = TRUE, stats = TRUE))

library() 有一些启发式方法来避免对 S4 覆盖发出警告。这些启发式方法屏蔽了 Matrix 中的覆盖,但不会屏蔽 BiocGenerics 中的覆盖。在强制执行严格冲突检查时,这些启发式方法默认禁用,因此必须以某种方式显式处理它们。

此规范允许使用严格检查加载 Matrix

conflictRules("Matrix",
              mask.ok = list(stats = TRUE,
                             graphics = "image",
                             utils = c("head", "tail"),
                             base = TRUE))

类似地,这将适用于 BiocGenerics

conflictRules("BiocGenerics",
              mask.ok = list(base = TRUE,
                             stats = TRUE,
                             parallel = TRUE,
                             graphics = c("image", "boxplot"),
                             utils = "relist"))

将来可能值得考虑是否可以将此信息编码在包的 DESCRIPTION 文件中。

其他严格性

已将参数 include.onlyattach.required 添加到 library()require() 中,以便在使用 "strict" 策略的脚本中提供其他控制

  • 如果 include.only 作为字符向量提供,则仅将其中命名的变量包含在附加的框架中。
  • 仅当 attach.requiredTRUE 时,才会附加在 DESCRIPTION 文件的 Depends 字段中指定的包。如果 include.only 缺失,则 attach.required 的默认值为 TRUE;如果提供了 include.only,则为 FALSE

其他方法

conflictedimport 提供了处理冲突的其他方法;还有几个名为 modules 的包。

conflicted 包安排在评估搜索路径上由多个包定义的全局变量时发出错误信号,除非已指定明确解决冲突的方法。

能够要求更严格的冲突处理肯定是个好主意,但它似乎更自然地属于基础 R 而不是包。包方法存在一些缺点,至少在 conflicted 中当前实现的方法中,包括相当重量级、令人困惑的 find,以及无法处理具有执行以下操作的功能的包

foo <- function(x) { require(bar); ... }

这并不是一个好主意,但它就在那里。

conflicted 中仅在使用符号时发出错误信号的方法可能被描述为动态方法。这种动态方法可以通过调整全局缓存机制在基础 R 中更有效地实现。相比之下,R 3.6.0 中引入的机制坚持在 library()require() 调用发生时解决冲突。这可能被称为静态或声明式方法。

要考虑这些选项,最好回顾一下本文开头列出的可能需要提高冲突处理严格程度的活动范围。对于 Rmarkdown 文档和脚本,静态方法显然更好,只要有一组好的工具可以简洁地表示冲突解决。对于笔记本,我认为静态方法可能也更好。对于交互式使用,这可能是个人的喜好问题。一旦我决定需要冲突帮助,我更愿意被迫在加载包时解决冲突,而不是在随机点中断我的工作流程去整理事情。