R包开发

| 振导社会  | 程序设计  R 

目录

概述

install.packages("x") # 安装包
library("x") # 加载包
help("x") # 寻求帮助(需要先加载)

包(package)常作为分享代码的基本单元,代码封装成包可以方便其他用户使用。包开发的哲学是能自动化完成的就应当被自动化。

devtools使包开变得轻松,并且加少了潜在的错误,使得可以专注于解决具体问题而非包开发。它与RStudio协同工作,将包开发从底层细节中中解脱出来。学习包开发的最好资源是官方文档“writing R extensions”,若缺乏必要的基础知识,理解起来仍然吃力。

包开发的几个部分,按重要程度递减大致为:

  • R代码:位于R/文件夹,放置R代码文件,只有这一个目录的文件夹也是有用的包。
  • 包的元信息(metadata):DESCRIPTION文件描述包工作的前提条件,如果分享该包,文件中一般还包含该包的功能说明、使用权限等。
  • 文档:介绍如何使用该包,可以采用roxygen2生成函数的文档。
  • Vignettes:描述包中每个函数细节的文档,展示了包中各部分配合使用解决具体问题。可用Rmarkdown和knitr创造惊艳的效果。
  • 测试集:确保包按设计工作,testthat包可将已完成的交互式测试转换为正式的自动化测试。
  • 命名空间(Namespace):定义该包的哪些函数可供其它包使用,以及需要哪些包的其它函数。命名空间可用roxygen2生成。它是包开发最具挑战性的工作之一。若要让包可靠的工作需要精通它。
  • 外部数据:data/目录为包提供数据,可以供R用户方便的使用,或者为文档提供令人信服的例子。
  • 可编译的代码:R代码让人使用高效,而不是让计算机高效工作。src/文件夹包含C和C++代码以解决包中的性能瓶颈。
  • 其它部分:demo/exec/po/tools/等。

R通过R CMD check提供了非常有用的自动化质量检测工具,通过它可以避免很多常见问题。

快速入门

创建包

安装包开发需要的相关包:

install.packages(c("devtools", "roxygen2", "testthat", "knitr"))

# 需要最新的RStudio(预览版)
install.packages("rstudioapi")
rstudioapi::isAvailable("0.99.149")

# 增强的devtools
devtools::install_github("hadley/devtools")

# 检查是否有编译C代码的相关工具链,若没有则需安装
devtools::has_devel()

创建包之前首先要为包选择一个合适的名字,名字只能包含字母、数字和英语句号,必须以字母开头且不能以句号结尾。

创建包可以用RStudio的图形化界面或者命令:

devtools::create("path/to/package/pkgname")

以上方法会为包增加一个.Rproj文件方便RStduio使用,如果现有的包没有这个文件,可以为其创建:

devtools::use_rstudio("path/to/package")

一般不要用package.skeleton()创建包,在让尽快包运行之前,还得删掉那些不必要的文件。

包的不同形式

包的生命周期可能会经历 sourcebundledbinaryinstalledin-memory五个阶段。

一、source包

开发时的源代码包,包含R/DESCRIPTION等。

二、bundled包

将source包压缩成一个.tar.gz文件的包,可以用devtools::build()创建。它和source包的区别在于:

  • Vignettes被编译成了HTML和PDF文件;
  • 没有了source包中的临时文件。

source包中.Rbuildignore文件中标明的文件被排除在bundled包外,.Rbuildignore的每一行是Perl兼容的正则表达式,大小写不敏感,即可匹配目录也可匹配文件。

^.*\.Rproj$         # Automatically added by RStudio,
^\.Rproj\.user$     # used for temporary files. 
^README\.Rmd$       # An Rmarkdown file used to generate README.md
^cran-comments\.md$ # Comments for CRAN submission
^NEWS\.md$          # A news file written in Markdown
^\.travis\.yml$     # Used for continuous integration testing with travis

正则表达式^notes$匹配包含notes的任意文件,比如:R/notes.R、man/important-notes.R、data/endnotes.Rdata等。比较保险的做法是采用devtools::use_build_ignore("notes"),自动添加了转意字符处理,将.变为\.并且添加了^$

三、binary包

binary包可供没有编译环境的R用户使用,它也是单个文件,但是解压开和source包差别很大:

  • R/文件夹中并没有R文件,而是3个解析后可供高效调用的文件。这是加载R代码用save()函数保存的结果。
  • Meta/文件夹包含一些rd文件,这些是包相关的元数据缓存,可以用readRDS()查看文件内容,这些文件加速了包的加载。
  • html/文件夹包含了HTML帮助需要的文件。
  • src/目录变成libs/目录,包含了将源代码编译成的库。
  • inst/的内容被移到了顶层目录。

binary包和平台相关,Mac的binary包.tgz和Windows的.zip不能通用,可以利用devtools::build(binary = TRUE)制作binary包。

包之间的文件转换关系

四、installed包

installed包是将binary包解压(安装)到特定的库目录,下图展示了包的不同安装方法。

包的安装方法

