5  The package within

本书的这一部分以与开始相同的方式结束,开发了一个小玩具包。 Chapter 1 建立了包开发的基本机制、工作流程和工具,但几乎没有提及包内的 R 代码。 本章主要关注包的 R 代码以及它与脚本中的 R 代码有何不同。

从数据分析脚本开始,您将学习如何找到隐藏在其中的包。 您将从脚本中分离并提取可重用的数据和逻辑,将其放入 R 包中,然后在简化得多的脚本中使用该包。 我们在此过程中包含了一些菜鸟错误,以突出包内 R 代码的特殊注意事项。

请注意,节标题包含北约音标(alfa、bravo 等),没有特定含义。 它们只是一种方便的方式来标记我们在工作包方面取得的进展。 仅通过阅读就可以跟进,并且本章是完全独立的,即它不是本书后面材料的先决条件。 但是,如果您希望在此过程中查看特定文件的状态,可以在 source files for the book 中找到它们。

5.1 Alfa: a script that works

让我们考虑一下 data-cleaning.R,这是一个虚构的数据分析脚本,用于收集游泳者的报告:

你在哪里游泳,外面有多热?

他们的数据通常以 CSV 文件的形式出现,例如 swim.csv

name,where,temp
Adam,beach,95
Bess,coast,91
Cora,seashore,28
Dale,beach,85
Evan,seaside,31

data-cleaning.R 首先将 swim.csv 读取到数据框中:

infile <- "swim.csv"
(dat <- read.csv(infile))
#>   name    where temp
#> 1 Adam    beach   95
#> 2 Bess    coast   91
#> 3 Cora seashore   28
#> 4 Dale    beach   85
#> 5 Evan  seaside   31

然后,他们根据选择用来描述海洋和陆地交汇处的沙地的词,将每个观察结果分类为使用美式(“US”)或英式(“UK”)英语。 where 列用于构建新的 english 列。

dat$english[dat$where == "beach"] <- "US"
dat$english[dat$where == "coast"] <- "US"
dat$english[dat$where == "seashore"] <- "UK"
dat$english[dat$where == "seaside"] <- "UK"

遗憾的是,温度通常以华氏度和摄氏度的混合形式报告。 在没有更好的信息的情况下,他们猜测美国人报告的温度是华氏度,因此这些观测值被转换为摄氏度。

dat$temp[dat$english == "US"] <- (dat$temp[dat$english == "US"] - 32) * 5/9
dat
#>   name    where temp english
#> 1 Adam    beach 35.0      US
#> 2 Bess    coast 32.8      US
#> 3 Cora seashore 28.0      UK
#> 4 Dale    beach 29.4      US
#> 5 Evan  seaside 31.0      UK

最后,这个清理过(更干净?)的数据被写回 CSV 文件。 他们喜欢在执行此操作时在文件名中捕获时间戳1

now <- Sys.time()
timestamp <- format(now, "%Y-%B-%d_%H-%M-%S")
(outfile <- paste0(timestamp, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile)))
#> [1] "2023-七月-02_15-54-30_swim_clean.csv"
write.csv(dat, file = outfile, quote = FALSE, row.names = FALSE)

这是 data-cleaning.R 的全部内容:

infile <- "swim.csv"
(dat <- read.csv(infile))

dat$english[dat$where == "beach"] <- "US"
dat$english[dat$where == "coast"] <- "US"
dat$english[dat$where == "seashore"] <- "UK"
dat$english[dat$where == "seaside"] <- "UK"

dat$temp[dat$english == "US"] <- (dat$temp[dat$english == "US"] - 32) * 5/9
dat

now <- Sys.time()
timestamp <- format(now, "%Y-%B-%d_%H-%M-%S")
(outfile <- paste0(timestamp, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile)))
write.csv(dat, file = outfile, quote = FALSE, row.names = FALSE)

