第四章:了解类型和派遣(learning julia)(完)

每种编程语言都需要了解它所提供的数据类型。 类型(通常称为数据类型)只是数据分类,它使计算机知道用户提供的输入类型之间的差异。 Julia还使用一种唯一标识整数,字符串,浮点数,布尔值和其他数据类型的类型系统。

在本章中,我们将向您详细介绍Julia的扩展类型系统,以及如何提供数据类型可以大大提高整体执行速度。 以下是我们将在本章中介绍的主题列表:
Julia的类型系统

  • 注释类型
  • 更多类型
  • 子类型和超类型
  • 用户定义和复合数据类型
  • 内部构造
  • 模块和接口
  • 模块文件路径
  • 多重调度解释

完成本章后,您将能够:

  • 了解Julia中的各种类型并能够提供它们
  • 使用现有数据类型创建新数据类型
  • 显然,要了解模块和接口之间的区别
  • 深入了解多个调度是什么,以及如何在Julia中使用它。

Julia的类型系统

在我们进一步开始详细探讨Julia的类型系统之前,我们需要知道它们是什么类型,以及为什么它们甚至是必需的。

什么是类型?

要回答这个问题,请考虑以下四行:

1
1.10
'j'
"julia"

我们在这看到什么? 当然,对于我们来说,理解所有四行都有不同类型的数据非常简单明了。 从第一行开始:我们有1,这是一个整数; 1.10,这是一个浮点数(或十进制数); ‘j’,代表单个字符; 最后,“julia”,这是一个简单的字符串,由一起使用的字符集合组成。

但是,即使我们对使用中的数据类型有先验知识,我们如何才能让机器知道相同的内容? 计算机如何知道1是整数,而不是浮点数或字符串? 那么,这个问题的答案就是类型!

静态类型与动态类型语言

在现代编程世界中,我们只有两种不同的方式告诉编译器或解释器所提供的数据类型。

在第一种方法中,我们有静态类型的语言,例如C,C ++或Java,其中我们需要事先明确地定义数据的类型。 这使编译器可以在程序实际执行之前了解传入的数据。 在第二种方法中,我们有动态类型的语言,如Perl,Python和Ruby,其中用户不需要事先声明数据类型,解释器将在运行时自动推断数据类型。

请注意,即使在动态类型语言中,您不需要在将变量分配给值之前声明变量的类型,语言仍然会在内部为数据指定类型,以便将其轻松识别为字符串,或者 整数或任何其他数据类型。

那么,Julia是动态类型还是静态类型的语言?

这个问题的答案是(是的!)。 但是,它倾向于成为动态类型语言,因为它能够在运行时推断数据类型。 话虽如此,但这并不意味着Julia没有丰富的类型系统。 您会惊讶地发现,已经声明类型的Julia代码在执行速度方面要快得多!

引自官方Julia文档:

