22

在重组我的代码库时,我想清理我的代码共享机制。到目前为止,我正在使用source许多小型、大部分独立的功能模块。

然而,这种方法存在许多问题,其中包括

  • 缺乏循环测试(意外的循环source链),
  • chdir=TRUE正确指定包含路径(参数、硬编码路径)所需的复杂语法,
  • 名称冲突的可能性(重新定义对象时)。

理想情况下,我想获得与 Python 模块机制类似的东西。R 包机制在这里会有点矫枉过正: 不想生成嵌套路径层次结构、包含大量元数据的多个文件并手动构建包只是为了获得一个小的、独立的、可重用的代码模块。

现在我正在使用一个代码片段,它可以让我解决上面提到的前两个问题。包含的语法如下:

import(functional)
import(io)
import(strings)

…并且模块被定义为驻留在本地路径中的简单源文件。的定义import很简单,但我无法解决第三点:我想将模块导入到单独的命名空间中,但据我所知,命名空间查找机制与包非常紧密。没错,我可以覆盖`::`orgetExportedValue并且可能asNamespaceisNamespace但这感觉很脏,并且有可能破坏其他包。

4

6 回答 6

17

这是一个完全自动化包创建、编译和重新加载的函数。正如其他人所指出的那样,实用程序功能package.skeleton()已经devtools::load_all()让您几乎一路走到了那里。这只是结合了它们的功能,package.skeleton()用于在临时目录中创建源目录,该目录在处理完成后会被清理load_all()

您需要做的就是指向要从中读取函数的源文件,并为包命名:import()其余的为您完成。

import <- function(srcFiles, pkgName) {
    require(devtools)
    dd <- tempdir()
    on.exit(unlink(file.path(dd, pkgName), recursive=TRUE))
    package.skeleton(name=pkgName, path = dd, code_files=srcFiles)
    load_all(file.path(dd, pkgName))
}

## Create a couple of example source files
cat("bar <- function() {print('Hello World')}", file="bar.R")
cat("baz <- function() {print('Goodbye, cruel world.')}", file="baz.R")

## Try it out
import(srcFiles=c("bar.R", "baz.R"), pkgName="foo")

## Check that it worked
head(search())
# [1] ".GlobalEnv"        "package:foo"       "package:devtools"
# [4] "package:stats"     "package:graphics"  "package:grDevices"
bar()
# [1] "Hello World"
foo::baz()
# [1] "Goodbye, cruel world."
于 2013-04-03T17:36:38.853 回答
15

康拉德,严肃地说,是对需求的回答

获得一个小型、独立、可重用的代码模块

创建一个包。这个福音已经在 SO 和其他地方重复了无数次。实际上,您可以用最少的 fuzz 创建最少的包。

另外,运行后

 setwd("/tmp")
 package.skeleton("konrad")

并删除一个临时文件,我只剩下

 edd@max:/tmp$ tree konrad/
 konrad/
 ├── DESCRIPTION
 ├── man
 │   └── konrad-package.Rd
 └── NAMESPACE

 1 directory, 3 files
 edd@max:/tmp$ 

真的有那么繁重吗?

于 2013-04-03T14:02:00.793 回答
13

包只是文件存储位置的约定(R 文件 in R/,文档 in man/,编译代码 in src,数据 in data/):如果您有多个文件,则最好遵守既定约定。换句话说,使用包比不使用包更容易,因为你不需要思考:你可以利用现有的约定,每个 R 用户都会明白发生了什么。

一个最小的包真正需要的是一个DESCRIPTION文件,它说明了包的作用,谁可以使用它(许可证),以及如果有问题可以联系谁(维护者)。这有点开销,但不是主要的。一旦你写好了,你只需根据需要填写额外的目录 - 不需要笨拙的package.skeleton().

也就是说,用于处理包的内置工具很麻烦——你必须重新构建/重新安装包,重新启动 R 并重新加载包。这就是devtools::load_all()Rstudio 的 build & reload 发挥作用的地方——它们对包使用相同的规范,但提供了从源代码更新包的更简单方法。您当然可以使用其他答案提供的代码片段,但为什么不使用数百(至少数十个)R 开发人员使用的经过良好测试的代码呢?

于 2013-04-04T12:23:32.137 回答
8

import我对 OP 问题的评论不太正确,但我认为对函数的 这种重写可以解决问题。foo.R并且bar.R是当前工作目录中的文件,其中包含一个函数 ( baz),该函数打印如下所示的输出。

import <- function (module) {
  module <- as.character(substitute(module))
  # Search path handling omitted for simplicity.
  filename <- paste(module, 'R', sep = '.')
  # create imports environment if it doesn't exist
  if ("imports" %in% search())
    imports <- as.environment(match("imports",search()))
  # otherwise get the imports environment
  else
    imports <- attach(NULL, name="imports")
  if (module %in% ls("imports"))
    return()
  # create a new environment (imports as parent)
  env <- new.env(parent=imports)
  # source file into env
  sys.source(filename, env)
  # ...and assign env to imports as "module name"
  assign(module, env, imports)
}
setwd(".")
import(foo)
import(bar)
foo$baz()
# [1] "Hello World"
bar$baz()
# [1] "Buh Bye"

请注意,baz()它本身不会被发现,但 OP 似乎::无论如何都想要明确性。

于 2013-04-03T15:34:29.103 回答
6

我完全同情@Dirk 的回答。制作最小包所涉及的少量开销似乎值得遵循“标准方式”。

但是,想到的一件事是source' 的local论点,让您可以将源放入一个environment,您可以像命名空间一样使用它,例如

assign(module, new.env(parent=baseenv()), envir=topenv())
source(filename, local=get(module, topenv()), chdir = TRUE)

要使用简单的语法访问这些导入的环境,请给这些导入环境一个新类(例如,'import'),并使其成为::泛型,默认为何getExportedValuepkg不存在。

import <- function (module) {
    module <- as.character(substitute(module))
    # Search path handling omitted for simplicity.
    filename <- paste(module, 'R', sep = '.')

    e <- new.env(parent=baseenv())
    class(e) <- 'import'
    assign(module, e, envir=topenv())
    source(filename, local=get(module, topenv()), chdir = TRUE)
}

'::.import' <- function(env, obj) get(as.character(substitute(obj)), env)
'::' <- function(pkg, name) {
    pkg <- as.character(substitute(pkg))
    name <- as.character(substitute(name))
    if (exists(pkg)) UseMethod('::')
    else getExportedValue(pkg, name)
}

更新

下面是一个更安全的选项,它可以防止在加载的包包含与正在访问的包同名的对象时出错::

'::' <- function(pkg, name) {
    pkg.chr <- as.character(substitute(pkg))
    name.chr <- as.character(substitute(name))
    if (exists(pkg.chr)) {
        if (class(pkg) == 'import')
            return(get(name.chr, pkg))
    }
    getExportedValue(pkg.chr, name.chr)
}

这将给出正确的结果,例如,如果您加载data.table了 ,并随后尝试使用 访问其对象之一::

于 2013-04-03T14:36:21.817 回答
4

我已经实现了一个全面的解决方案并将其作为一个包发布,'<a href="https://github.com/klmr/box" rel="nofollow noreferrer">box'。

在内部,“盒子”模块使用类似于包的方法;也就是说,它将代码加载到专用的命名空间环境中,然后将选定的符号导出到返回给用户的模块环境中,并且可以选择附加。与包的主要区别在于模块更轻量级且更易于编写(每个 R 文件都是其自己的模块),并且可以嵌套。

该软件包的使用在其网站上有详细描述。

于 2016-03-16T13:24:33.413 回答