即使您的典型分析任务非常不同,希望您在这里看到一些熟悉的模式。 很容易想象,随着时间的推移,这个小组对许多相似的数据文件进行了非常相似的预处理。 如果他们将这些标准数据操作作为包中的函数提供给他们自己使用,而不是将相同的数据和逻辑内联到几十个或数百个数据摄取脚本中,他们的分析就会更加高效和一致。

5.2 Bravo: a better script that works

隐藏在原始脚本中的包实际上很难看到! 它被一些次优的编码实践所掩盖,例如使用重复的复制/粘贴式代码以及代码和数据的混合。 因此,好的第一步是重构此代码,分别在适当的对象和函数中隔离尽可能多的数据和逻辑。

出于几个原因,这也是介绍一些附加包的使用的好时机。 首先,我们实际上会使用 tidyverse 进行此类数据整理。 其次,许多人在他们的脚本中使用附加包,因此很高兴了解如何在包内处理附加包。

这是脚本的新改进版本。

library(tidyverse)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

lookup_table <- tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

dat <- dat %>% 
  left_join(lookup_table)

f_to_c <- function(x) (x - 32) * 5/9

dat <- dat %>% 
  mutate(temp = if_else(english == "US", f_to_c(temp), temp))
dat

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}
write_csv(dat, outfile_path(infile))

需要注意的主要变化是:

  • 我们正在使用 tidyverse 包中的函数(特别是来自 readr 和 dplyr 的函数),并且我们通过 library(tidyverse) 提供它们。
  • 不同 “beach” 单词之间的映射以及它们被认为是美国英语还是英国英语现在被隔离在一个查找表中,这让我们可以使用 left_join() 一次性创建英语列。 这个查找表使映射更容易理解,并且将来更容易用新的 “beach” 词进行扩展。
  • f_to_c()timestamp()outfile_path() 是新的辅助函数,它们包含转换温度和形成带时间戳的输出文件名的逻辑。

识别此脚本的可重用位变得越来越容易,即与特定输入文件无关的位,如 swim.csv。 这种重构通常在创建您自己的包的过程中自然发生,但如果没有发生,最好有意这样做。

5.3 Charlie: a separate file for helper functions

典型的下一步是将可重用数据和逻辑从分析脚本中移出,并移到一个或多个单独的文件中。 如果您想在多个分析中使用这些相同的帮助文件,这是一个常规的开场白。

以下是 beach-lookup-table.csv 的内容:

where,english
beach,US
coast,US
seashore,UK
seaside,UK

下面是 cleaning-helpers.R 的内容:

library(tidyverse)

localize_beach <- function(dat) {
  lookup_table <- read_csv(
    "beach-lookup-table.csv",
    col_types = cols(where = "c", english = "c")
  )
  left_join(dat, lookup_table)
}

f_to_c <- function(x) (x - 32) * 5/9

celsify_temp <- function(dat) {
  mutate(dat, temp = if_else(english == "US", f_to_c(temp), temp))
}

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

我们已将一些高级帮助函数 localize_beach()celsify_temp() 添加到预先存在的帮助函数(f_to_c()timestamp()outfile_path())中。

这是数据清理脚本的下一个版本,现在我们已经提取了帮助函数(和查找表)。

library(tidyverse)
source("cleaning-helpers.R")

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

(dat <- dat %>% 
    localize_beach() %>% 
    celsify_temp())

write_csv(dat, outfile_path(infile))

请注意,脚本变得越来越短,并且希望更易于阅读和修改,因为重复和繁琐的杂乱已经消失了。 代码是否真的更容易使用是主观的,取决于实际预处理游泳数据的人对“界面”的感觉有多自然。 这些类型的设计决策是一个单独项目的主题:design.tidyverse.org

让我们假设小组同意我们的设计决策是有前途的,即我们似乎正在让事情变得更好,而不是更糟。 当然,现有代码并不完美,但这是一个典型的开发阶段,当您试图弄清楚辅助函数应该是什么以及它们应该如何工作时。

5.4 Delta: a failed attempt at making a package

虽然第一次创建包的尝试将以失败告终,但通过一些常见的失误仍然有助于阐明幕后发生的事情。

