在Julia中模块是分离的变量工作区,例如引入一个新的全局作用域。
是带分隔符的语法(delimited syntactically),在module Name
和end
内。
模块允许创建顶层定义(即全局变量)而不用担心自己的代码和别人的代码一起用的时候的命名冲突。
在一个模块内部,可以控制来自别的模块的哪些命名可见(通过导入),可以指定哪些本模块的命名可暴露(通过导出)。
下述例程演示包的主要特征。 并非意味着可运行,但为解说目的而展示:
module MyModule
using Lib
using BigLib: thingx, thingy
import Base.show
export MyType, fuck
struct MyType
x
end
bar(n) = 2n
fuck(mt::MyType) = bar(mt.x) + 9527
show(io::IO, mt::MyType) = print(io, "1314")
end
注意上述代码风格没有缩进模块体,因为会通常导致整个文件都缩进。
该模块定义了一个MyType
类型,两个函数bar
和fuck
。
函数fuck
和类型MyType
被导出,因此可用于导入到别的模块中。
函数bar
是MyModule
模块的私有函数。
声明using Lib
意思是该模块调用Lib
将被用于按需解决命名。
当碰到未在当前模块中定义的全局变量,系统将会在Lib
导出的变量中搜索,并将搜索到的命名导入其中。
这意味着,所有当前模块中用到的那个全局变量都会解析到Lib
中的定义。
声明using BigLib: thingx, thingy
仅从BigLib
引入标识符thingx
和thingy
到当前作用域。
如果这些命名引用的是函数,则给这些函数添加方法是不允许的(只能用而不能扩展)。
关键字import
支持和using
相同的语法,但一次只操作在一个命名。
并且import
并未像using
一样将模块添加到搜索路径。
导入函数上import
还和using
有区别,前者导入的函数可在当前作用域中扩展新方法。
在上述MyModule
中,想要给标准show
函数添加方法,因此需要些import Base.show
。
仅通过using
可见的函数命名不可被扩展。
一旦一个变量被using
或import
操作可见,当前模块便不能创建同名对象。
导入的变量是只读的;给全局变量赋值总是影响当前模块自己的变量,或者抛出错误。
加载一个模块,要用到两个关键字:using
和import
。
要理解这两个关键字的区别,考虑下述例程:
module MyModule
export x, y
x() = "x"
y() = "y"
z() = "z"
end
在该模块,咱导出x
和y
函数(用export
关键字),还有个未导出的z
函数。
加载模块及其内部函数到当前工作区有若干方式:
导入命令 | 给当前作用域带来了什么 | 可用的方法扩展 |
---|---|---|
using MyModule |
所有导出的命名(x 和y ),MyModule.x 、MyModule.y 和MyModule.z 。 |
MyModule.x 、MyModule.y 和MyModule.z 。 |
using MyModule: x, z |
x 和z 。 |
无 |
import MyModule |
MyModule.x 、MyModule.y 和MyModule.z 。 |
MyModule.x 、MyModule.y 和MyModule.z 。 |
import MyModule.x, MyModule.z |
x 和z 。 |
x 和z 。 |
import MyModule: x, z |
x 和z 。 |
x 和z 。 |
文件和文件名几乎和模块无关;模块仅和模块表达式相关。 每个模块可以有多个文件,也可以一个文件多个模块。
module Fuck
include("alice.jl")
include("bob.jl")
end
在不同模块中包含相同代码提供类似混合(mixin)的行为。 可以运用该特性以不同基础定义执行相同代码,举个例子,以某些操作的安全版本执行测试代码:
module Normal
include("normal.jl")
end
module Testing
include("safes.jl")
include("normal.jl")
end
有三个重要的标准模块:Main、Core、Base。
Main是顶层模块,且Julia以Main为当前模块开始。在提示符(prompt)进入Main时定义的变量,可通过varinfo()
列出。
Core包含所有认为是语言内建的标识符,也就是,是编程语言核心的一部分,而不是库的。每个模块隐式指定using Core
,因为没有这些定义,啥也干不了。
Base是包含基本功能(即base/
下的内容)的模块。所有模块隐式包含using Base
,因为绝大多数情况下需要Base中的定义。
除了using Base
,模块也自动包含eval
和include
函数定义,这些函数用来计算该模块全局作用域内的表达式和文件。
如果这些默认的定义不是想要的,可以用baremodule
关键字定义(注意Core
仍然如上述被导入)。
按照baremodule
,标准module
看起来如是:
baremodule BM
using Base
eval(x) = Core.eval(BM, x)
include(p) = Base.include(BM, p)
# 继续表演
end
给定using Fuck
声明,系统查阅内部顶层模块表寻找名为Fuck
的模块。
如果该模块不存在,系统尝试require(:Fuck)
,它典型地导致从已安装地包中加载代码。
然而,某些模块包含子模块,这意味着有些时候需要访问一个非顶层模块。
有两种方式。
第一种用绝对路径,如using Base.Sort
。
第二种用相对路径,让导入当前模块地子模块或任何它的闭包模块很容易:
module Parent
module Utils
# 继续表演
end
using .Utils
# 更多节目
end
这里的Parent
模块包含Utils
子模块,而且在Parent
中的代码想要看到Utils
的内容。
这可以using
以句号开始的路径来实现。
添加更多前导句号上移更多模块层级。
如using ..Utils
将在Parent
闭包模块中查找Utils
,而不是在Parent
自身内。
注意,相对导入修饰语仅在using
和import
声明中有效。
全局变量LOAD_PATH
包含Julia调用require
时查找模块的目录集合。
可以通过push!
扩展。
push!(LOAD_PATH, "/Path/To/My/Module/")
将该声明放入~/.julia/config/startup.jl
将会在每次启动Julia时扩展LOAD_PATH
,模块加载路径可通过定义环境变量JULIA_LOAD_PATH
来扩展。
如果命名是合格的(如Base.sin
),则可被访问,即使未被导出。
调试时,这通常有用。
也可以将合格的用作函数名并添加方法。
然而,因为引起语法歧义,如果希望给不同模块中仅包含符号的名称函数添加方法,举个例子,如操作符“Base.+”,必须用Base.:+
引用之。
如果操作符不止一个字符,必须用圆括号包裹起来,如Base.:(==)
。
在导入导出声明中,宏名用@
编写,例如import M.@m
。
别的模块中的宏,可用M.@m
或@M.m
调用。
语法M.x = y
在别的模块中给一个全局变量赋值无效;全局赋值总是模块本地的。
变量名无须通过声明为global x
被保留。
这避免加载之后全局初始化的命名冲突。
大的模块需要若干秒来加载,因为执行模块中全部声明经常包含编译大量的代码。 Julia创建模块的预编译缓存来减少这个耗时。
一个增量预编译模块文件被创建且当用import
或using
加载模块是被自动使用。
这将导致第一次导入模块时该模块被编译。
可选地,可以手动调用Base.compilecache(modulename)
。
作为结果的缓存文件将保存在DEPOT_PATH[1]/compiled/
。
随后,模块基于using
或import
自动预编译,无论任何依赖变化时;依赖是模块所导入的,Julia构建的,所包含的文件,或在模块文件中include_dependency(path)
声明的隐式依赖。
对于文件依赖的更新,通过检测每个用include
加载或用include_dependency
添加的修改时间(mtime)为变化否,或等于截断到最近修改时间(秒)(适应不能以压秒精度拷贝mtime
的系统)决定。
文件路径匹配require
搜索逻辑选择的路径创建预编译文件与否,也纳入考虑范围。
已经加载到当前进程的以来集合可算入,但不会重新编译这些模块,即使相关文件被修改或消失,为了避免创建运行中系统和预编译缓存的不兼容。
如果想要变更的源代码反映到运行中的系统,应当在变化的模块上调用reload("Module")
,任何依赖该模块的模块,想要让变更生效,也需要这样做。
如果已知某个模块预编译客户模块是不安全的(如后述原因之一),需要在模块文件中放置__precompile__(false)
(通常放在顶部)。
这导致Base.compilecache
抛出错误,也会导致using
/import
直接加载到当前进程,跳过预编译和缓存。
这也因此放置模块被任何别的预编译模块导入。
可能需要注意继承的增量共享库创建的特定行为,编写客户模块时须要小心。
举个例子,外部状态未被维护。
适应这个情况,显式分离任何在运行时必然繁盛的初始化步骤和编译时发生的步骤。
为了这个目的,Julia允许用户在自己的模块定义__init__()
函数,执行任何必须在运行时发生的初始化步骤。
该函数在编译过程中不会被调用(--output-*
)。
高效地,假定在代码生命周期恰好仅返回一次。
当然,可以手动调用,如果需要的话,但是默认假定该函数处理计算本地机器的状态,不需要或不应该在编译镜像中捕捉。
在模块加载到进程中之后会被调用,包括加载到增量编译(--output-incremental=yes
)的情况,不包括加载到全量编译过程的情况。
特别是,如果在模块中定义function __init__()
,则Julia将会在运行时模块第一次被加载(import
/using
/require
)后直接调用__init__()
(也就是说__init__()
仅被调用一次且在模块中全部声明被执行后)。
因为它在模块被全部导入后调用,任何子模块或被导入的模块都有其__init__
函数,在闭包模块的__init__
函数被调用之前被调用。
__init__
两种典型用发时调用外部C库运行时初始化函数和初始化包含外部库返回指针的全局常量。
例如,假定调用C库libfuck
,须要在运行时调用fuck_init()
初始化函数。
假定咱也想要定义一个全局常量fuck_data_ptr
把持libfuck
定义的void *fuck_data()
函数返回值,该常量必须在运行时初始化,因为指针地址在每次运行时会变化。
通过在自己模块中定义__init__
函数完成这个目标:
const fuck_data_ptr = Ref{Ptr{Cvoid}}(0)
function __init__()
ccall((:fuck_init, :libfuck), Cvoid, ())
fuck_data_prt[] = ccall((:fuck_data, :libfuck), Ptr{Cvoid}, ())
nothing
end
注意,可以在如__init__
函数内定义全局变量棒极了;这是运用动态编程语言的一种高级货色。
但是通过使其成为全局作用域的常量,可以保证编译器知道该类型,并且允许产生更优化的代码。
显然(想起了中学数理化证明题),任何客户模块中的别的基于fuck_data_ptr
的全局变量也必须在__init__
中初始化。
常量,包括大多数Julia对象,不是由ccall
产出的就没必要放在__init__
中:这些乡党的定义可以被预编译并从缓存模块景象中导入。
这包括复杂的分配在堆上的对象,如数组。
然而,任何返回原始指针值的协程必须在运行时被调用,已让预编译工作(Ptr
对象将变为null
指针除非隐藏在比特(isbits)对象中)。
这包括Julia函数cfunction
和pointer
的返回值。
字典和集合类型,或一般的基于hash(key)
方法输出的任何事物,是奸诈把戏(trickier case)。
在键是数字、字符串、符号、区间、Expr
或这些类型合成物(通过数组、元组、集合、对等)的普通情况下,可被安全地预编译。
然而,对于少数键类型,如Function
或DataType
和一般用户自定义却没有定义hash
方法的类型,后手hash
方法基于对象的内存地址(通过对象的objectid
),并且因此每次运行都不同的兄弟姊妹。
如果有这种键类型,或者如果不确定有无,保险起见,可以在骚年的__init__
函数中初始化字典。
作为备选,可用IdDict
字典类型,它被预编译特殊处理,因此在编译时可安全初始化。
当使用预编译,保持对编译阶段和执行阶段却别清醒的认识很重要。 在该模式中,表面上经常更清楚Julia是一个编译器,允许执行任意的Julia代码,而不是孤立的也产生被编译的代码的解释器。
其余已知的潜在失败场景包括:
-
全局计数器(例如尝试唯一识别对象),考虑如下代码片段:
mutable struct UniquedById myid::Int let counter = 0 UniquedById() = new(counter += 1) end end
虽然代码的意图是给每个实例一个唯一的标识,计数值在编译结束时被记录。 该增量编译模块的后续使用将从相同的计数值开始。 注意
objectid
(哈希内存指针)有类似的问题(见后边Dict
表奏)。 另一种可选的方案是用宏来捕捉@__MODULE__
并和当前counter
值一起保存,然而,最好还是重新设计代码,不要依赖全局状态。 -
组合的收纳(如
Dict
和Set
)须要在__init__
中重新哈希。未来,将提供注册初始化函数的机制。 -
贯穿加载时,受编译时副作用的影响。这种例子包括:修改数组或别的Julia模块中的别的变量;维护打开的文件或设备句柄;保存别的系统资源的指针(包括内存)。
-
创建来自别的模块的全局状态的副本,通过直接引用它而不是查询搜索路径。举个例子(在全局作用域):
# mystdout = Base.stdout #= 不能直接工作,因为将会拷贝`Base.stdout`到该模块中。 =# # 换用访问函数: getstdout() = Base.stdout #= 最佳选项 =# # 或将赋值移入运行时: __init__() = global mystdout = Base.stdout #= 也有效的 =#
在预编译代码过程中,可做的操作有若干额外的限制,有助于用户避免别的错误行为的情况:
- 调用
eval
给别的模块引入副作用。当设置了增量预编译标志,也将导致警告被忽略。 - 来自
__init__()
之后的本地作用域global const
声明已经开始(查看问题计划为此添加错误)。 - 在做增量预编译过程中替换模块是运行时错误。
些许要知道的点:
- 修改源代码文件自身后并没有代码重载或缓存失效,包括通过
Pkg.update
,在Pkg.rm
之后不会清理。 - 重塑数组形状的内存共享被预编译无视(每个视图获取自个儿的副本)。
- 在编译时和运行时期望文件系统不被修改,例如在运行时用
@__FILE__/source_path()
查找资源,或BinDeps
的checked_lib
宏。有些时候这不可避免。可是,当可能时,在编译时拷贝资源到模块中被认为是优秀实践,因而不需要在运行时再查找。 WeakRef
对象和终结者(finalizer)目前尚未被序列化(串行器:serializer)正确处理(将在即将发布的版本中修复)。- 经常最好避免捕捉引用内部原属对象的实例,如
Method
、MethodInstance
、MehtodTable
、TypeMapLevel
、TypeMapEntry
和这种对象的字段,因为这会使系列化混乱且可能不会导致期望的结果。此处不必要错误,但可以简单的为需准备,系统尝试拷贝某些这种东西并创建其余的单个唯一的实例。
有些时候,关闭增量预编译在模块开发种是有助的。
命令行标志--compiled-modules={ye|no}
使得码农切换模块预编译,打开或关闭。
当Julia以--compiled-modules=no
启动,预编译缓存中的序列化模块将在加载模块和模块依赖时被忽略。
仍然可手动调用Base.compilecache
。
该命令行标志传递给Pkg.build
禁止当安装、更新和显式构建包时触发的自动预编译。
- 太像Python啦。