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。