为了将 cleaning-helpers.R 转换为合适的包,您可能会采取以下最简单的步骤:

  • 使用 usethis::create_package("path/to/delta") 搭建一个名为 “delta” 的新 R 包。
    • 这是很好的第一步!
  • cleaning-helpers.R 复制到新包中,具体为 R/cleaning-helpers.R
    • 这在道德上是正确的,但在几个方面在机制上是错误的,我们很快就会看到。
  • beach-lookup-table.csv 复制到新包中。但是哪里?让我们试试源码包的顶层。
    • 这不会有好结果。在包中运送数据文件是一个特殊主题,将在 Chapter 7 中介绍。
  • 安装此包,可能使用 devtools::install() 或通过 Ctrl + Shift + B(Windows 和 Linux)或 RStudio 中的 Cmd + Shift + B。
    • 尽管存在上述所有问题,但这确实有效!这很有趣,因为我们可以(尝试)使用它并看看会发生什么。

这是您希望在成功安装此软件包(我们称之为 “delta”)后运行的下一个数据清理脚本版本。

library(tidyverse)
library(delta)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()

write_csv(dat, outfile_path(infile))

我们之前脚本的唯一变化是

source("cleaning-helpers.R")

被替换成了

library(delta)

如果您安装 delta 包并尝试运行数据清理脚本,实际会发生以下情况:

library(tidyverse)
library(delta)

infile <- "swim.csv"
dat <- read_csv(infile, col_types = cols(name = "c", where = "c", temp = "d"))

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()
#> Error in localize_beach(.) : could not find function "localize_beach"

write_csv(dat, outfile_path(infile))
#> Error in outfile_path(infile) : could not find function "outfile_path"

即使您调用 library(delta),实际上也没有任何帮助函数可供使用! 与 source()ing 帮助函数文件相比,附加包不会将其函数转储到全局工作区中。 默认情况下,包中的函数仅供内部使用。 您需要导出 localize_beach()celsify_temp()outfile_path(),以便您的用户可以调用它们。 在 devtools 工作流程中,我们通过将 @export 放在每个函数上方的特殊 roxygen 注释中来实现这一点(名称空间管理在 Section 11.3 中介绍),如下所示:

#' @export
celsify_temp <- function(dat) {
  mutate(dat, temp = if_else(english == "US", f_to_c(temp), temp))
}

在将 @export 标记添加到 localize_beach()celsify_temp()outfile_path() 之后,运行 devtools::document() 以(重新)生成 NAMESPACE 文件,并重新安装 delta 包。 现在,当您重新执行数据清理脚本时,它就可以工作了!

更正:它有时会起作用。 具体来说,当且仅当工作目录设置为源包的顶层时,它才有效。 从任何其他工作目录,您仍然会收到错误消息:

dat <- dat %>% 
  localize_beach() %>% 
  celsify_temp()
#> Error: 'beach-lookup-table.csv' does not exist in current working directory ('/Users/jenny/tmp').

找不到在 localize_beach() 内部查询的查找表。 人们不会简单地将 CSV 文件转储到 R 包的源代码中并期望事情“正常工作”。 我们将在包的下一次迭代中解决这个问题(Chapter 7 全面介绍了如何在包中包含数据)。

在我们放弃这个最初的实验之前,让我们也惊叹于您能够安装、附加并在一定程度上使用一个根本损坏的包这一事实。 devtools::load_all() 也工作正常! 这是一个发人深省的提醒,您应该在开发过程中经常运行 R CMD check,可能是通过 devtools::check()。 这将迅速提醒您注意许多简单安装和使用无法揭示的问题。

事实上,这个包的 check() 失败了,你会看到:

 * installing *source* package ‘delta’ ...
 ** using staged installation
 ** R
 ** byte-compile and prepare package for lazy loading
 Error in library(tidyverse) : there is no package called ‘tidyverse’
 Error: unable to load R code in package ‘delta’
 Execution halted
 ERROR: lazy loading failed for package ‘delta’
 * removing ‘/Users/jenny/rrr/delta.Rcheck/delta’