Julia的类型系统是动态的,但通过指示某些值是特定类型,可以获得静态类型系统的一些优点。 这对于生成有效代码非常有帮助,但更重要的是,它允许对函数参数类型的方法调度与语言深度集成。 (更多信息,请访问https://docs.julialang.org/en/stable/manual/types/。)

注释类型

在上一章中,我们阅读了Julia中的函数以及如何在函数定义中静态声明参数的数据类型。 在本节中,我们将重点关注类型声明和转换,同时使用我们新获得的有关函数的知识,以便通过示例补充每个部分。

让我们看看下面的例子,我们在其中声明一个简单的数学函数来查找数字的立方体:

# declare the function
julia> function cube(number::Int64)
return number ^ 3
end
cube (generic function with 1 method)
# function call
julia> cube(10)
1000

如果您密切关注,您会注意到在声明此函数多维数据集时使用了运算符::,以及Int64。 ::只不过是一个简单的运算符
在Julia中可用,它允许您将类型注释附加到程序中的表达式或变量。 Int64是一种类型,用于表示参数号是Integer数据类型。 我们将在稍后进一步研究Int64整数数据类型以及许多其他类型。 但首先,我们先来看看与Julia中的类型有什么关系。

使用::的一个主要原因是确认我们所具有的功能或程序是否符合预期的期望值。 回到函数多维数据集,我们只想在整数值上运行它,所以我们使用::运算符将参数号声明为整数。

如果这有点过于庞大,那么让我们看看如果我们将一个String参数传递给函数多维数据集会发生什么:

julia> cube("10")
MethodError: no method matching cube(::String)
Closest candidates are:
cube(::Int64) at In[3]:2

我们收到错误,因为我们传递了String参数而不是Int64值! 因此,::运算符确保程序的结果不会偏离我们想要实现的目标。

使用::运算符的第二个也是非常重要的原因是确保Julia LLVM编译器不必自己推断类型并浪费时间。 相反,它会知道参数的类型,因此,性能会大大提高。

对于那些来自Python背景的人,您将非常熟悉isinstance()函数。 ::运算符在Julia中的类似于以上工作。 这是一个例子:

julia> 2::Int64
2
julia> 2::Float64
TypeError: typeassert: expected Float64, got Int64

注意我们得到的错误。 在第一行中,我们没有错误,因为2是整数。 但是当我们尝试运行第二行时,我们遇到了typeassert错误,这基本上意味着::运算符左侧提供的值与::右侧提供的类型不匹配。 也就是说,2不是Float类型,而是Integer类型。

更多类型

抽象数据类型是无法实例化的数据类型。 它们是Julia类型系统的基本支柱。 或者,换句话说,Julia中的其他类型可以继承这些基类型中的任何一种。 抽象数据类型的示例是Number,Integer和Signed。

Julia支持所有基本数据类型,以及添加新的用户定义数据类型以及复合类型的功能。 当我们阅读有关子类型和超类型的内容时,我们将在下一节中了解它们的优先级。

在我们开始详细讨论不同的数据类型之前,我想分享三个简单的函数,我们将使用它们来更多地了解类型:

  • typeof():用于表示提供给它的数据类型
  • typemax():用于了解特定类型支持的最大值
  • typemin():用于了解特定类型支持的最小值

由于我们现在正在进入一个我们的机器位将开始重要的领域,我们需要更好地了解我们正在研究的机器类型。 有利的是,Julia为我们提供的功能可以代表机器支持的位数中的任意数字!

bits()函数:

julia> bits(1)
"0000000000000000000000000000000000000000000000000000000000000001"
julia> sizeof(bits(1))
64

整数类型

Integer类型用于告知Julia LLVM编译器有关传入的整数类型对象的信息。 它被称为Int8,Int16,Int32,Int64或Int128,具体取决于您使用的机器。 例如,如果您正在使用64位计算机,则Integer类型将为Int64:

# Knowing the type of the data type passed
julia> typeof(16)
Int64
# Highest value represented by the In64 type
julia> typemax(Int64)
9223372036854775807
# Lowest value resprented by the Int64 type
julia> typemin(Int64)
-9223372036854775808

我们也可以使用无符号整数,Julia表示它们的方式是使用类型Uint8,Uint16,Uint32,Uint64和Uint128。

浮点类型

这个用于表示Float类型; 换句话说,带小数的整数。 我们在这里使用Float64类型。 请注意,从现在开始,我将使用64位类型作为我的所有示例和代码,因为我将使用64位计算机:

julia> typeof(1.10)
Float64
julia> typemax(Float64)
Inf
julia> typemin(Float64)
-Inf

在这里,Inf表示无穷大,这是Julia表达无限值的独特方式

Char类型

Char类型用于表示单个字符:

julia> typeof('c')
Char
字符串类型

String类型用于表示字符串数据,字符串数据是字符的集合。 请注意,我们在String类型周围有双引号,与字符类型数据周围的单引号不同:

julia> typeof("Julia")
String
Bool类型

Bool类型用于表示布尔类型true和false的值:

julia> typeof(true)
Bool

找出我们正在使用的数据类型的一种快速方法是isa()函数。 在检查原始文档时,我们看到以下内容:

isa(x, type) -> Bool

确定x是否为给定类型。

这几乎与Python中的isinstance()函数提供的功能相同,后者还检查数据类型并返回布尔值。 实际上,它就像以下命令一样简单:

julia> isa(5, Int64)
true
julia> isa(5.5, Float64)
true
julia> isa(5.5, UInt64)
false
类型转换

到目前为止,在本章中,我们一直在讨论Julia中的各种数据类型,主要是它们的具体类型以及我们如何声明它们。 现在,如果我们想要将特定数据类型的值转换为另一种数据类型,该怎么办?

例如,我们有一个名为distance_covered(speed :: Int64,time_taken :: Int64)的函数,它返回移动车辆的速度和所用时间所涵盖的距离。 现在,正如我们所知,距离是计算速度与所用时间的乘积。 或者,换句话说,我们有这样的函数定义:

julia> function distance_covered( speed::Int64, time_taken::Int64)
return speed * time_taken
end
distance_covered (generic function with 1 method)

现在,无论这个产品的结果如何,我们只想要一个整数输出。 但是有一个问题! 整数和整数的乘积总是整数。 这意味着distance_covered()函数的结果总是一个整数。 因此,要获得浮点值,我们需要在此处使用类型转换,以便我们可以获得浮点解决方案:

# Distance covered by vehicle having a speed of 64 kmph and traveling for 2
hours.
julia> distance_covered(64, 2)
128
# testifying that the result was a integer!
julia> typeof(ans)
Int64

为了帮助我们解决这个问题,Julia为我们提供了一个名为convert()的函数。 在我们了解有关convert()函数的更多信息之前,让我们首先使用它来从float中的distance_covered()函数中获取所需的结果。

convert()函数有两个参数。 第一个参数是数据类型,您希望在最终结果中使用该数据类型,或者要将现有值转换为该数据类型; 而第二个参数是您要转换的值.

#converting 128kms in float value
julia> convert(Float64, 128)
128.0
julia> typeof(128.0)
Float64

所以现在,我们得到Float64中函数距离的结果,这就是我们想要的。

在上面的函数中,您将看到我们将128转换为128.0(即从Int64转换为Float64)。 但是,如果我们想将浮点值转换为整数值呢? 看一下下面的代码:

# easily done
julia> convert(Int64, 128.0)
128
# breaks! but why???
julia> convert(Int64, 128.5)
ERROR: InexactError()
in convert(::Type{Int64}, ::Float64) at ./int.jl:239
in convert(::Type{Int64}, ::Float64) at
/Applications/Julia-0.5.app/Contents/Resources/julia/lib/julia/sys.dylib:?

第一次,它能够轻松地将128.0转换为128,但第二次会发生什么? 该函数无法在小数点后转换具有非零值的浮点数! 由于Julia无法将值转换为float,因此引发了名为InexactError()的错误。

作为一种技术语言,Julia努力保持数值计算的准确性。 由于浮点数不能准确地表示为整数,因此Julia会引发错误。

我们如何强迫Julia进行转换? 我们可以通过舍入为Julia提供从Float转换为Int的具体指令。

julia> ceil(Int, 3.4)
4
julia> floor(Int, 3.4)
3
julia> round(Int, 3.4)
3

官方文档有关于此行为的声明:

“如果T是整数类型,如果x不能由T表示,则会引发InexactError,例如,如果x不是整数值,或者超出T支持的范围。”

子类型和超类型

Julia的类型系统被组织成一个干净的数据类型层次结构。 某些数据类型位于其他数据类型之上,反之亦然。 但是,这不应该与类型的优先级混淆,因为我们在这里不是在谈论它。 相反,我们的重点主要是了解Julia的类型系统是如何组织起来成一个树状结构。

首先,Julia中所有数据类型的起点是Any数据类型。 Any类型类似于树的父节点,所有其他可能的数据类型直接或间接地是其子节点。

以下是Julia的类型层次结构(示例):
这里写图片描述

清楚地看一下树结构,用最简单的术语你可以理解,Number类型是Any类型的子类型,它充当父类型Any的子类型。 那么关于Number类型我们称之为父类型呢? 它被称为超级型。 如果这有点难以理解,那么让我们介绍两个用于演示和展示确切解释的函数。

supertype()函数

此函数用于返回作为参数传递的类型的超类型函数。 打开Julia REPL,我们现在可以检查Number类型的超类型:

julia> supertype(Number)
Any

Any类型的超类型怎么样? 我们也试试吧!

julia> supertype(Any)
Any
julia> typeof(Any)
DataType

没有惊喜,因为Any是所有其他数据类型的起点。

subtype()函数

此函数用于返回作为参数传递的类型的子类型函数。 让我们检查数字类型的子类型:

julia> subtypes(Number)
2-element Array{Any,1}:
Complex{T<:Real}
Real

这正是我们在Julia的类型层次结构中讨论的内容。 要记住的一个重要关系是,现在,对于Complex和Real类型,Number类型将被视为超类型函数。 因此,取决于类型层次结构,特定数据类型是否变为子类型函数和/或超类型函数。

检查Any的子类型,我们有很多:

julia> subtypes(Any)
269-element Array{Any,1}:
AbstractArray{T,N}
AbstractChannel
AbstractRNG
AbstractSerializer
AbstractString
Any
Associative
Base.AbstractCartesianIndex
Base.AbstractCmd
Base.AsyncCollector
Base.AsyncCollectorState
Base.AsyncCondition
Base.AsyncGenerator
Base.AsyncGeneratorState
Base.BaseDocs.Keyword
Base.Cartesian.LReplace
Base.ChannelIterState
Base.CodegenHooks
Base.CodegenParams
Base.DFT.FFTW.fftw_plan_struct
Base.DFT.Plan
Base.DataFmt.DLMHandler
...

让我们稍微讨论类型,并将注意力集中在学习给定抽象类型的所有子类型上。 因此,让我们使用现有的子类型创建一个函数,以便我们了解Number的所有子类型,或者可能是AbstractString类型:

# function to check and print all the subtypes of a given abstract type
julia> function check_all_subtypes(T, space = 0)
println("\t" ^ space, T)
for t in subtypes(T)
if t != Any
check_all_subtypes(t, space+1)
end
end
end
check_all_subtypes (generic function with 2 methods)

通过Number类型调用此函数,我们有:

julia> check_all_subtypes(Number)
Number
Complex{T<:Real}
Real
AbstractFloat
BigFloat
Float16
Float32
Float64
Integer
BigInt
Bool
Signed
Int128
Int16
Int32
Int64
Int8
Unsigned
UInt128
UInt16
UInt32
UInt64
UInt8
Irrational{sym}
Rational{T<:Integer}

注:此代码排版有问题,请自行测试

类似地,在抽象类型AbstractString上执行相同的函数,我们有:

julia> check_all_subtypes(AbstractString)
AbstractString
Base.SubstitutionString{T<:AbstractString}
Base.Test.GenericString
DirectIndexString
RepString
RevString{T<:AbstractString}
String
SubString{T<:AbstractString}

用户定义和复合数据类型

到目前为止,我们一直在处理Julia提供的用户可用的数据类型。 现在,我们将探讨如何使用不是由Julia向我们提供的类型,我们需要以解决我们面对的问题,或打算处理来创建它们。

我们创建用户定义数据类型所需的最重要的关键字称为类型。 以下是如何在Julia中创建简单数据类型的示例:

julia> type Person
name::String
age::Int64
end
julia> rahul = Person("rahul",27)
Person("rahul",27)
julia> typeof(rahul)
Person
julia> rahul.name
"rahul"
julia> rahul.age
27

这个例子是我们在Julia中可以考虑用户定义类型的最简单的例子之一。 仔细观察,Person类型有两个字段,即name和age。 可以使用句点(。)表示法轻松访问这些字段。

有趣的是,如果你试图获得这个Person类型的超类型或子类型函数,这就是你得到的:

julia> supertype(Person)
Any
julia> subtypes(Person)
0-element Array{Any,1}

这表明Person类型直接放在Julia类型系统的树层次结构中的Any类型下面。 此时,引入一个名为fieldnames()的新函数也很有趣,该函数用于列出任何已定义类型的所有字段名称:

julia> fieldnames(Person)
2-element Array{Symbol,1}:
:name
:age

函数fieldnames(T)接受数据类型T并输出在该数据类型中声明的所有字段名称。 字段名是Symbol类型,它又是DataType类型的不同数据类型:

julia> typeof(:name)
Symbol
julia> typeof(Symbol)
DataType

与传统的面向对象语言(如Java,Python或C ++)不同,Julia没有类! 是的,这是正确的。 Julia将类型与其方法分开,使用多重派发作为代替。

复合类型

复合类型是命名字段的集合,可以将其视为单个值。 我们可以快速定义复合类型:

julia> type Points
x :: Int64
y :: Int64
z :: Int64
end

我们已经介绍了上一节中的用法。 通过可变,我们的意思是一旦分配了字段名类型,就可以更改并重新分配给某个新值,而不是创建对象时使用的值。 这可以通过如下给出的例子来理解:

julia> struct Point
x::Int
y::Int
z::Int
end
julia> p = Point(1,2,3)
Point(1, 2, 3)
julia> p.x = 10
ERROR: type Point is immutable

我们看到,在创建对象时,我们无法更改x,y和z的值。 错误:类型Point是不可变的错误,用简单的词汇总结了这个问题。

我们需要使用关键字mutable来使类型变为可变。

julia> mutable struct MutPoint
x::Int
y::Int
z::Int
end
julia> p = MutPoint(1,2,3)

MutPoint(1, 2, 3)
julia> p.x = 10
10
julia> p
MutPoint(10, 2, 3)

因此,我们能够将x,y和z的值分配给不同的值,而不是在创建对象样本时声明或分配的值。

内部构造

到目前为止,我们已经了解了如何使用Julia的预定义类型以及用户定义的复合类型。 现在,我们要深入一点深入学习用户定义类型被创建时,会发生什么,并让这个功能更加有用到最终用户的应用程序。

正如我们已经讨论过的,当我们创建一个新类型时,我们可能会或可能不会在正文中包含字段名称。 另一方面,函数仍然很遥远,与其他面向对象的语言不同,这些方法不存在。

因此,当我们创建一个新类型的对象时,默认构造函数就会生效:

julia> type Family
num_members :: Int64
members :: Array{String, 1}
end
julia> f1 = Family(2, ["husband", "wife"])
Family(2, String["husband", "wife"])

我们已经声明了一个复合类型Family,它有两个字段num_members和members。 第一个字段名称用于声明族成员的数量,第二个字段名称列出数组中的所有族成员。

但是,如果我们想要验证Family类型的字段呢? 一种方法是创建一个可以验证同样的另一个功能,并将其应用在同一类型正在创建的对象:

julia> f2 = Family(1, ["husband", "wife"])
Family(1,String["husband","wife"])
julia> function validate(obj :: Family)
if obj.num_members != length(obj.members)
println("ERROR! Not all members listed!! ")
end
end
validate (generic function with 1 method)
julia> validate(f2)
ERROR! Not all members listed!!

因此,正如您所看到的,我们创建了一个名为validate的函数,它将获取Family类型的对象,然后验证提到的成员数是否与数组中列出的成员完全相等!

或者,通过声明内部构造函数,我们可以使用更简单的方法执行相同的操作。 它们甚至可以在创建对象之前用于验证字段,因此可以更好地控制对象创建过程,同时节省我们的有用时间。

我们将使用new创建对象,类似于Java和其他语言,用于为给定的特定类型创建对象。

以下是我们可以做的事情:

julia> type Family
num_members :: Int64
members :: Array{String, 1}
Family(num_members, members) = num_members != length(members) ?
error("Not equal") : new(num_members, members)
end
julia> f1 = Family(1, ["husband", "wife"])

在运行上一行代码时,我们得到错误的第一行打印出我们想要的结果:

ERROR: LoadError: Not equal
in Family(::Int64, ::Array{String,1}) at ../sample.jl:4
in include_from_node1(::String) at ./loading.jl:488
in include_from_node1(::String) at ../julia/lib/julia/sys.dylib:?
in process_options(::Base.JLOptions) at ./client.jl:265
in _start() at ./client.jl:321
in _start() at ../julia/lib/julia/sys.dylib:?
while loading /../sample.jl, in expression starting on line 7

声明内部构造函数可以节省时间,并且如果不满足特定条件,则可以通过停止创建不需要的对象来提高代码的效率。

请考虑以下示例:

julia> type A
x
y
end
julia> type B
x
y
B(x, y) = new(x, y)
end
julia> a = A(1,1)
A(1,1)
julia> b = B(1,1)
B(1,1)

有趣的是,两种形式都是相同的,尽管我们已经在类型B中声明了内部构造函数形式.Julia自动生成类型的构造函数。 用户可以使用内部构造函数声明其他构造函数或覆盖默认构造函数。

不鼓励使用内部构造函数。 从手册:“提供尽可能少的内部构造方法被认为是好的形式:只有那些明确地采用所有参数并执行必要的错误检查和转换。”

模块和接口

与许多其他语言一样,Julia也有一种方法可以将类似的逻辑代码集合在一起组成工作空间,或者也称为命名空间。 它们帮助我们创建顶级定义 - 即全局变量,而不会承担代码冲突的风险,因为模块中使用的名称和变量保持唯一。

在Julia中,我们创建了一个模块如下:

module SampleModule
..
..
end

模块帮助我们指出可以导入程序其他部分的代码,以及用于(或可见)外部世界的代码集。

以下是功能模块的一个小例子:

# marks the start of the module named Utilities
julia> module Utilities
# marks the type, variable or functions to be made
available
export Stype, volume_of_cube
type Stype
name::Int64
end
function area_of_square(number)
return number ^ 2
end
function volume_of_cube(number)
return area_of_square(number) * number
end
# marks the end of the module
end

在这里,我们有一个名为Utilities的模块,它有两个名为volume_of_cube和area_of_square的函数,以及一个名为Stype的用户定义类型。

在这三个中,我们有volume_of_cube和Stype导出,这些可供外面的世界使用。 area_of_square函数保持私有。

一旦制作了一个模块,它就可以被程序的其他部分使用了。 为此,Julia提供了许多便于使用的保留关键字。 最常用的两个是使用和导入。

我们将使用一个非常简单的例子来关注这两者的用法:

julia> module MyModule
foo = 10
bar = 20
function baz()
return "baz"
end

function qux()
return "qux"
end
export foo, baz
end
MyModule

我们将尝试通过导入MyModule来使用foo。

# No public variables are brought into scope
julia> import MyModule
julia> foo
ERROR: UndefVarError: foo not defined
julia> baz()
ERROR: UndefVarError: baz not defined

我们现在尝试使用它。

julia> using MyModule
julia> foo
10
julia> bar
ERROR: UndefVarError: bar not defined
julia> MyModule.bar
20
julia> baz()
"baz"

当我们运行命令时:
using MyModule,然后将所有导出的函数以及其他函数放入范围。 我们也可以选择使用MyModule.foo单独调用MyModule模块中声明的函数。 第二种形式可以使用MyModule:foo。 在这两种情况下,我们只得到我们所说的 - 那就是,只有那些功能进入已明确称为当前范围。

导入MyModule,MyModule模块中的所有函数都将填充当前作用域。 还有另一个名为importall的关键字,它与导入稍有不同,它只是获取模块导出的函数或变量。

但是,使用和导入之间的最大区别在于,使用时,模块中没有任何函数可用于方法扩展,而使用导入时,方法扩展具有灵活性。

在模块中包含文件

我们已经看到了如何使用module关键字创建模块。 但是,如果我们的软件包或模块分散到不同的文件中呢? Julia解决此问题的方法是引入include,它可用于在模块中包含任何Julia文件(扩展名为.jl)。

这是一个让事情变得清晰的例子。 假设我们有一个带有PointTypes名称的模块,我们的代码写在transformations.jl中。 它包含:

function move(p::Point, x, y)
slidex(p, x)
slidey(p, y)
end
function slidex(p::Point, dist)
p.x += dist
end
function slidey(p::Point, dist)
p.y += dist
end

完成模块创建后,下一步就是测试相同的模块。 这可以按如下方式完成:

julia> module PointTypes
mutable struct Point
x::Int
y::Int
end
# defines point transformations
include("transformations.jl")
export Point, move
end
PointTypes
julia> using PointTypes

julia> p = Point(0,0)
PointTypes.Point(0, 0)
julia> move(p, 1, -2)
-2
julia> p
PointTypes.Point(1, -2)
julia> slidex(p, 1)
ERROR: UndefVarError: slidex not defined

模块文件路径

通常,当您使用SomeSampleModule编写文件路径时,Julia会在main中查找该包(它充当Julia的顶级模块):

julia> using SomeSampleModule
ERROR: ArgumentError: Module SomeSampleModule not found in current path.
Run `Pkg.add("SomeSampleModule")` to install the SomeSampleModule package.
in require(::Symbol) at ./loading.jl:365
in require(::Symbol) at
/Applications/Julia-0.5.app/Contents/Resources/julia/lib/julia/sys.dylib:?

如果未在前面的错误中显示,那么它可能会尝试在内部查找从外部源安装的包,通常调用require函数(SomeSampleModule)。

相反,让我们假设您正在开发一个包含许多模块的大项目。 现在,可能会出现这样的情况:您需要将一个模块的函数调用到其他模块的代码中。 工作的第一个也是最重要的要求是在同一条路径中存在这两个模块!

但是,在你有一个模块然后有许多子模块的情况下会发生什么,每个子模块都是该主模块的一部分? 这个问题的答案是使用我们称之为相对导入的东西。 看看这个例子:

julia> module shape
# the submodule areas
module areas
# names of the exported functions
export area_of_square
# the function
function area_of_square(num)
return num ^ 2

end
end
# the relative calling of the submodule areas
using .areas
# calling the function from the submodule
println(areas.area_of_square(2))
end
4
shape

重要的提示!
如何在Julia中设置默认文件路径?

  • 用push! 实际设置名为LOAD_PATH的参数。 这可以作为push(LOAD_PATH,“/ module_path /”)来完成。
  • 也可以通过定义环境变量JULIA_LOAD_PATH来设置加载路径。

什么是模块预编译?

无论何时创建完整模块并尝试加载它,都可能需要几秒钟才能完全加载模块以供使用。

每当Julia加载一个模块时,它就会开始一次从顶级语句中读取代码。 然后这些顶级语句进一步降低,然后可能被解释或编译并执行。

让我们首先看一个简单的模块,我们尝试计算一个巨大的金额,模拟系统中重型模块的加载。 为了使预编译工作,模块必须以模块格式保存并添加到Julia的加载路径中。

julia> module SampleCode
export sum_of_numbers
sum_of_numbers = 0
for num in 1:1000000000
sum_of_numbers += num
end
end

在这里,我们有一个模块,它只是输出一个名为sum_of_numbers的变量,并计算前1,000,000,000(或10 ^ 9)个数字的总和! 相当巨大。 现在让我们尝试在我的机器上第一次加载它:

julia> @time using SampleCode
45.078140 seconds (2.00 G allocations: 29.802 GB, 2.08% gc time)

花了很多时间。 现在,可以做些什么来减少时间呢? 为了解决这个问题,Julia为我们提供了一个名为precompile _()的指令或函数。
这样做是在第一次导入时编译所有内容! 通过这样做,Julia获取所有可以序列化的信息(例如类型推断,解析中的AST等),并将它们保存到缓存中! 然后,此缓存可以充当下次调用时要使用或引用的模块的源。

我们只需修改我们最初的代码来计算1,000,000,000个数字的总和:

__precompile__()
module SampleCode
export sum_of_numbers
sum_of_numbers = 0
for num in 1:1000000000
sum_of_numbers += num
end
end

因此,正如您所看到的,通过添加precompile ()指令,只对原始代码进行了一处更改。 让我们再次尝试在机器上加载这个模块:

julia> @time using SampleCode
INFO: Precompiling module SampleCode.
48.474224 seconds (312.21 k allocations: 13.075 MB)
julia> workspace()
julia> @time using SampleCode
0.001261 seconds (606 allocations: 32.992 KB)

加载时间的差异要大得多,好多了。 当我们在本书后面探讨Julia的性能提示和技巧时,我们将详细回到这个主题。

多次派发与解释

在第2章“定义函数”中,我们介绍了Julia中多重调度的概念,它深深地融入了语言中。 在这里,我们将扩展相同的主题以涵盖类型。

要快速查看多个调度在函数中的含义,以下是打印出数字多维数据集的函数的相同代码:

julia> function calculate_cube(num::Int64)
return num ^ 3
end
calculate_cube (generic function with 1 method)
julia> function calculate_cube(num::Float64)
return num ^ 3
end
calculate_cube (generic function with 2 methods)
# 2 methods for generic function "calculate_cube":
calculate_cube(num::Float64) at REPL[1]:2
calculate_cube(num::Int64) at REPL[1]:2
julia> calculate_cube(10)
1000
julia> calculate_cube(10.10)
1030.301

这里,该函数支持两种具体类型(Int64和Float64)值。 一个给出整数输出,另一个给出Float类型输出。

但是,如何将相同的技术应用于用户类型? 为此,我们首先必须装备我们称之为参数类型的东西。 我们先来看一个例子:

julia> type Coordinate{T}
x::T
y::T
z::T
end

这里,我们有一个名为Coordinate的用户定义类型,它表示具有x,y和z轴的3D空间中的点。 仔细观察语法,我们看到{T},它包含在花括号中。 这里,T充当任意类型,可以作为参数在此Coordinate用户定义类型上传递。

Coordinate类型的主体具有值x,y和z,它们都使用::注释到相同的类型T.前面,我们现在创建一个这种新类型的对象 - 让我们看看它是如何工作的:

# when T is of Int64 type
julia> point = Coordinate{Int64}(1,2,3)
Coordinate{Int64}(1,2,3)
julia> point.x
1
julia> point.y
2
julia> point.z
3
# when T is of Float64 type
julia> point = Coordinate{Float64}(1.0,2.0,3.0)
Coordinate{Float64}(1.0,2.0,3.0)
julia> point.x
1.0
julia> point.y
2.0
julia> point.z
3.0

这里,变量点保存Int64类型的值,然后保存Float64类型值。

让我们回到多次派遣。 在清楚地了解参数化复合类型是如何制作或实例化之后,让我们使用参数类型实现多个调度:

# Creating a parametric type
julia> type Coordinate{T}
x::T
y::T
z::T
end
# Method that works on Int64
julia> function calc_sum(value::Coordinate{Int64})
value.x + value.y +value.z
end
calc_sum (generic function with 1 method)
# Method that works on Float64
julia> function calc_sum(value::Coordinate{Float64})
value.x + value.y +value.z
end
calc_sum (generic function with 1 method)
# Multiple Dispatch
julia> methods(calc_sum)
# 2 methods for generic function "calc_sum":
calc_sum(value::Coordinate{Int64}) at REPL[61]:2
calc_sum(value::Coordinate{Float64}) at REPL[60]:2
# Calling the method on Int64
julia> calc_sum(Coordinate(1,2,3))
6
julia> typeof(ans)
Int64
# Calling the method on Float64
julia> calc_sum(Coordinate(1.0,2.0,3.0))
6.0
julia> typeof(ans)
Float64

概要

在本章中,我们将了解Julia中的类型及其实现方式。 我们看到了Julia的类型系统是多么庞大和多样化,以及如何使用子类型和超类型将这些类型进一步分类为层次结构。 我们花时间了解用户如何定义类型,然后深入挖掘构造函数方法,探索内部和外部构造函数。 接下来,我们重新审视了多个调度并引入了参数类型,最终帮助我们实现了调度技术。

在下一章中,我们将看到控制流如何在Julia中工作,您将详细阅读该语言使用的各种循环技术。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值