Julia 代碼樣式

2018-08-12 21:26 更新

代碼樣式

以下各節(jié)從幾方面介紹了符合語言習(xí)慣的 Julia 編碼風(fēng)格。這些規(guī)則都不是絕對的;它們僅僅是幫您熟悉這門語言,或是幫您可以在許多可替代性設(shè)計(jì)中能夠做出選擇的一些建議而已。

寫成函數(shù),別寫成腳本

編寫代碼作為在一系列步驟中最高級的辦法,是可以快速開始解決問題的,但您應(yīng)該試著盡快把一個(gè)程序分成許多函數(shù)。函數(shù)具有更好的可重用性和可測試性,并可以更好闡明它們正在做什么,它們的輸入和輸出是什么。此外,由于 Julia 的編譯器工作原理,在函數(shù)中的代碼往往比最高級別的代碼運(yùn)行得更快。

同樣值得強(qiáng)調(diào)的是,函數(shù)應(yīng)該以參數(shù)來代替,而不是直接在全局變量(除了像 pi 那樣的常量)上操作。

避免類型過于嚴(yán)格

代碼應(yīng)盡可能通用。相較于這樣的代碼書寫:

    convert(Complex{Float64}, x)

使用有效的泛型函數(shù)是更好的:

    complex(float(x))

第二種寫法把 x 轉(zhuǎn)換成一個(gè)適當(dāng)?shù)念愋停皇且恢庇靡粋€(gè)相同的類型。

這種類型特點(diǎn)是特別地與函數(shù)自變量相關(guān)。例如,不聲明一個(gè)參數(shù)是 Int 類型或 Int32 類型,如果在這種情況下還可以保持是任何整數(shù),那就應(yīng)該是用 Integer 抽象表達(dá)出來的。事實(shí)上,在許多情況下您都可以把自變量類型給忽視掉,除非一些需要消除歧義的時(shí)候,由于如果一個(gè)類型不支持任何必要操作就會被忽略,那么一個(gè) MethodError 不管怎樣也都會被忽略掉。(這被大家認(rèn)為是 duck typing。)

例如,考慮以下 addone 函數(shù)中的定義,這個(gè)功能可以返回 1 加上它的自變量。

    addone(x::Int) = x + 1             # works only for Int
    addone(x::Integer) = x + one(x)    # any integer type
    addone(x::Number) = x + one(x)     # any numeric type
    addone(x) = x + one(x)             # any type supporting + and one

最后一個(gè) addone 的定義解決了所有類型的有關(guān)自變量的 one 函數(shù)(像 x 類型一樣返回 1 值,可以避免不想要的類型提供)和 + 函數(shù)的問題。關(guān)鍵是要意識到,僅僅是定義通用的 addone(x) = x + one(x) 寫法也是沒有性能缺失的,因?yàn)?Julia 會根據(jù)需要自主編譯到專業(yè)的版本。舉個(gè)例子,您第一次調(diào)用 addone(12) 的時(shí)候, Julia 會自動為 x::Int 自變量編譯一個(gè) addone 函數(shù),通過調(diào)用一個(gè)內(nèi)聯(lián)值 1 代替 one。因此,上表前三個(gè)定義全都是重復(fù)的。

在調(diào)用程序中解決額外的自變量多樣性問題

取代這種寫法:

    function foo(x, y)
        x = int(x); y = int(y)
        ...
    end
    foo(x, y)

利用以下的寫法更好:

    function foo(x::Int, y::Int)
        ...
    end
    foo(int(x), int(y))

第二種寫法更好的方式,因?yàn)?foo 并沒有真正接受所有類型的數(shù)據(jù);它真正需要的是 Int S。

這里的一個(gè)問題是,如果一個(gè)函數(shù)本質(zhì)上需要整數(shù),可能更好的方式是強(qiáng)制調(diào)用程序來決定怎樣轉(zhuǎn)換非整數(shù)(例如最低值或最高值)。另一個(gè)問題是,聲明更具體的類型會為未來的方法定義提供更多的“空間”。

如果函數(shù)修改了它的參數(shù),在函數(shù)名后加 !

取代這種寫法:

    function double{T<:Number}(a::AbstractArray{T})
        for i = 1:endof(a); a[i] *= 2; end
    a
    end

利用以下寫法更好:

    function double!{T<:Number}(a::AbstractArray{T})
        for i = 1:endof(a); a[i] *= 2; end
    a
    end

Julia 標(biāo)準(zhǔn)庫在整個(gè)過程中使用以上約定,并且 Julia 標(biāo)準(zhǔn)庫還包含一些函數(shù)復(fù)制和修飾形式的例子(例如 sortsort!),或是其它只是在修飾(例如 push!, pop!,splice!)的例子。這對一些也要為了方便而返回修改后數(shù)組的函數(shù)來說是很典型的。

避免奇葩的類型集合

Union(Function,String) 這樣的類型,說明你的設(shè)計(jì)有問題。

盡量避免空域

當(dāng)使用 x::Union(Nothing,T) 時(shí),想想把 x 轉(zhuǎn)換成 nothing 這個(gè)選項(xiàng)是否是必要的。以下是一些可供選擇的替代選項(xiàng)

  • 找到一個(gè)安全的默認(rèn)值來和 x 一起初始化
  • 介紹另一種缺少 x 的類型
  • 如果有許多類似 x 的域,就把它們存儲在字典中
  • 確定當(dāng) xnoting 時(shí)是否有一個(gè)簡單的規(guī)則。例如,域通常是以 nothing 開始的,但是是在一些定義良好的點(diǎn)被初始化。在這種情況下,要首先考慮它可能沒被定義。

避免復(fù)雜的容器類型