“there is no package called ‘tidyverse’” 是什么意思? 我们在主脚本中使用它,没有任何问题! 另外,我们已经安装并使用了这个包,为什么 R CMD check 找不到呢?

R CMD check 的严格性符合 R/cleaning-helpers.R 的第一行时会发生此错误:

这不是您声明包依赖于另一个包(在本例中为 tidyverse)的方式。 这也不是您使另一个包中的函数可用于您的包的方式。 依赖关系必须在 DESCRIPTION 中声明(这还不是全部)。 由于我们声明没有依赖关系,R CMD check 相信我们并尝试安装我们的包,只有可用的基础包,这意味着这个 library(tidyverse) 调用失败。 “常规”安装成功,仅仅是因为 tidyverse 在您的常规库中可用,它隐藏了这个特定的错误。

回顾一下,将 cleaning-helpers.R 复制到 R/cleaning-helpers.R,而无需进一步修改,在(至少)以下方面存在问题:

  • 不考虑导出函数与非导出函数。
  • 在已安装的包中找不到包含查找表的 CSV 文件。
  • 没有正确声明我们对其他附加包的依赖。

5.5 Echo: a working package

我们已经准备好制作这个包的最小版本。

这是 R/cleaning-helpers.R 的新版本2

lookup_table <- dplyr::tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

#' @export
localize_beach <- function(dat) {
  dplyr::left_join(dat, lookup_table)
}

f_to_c <- function(x) (x - 32) * 5/9

#' @export
celsify_temp <- function(dat) {
  dplyr::mutate(dat, temp = dplyr::if_else(english == "US", f_to_c(temp), temp))
}

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")

#' @export
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

我们已经回到使用 R 代码定义 lookup_table,因为最初尝试从 CSV 读取它会造成某种文件路径混乱。 这对于小的、内部的、静态的数据是可以的,但是请记住参阅 Chapter 7 以了解在包中存储数据的更通用的技术。

现在,所有对 tidyverse 函数的调用都已使用实际提供该函数的特定包的名称进行限定,例如 dplyr::mutate()。 还有其他方法可以访问另一个包中的函数,如 Section 11.4 所述,但这是我们推荐的默认方法。 我们也强烈建议没有人依赖 package 中的 tidyverse meta-package3。 相反,最好确定您实际使用的特定包。 在这种情况下,包只使用 dplyr。

library(tidyverse) 调用消失了,取而代之的是我们在 DESCRIPTIONImports 字段中声明使用 dplyr

Package: echo
(... other lines omitted ...)
Imports: 
    dplyr

这与使用命名空间限定的调用(如 dplyr::left_join())一起,构成了在您的包中使用另一个包的有效方法。 通过 DESCRIPTION 传送的 metadata 在 Chapter 9 中介绍。

所有面向用户的函数在其 roxygen 注释中都有一个 @export 标记,这意味着 devtools::document() 将它们正确添加到 NAMESPACE 文件中。 请注意,f_to_c() 目前仅在内部使用,在 celsify_temp() 内部,因此不会导出(对于 timestamp() 也是如此)。

这个版本的包可以安装、使用,并且它在技术上通过了 R CMD check,尽管有 1 warning 和 1 note。

* checking for missing documentation entries ... WARNING
Undocumented code objects:
  ‘celsify_temp’ ‘localize_beach’ ‘outfile_path’
All user-level objects in a package should have documentation entries.
See chapter ‘Writing R documentation files’ in the ‘Writing R
Extensions’ manual.

* checking R code for possible problems ... NOTE
celsify_temp: no visible binding for global variable ‘english’
celsify_temp: no visible binding for global variable ‘temp’
Undefined global functions or variables:
  english temp

“no visible binding” note 是在包内使用 dplyr 和不带引号的变量名的一个特点,其中使用裸变量名(englishtemp)看起来很可疑。 您可以将这些行中的任何一行添加到 R/ 下面的任何文件中以删除此注释(例如 Section 16.7 中描述的包级文档文件):

