如何在Julia中导入自定义模块

62

我有一个模块,写在这里:

# Hello.jl
module Hello
    function foo
        return 1
    end
end

而且

# Main.jl
using Hello
foo()
当我运行主模块(Main)时:
$ julia ./Main.jl

我收到了这个错误:

ERROR: LoadError: ArgumentError: Hello not found in path
 in require at ./loading.jl:249
 in include at ./boot.jl:261
 in include_from_node1 at ./loading.jl:320
 in process_options at ./client.jl:280
 in _start at ./client.jl:378
while loading /Main.jl, in expression starting on line 1
7个回答

71
自从Julia v0.7和v1.0发布以来,对于这个问题有了一个新的答案,略有不同。我必须这样做,所以我想在这里发布我的发现。
如其他解决方案中已经解释的那样,需要包含定义模块的相关脚本。然而,由于自定义模块不是一个包,在旧版Julia中可以使用相同的usingimport命令加载为包,但在新版中无法这样做。
因此,Main.jl脚本将使用相对导入方式编写,如下所示:
include("./Hello.jl")
using .Hello
foo()

我在类似问题的Stefan Karpinski的讨论评论中找到了简单的解释。正如他所描述的那样,当处理子模块时,情况也可以变得更加复杂。模块路径文档部分也是一个很好的参考。


4
第二个链接已失效。截至本文撰写时,相关信息可以在此处获得:https://docs.julialang.org/en/v1/manual/modules/#Modules-and-files-1 - JKRT
2
我正在使用Julia 1.2,似乎不再需要使用using .name语句。执行include("./name")就足以将文件及其函数导入到当前脚本中。 - theferrit32
2
@theferrit32 includeusing本质上是不同的。就像C/C++一样, include将指定文件的源代码带入调用文件中,就好像在调用文件中直接编写了其他文件一样。另一方面, using并不关心文件(除非在LOAD_PATH中找到具有相同名称的文件)。 它用于将特定模块的符号导入到调用模块中。 一个文件可以定义多个模块,每个模块都需要对兄弟模块调用 usingimport以便访问它们的函数。 - Kiruse
我发现这个解决方案会带来额外的问题。在“Hello.jl”中生成的类型与您在“main.jl”中使用的类型不同,即使您已经导入/使用它们(请参见https://discourse.julialang.org/t/error-with-modules-sharing-types/37119)。 - Lukas Hebing

32

编辑:更新代码以应用于v1.0之后的版本。其他答案仍存在一个根本性问题:如果您定义了一个模块,然后在多个地方include该模块定义,您将获得意外难以理解的错误。只要你只include文件一次,@kiliantics'的回答是正确的。如果您有一个跨多个文件使用的模块,请将该模块打包成一个软件包,使用add MyModule,然后在尽可能多的地方键入using MyModule,让Pkg为您处理模块标识。


虽然张实唯的回答最方便,但不应该在REPL之外使用include(或者只在包含大型模块时作为简单实践,在第一个示例here中)。如果你正在编写程序文件,麻烦一点添加适当的目录到LOAD_PATH中。Remy给出了非常好的解释如何这样做,但值得解释一下为什么首先要这样做。(另外从文档中可以看到:push!(LOAD_PATH, "/ Path / To / My / Module /"),但要注意模块和文件必须具有相同的名称)

问题在于你include的任何内容都将在调用include 的地方定义,即使它也在别处定义。由于模块的目标是可重用性,您可能最终会在多个文件中使用MyModule。如果在每个文件中调用include,那么每个文件将具有自己的MyModule定义,即使它们是相同的,这些定义也是不同的。这意味着在MyModule(如数据类型)中定义的任何数据将不同。

为了看到这是一个巨大的问题,请考虑这三个文件:

types.jl

module TypeModule
struct A end
export A
end

a_function.jl

include("types.jl")
module AFunctionModule
using ..TypeModule
function takes_a(a::A)
    println("Took A!")
end
export takes_a
end

function_caller.jl

include("a_function.jl")
include("types.jl")  # delete this line to make it work
using .TypeModule, .AFunctionModule
my_a = A()
takes_a(my_a)
如果你运行 julia function_caller.jl,会得到 MethodError: no method matching takes_a(::A) 错误。这是因为在 function_caller.jl 中使用的 A 类型与 a_function.jl 中使用的不同。在这种简单情况下,你实际上可以通过颠倒 function_caller.jl 中的 include 顺序(或者完全删除 function_caller.jl 中的 include("types.jl")!但这样做并不好)。但如果你想要另一个文件 b_function.jl 也使用在 TypeModule 中定义的类型,你就需要进行非常棘手的操作。或者你可以修改你的 LOAD_PATH,以便模块只被定义一次。
编辑:针对 xji 的回复,在分发模块时,可以使用 Pkg文档)。我理解这个问题的前提是一个自定义的个人模块。如果你知道每个需要加载该模块的文件的目录的相对路径,例如如果所有文件都在同一个文件夹中,那么你只需要使用 push!(LOAD_PATH, @__DIR__) 即可。
顺便说一下,如果你真的不喜欢修改你的加载路径的想法(即使它只在单个脚本的范围内……),你可以将你的模块链接到一个包目录中(例如 ~/.julia/v0.6/MyModule/MyModule.jl),然后使用 Pkg.add(MyModule),然后像正常导入一样导入。我觉得那样会麻烦一些。

