模块
Julia中的模块有助于将代码组织成连贯的单元。 它们在语法上是分隔的 模块NameOfModule。.. 结束,并具有以下特点:
-
模块是独立的命名空间,每个命名空间都引入了一个新的全局作用域。 这很有用,因为它允许相同的名称用于不同的函数或全局变量而不冲突,只要它们在单独的模块中。
-
模块具有用于详细命名空间管理的设施:每个模块都定义了一组名称
出口s和标记为公众人士,并可以从其他模块导入名称使用和进口(我们在下面解释这些)。 -
模块可以预编译以加快加载速度,并且可能包含用于运行时初始化的代码。
通常,在较大的Julia包中,您会看到模块代码被组织成文件,例如
module SomeModule
# export, public, using, import statements are usually here; we discuss these below
include("file1.jl")
include("file2.jl")
end
文件和文件名大多与模块无关;模块仅与模块表达式相关联。 每个模块可以有多个文件,每个文件可以有多个模块。 包括 表现得好像源文件的内容是在including模块的全局范围内评估的。 在本章中,我们使用简短和简化的示例,因此我们不会使用 包括.
建议的样式是不要缩进模块的主体,因为这通常会导致整个文件被缩进。 此外,它是常见的使用 [医]上肢 对于模块名称(就像类型一样),如果适用,请使用复数形式,特别是如果模块包含类似命名的标识符,以避免名称冲突。 例如,
module FastThings
struct FastThing
...
end
end
命名空间管理
命名空间管理是指该语言为使模块中的名称在其他模块中可用而提供的设施。 我们在下面详细讨论相关的概念和功能。
限定名称
全局作用域中的函数、变量和类型的名称,如 罪, 阿格斯,而 单位范围 总是属于一个模块,称为_parent module_,它可以与 [医父母模式],例如
julia> parentmodule(UnitRange)
Base
人们也可以在父模块之外引用这些名称,方法是在它们的模块前面加上前缀,例如 基地。单位范围. 这被称为_qualified name_。 父模块可以使用子模块链访问,如 基地。数学。罪,在哪里 基地。数学 被称为_module path_。 由于语法含糊不清,限定仅包含符号(如运算符)的名称需要插入冒号,例如 基地。:+. 少量运算符还需要括号,例如 基地。:(==).
如果一个名称是限定的,那么它总是_accessible_,并且在函数的情况下,它也可以通过使用限定名称作为函数名来添加方法。
在模块中,变量名可以是"`预留`"而不是通过将其声明为 全球x. 这可以防止在加载时间后初始化的全局变量的名称冲突。 语法 M.x=y 在另一个模块中分配全局不起作用;全局分配始终是模块本地的。
出口清单
名称(指函数,类型,全局变量和常量)可以添加到模块的_export list_中 出口:这些是导入时的符号 使用 的模块。 通常,它们位于模块定义的顶部或附近,以便源代码的读者可以很容易地找到它们,如
julia> module NiceStuff
export nice, DOG
struct Dog end # singleton type, not exported
const DOG = Dog() # named instance, exported
nice(x) = "nice $x" # function, exported
end;
但这只是一个风格建议-一个模块可以有多个 出口 任意位置的语句。
通常导出构成API(应用程序编程接口)一部分的名称。 在上面的代码中,导出列表建议用户应该使用 不错 和 狗只. 但是,由于限定名称总是使标识符可访问,这只是组织Api的一个选项:与其他语言不同,Julia没有真正隐藏模块内部的设施。
此外,某些模块根本不导出名称。 如果他们使用常用词,例如 衍生品,在他们的API中,这很容易与其他模块的导出列表发生冲突。 我们将在下面看到如何管理名称冲突。
要将名称标记为public,而不将其导出到调用的人员的命名空间中 使用NiceStuff,一个可以使用 公众人士 而不是 出口. 这将公共名称标记为公共API的一部分,但没有任何命名空间含义。 该 公众人士 关键字仅在Julia1.11及更高版本中可用。 要保持与Julia1.10及以下版本的兼容性,请使用 @compat 宏从https://github.com/JuliaLang/Compat.jl[Compat]包,或版本感知构造
VERSION >= v"1.11.0-DEV.469" && eval(Meta.parse("public a, b, c"))
独立的 使用 和 进口
对于交互式使用,加载模块的最常用方法是 使用ModuleName. 这 加载与 模块化名称,并带来
-
模块名称
-
和导出列表的元素到周围的全局命名空间中。
从技术上讲,声明 使用ModuleName 表示一个模块调用 模块化名称 将可用于根据需要解析名称。 当遇到在当前模块中没有定义的全局变量时,系统将在由 模块化名称 并使用它,如果它被发现在那里。 这意味着在当前模块中对该全局变量的所有使用都将解析为 模块化名称.
若要从包中加载模块,请使用以下语句 使用ModuleName 可以使用。 要从本地定义的模块加载模块,需要在模块名称之前添加一个点,如 使用。模块化名称.
继续我们的例子,
julia> using .NiceStuff
会加载上面的代码,使得 尼斯图夫 (模块名称), 狗只 和 不错 可用。 狗只 不在导出列表中,但如果名称与模块路径(这里只是模块名称)限定为 漂亮的。狗只.
重要的是, *`使用ModuleName` 是唯一一个出口清单重要的形式*。
相比之下,
julia> import .NiceStuff
将_only_模块名称带入作用域。 用户需要使用 漂亮的。狗只, 漂亮的。狗只,而 漂亮的。不错 访问其内容。 通常情况下, 导入ModuleName 在用户希望保持命名空间清洁的上下文中使用。 正如我们将在下一节中看到的那样 进口。尼斯图夫 相当于 使用。NiceStuff:脟毛碌脟脗录.
您可以组合多个 使用 和 进口 逗号分隔表达式中的同类语句,例如
julia> using LinearAlgebra, Random
使用 和 进口 具有特定的标识符,并添加方法
何时 使用ModuleName: 或 导入ModuleName: 后跟一个逗号分隔的名称列表,模块被加载,但是_only这些特定的名称被语句带入namespace_。 例如,
julia> using .NiceStuff: nice, DOG
将导入名称 不错 和 狗只.
重要的是,模块名称 尼斯图夫 将_not_在命名空间中。 如果你想让它可以访问,你必须明确地列出它,如
julia> using .NiceStuff: nice, DOG, NiceStuff
当两个或多个包/模块导出一个名称,并且该名称在每个包中不引用相同的东西时,包将通过 使用 如果没有明确的名称列表,则在没有限定的情况下引用该名称是错误的。 因此,建议打算向前兼容其依赖项和Julia的未来版本的代码,例如发布包中的代码,列出它在每个加载包中使用的名称,例如, 使用Foo:Foo,f 而不是 使用Foo.
朱莉娅有两种形式看似相同的东西,因为只有 导入ModuleName:f 允许将方法添加到 f 没有一个模块路径。 也就是说下面的例子会报错:
julia> using .NiceStuff: nice
julia> struct Cat end
julia> nice(::Cat) = "nice 😸"
ERROR: invalid method definition in Main: function NiceStuff.nice must be explicitly imported to be extended
Stacktrace:
[1] top-level scope
@ none:1
此错误可防止意外地将方法添加到您只打算使用的其他模块中的函数。
有两种方法来处理这个问题。 您始终可以使用模块路径限定函数名称:
julia> using .NiceStuff
julia> struct Cat end
julia> NiceStuff.nice(::Cat) = "nice 😸"
或者,您可以 进口 具体函数名:
julia> import .NiceStuff: nice
julia> struct Mouse end
julia> nice(::Mouse) = "nice 🐭"
nice (generic function with 3 methods)
你选择哪一个是风格的问题。 第一种形式清楚地表明您正在向另一个模块中的函数添加方法(请记住,导入和方法定义可能在单独的文件中),而第二种形式更短,如果您定义多个方法,
一旦一个变量通过 使用 或 进口,模块不能创建自己的同名变量。 导入的变量是只读的;分配给全局变量总是影响当前模块拥有的变量,否则会引发错误。
重命名与 作为
带入范围的标识符 进口 或 使用 可以用关键字重命名 作为. 这对于解决名称冲突以及缩短名称非常有用。 例如, 基地 导出函数名称 讀!,但CSV。jl包还提供 CSV。讀!. 如果我们要多次调用CSV读取,那么删除 CSV。 限定符。 但我们是否指的是模棱两可的 基地。讀! 或 CSV。讀!:
julia> read;
julia> import CSV: read
WARNING: ignoring conflicting import of CSV.read into Main
重命名提供了解决方案:
julia> import CSV: read as rd
导入的包本身也可以重命名:
import BenchmarkTools as BT
作为 与 使用 仅当单个标识符被带入范围时。 例如 使用CSV:读作rd 有效,但是 使用CSV作为C 不会,因为它对所有导出的名称进行操作 CSV档案源.
混合多个 使用 和 进口 声明
当多个 使用 或 进口 使用上述任何形式的陈述,它们的效果按照它们出现的顺序组合在一起。 例如,
julia> using .NiceStuff # exported names and the module name
julia> import .NiceStuff: nice # allows adding methods to unqualified functions
会带来所有出口的名字 尼斯图夫 模块将自己命名为作用域,并且还允许将方法添加到 不错 没有在它前面加上模块名称。
处理名称冲突
考虑两个(或更多)包导出相同名称的情况,如
julia> module A
export f
f() = 1
end
A
julia> module B
export f
f() = 2
end
B
声明 使用。一个,。B 工作,但是当你试图打电话 f,你得到一个提示错误
julia> using .A, .B
julia> f
ERROR: UndefVarError: `f` not defined in `Main`
Hint: It looks like two or more modules export different bindings with this name, resulting in ambiguity. Try explicitly importing it from a particular module, or qualifying the name with the module it should come from.
在这里,朱莉娅无法决定哪个 f 你指的是,所以你必须做出选择。 以下解决方案是常用的:
-
只需继续使用合格的名称,如
A.f和B.f.. 这使您的代码的读者清楚上下文,特别是如果f恰好重合,但在各种包装中具有不同的含义。 例如,学位在数学、自然科学和日常生活中有各种用途,这些意义应该分开。 -
使用
作为上面的关键字重命名一个或两个标识符,例如
julia> using .A: f as f
julia> using .B: f as g
会使 B.f. 可用作 g. 在这里,我们假设你没有使用 使用 以前,这会带来 f 到命名空间中。
-
当有问题的名称_do_共享一个含义时,一个模块从另一个模块导入它是很常见的,或者有一个轻量级"`基地`"包具有这样定义接口的唯一功能,可供其他包使用。 通常,这样的包名称以
...基地(这与朱莉娅的无关基地模块)。
定义的优先顺序
一般有四种绑定定义:
-
通过隐式导入提供的那些
使用M -
通过显式导入提供的那些(例如
使用M:x,进口M:x) -
那些隐式声明为全局的(通过
全球x无类型规范) -
使用定义语法显式声明的那些(
康斯特,全局x::T,结构体等。)
从语法上讲,我们将它们分为三个优先级(从最弱到最强)
-
隐式导入
-
隐式声明
-
显式声明和导入
一般来说,我们允许用更强的绑定替换较弱的绑定:
julia> module M1; const x = 1; export x; end
Main.M1
julia> using .M1
julia> x # Implicit import from M1
1
julia> begin; f() = (global x; x = 1) end
julia> x # Implicit declaration
ERROR: UndefVarError: `x` not defined in `Main`
Suggestion: add an appropriate import or assignment. This global was declared but not assigned.
julia> const x = 2 # Explicit declaration
2
但是,在显式优先级级别中,在语法上不允许替换:
julia> module M1; const x = 1; export x; end
Main.M1
julia> import .M1: x
julia> const x = 2
ERROR: cannot declare Main.x constant; it was already declared as an import
Stacktrace:
[1] top-level scope
@ REPL[3]:1
或忽略:
julia> const y = 2
2
julia> import .M1: x as y
WARNING: import of M1.x into Main conflicts with an existing identifier; ignored.
隐式绑定的解析取决于所有的集合 使用'd在当前世界时代可见的模块。 见 世界年龄手册章节了解更多详情。
默认顶级定义和裸模块
如果不需要这些默认定义,可以使用关键字定义模块 裸模,裸模代替(注: 核心 仍然是进口的)。 就 裸模,裸模,一个标准 模块 看起来像这样:
baremodule Mod using Base eval(x) = Core.eval(Mod, x) include(p) = Base.include(Mod, p) ... end
julia> arithmetic = Module(:arithmetic, false, false)
Main.arithmetic
julia> @eval arithmetic add(x, y) = $(+)(x, y)
add (generic function with 1 method)
julia> arithmetic.add(12, 13)
25
子模块和相对路径
模块可以包含_submodules_,嵌套相同的语法 模块。.. 结束. 它们可用于引入单独的命名空间,这有助于组织复杂的代码库。 请注意,每个 模块 介绍自己的 作用域,所以子模块不会自动"`继承`"他们父母的名字。
建议子模块使用_relative module qualifiers_引用封闭父模块内的其他模块(包括后者 使用 和 进口 发言。 相对模块限定符以句点(.),其对应于当前模块,并且每个相继 . 导致当前模块的父级。 这应该在必要时跟随模块,并最终访问实际名称,所有这些都由 .然而,作为一个特殊情况,引用模块根可以在没有 .,避免了需要计数到达该模块的深度。
考虑以下示例,其中子模块 苏巴 定义一个函数,然后在其"`兄弟姐妹`"模块:
julia> module ParentModule
module SubA
export add_D # exported interface
const D = 3
add_D(x) = x + D
end
using .SubA # brings `add_D` into the namespace
export add_D # export it from ParentModule too
module SubB
import ..SubA: add_D # relative path for a “sibling” module
# import ParentModule.SubA: add_D # when in a package, such as when this is loaded by using or import, this would be equivalent to the previous import, but not at the REPL
struct Infinity end
add_D(x::Infinity) = x
end
end;
您可能会在包中看到代码,在类似的情况下,它使用导入而不使用 .:
julia> import ParentModule.SubA: add_D
ERROR: ArgumentError: Package ParentModule not found in current path.
然而,由于这是通过 代码加载,它只适用于 [医]父母模式 是在一个文件中的包中。 如果 [医]父母模式 是在REPL定义的,有必要使用相对路径:
julia> import .ParentModule.SubA: add_D
请注意,如果要计算值,定义的顺序也很重要。 考虑一下
module TestPackage
export x, y
x = 0
module Sub
using ..TestPackage
z = y # ERROR: UndefVarError: `y` not defined in `Main`
end
y = 1
end
哪里 子 正在尝试使用 测试包装。y 在它被定义之前,所以它没有一个值。
出于类似的原因,您不能使用循环排序:
module A
module B
using ..C # ERROR: UndefVarError: `C` not defined in `Main.A`
end
module C
using ..B
end
end
模块初始化和预编译
大型模块可能需要几秒钟才能加载,因为执行模块中的所有语句通常需要编译大量代码。 Julia创建模块的预编译缓存以减少这段时间。
预编译的模块文件(有时称为"缓存文件")会在以下情况下自动创建和使用 进口 或 使用 加载模块。 如果缓存文件还不存在,模块将被编译并保存以供将来重用。 您也可以手动调用 基地。compilecache(基地。identify_package("modulename"))在不加载模块的情况下创建这些文件。 生成的缓存文件将存储在 已编译 的子文件夹 DEPOT_PATH[1]. 如果您的系统没有任何更改,则在加载模块时将使用此类缓存文件 进口 或 使用.
预编译缓存文件存储模块、类型、方法和常量的定义。 它们还可以存储方法特化和为它们生成的代码,但这通常需要开发人员添加显式 预编译指令或执行在包构建期间强制编译的工作负载。
但是,如果您更新模块的依赖关系或更改其源代码,则会自动重新编译模块 使用 或 进口. 依赖关系是它导入的模块、Julia构建、它包含的文件或由 include_dependency(路径)在模块文件中。
对于由加载的文件依赖项 包括,通过检查文件大小(大小/大小)或内容(浓缩成散列)不变。 对于由加载的文件依赖项 包括独立性 通过检查修改时间(m时间)不变,或等于截断到最近秒的修改时间(以适应不能以亚秒精度复制mtime的系统)。 它还考虑到搜索逻辑选择的文件的路径是否在 要求 匹配创建预编译文件的路径。 它还考虑了已经加载到当前进程中的依赖项集,并且不会重新编译这些模块,即使它们的文件更改或消失,以避免在正在运行的系统和预编译缓存之间 最后,它考虑到任何变化 编译时首选项。
如果你知道一个模块是_not_安全的预编译(例如,对于下面描述的原因之一),你应该把 __预编译__(false) 在模块文件中(通常放置在顶部)。 这将导致 基地。[医]编译 抛出一个错误,并会导致 使用 / 进口 以将其直接加载到当前进程并跳过预编译和缓存。 这也由此防止该模块被任何其他预编译模块导入。
您可能需要了解创建增量共享库时固有的某些行为,这些行为在编写模块时可能需要小心。 例如,不保留外部状态。 为了适应这一点,显式地将必须在_runtime_发生的任何初始化步骤与可以在_compile time_发生的步骤分开。 为此,Julia允许您定义一个 __init__() 在您的模块中执行必须在运行时发生的任何初始化步骤的函数。 编译过程中不会调用此函数(--输出-*). 实际上,您可以假设它将在代码的生命周期中运行一次。 当然,如果需要,您可以手动调用它,但默认情况是假设此函数处理本地机器的计算状态,该状态不需要-甚至不应该-在编译映像中捕获。 它将在模块加载到进程之后被调用,包括如果它正在加载到增量编译(--输出-增量=是),但如果它正在加载到完整编译过程中,则不会。
特别是,如果你定义一个 函数__init__() 个模块中,那么Julia就会调用 __init__() 立即_after_模块被加载(例如,通过 进口, 使用,或 要求)在_first_时间的运行时(即, __init__ 只被调用一次,并且只有在模块中的所有语句都被执行完之后)。 因为它是在模块完全导入后调用的,所以任何子模块或其他导入的模块都有它们的 __init__ 名为_before_的函数 __init__ 的封闭模块。 这也是跨线程同步的,因此代码可以安全地依赖于这种效果顺序,使得所有 __init__ 将在依赖项排序之前运行 使用 果完成。 它们可以与其他同时运行 __init__ 但是,不是依赖关系的方法,因此在访问当前模块外部的任何共享状态时要小心,以便在需要时使用锁。
两个典型的用途 __init__ 正在调用外部C库的运行时初始化函数,并初始化涉及外部库返回的指针的全局常量。 例如,假设我们正在调用一个C库 libfoo的 这需要我们打电话给 foo_init() 运行时的初始化函数。 假设我们还要定义一个全局常量 foo_data_ptr 它持有a的返回值 void*foo_data() 函数定义 libfoo的 --此常量必须在运行时初始化(而不是在编译时),因为指针地址将从运行更改为运行。 您可以通过定义以下内容来完成此操作 __init__ 模块中的函数:
const foo_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
ccall((:foo_init, :libfoo), Cvoid, ())
foo_data_ptr[] = ccall((:foo_data, :libfoo), Ptr{Cvoid}, ())
nothing
end
请注意,在函数中定义全局是完全可能的,比如 __init__;这是使用动态语言的优点之一。 但是,通过使其在全局范围内成为常量,我们可以确保编译器知道该类型,并允许它生成更好的优化代码。 显然,你的模块中的任何其他全局变量依赖于 foo_data_ptr 也必须在 __init__.
涉及大多数Julia对象的常量,这些对象不是由 ccall不需要放入 __init__:它们的定义可以从缓存的模块映像中预编译和加载。 这包括复杂的堆分配对象,如数组。 但是,任何返回原始指针值的例程都必须在运行时调用,以便预编译工作(Ptr对象将变成空指针,除非它们隐藏在 等位,等位对象)。 这包括Julia函数的返回值 @cfunction和 指针.
在使用预编译时,重要的是要清楚地了解编译阶段和执行阶段之间的区别。 在这种模式下,Julia是一个允许执行任意Julia代码的编译器,而不是一个也生成编译代码的独立解释器。
其他已知的潜在故障情况 include:
-
全局计数器(例如,用于尝试唯一标识对象)。 考虑以下代码片段:
mutable struct UniquedById
myid::Int
let counter = 0
UniquedById() = new(counter += 1)
end
end
虽然这段代码的目的是给每个实例一个唯一的id,但计数器值在编译结束时被记录下来。 此增量编译模块的所有后续用法都将从相同的计数器值开始。
请注意 对象,对象 (通过散列内存指针起作用)也有类似的问题(请参阅 Dict,Dict 下面的用法)。
一种替代方法是使用宏来捕获 @__模块__并与电流单独存储 柜台 值,但是,重新设计代码以不依赖于此全局状态可能会更好。
-
关联集合(如
Dict,Dict和套装)需要重新散列__init__. (将来,可能会提供一种机制来注册初始化函数。) -
取决于编译时的副作用,通过加载时间持续存在。 例子: include: modifying arrays or other variables in other Julia modules; maintaining handles to open files or devices; storing pointers to other system resources (including memory);
-
通过直接引用而不是通过其查找路径从另一个模块创建全局状态的意外"副本"。 例如,(在全局范围内):
#mystdout = Base.stdout #= will not work correctly, since this will copy Base.stdout into this module =#
# instead use accessor functions:
getstdout() = Base.stdout #= best option =#
# or move the assignment into the runtime:
__init__() = global mystdout = Base.stdout #= also works =#
对预编译代码时可以执行的操作设置了一些额外的限制,以帮助用户避免其他错误行为情况:
-
打电话来
埃瓦尔在另一个模块中引起副作用。 这也会导致在设置增量预编译标志时发出警告。 -
全球const本地范围后的语句__init__()已启动(有关为此添加错误的计划,请参阅issue#12010) -
在执行增量预编译时,替换模块是运行时错误。
需要注意的其他几点:
-
在对源文件本身进行更改后,不执行代码重新加载/缓存无效(包括
Pkg。更新资料),并且之后不进行清理Pkg。rm -
预编译忽略了重塑数组的内存共享行为(每个视图都有自己的副本)
-
期望文件系统在编译时和运行时之间保持不变,例如
@__文件__/source_path()在运行时查找资源,或绑定@checked_lib宏。 有时这是不可避免的。 但是,如果可能的话,最好在编译时将资源复制到模块中,这样就不需要在运行时找到它们。 -
弱href对象和终结器当前未由序列化程序正确处理(这将在即将发布的版本中修复)。 -
通常最好避免捕获对内部元数据对象实例的引用,例如
方法,方法/方法,方法表,[医]打字机,打字/打字和这些对象的字段,因为这可能会混淆序列化器,并且可能不会导致您想要的结果。 这样做不一定是错误,但您只需要做好准备,系统将尝试复制其中的一些,并创建其他的单个唯一实例。
在模块开发过程中,关闭增量预编译有时会很有帮助。 命令行标志 --编译模块={yes|no|existing} 使您能够打开和关闭模块预编译。 当朱莉娅开始时 --编译模块=没有 加载模块和模块依赖关系时,将忽略编译缓存中的序列化模块。 在某些情况下,您可能希望加载现有的预编译模块,但不创建新模块。 这可以通过从Julia开始来完成 --编译模块=现有. 更细粒度的控制可与 --pkgimages={yes|no|existing},这只影响预编译期间的本机代码存储。 基地。[医]编译 仍然可以手动调用。 此命令行标志的状态传递给 Pkg。建造工程 在安装、更新和显式构建软件包时禁用自动预编译触发。
您还可以使用环境变量调试一些预编译失败。 设置 JULIA_VERBOSE_LINKING=true 可能有助于解决已编译本机代码的共享库链接失败。 请参阅Julia手册的*开发人员文档*部分,您将在"包图像"下记录Julia内部的部分中找到更多细节。