# option 1 (then you should also put utils in Imports)
utils::globalVariables(c("english", "temp"))

# option 2
english <- temp <- NULL

我们看到围绕像 dplyr 这样大量使用非标准评估的包进行编程可能很棘手。 在幕后,这是允许 dplyr 的最终用户使用裸(未引用)变量名的技术。 像 dplyr 这样的软件包优先考虑典型最终用户的体验,但代价是让他们更难以依赖。 上面显示的两个用于抑制 “no visible binding” note 的选项代表入门级解决方案。 有关这些问题的更复杂处理,请参阅 vignette("in-packages", package = "dplyr")vignette("programming", package = "dplyr")

关于缺少文档的 warning 是因为没有正确记录导出的函数。 这是一个合理的问题,您绝对应该在一个真正的包中解决这个问题。 您已经在 Section 1.12 中了解了如何创建带有 roxygen 注释的帮助文件,我们将在 Chapter 16 中全面介绍该主题。

5.6 Foxtrot: build time vs. run time

echo 包很好用,但小组成员注意到时间戳有些奇怪:

Sys.time()
#> [1] "2023-03-26 22:48:48 PDT"

outfile_path("INFILE.csv")
#> [1] "2020-September-03_11-06-33_INFILE_clean.csv"

带时间戳的文件名中的日期时间不反映系统报告的时间。 事实上,用户声称时间戳似乎永远不会改变! 为什么是这样?

回想一下我们是如何形成输出文件的文件路径的:

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

事实上,我们在 outfile_path() 的定义之外捕获了 now <- Sys.time() 可能已经让一些读者烦恼了一段时间。 now 反映了我们执行 now <- Sys.time() 时的时间。 在最初的方法中,现在是在我们 source()d cleaning-helpers.R 时分配的。 这并不理想,但这可能是一个非常无害的错误,因为帮助文件将在我们编写输出文件之前不久被 source()d。

但这种方法在包的上下文中是相当具有破坏性的。 now <- Sys.time() 在构建包时执行4。 再也不会了。 很容易假设您的包裹代码在附加或使用包裹时被重新评估。 但事实并非如此。 是的,函数中的代码在调用时绝对会运行。 但是您的函数——以及在 R/ 下面的顶级代码中创建的任何其他对象——在构建时只定义一次。

通过在 R/ 下使用顶级代码定义 now,我们注定了我们的包会用相同(错误)的时间为其所有输出文件加上时间戳。 解决方法是确保 Sys.time() 调用在运行时发生。

我们再看一下 R/cleaning-helpers.R 的部分内容:

lookup_table <- dplyr::tribble(
      ~where, ~english,
     "beach",     "US",
     "coast",     "US",
  "seashore",     "UK",
   "seaside",     "UK"
)