R CMD INSTALL命令可以安装各种不同的包,devtools的函数对该命令封装后可在R环境中执行。

  • devtools::install()是对R CMD INSTALL的封装,devtools::build()是对R CMD build的封装。
  • devtools::install_github()从github下载源码包后用build()编译后再R CMD INSTALLdevtools::install_gitorious()devtools::install_bitbucket()包与其类似。
  • install.packages()devtools::install_github()可以安装远程包。

bundled包中的定制.Rinstignore文件可以阻止哪些文件被安装。

五、memory包

使用包之前,必须先加载到内存。用library()加载包后,使用包时不需要用包名,比如:用install()而不用devtools::install()。各种包的不同加载方法:

包的加载方法

库是包含installed包的文件夹,可以同时拥有多个库,可以用.libPaths()查看库的位置:

# 查看所有库的路径
.libPaths()

# 查看每个库中的所有包
lapply(.libPaths(), dir)

library(pkg)require(pkg)加载包时,R会到.libPaths()的路径下去搜索,如果该库不存在就会报错。library()require()的最大区别:找不到包时,library()抛出错误,require()打印警告信息并返回FALSE

packrat应用可以管理项目的包依赖关系,通过packrat升级项目中的包只会影响该项目。

R编程风格

所有的R代码位于R/目录,通常建议用文件组织函数。脚本中的函数和包中的函数有区别。重新加载所有代码用devtools::load_all()或RStudio的快捷键Ctrl/Cmd + Shift + L,它同时会保存所有打开的文件。

虽然可以按任意文件组织函数,但是不建议将所有函数都放入一个文件,也不建议将每个函数作为一个文件。不要用大小写区分文件名,因为有的操作系统不区分大小写。

编码风格

推荐遵循Google的“R style guide”。如果要继续在前人的基础上开发,最好继续遵循以前的编码风格,而非使用自己的新风格。

formatR包可以方便整洁化糟糕的代码风格,但使用前请阅读说明文档

install.packages("formatR")
formatR::tidy_dir("R")

代码linter可作为补充工具,它只是给出警告信息而不修改代码,因此可以发现更多潜在问题,但也会出现误报的情况。

install.packages("lintr")
lintr::lint_package()

对象命名

  • 变量和函数名应为小写,可用下划线分割单词,英文句号是S3方法的保留字;
  • 驼峰命名法亦可作为备选方案;
  • 变量名为名词,函数名为动词;
  • 名字应当简洁且有意义,避免采用已经有的名字。
# Good
day_one
day_1

# Bad
first_day_of_the_month
DayOne
dayone
djm1
T <- FALSE
c <- 10
mean <- function(x) sum(x)

空格

  • 中缀表达式的运算符前后加空格,逗号后通常加空格;
  • 不要为:::::加空格;
  • 除函数调用外,左括号左加空格;
  • 为了美化对其方案,可以增加额外的空格;
  • 圆括号和方括号内侧不要加空格,除非紧邻逗号。
# Good
average <- mean(feet / 12 + inches, na.rm = TRUE)
x <- 1:10
base::get
if (debug) do(x)
plot(x, y)
list(
  total = a + b + c, 
  mean  = (a + b + c) / n
)
diamonds[5, ]

# Bad
average<-mean(feet/12+inches,na.rm=TRUE)
x <- 1 : 10
base :: get
if(debug)do(x)
plot (x, y)
if ( debug ) do(x)  # No spaces around debug
x[1,]   # Needs a space after the comma
x[1 ,]  # Space goes after comma not before

花括号

  • 开花括号不要新启行;
  • 闭花括号独占新行,除非有else语句;
  • 非常简单的语句可位于同一行。
# Good

if (y < 0 && debug) {
  message("Y is negative")
}

if (y == 0) {
  log(x)
} else {
  y ^ x
}

if (y < 0 && debug) message("Y is negative")

# Bad

if (y < 0 && debug)
message("Y is negative")

if (y == 0) {
  log(x)
} 
else {
  y ^ x
}

行长度

每行代码长度不要超过80个字符。如果超越这个长度,或许意味着该用新的函数实现这个功能。

缩进

  • 用2个空格缩进代码,不要用tab或者混用tab和空格;
  • 若函数的定义占用了多行,第2行对其定义起始点。
long_function_name <- function(a = "a long argument", 
                               b = "another argument",
                               c = "another long argument") {
  # As usual code is indented by two spaces.
}

赋值

<-赋值,而非=

# Good
x <- 5
# Bad
x = 5

注释风格

  • 注释行以#开始,紧接一个空格;
  • 注释指出为何这样做,而非做什么;
  • -=注释行,将文件分割成易于阅读的片段。
# Load data ---------------------------

# Plot data ---------------------------

包与脚本中代码的区别

保存为文件的R脚本通过source()函数加载。脚本中的代码和包中的代码有两个主要区别:

  1. 脚本中的代码在加载时运行;包中的代码在生成(build)时运行。这就意味着包中的代码只应当用于创建对象,大多数应当是函数。
  2. 包中的函数将会用在那些你无法预见的场景,设计时务必考虑周全。

一、慎写顶层代码

