Introducing Julia/Types
Types
编辑这一节是关于类型的,下一节是关于函数和方法的,理想情况下应该同时阅读这两个主题,因为这两个主题是紧密地联系在一起的。
Types of type
编辑数据元素有不同的形状和大小,称为类型。
考虑以下数值:浮点数、有理数和整数:
0.5 1//2 1
对我们人类来说,不假思索地将这些数字相加是很容易的,但计算机不能使用简单的加法程序将所有三个值相加,因为它们的类型是不同的。添加有理数的代码必须考虑分子和分母,而添加整数的代码则不必考虑分子和分母。计算机可能必须将这些值中的两个转换为与第三个值相同的类型(通常是将 整数 和 有理数 首先转换为浮点数),然后将三个浮点数相加在一起。
这种转换类型的工作显然需要时间。因此,要编写真正快速的代码,您需要确保不会不断地将值从一种类型转换为另一种类型,从而浪费计算机的时间。当 Julia 编译您的源代码时(每次您第一次对函数求值时都会发生这种情况),您提供的任何类型指示都允许编译器生成更高效的可执行代码。
转换类型的另一个问题是在某些情况下,您将丢失精度:将有理数转换为浮点数可能会丢失一些精度。
Julia 的设计师的官方说法是,类型是可选的。换句话说,如果您不想担心类型(如果您不介意代码的运行速度慢于它可能),那么您可以忽略它们。但是您将在错误消息和文档中遇到它们,因此您最终将不得不处理它们…
折中的方法是编写 top-level 的代码,而不必担心类型,但是,如果您希望加快代码速度,请找出程序花费时间最多的瓶颈,并清理该区域中的类型。
类型系统
编辑关于Julia的类型系统有很多需要了解的地方,所以正式文档才是真正的去处。但这里有一个简短的概述。
类型层次
编辑在Julia中,类型是按层次结构组织的,具有树状结构。
在树的根中,我们有一个称为 Any
的特殊类型,所有其他类型都直接或间接地连接到它。非正式地说,我们可以说这种 Any
有孩子。它的孩子被称为 Any
的子类。单个孩子 超类 是 Any
。(但是,请注意,类型之间的层次关系是 explicitly declared 显式声明的,而不是 implied by compatible structure 兼容的结构暗示的。)
通过查看 Number 类型,我们可以看到 Julia 类型层次结构的一个很好的示例。
类型 Number
是类型 Any
的一个直接子类。 查看 Number
的超类是谁,我们可以使用 supertype()
函数:
julia> supertype(Number) Any
我们还能尝试找出 Number
的所有子类 (Number 的子辈,因此也就是, Any 的孙辈). 想要找到,我们可以用 subtypes()
函数:
julia> subtypes(Number) 2-element Array{Union{DataType, UnionAll},1}: Complex Real
我们可以观察到,我们有两个 Number
的子类型:复数 Complex
和实数 Real
。对于数学家来说,实数和复数都是数字。作为一般规则,Julia的类型层次结构反映了现实世界的层次结构。
作为另一个例子,如果 美洲豹 Jaguar
和 狮子 Lion
都是 Julia 的类型,很自然的他们的超类会是猫科动物 Feline
:
julia> abstract type Feline end julia> mutable struct Jaguar <: Feline end julia> mutable struct Lion <: Feline end julia> subtypes(Feline) 2-element Array{Any,1}: Jaguar Lion
具体和抽象类型
编辑Julia中的每个对象(非正式地说,这意味着在Julia中可以放入变量的所有内容)都有一个类型。但并非所有类型都可以有各自的对象(该类型的实例)。唯一可以有实例的是所谓的具体类型。这些类型不能有任何子类型。可以有子类型(例如Any
, Number
)的类型称为抽象类型。因此,我们不能拥有Number类型的对象,因为它是抽象类型。换句话说,只有类型树的叶子是具体的类型,并且可以实例化。
如果我们不能创建抽象类型的对象,为什么它们有用呢?有了它们,我们可以为它的任何子类型编写泛化的代码。例如,假设我们编写了一个期望类型为 Number
的变量的函数:
#this function gets a number, and returns the same number plus one
function plus_one(n::Number)
return n + 1
end
在本例中,函数需要一个变量 n
. n
的类型必须是 Number
的子类型(直接的或间接的) 。用 :: 语法表示(但是不要担心语法)。这是什么意思?无论n
的类型是Int
(整数) 还是 Float64
(浮点数), 函数 plus_one()
都可以正常工作. 此外, plus_one()
不适用于任何不是 Number
的子类型 的类型 (例如文本字符串、数组)。
我们可以将具体类型分为两类:基元 primitive (或基本) 和 复杂(或复合)。基元类型用来构建代码块,通常硬编码到 Julia 的心脏中,而复合类型将许多其他类型组合在一起,以表示更高级别的数据结构。
您可能会看到以下基本类型:
- 基本整数和浮点数类型(有符号和无符号):
Int8
,UInt8
,Int16
,UInt16
,Int32
,UInt32
,Int64
,UInt64
,Int128
,UInt128
,Float16
,Float32
, andFloat64
- 更高级的数字类型:
BigFloat
,BigInt
- 布尔和角色类型:
Bool
andChar
- 文本字符串类型:
String
复合类型的一个简单示例是 Rational
,用于表示分数。它由两个部分组成,一个分子和一个分母,两个整数(类型为 Int
)。
调查一下类型
编辑Julia 提供了两个用于导航类型层次结构的函数:subtypes()
和 supertype()
。
julia> subtypes(Integer) 4-element Array{Union{DataType, UnionAll},1}: BigInt Bool Signed Unsigned julia> supertype(Float64) AbstractFloat
sizeof()
函数告诉您此类型的项占用的字节数:
julia> sizeof(BigFloat) 32 julia> sizeof(Char) 4
如果您想知道某个特定类型可以容纳的数字有多大,以下两个函数非常有用:
julia> typemax(Int64) 9223372036854775807 julia> typemin(Int32) -2147483648
在基本Julia系统中有340多种类型。可以使用以下函数调查类型层次结构:
function showtypetree(T, level=0)
println("\t" ^ level, T)
for t in subtypes(T)
if t != Any
showtypetree(t, level+1)
end
end
end
showtypetree(Number)
它会为不同的数字类型生成如下所示:
julia> showtypetree(Number) Number Complex Real AbstractFloat BigFloat Float16 Float32 Float64 Integer BigInt Bool Signed Int128 Int16 Int32 Int64 Int8 Unsigned UInt128 UInt16 UInt32 UInt64 UInt8 Irrational Rational
例如,这显示了实数 Real
的四个主要子类型:抽象浮点 AbstractFloat
、整数 Integer
、有理数 Rational
和无理数 Irrational
。
指定变量类型
编辑我们已经看到,如果您不指定变量类型的话,Julia会尽最大努力计算出您在代码中放入的内容类型,:
julia> collect(1:10) 10-element Array{Int64,1}: 1 2 3 4 5 6 7 8 9 10 julia> collect(1.0:10) 10-element Array{Float64,1}: 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0
我们还看到,您可以为新的空数组指定类型:
julia> fill!(Array{String}(undef, 3), "Julia") 3-element Array{String,1}: "Julia" "Julia" "Julia"
对于变量,可以指定其值必须具有的类型。由于技术原因,您不能在 top-level 中指定类型。在REPL中,您只能在定义的里面这样做。使用 ::
语法,表示“是这个类型”。所以:
function f(x::Int64)
表示函数 f
有一个接受参数 x
的方法,该参数类型预期为 Int64。请参见函数 Functions。
类型稳定性
编辑下面的示例说明了变量类型的选择如何影响 Julia 代码的性能。您能找出这两个函数之间的唯一区别吗:
function t1(n)
s = 0
t = 1
for i in 1:n
s += s/i
t = div(t, i)
end
return t
end
function t2(n)
s = 0.0
t = 1
for i in 1:n
s += s/i
t = div(t, i)
end
return t
end
这两个定义几乎是相同的,除了每个定义开头的s的类型。在运行它们几次之后,计时结果是值得注意的:
julia> @time t1(10000000) 0.658128 seconds (60.00 M allocations: 915.520 MiB, 25.01% gc time)
julia> @time t2(10000000) 0.118332 seconds (4 allocations: 160 bytes)
原因是s开始时是一个整数(s=0表示Julia最初将s视为整数),但是在循环中,它被分配用来保存s/i的结果,这是一个浮点值:必须将其从整数转换为浮点以进行匹配。因此该函数不是类型稳定的-Julia编译器无法对其内容进行假设,因此它不能生成纯整数代码或纯浮点代码。因此,它最终生成的代码并不像它所能产生的那样快。
t1()
的表现明显低于 t2()
. 原因是 s
一开始是一个整数。(s = 0
表示 Julia 最初将 s
视为整数), 但是在循环中,它被分配用来保存s/i
的结果,这是一个浮点值:必须将其从整数转换为浮点以进行匹配。因此此该函数不是类型稳定的,Julia编译器无法对其内容进行假设,也就不能生成纯整数代码或纯浮点代码。因此,它最终生成的代码并不像它写的时候所能产生的那样快。
如果要比较这个小差异导致 Julia 编译器生成的额外编译代码的数量,请运行 @code_native t1(100)
和 @code_native t2(100)
命令。
创建类型
编辑在Julia中,程序员很容易就能创建新的类型,受益于 native 类型(由Julia的创建者创建的类型)所具有的相同的性能和语言方面的集成。
In Julia, it's very easy for the programmer to create new types, benefiting from the same performance and language-wise integration that the native types (those made by Julia's creators) have.
抽象类型
编辑假设我们要创建一个抽象类型。为此,我们使用 Julia 的关键字 abstract
,后跟要创建的类型的名称:
abstract type MyAbstractType end
默认情况下,您创建的类型是 Any
的直接子类型:
julia> supertype(MyAbstractType) Any
您可以使用 <:
运算符来更改它。例如,如果希望新的抽象类型是 Number
的子类型,则可以声明:
abstract type MyAbstractType2 <: Number end
现在我们得到:
julia> supertype(MyAbstractType2) Number
请注意,在同一Julia会话中(在不退出REPL或结束脚本的情况下),不可能重新定义类型。这就是为什么我们必须创建一个名为 MyAbstractType2
的类型。
具体类型和组合
编辑现在可以创建新的复合类型了。请使用 struct
或 mutable struct
关键字,它们的语法与声明超类的语法相同。新类型可以包含多个字段,对象在这些字段中存储值。作为示例,让我们定义一个具体的类型,它是 MyAbstractType
的子类型:
mutable struct MyType <: MyAbstractType
foo
bar::Int
end
我们刚刚创建了一个名为 MyType
的复合结构类型,它是 MyAbstractType
的一个子类型,有两个字段:foo
可以是任何类型,bar
可以是 Int
类型。
如何创建 MyType
对象?默认情况下,Julia 会自动创建一个构造函数,该函数返回该类型的对象。该函数具有相同的类型名称,并且该函数的每个参数对应于每个字段。在此示例中,我们可以通过键入以下命令来创建新对象:
julia> x = MyType("Hello World!", 10) MyType("Hello World!", 10)
这将创建一个 MyType
对象,并赋值 Hello World!
到 foo
字段和 10
到 bar
字段。我们可以使用点符号访问 x
的字段:
julia> x.foo "Hello World!" julia> x.bar 10
此外,我们还可以轻松地更改可变结构的字段值:
julia> x.foo = 3.0 3.0 julia> x.foo 3.0
注意,由于我们在创建类型定义时没有指定 foo
的类型,所以可以随时更改它的类型。
而不同指出在于,当我们尝试更改 x.bar
字段的类型(根据 MyType
的定义,我们将其指定为 Int
)时:
julia> x.bar = "Hello World!" LoadError: MethodError: Cannot `convert` an object of type String to an object of type Int64 This may have arisen from a call to the constructor Int64(...), since type constructors fall back to convert methods.
错误消息告诉我们 Julia 无法更改 x.bar
的类型。这确保了类型稳定的代码,并且可以在编程时提供更好的性能。作为性能提示,在定义类型时指定字段的类型通常是很好的做法。
缺省构造函数用于简单的情况,在这种情况下,您键入类似 typename(field1, field2) 之类的内容来生成该类型的新实例。但有时您希望在构造新实例时执行更多操作,例如检查传入的值。为此,您可以使用内部构造函数,即类型定义中的函数。下一节将展示一个实例。
例子:英国货币
编辑下面是一个示例,说明如何创建一个可以处理老式英国货币的简单复合类型。在英国看到曙光并引入十进制货币之前,货币体系用了英镑、先令和便士,其中一英镑由20先令组成,一先令由12便士组成。这就是所谓的 £sd 或 LSD 系统(拉丁语的LiBRAE,Solidii,Denarii,因为该系统起源于罗马帝国)。
要定义合适的类型,开始一个新的复合类型声明:
struct LSD
要包含以英镑、先令和便士为单位的价格,此新类型应包含三个字段:英镑、先令和便士:
pounds::Int
shillings::Int
pence::Int
重要的任务是创建 构造函数。它与类型具有相同的名称,并接受三个值作为参数。在检查了几个无效值之后,特殊的 new()
函数将创建一个带有传入值的新对象。请记住,我们仍然在类型定义中:这是一个内部构造函数。
function LSD(a,b,c)
if a < 0 || b < 0 || c < 0
error("no negative numbers")
end
if c > 12 || b > 20
error("too many pence or shillings")
end
new(a, b, c)
end
现在我们完成类型的定义
end
下面是完整的类型定义:
struct LSD
pounds::Int
shillings::Int
pence::Int
function LSD(a, b, c)
if a < 0 || b < 0
error("no negative numbers")
end
if c > 12 || b > 20
error("too many pence or shillings")
end
new(a, b, c)
end
end
现在有可能创造出新的物品来储存老式的英国价格。使用其名称(调用构造函数)创建此类型的新对象:
julia> price1 = LSD(5, 10, 6) LSD(5, 10, 6) julia> price2 = LSD(1, 6, 8) LSD(1, 6, 8)
而且,由于构造函数中添加了简单的检查,所以不能创建糟糕的价格:
julia> price = LSD(1,0,13) ERROR: too many pence or shillings Stacktrace: [1] LSD(::Int64, ::Int64, ::Int64)
如果检查我们创建的某个价格“对象”的字段:
julia> fieldnames(typeof(price1)) 3-element Array{Symbol,1}: :pounds :shillings :pence
您可以看到以下三个字段,它们存储这些值:
julia> price1.pounds 5 julia> price1.shillings 10 julia> price1.pence 6
下一项任务是使此新类型的行为方式与其他 Julia 对象相同。例如,我们无法让两个价格相加:
julia> price1 + price2 ERROR: MethodError: no method matching +(::LSD, ::LSD) Closest candidates are: +(::Any, ::Any, ::Any, ::Any...) at operators.jl:420
而且输出肯定可以得到改善:
julia> price2 LSD(5, 10, 6)
Julia已经有了加法函数(+
),其中包含为许多类型的对象定义的方法。下面的代码添加了另一个可以处理两个LSD对象的方法:
function Base.:+(a::LSD, b::LSD)
newpence = a.pence + b.pence
newshillings = a.shillings + b.shillings
newpounds = a.pounds + b.pounds
subtotal = newpence + newshillings * 12 + newpounds * 240
(pounds, balance) = divrem(subtotal, 240)
(shillings, pence) = divrem(balance, 12)
LSD(pounds, shillings, pence)
end
这个定义教 Julia 如何处理新的 LSD 对象,并向 +
函数添加一个新方法,这一个方法接受两个 LSD 对象,将它们相加,并生成一个新的 LSD 对象。
现在你可以将两个价格相加:
julia> price1 + price2 LSD(6,17,2)
这的确是将 LSD(5,10,6) 和 LSD(1,6,8) 相加的结果
下一个要解决的问题是 LSD 对象的不吸引人的表示。我们通过添加新方法,以完全相同的方式修复了此问题,但这次是添加到 show()
函数中,该函数属于 Base 环境:
function Base.show(io::IO, money::LSD)
print(io, "£$(money.pounds).$(money.shillings)s.$(money.pence)d")
end
在这里,io
是所有 show()
方法当前使用的输出通道。我们添加了一个简单的表达式,用适当的标点符号和分隔符显示字段值。
julia> println(price1 + price2) £6.17s.2d
julia> show(price1 + price2 + LSD(0,19,11) + LSD(19,19,6)) £27.16s.7d
可以添加一个或多个别名,这些别名是特定类型的备用名称。由于 Price 是 LSD 的一种更好的表达方式,我们将创建一个有效的替代方案:
julia> const Price=LSD LSD julia> show(Price(1, 19, 11)) £1.19s.11d
到目前为止,还不错,但这些 LSD 对象还没有完全开发出来。如果要执行减法、乘法和除法运算,则必须为这些函数定义用于处理LSD的其他方法。减法很简单,只要用先令和便士来摆弄就行了,所以我们暂时不谈这个问题,但是乘法呢?价格乘以数字涉及两种类型的对象,一种是 Price / LSD对象,另一种是--嗯,任何正实数都应该是可能的:
function Base.:*(a::LSD, b::Real)
if b < 0
error("Cannot multiply by a negative number")
end
totalpence = b * (a.pence + a.shillings * 12 + a.pounds * 240)
(pounds, balance) = divrem(totalpence, 240)
(shillings, pence) = divrem(balance, 12)
LSD(pounds, shillings, pence)
end
与我们添加到 Base 的 +
函数中的 +
方法一样,Base 的 *
函数的这种新的 *
方法专门定义为将价格乘以数字。第一次尝试的效果出乎意料的好:
julia> price1 * 2 £11.1s.0d julia> price1 * 3 £16.11s.6d julia> price1 * 10 £55.5s.0d julia> price1 * 1.5 £8.5s.9d julia> price3 = Price(0,6,5) £0.6s.5d julia> price3 * 1//7 £0.0s.11d
然而,有些失败是可以预料到的。我们没有考虑到一分钱中真正过时的部分:半分钱和一分钱:
julia> price1 * 0.25 ERROR: InexactError() Stacktrace: [1] convert(::Type{Int64}, ::Float64) at ./float.jl:675 [2] LSD(::Float64, ::Float64, ::Float64) at ./REPL[36]:40 [3] *(::LSD, ::Float64) at ./REPL[55]:10
(答案应该是 1.7欧元7又二分之一欧元。不幸的是,我们的 LSD 类型不允许一分钱的零碎。)
但还有一个更紧迫的问题。此时此刻,你必须先给出价格,然后再乘以乘数;反过来说,结果是失败的:
julia> 2 * price1 ERROR: MethodError: no method matching *(::Int64, ::LSD) Closest candidates are: *(::Any, ::Any, ::Any, ::Any...) at operators.jl:420 *(::Number, ::Bool) at bool.jl:106 ...
这是因为,尽管Julia可以找到匹配的方法 (a::LSD, b::Number)
,但却找不到另一种方法:(a::Number, b::LSD)
。但是添加它是非常容易的:
function Base.:*(a::Number, b::LSD)
b * a
end
它向Base的 *
函数添加了另一个方法。
julia> price1 * 2 £11.1s.0d
julia> 2 * price1 £11.1s.0d
julia> for i in 1:10 println(price1 * i) end £5.10s.6d £11.1s.0d £16.11s.6d £22.2s.0d £27.12s.6d £33.3s.0d £38.13s.6d £44.4s.0d £49.14s.6d £55.5s.0d
现在的价格看起来就像一家19世纪的英国老店。真的!
如果您想查看到目前为止已添加了多少个方法来处理这种旧的英磅类型,请使用 methodswith
函数:
julia> methodswith(LSD)
4-element Array{Method,1}: *(a::LSD, b::Real) at In[20]:4 *(a::Number, b::LSD) at In[34]:2 +(a::LSD, b::LSD) at In[13]:2 show(io::IO, money::LSD) at In[15]:2
到目前为止只有四个……您还可以继续添加方法,使该类型更加有用。这将取决于您对自己或其他人使用它的设想。例如,您可能希望添加除法和模数方法,并对负货币值进行智能操作。
可变结构
编辑这种用于持有英国价格的复合类型被定义为不可变类型。创建价格对象后,不能更改这些对象的值:
julia> price1.pence 6 julia> price1.pence=10 ERROR: type LSD is immutable
要基于现有价格创建新价格,您必须执行以下操作:
julia> price2 = Price(price1.pounds, price1.shillings, 10) £5.10s.10d
对于这个特定的示例,这不是一个大问题,但是当您可能希望修改或更新某个类型中的字段的值,而不是创建一个具有正确值的新字段时,会出现许多应用场景。
对于这些情况,您需要创建一个可变结构 mutable struct
。根据对类型的要求,在 struct
和 mutable struct
之间进行选择。
有关模块和从其他模块导入函数的更多信息,请参见 Modules and packages。