now <- Sys.time()
timestamp <- function(time) format(time, "%Y-%B-%d_%H-%M-%S")
outfile_path <- function(infile) {
  paste0(timestamp(now), "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

这段摘录中有四个顶级 <- 赋值。 数据框 lookup_table 和函数 timestamp()outfile_path() 的顶级定义是正确的。 这些在构建时只定义一次是合适的。 now 的顶级定义,然后在 outfile_path() 内部使用,令人遗憾。

以下是 outfile_path() 的更好版本:

# always timestamp as "now"
outfile_path <- function(infile) {
  ts <- timestamp(Sys.time())
  paste0(ts, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

# allow user to provide a time, but default to "now"
outfile_path <- function(infile, time = Sys.time()) {
  ts <- timestamp(time)
  paste0(ts, "_", sub("(.*)([.]csv$)", "\\1_clean\\2", infile))
}

这说明在包内定义对象时需要有不同的思维方式。 这些对象中的绝大多数应该是函数,并且这些函数通常应该只使用它们创建的数据或通过参数传递的数据。 有一些类型的草率在函数使用前立即定义时是相当无害的,但对于作为包分发的函数来说,这种草率的代价可能更高。

5.7 Golf: side effects

时间戳现在反映了当前时间,但该小组提出了一个新的问题。 就目前而言,时间戳反映了谁完成了数据清理以及他们在世界的哪个部分。 时间戳策略的核心是这种格式 string5

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> [1] "2023-七月-02_15-54-31"

这会将 Sys.time() 格式化为包含月份名称(而非数字)和本地时间6

Table 5.1 显示了当这样一个时间戳是由几个假设的同事在完全相同的时间及时清理一些数据时产生的。

Table 5.1: Timestamp varies by locale and timezone.
location timestamp LC_TIME tz
Rome, Italy 2020-settembre-05_00-30-00 it_IT.UTF-8 Europe/Rome
Warsaw, Poland 2020-wrzesień-05_00-30-00 pl_PL.UTF-8 Europe/Warsaw
Sao Paulo, Brazil 2020-setembro-04_19-30-00 pt_BR.UTF-8 America/Sao_Paulo
Greenwich, England 2020-September-04_23-30-00 en_GB.UTF-8 Europe/London
“Computer World!” 2020-September-04_22-30-00 C UTC

请注意,月份名称会有所不同,时间也会有所不同,甚至日期也会有所不同! 最安全的选择是根据固定的语言环境和时区形成时间戳(大概是上面 “Computer World!” 所代表的非地理选择)。

您进行了一些研究,了解到可以通过 Sys.setlocale() 强制使用特定区域设置,并通过设置 TZ 环境变量强制使用特定时区。 具体来说,我们将语言环境的 LC_TIME 组件设置为 “C”,将时区设置为 “UTC”(协调世界时)。 这是您改进 timestamp() 的第一次尝试:

timestamp <- function(time = Sys.time()) {
  Sys.setlocale("LC_TIME", "C")
  Sys.setenv(TZ = "UTC")
  format(time, "%Y-%B-%d_%H-%M-%S")
}

但是您的巴西同事注意到,在她使用您的包中的 outfile_path() 之前和之后,日期时间打印不同:

之前:

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> [1] "2023-julho-02_04-54-33"

之后:

outfile_path("INFILE.csv")
#> [1] "2023-July-02_07-54-31_INFILE_clean.csv"

format(Sys.time(), "%Y-%B-%d_%H-%M-%S")
#> [1] "2023-July-02_07-54-33"

请注意,她的月份名称从葡萄牙语切换为英语,而且时间显然是在不同的时区报告的。 在 timestamp() 中对 Sys.setlocale()Sys.setenv() 的调用对她的 R 会话进行了持续(并且非常令人惊讶)的更改。 这种副作用是非常不受欢迎的,并且极难追踪和调试,尤其是在更复杂的设置中。

以下是 timestamp() 的更好版本:

# use withr::local_*() functions to keep the changes local to timestamp()
timestamp <- function(time = Sys.time()) {
  withr::local_locale(c("LC_TIME" = "C"))
  withr::local_timezone("UTC")
  format(time, "%Y-%B-%d_%H-%M-%S")
}

# use the tz argument to format.POSIXct()
timestamp <- function(time = Sys.time()) {
  withr::local_locale(c("LC_TIME" = "C"))
  format(time, "%Y-%B-%d_%H-%M-%S", tz = "UTC")
}

# put the format() call inside withr::with_*()
timestamp <- function(time = Sys.time()) {
  withr::with_locale(
    c("LC_TIME" = "C"),
    format(time, "%Y-%B-%d_%H-%M-%S", tz = "UTC")
  )
}

这些显示了各种方法来限制我们对 LC_TIME 和时区的更改范围。 一个好的经验法则是尽可能缩小此类更改的范围并切实可行。 format()tz 参数是处理时区的最手术方法,但 LC_TIME 不存在类似的方法。 我们使用 withr 包进行临时区域设置修改,它为临时状态更改提供了一个非常灵活的工具包。 这(和 base::on.exit())将在 Section 6.5 中进一步讨论。 请注意,如果您像我们上面那样使用 withr,则需要在 ImportsDESCRIPTION 中列出它(Chapter 11, Section 10.1.3)。

这强调了上一节的一点:在包内定义函数时需要采用不同的思维方式。 尽量避免对用户的整体状态进行任何更改。 如果此类更改不可避免,请确保撤销它们(如果可能)或明确记录它们(如果与功能的主要目的相关)。

5.8 Concluding thoughts

最后,经过多次迭代,我们成功将游泳调查的重复数据清理代码提取到 R 包中。 这个例子结束了本书的第一部分,标志着过渡到关于特定包组件的更详细的参考资料。 在我们继续之前,让我们回顾一下本章中学到的教训。

5.8.1 Script vs.package

当您第一次听说 R 专家用户经常将他们的代码放入包中时,您可能想知道这到底是什么意思。 具体来说,您现有的 R 脚本、R Markdown 报告和 Shiny 应用程序会发生什么变化? 是否所有这些代码都以某种方式被放入一个包中? 在大多数情况下,答案是否定的。

通常,您会识别跨多个项目发生的某些重复操作,这就是您提取到 R 包中的内容。 你仍然会有 R 脚本、R Markdown 报告和 Shiny 应用程序,但是通过将特定的代码片段移动到一个正式的包中,你的数据产品往往会变得更加简洁和易于维护。

5.8.2 Finding the package within

尽管本章中的示例相当简单,但它仍然捕获了开发供个人或组织使用的 R 包的典型过程。 您通常从分散在不同项目中的一组特殊且相关的 R 脚本开始。 随着时间的推移,您开始注意到某些需求一遍又一遍地出现。

每次重新访问类似的分析时,与上一次迭代相比,您可能会尝试提升游戏水平。 您使用更健壮的模式重构复制/粘贴式代码,并开始将关键“移动”封装在辅助函数中,这些函数最终可能会迁移到它们自己的文件中。 一旦你到达这个阶段,你就可以采取下一步并创建一个包。

5.8.3 Package code is different

编写包代码与编写 R 脚本有点不同,在进行这种调整时自然会感到有些不适。 以下是一开始让我们中的许多人感到困惑的最常见陷阱:

  • 包代码需要新的方法来处理其他包中的函数。DESCRIPTION 文件是声明依赖关系的主要方式;我们不通过 library(somepackage) 来做到这一点。
  • 如果您希望数据或文件持久可用,可以使用包特定的存储和检索方法。您不能只是将文件放入包中并希望得到最好的结果。
  • 有必要明确哪些功能是面向用户的,哪些是内部助手。默认情况下,函数不会被导出供其他人使用。
  • 需要新级别的纪律来确保代码在预期时间(构建时间与运行时间)运行并且没有意外的副作用。

  1. Sys.time() 返回一个 POSIXct 类的对象,因此当我们对其调用 format() 时,我们实际上使用的是 format.POSIXct()。 如果您不熟悉此类格式字符串,请阅读 ?format.POSIXct 的帮助。↩︎

  2. 将所有内容都放在一个文件中并使用此名称并不理想,但技术上是允许的。 我们在 Section 6.1 中讨论 R/ 下面的文件的组织和命名。↩︎

  3. 博客文章 The tidyverse is for EDA, not packages 对此进行了详细说明。↩︎

  4. 这里我们指的是编译包代码的时间,可以是生成二进制文件时(for macOS or Windows; Section 3.4),也可以是从源代码安装包时(Section 3.5)。↩︎

  5. Sys.time() 返回一个 POSIXct 类的对象,因此当我们对其调用 format() 时,我们实际上使用的是 format.POSIXct()。 如果您不熟悉此类格式字符串,请阅读 ?format.POSIXct 的帮助。↩︎

  6. 显然,根据 ISO 8601 进行格式化会更好,ISO 8601 按数字对月份进行编码,但是为了使这个示例更加明显,请迁就我。↩︎