没有包的命名空间

Posted

技术标签:

【中文标题】没有包的命名空间【英文标题】:Namespaces without packages 【发布时间】:2013-03-25 05:14:51 【问题描述】:

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

但是,这种方法存在许多问题,其中包括

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

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

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

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

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

【问题讨论】:

您能否详细说明为什么将每个文件的内容添加到搜索路径上的单独环境中(如?sys.source 的示例所示)是不够的? @Joshua 这就是我现在正在做的事情(我的例子被简化了)——我认为有一种明确限定命名空间的方法很好。当然我可以对getassign 做同样的事情,但是:: 的语法要好一些。 我很困惑,因为您的 import 函数没有这样做。如果将每个文件的内容放在搜索路径上的单独环境中,则可以使用$ 运算符(例如strings$concatenate())访问特定环境。 相关:Attaching a temporary namespace to the search path. 我认为这需要更多的答案来责备 OP 想要避免创建包的开销;=) 【参考方案1】:

这是一个完全自动化包创建、编译和重新加载的函数。正如其他人所指出的那样,实用函数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."

【讨论】:

@hadley -- 你能详细说明一下吗?使用devtools::create() 相当于我的import(c("bar.R", "baz.R"), pkgName="foo") create(path); file.copy(srcFiles, file.path(path, "R")) - 不是很大的改进,只是避免了创建您从不使用的文件。但是对于这种情况,你甚至不需要create @hadley,你称之为笨拙的函数的好处是它没有代表我签署 GPL-3 许可证。 嗯,这实际上对我来说几乎是完美的——有一个缺陷:它会在系统上持续安装包。我希望这仅限于当前会话,以便重新启动 R 不能再加载临时创建的包而不重新import -ing它。我在 devtools 中找不到“卸载”功能。有没有好的方法来实现这一目标?此外,我如何确保执行此操作?简单地将其填充到 .Last 似乎是不够的,因为 .Last 可以在其他地方被覆盖。 @GSee 创建了有关默认许可证的问题:github.com/hadley/devtools/issues/282【参考方案2】:

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

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

来创建一个包。这个福音已经在 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$ 

真的那么繁重吗?

【讨论】:

@KonradRudolph 但是,如果您只有一个双功能文件,那么您不必担心循环性,避免碰撞应该不会太难?但说真的,将某些东西变成一个包真的难以置信的简单。没有人说您需要完成使其能够通过所有 CRAN 检查的工作(这需要工作)。但是,如果您获得了基本的包结构,则可以使用 devtools 包中的 load_all 函数来获得您所要求的所有功能,而无需显式安装包。 真的,制作包。您反复担心的巨大开销是每个包的 ONE-OFF,然后您将所有相关的 R 文件保存在该包中,并且 load_all 是您的导入函数。将相关的 R 文件保存在单个包中的好处是值得开销的,因为在您的项目中的某个时刻,您将拥有特定于该项目的代码,以及可以在其他项目中工作的可重用代码。你是怎么组织的?很简单,两个包。然后 devtools 的所有其他优势对您开放(例如 roxygen)。 devtools 有很大帮助。它将打包工作从 X 减少到 X/5,但 R 中的 X/5 仍然很重要。在合理的解释语言中,X 等于零! R 中的包过于繁琐的确切证据是 source() 甚至被任何人使用/。比较 Python。谁曾经使用execfile() 在 Python 中导入函数定义?没有人。您只需编写一个文件并import 它;有你的命名空间。 “入场费”为零。你能想象编写一本 / 从未提及 source()/ 的介绍性 R 教科书吗? 请注意,当您开始在文件夹中组织代码并且必须创建 __init__.py 文件时,python 价格会上涨 0.01 英镑。但是,是的,R 不是一种明智的解释型语言。 @KonradRudolph 敏捷开发适用于包。只需 source (或从您的编辑器中通过管道)将已编辑的函数导入 R 会话。然后执行 assignInNamespace(....) 将工作空间中的副本推送到包 NAMESPACE 中。一旦你完成了这一天的敏捷,你就可以重建并安装包含新更新的包。【参考方案3】:

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

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

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

【讨论】:

此方法的另一个优点是简单地包含文档和自动生成Namespace,包括imports等,通过使用ROxygen。然后当然是单元测试等,使用testthat 完全赞同这一点。出于这个原因,我使用 R 包结构来组织我的所有项目。我注意到,如果需要的话,R 包提供了一种很好的方式来组织数据、文档,甚至与项目相关的 pubs。 (例如github.com/cboettig)devtools 让这个工作流程变得非常简单。【参考方案4】:

我对 OP 问题的评论不太正确,但我认为这种对 import 函数的重写可以解决问题。 foo.Rbar.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 似乎无论如何都想要 :: 的明确性。

【讨论】:

【参考方案5】:

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

然而,我想到的一件事是sourcelocal 参数,让您可以将其作为environment 的来源,您可以像命名空间一样使用它,例如

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

要使用简单的语法访问这些导入的环境,请给这些导入环境一个新类(例如,'import'),并使:: 通用,当pkg 不存在时默认为getExportedValue

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,然后尝试使用:: 访问其中一个对象。

【讨论】:

我认为我们真的不同意“小开销”的构成——正如我在另一条评论中所说,小模块的开销大约是一个数量级,并且惩罚小而多的包,而支持大的、单一的包。它只是支持不同的开发方法。也就是说,我会尝试与devtools 混在一起,看看这是否能让小包裹变得不那么麻烦。最后,关于您的答案:您的方法将所有内容放在 environment 而不是 namespace @KonradRudolph:来自R Internals 手册,Section 1.2.2: Namespaces,“命名空间是与包关联的环境......”。也就是说,没有包就不能有命名空间。 @Joshua 好的,但这就是问题所在。正如评论中进一步提到的,我想要的是一种使用良好语法显式限定名称空间/环境的方法。诚然,R 将其与包结合起来,但(据我所知)只是按照惯例。您可以编写自己的 :: 版本来规避这一点。 @KonradRudolph 那么为什么不这样做呢?使:: 通用并为您的命名空间环境提供一个新类。你可能仍然觉得它很“脏”,但这应该避免破坏其他包。 @MatthewPlourde 好吧,这听起来像是一个答案……【参考方案6】:

我已经实现了一个综合解决方案并将其作为一个包发布,“box”。

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

包的使用有详细说明on its website。

【讨论】:

以上是关于没有包的命名空间的主要内容,如果未能解决你的问题,请参考以下文章

如何导入 Python 命名空间包的所有子模块?

.NET 命名空间子目录

如何处理将作为另一个包的依赖项的命名空间包

处理 R 中冲突的命名空间(不同包中的相同函数名):重置包命名空间的优先级

如何覆盖包命名空间中的不可见函数?

如果没有定义命名空间,一个类会有啥命名空间