当采用source()加载脚本时,每行代码被执行,结果立即可用。包的加载分两步。当生成包时(例如CRAN),R/中所有代码被执行,结果被保存。当用library()require()加载包时,缓存的结果就可用了。若要像使用包一样加载脚本,可以用如下方式:

# Load a script into a new environment and save it
env <- new.env(parent = emptyenv())
source("my-script.R", local = env)
save(envir = env, "my-script.Rdata")

# Later, in another R session
load("my-script.Rdata")

对于x <- Sys.time():在脚本中,x为执行source()的时间;在包中,x为生成包的时间。这就意味着,不要在顶层运行代码。包的代码只应用于创建对象,多数为函数。假设foo包中包含如下代码:

library(ggplot2)

show_mtcars <- function() {
  qplot(mpg, wt, data = mtcars)
}

若以如下方式调用:

library(foo)
show_mtcars()

由于qplot()不可用,代码不会工作。顶层代码只在生成包时执行,用library(foo)加载时不会重新执行library(gpplot2)。可以改为如下方案:

show_mtcars <- function() {
  library(ggplot2)
  qplot(mpg, wt, data = mtcars)
}

二、注重代码的普适性

其他用户可能将包用在作者不可预知的场景。因此,要要重视R的应用环境,不仅是函数和对象还包括所有的全局设置。通过library()加载包会改变R的应用环境,或者通过options()setwd()等改变。改变R的应用环境让代码难以理解,不是好习惯。

不要使用那些会改变全局设置的函数,因为有更好的选择:

  • 不要使用library()require():这会改变搜索路径,影响全局环境中函数的可用性。更好的选择是使用DESCRIPTION指出包的需求,这也使得安装包时,相关的依赖包也被安装。
  • 不要使用source()从文件中加载代码:source()改变了当前环境,插入了程序的执行结果。可以使用devtools::load_all()自动source在R/目录中的所有文件。用data/替代source()创建数据集。

其它一些函数也务必当心使用,若使用它们,用on.exit()清除它们的影响:

# 1
old <- options(stringsAsFactors = FALSE)
on.exit(options(old), add = TRUE)

# 2
old <- setwd(tempdir())
on.exit(setwd(old), add = TRUE)

绘图和打印通常也会影响全局R环境,可以通过使用函数将它们和全局环境隔离开。

另一方面,或许应当避免对用户环境的依赖。用户和开发者的环境可能不一致。read.csv()的参数stringsAsFactors的取值来自全局变量stringsAsFactors,用户可能是TRUE,也可能是FALSE

合理利用副作用

.onLoad().onAttach()

包在与外部系统衔接过程中,可能需要在加载时进行初始化设置,这可以通过.onLoad().onAttach()函数实现。当包load和attach时会调用这两个函数。这两个函数可以用在如下场景:

  • 加载包时显示相关信息,启动信息用.onAttach()显示,而非用.onLoad(),显示信息通常用packageStartupMessage()message()
.onAttach <- function(libname, pkgname) {
  packageStartupMessage("Welcome to my package")
}
  • 利用options()自定义用户设置。为避免和其它包冲突,设置项名称钱应当有包名作为前缀,同时要注意不要覆盖已有设置。可用getOption("devtools.name")获得设置信息。
.onLoad <- function(libname, pkgname) {
  op <- options()
  op.devtools <- list(
    devtools.path = "~/R-dev",
    devtools.install.args = "",
    devtools.name = "Your name goes here",
    devtools.desc.author = '"First Last <first.last@example.com> [aut, cre]"',
    devtools.desc.license = "What license is it under?",
    devtools.desc.suggests = NULL,
    devtools.desc = list()
  )
  toset <- !(names(op.devtools) %in% names(op))
  if(any(toset)) options(op.devtools[toset])

  invisible()
}
  • R链接其它编程语言时,比如用rJava::.jpackage()Rcpp::loadRcppModules()等。
  • 通过tools::vignetteEngine()注册vignette引擎。

调用.onLoad().onAttach()的参数libnamepkgname很少被使用,可用.onUnload()清除设置。这些函数通常保存在文件zzz.R中。1

S4类、范型与方法

R包捕获这些副作用,使得他们能在重新加载时被重现,但是它们必须以正确的顺序调用。比如:在定义方法前,必须先定义范型和类。这要求R文件按特定的顺序被source,这个顺序通过DESCRIPTION文件中的Collate来控制。

CRAN事项

提交到CRAN的包的R文件必须只包含ASCII字符。可以在字符串中包含Unicode字符,但必须使用形如\u1234的转意方式,最容易的是使用stringi::stri_escape_unicode()函数:

x <- "This is a bullet •"
y <- "This is a bullet \u2022"
identical(x, y)
#> [1] TRUE

cat(stringi::stri_escape_unicode(x))
#> This is a bullet \u2022

包的元信息

参考资料

    脚注

    1. Note that .First.lib() and .Last.lib() are old versions of .onLoad() and .onUnload() and should no longer be used. 


    打赏作者


    上一篇:PostgreSQL Essential     下一篇:高性能R程序