通常情況下,像下面這樣創(chuàng)建數(shù)組是沒什么幫助的:

    a = Array(Union(Int,String,Tuple,Array), n)

在這種情況下 cell(n) 這樣寫更好一些。 這也有助于對編譯器進(jìn)行注釋這一特定用途,而不是試圖將許多選擇打包成一種類型。

使用和 Julia base/ 相同的命名傳統(tǒng)

  • 模塊和類型名稱以大寫開頭, 并且使用駝峰形式: module SparseMatrix, immutable UnitRange.
  • 函數(shù)名稱使用小寫 (maximum, convert). 在容易讀懂的情況下把幾 個(gè)單詞連在一起寫 (isequal, haskey). 在必要的情況下, 使用下劃 線作為單詞的分隔符. 下劃線也可以用來表示多個(gè)概念的組合 (remotecall_fetch 相比 remotecall(fetch(...)) 是一種更有效的 實(shí)現(xiàn)), 或者是為了區(qū)分 (sum_kbn). 簡潔是提倡的, 但是要避免縮寫 (indexin 而不是 indxin) 因?yàn)楹茈y記住某些單詞是否縮寫或者怎么 縮寫的.

如果一個(gè)函數(shù)需要多個(gè)單詞來描述, 想一下這個(gè)函數(shù)是否包含了多個(gè)概念, 這樣 的情況下最好分拆成多個(gè)部分.

不要濫用 try-catch

避免錯(cuò)誤要比依賴找錯(cuò)好多了。

不要把條件表達(dá)式用圓括號括起來

Julia 在 if 和 while 語句中不需要括號。所以要這樣寫:

    if a == b

來取代:

    if (a == b)

不要濫用 ...

剪接功能參數(shù)可以讓人很依賴。取代 [a..., b...] 這種寫法,簡單的 [a, b] 這樣寫就已經(jīng)連接數(shù)組了。collect(a) 的寫法要比 [a...] 好,但是因?yàn)?a 已經(jīng)是可迭代的了,直接用 a 而不要把它轉(zhuǎn)換到數(shù)組中也許會更好。

不要使用不必要的靜態(tài)參數(shù)

信號函數(shù):

    foo{T<:Real}(x::T) = ...

應(yīng)該這樣寫:

    foo(x::Real) = ...

特別是如果 T 沒被用在函數(shù)主體。即使 T 被用在函數(shù)主體了,如果方便的話也可以被 typeof(x) 替代。這在表現(xiàn)上并沒有什么差異。要注意的是,這不是對一般的靜態(tài)參數(shù)都要謹(jǐn)慎,只是在它們不會被用到時(shí)要特別留心。

還要注意容器類型,特別是函數(shù)調(diào)用中可能需要的類型參數(shù)??梢缘?FAQ 如何聲明“抽象容器類型”的域 來查看更多信息。

避免對實(shí)例或類型判斷的困擾

一些如以下的定義是十分讓人困擾的:

    foo(::Type{MyType}) = ...
    foo(::MyType) = foo(MyType)

您要決定問題的概念是應(yīng)被寫作 MyType 或是 MyType(),并要堅(jiān)持下去。

最好的類型是用默認(rèn)的實(shí)例,并且在解決某些問題需要方法時(shí),再添加包括 Type{MyType} 的一些方法好一些。

如果一個(gè)類型是一個(gè)有效的枚舉,它就應(yīng)該被定義為一個(gè)單一的(理想情況下不變的)類型,而枚舉變量是它的實(shí)例。構(gòu)造函數(shù)和一些轉(zhuǎn)換可以檢測值是否有效。這項(xiàng)設(shè)計(jì)最好把枚舉做成抽象類型,把“值”做成其子類型。

不要濫用 macros

您要注意什么時(shí)候一個(gè) macros 可以真的代替函數(shù)。

在 macros 中調(diào)用 eval 實(shí)在是個(gè)危險(xiǎn)的標(biāo)志;這意味著 macros 只有在被最高級調(diào)用的時(shí)候才會工作。如果這樣一個(gè) macros 被寫為一個(gè)函數(shù),它將自然地訪問它需要的運(yùn)行時(shí)值。

不要在接口層暴露不安全的操作

如果您有一個(gè)使用本地指針的類型:

    type NativeType
        p::Ptr{Uint8}
        ...
    end

不要像下面這樣寫定義:

    getindex(x::NativeType, i) = unsafe_load(x.p, i)

問題是,這種類型的用戶可能在不知道該操作是不安全的情況下就寫 [i],這容易導(dǎo)致內(nèi)存錯(cuò)誤。

這樣的函數(shù)應(yīng)該能檢查操作,以確保它是安全的,或是在它的名字中有不安全的地方時(shí)可以提醒調(diào)用程序。

不要重載基容器類型的方法

像下面這樣書寫定義是有可能的:

    show(io::IO, v::Vector{MyType}) = ...

這樣寫將提供一個(gè)特定新元素類型的向量的自定義顯示。雖然很讓人想嘗試,但卻是應(yīng)該避免的。麻煩的是,用戶會想用一個(gè)眾所周知的類型比如向量在一個(gè)特定的方式下的行為,也會過度定制它的行為,這都會使工作更困難。

注意類型的相等性

您一般要使用 isa<: (issubtype) 來測試類型而不會用 ==。在與已知的具體類型的類型進(jìn)行比較時(shí),要精確檢查類型的的相等性(例如 T == Float64),或者是您真的明白您究竟在干什么。

不要寫 x->f(x)

高階函數(shù)經(jīng)常被用作匿名函數(shù)來調(diào)用,雖然這樣很方便,但是盡量少這么寫。例如,盡量把 map(x->f(x), a) 寫成 map(f, a)

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號