目录
概述
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()
创建包,在让尽快包运行之前,还得删掉那些不必要的文件。
包的不同形式
包的生命周期可能会经历 source
、bundled
、binary
、installed
和in-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 INSTALL
,devtools::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()
函数加载。脚本中的代码和包中的代码有两个主要区别:
- 脚本中的代码在加载时运行;包中的代码在生成(build)时运行。这就意味着包中的代码只应当用于创建对象,大多数应当是函数。
- 包中的函数将会用在那些你无法预见的场景,设计时务必考虑周全。
一、慎写顶层代码
当采用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()
的参数libname
和pkgname
很少被使用,可用.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
包的元信息
参考资料
脚注
-
Note that
.First.lib()
and.Last.lib()
are old versions of.onLoad()
and.onUnload()
and should no longer be used. ↩