5
如果要在其他用户的系统上分发和运行程序,那似乎并不可移植,对吗?这样每个用户都需要修改他们的环境变量才能在自己的系统上运行每个新的Julia程序? - xji
这个答案适用于Julia 0.7+吗? - Buttons840
我同意你的看法,被接受的答案似乎是解决这个问题最简单的方法,但是也有代价。我已经在该主题下创建了一个讨论,欢迎大家贡献自己的想法。 https://discourse.julialang.org/t/how-to-import-a-local-module-into-another-in-different-file/42058 - abann sunny

23

此答案已过时,请查看其他优秀的解释。

===

在使用 Hello 之前,应该先 include("./Hello.jl")


7
使用using的意义是什么?我以为这个关键字会为我包含模块... - dopatraman
@dopatraman using 的作用是将模块名称引入当前作用域,但模块本身不会自动被 include()(除了在“LOAD_PATH”中的模块)。 - 张实唯
如果您在 module 中有定义,那么您也需要将它们导出。 - m33lky
2
在更新的Julia版本中,您只需要使用"include",不再需要使用"using"了。 - Lucas
2
@LucasCavalcantiRodrigues,不完全正确。包含在文件中的模块的副作用是文件内容在全局范围内进行评估。这并不总是(如果有的话)理想的。请参见:https://docs.julialang.org/en/v1/manual/code-loading/ - PatrickT
似乎在我的 Julia 1.7.2 安装中需要使用 using .Hello,或者说现在不再需要使用 using .Hello 了? - Petri

21

这篇回答最初是针对 Julia 0.4.5 写的。现在有一种更简单的方法可以导入本地文件(请参见 @kiliantics 的回答)。但是,我会保留这篇回答,因为它解释了从其他目录加载文件的几种方法,这些方法仍然可能有用。


已经有一些简短的答案了,但如果可能的话,我希望提供一个更完整的答案。

当您运行 using MyModule 时,Julia 仅会在称为您的 LOAD_PATH 的目录列表中搜索该模块。如果您在 Julia REPL 中键入 LOAD_PATH,您将得到类似以下内容的输出:

2-element Array{ByteString,1}:
 "/Applications/Julia-0.4.5.app/Contents/Resources/julia/local/share/julia/site/v0.4"
 "/Applications/Julia-0.4.5.app/Contents/Resources/julia/share/julia/site/v0.4"

这些是Julia搜索模块以包含在 using Hello 中的目录。在您提供的示例中,由于 Hello 不在您的 LOAD_PATH 中,Julia无法找到它。

如果您想要包含本地模块,可以相对于当前工作目录指定其位置。

julia> include("./src/Hello.jl")

一旦文件被包含,您可以像平常一样运行using Hello来获得完全相同的行为。对于一次性脚本,这可能是最好的解决方案。但是,如果您发现自己经常需要include()某个特定集合的目录,则可以将它们永久添加到您的LOAD_PATH中。

将目录添加到LOAD_PATH

如果您希望经常使用存储在Julia LOAD_PATH之外的特定模块,手动将目录添加到LOAD_PATH可能会很麻烦。在这种情况下,您可以将额外的目录附加到LOAD_PATH环境变量中。然后,每当您发出importusing命令时,Julia都会自动搜索这些目录。

其中一种方法是将以下内容添加到您的.basrc, .profile, .zshrc文件中。

export JULIA_LOAD_PATH="/path/to/module/storage/folder"

这将把该目录追加到Julia将搜索的标准目录中。如果您随后运行

julia> LOAD_PATH

它应该返回

3-element Array{ByteString,1}:
 "/path/to/module/storage/folder"
 "/Applications/Julia-0.4.5.app/Contents/Resources/julia/local/share/julia/site/v0.4"
 "/Applications/Julia-0.4.5.app/Contents/Resources/julia/share/julia/site/v0.4"

现在,您可以自由运行using Hello, 只要模块存储在/path/to/module/storage/folder文件夹下,Julia将自动查找该模块。

有关更多信息,请查看Julia文档中的此页面


1
这似乎并不具备可移植性,如果程序需要分发并在其他用户的系统上运行,是吗?这样每个用户都必须修改他们的环境变量才能在他们的系统上运行每个新的Julia程序? - xji
1
@xji 不是的;这只是为了从Julia中加载库,它的工作方式与C、Python等完全相同。如果您想运行Julia程序,请使用“julia myprogram.jl”(就像“python myprogram.py”一样)。但是,如果您想导入库到Julia程序中,Julia需要知道在哪里查找 - 就像GCC / Clang需要知道在哪里查找非系统C / C ++头文件,或者Python需要知道在哪里查找非系统或用户安装的软件包。对于任何语言都没有可移植的替代方案。如果您不想修改路径,请使用Julia软件包管理器安装软件包。 - hyperdelia
对于 Julia >= 0.7 版本,请参考 @kiliantics 的回答 https://dev59.com/jFoU5IYBdhLWcg3wsoZ8#52298205。基本上,您需要在 include 语句之后添加一个 . 来进行相对导入。 - Pierre H.

3

除非您显式地加载文件(include("./Hello.jl")),否则Julia会在LOAD_PATH变量定义的目录中查找模块文件。

请参见此页面


3

我当前使用的是 Julia Version 1.4.2 (2020-05-23) 版本。只输入 using .Hello 命令,Hello 模块就可以正常工作了。 然而,在执行 using .Hello 命令之前,我必须先编译 Hello 模块。这是因为定义和使用 Hello 模块的脚本在同一个文件中,需要先进行编译。

不过,我们也可以将 Hello 模块定义在一个文件中,然后在另一个文件中使用它,使用命令 include("./Hello.jl");using .Hello


1
如果你想在使用"using"导入模块时访问函数foo,你需要在模块头部添加"export foo"。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接