Julia編程基礎(一):初識Julia,除了性能堪比C語言還有哪些特性?

這裏是《Julia 編程基礎》的開源版本。這本書旨在幫助編程愛好者和專業程序員快速地熟悉 Julia 編程語言,並能夠在夯實基礎的前提下寫出優雅、高效的程序。這一系列文章由 郝林 採用 CC BY-NC-ND 4.0知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議)進行許可,請在轉載之前仔細閱讀上述許可協議。

本書的示例項目名爲Programs.jl,地址在這裏。其中會包含本書所講的大部分代碼,但並不是那些代碼的完全拷貝。這個示例項目中的代碼旨在幫助本書讀者更好地記憶和理解書中的要點。它們算是對書中代碼的再整理和補充。

1.1 初識 Julia

1.1.1 爲什麼要有 Julia

首先,我們要知道的是,Julia 是一門計算機編程語言。也就是說,我們可以使用符合 Julia 語言規範的代碼來編寫程序。這些程序可以用於純粹的數學和科學計算、存取本地文件、通過網絡收發數據,等等。雖然 Julia 針對各種科學計算任務做了特別的設計和優化,但它也適用於通用目的的編程。

科學計算往往對代碼的表現力和性能都有着較高的要求。然而,一些善於科學計算的編程語言(如 Python 和 R)卻都在這兩個方面有所欠缺。不過,我相信它們的流行必有其原因。而且,我也很喜歡這些編程語言。它們是我的編程工具箱中必不可少的一部分,尤其是 Python。

現代語言設計和編譯器技術基本上能讓我們省去性能調試的工作量,並且可以提供一個單一的環境讓我們打造程序原型和有效部署高性能的應用。Julia 編程語言就是在此基礎之上誕生的。Julia 是一門靈活的動態編程語言,但其性能堪比傳統的靜態編程語言。

1.1.2 主要特性

Julia 擁有可選的類型標註(type annotation)、多重分派(multiple dispatch)機制,以及優良的性能。它還藉由 LLVM 實現了類型推斷和即時(JIT)編譯。Julia 是多範式的,融入了命令式、函數式和麪向對象編程的特性。Julia 爲高級數值計算提供了足夠的易用性和表現力。當然,Julia 也適用於通用編程。爲此,它從那些流行的動態編程語言(包括 Lisp、Perl、Python、Lua 和 Ruby)中借鑑了很多。

Julia 與典型的動態編程語言之間最大的區別在於:

  • Julia 的 Base 包和標準庫都是由 Julia 語言編寫的。這包括像整數運算那樣的基本操作。也就是說,Julia 實現了一定程度的自舉。
  • Julia 擁有豐富的類型構造和對象描述方式,並且代碼中的類型標註是可選的。
  • Julia 擁有多重分派機制。它可以根據參數類型的不同去調用衍生自同一個函數定義的不同方法。並且,它還可以針對不同的參數類型自動生成有效的專用代碼。
  • Julia 的性能優良,並已接近那些靜態編譯的編程語言(如 C 語言)。

雖然那些動態編程語言從表面上看是“無類型的”,但是它們在內部卻是“有類型的”。這些內部的類型一般都是在其讀取代碼後附加上去的。不過,由於這些語言的代碼缺少類型標註,所以它們並不能向編譯器明確地指示某個值的類型,甚至都不能顯式地提及類型。另一方面,靜態編程語言雖然可以(且必須)爲編譯器指示值的類型,但這些類型往往只在編譯時存在,並不能在運行時操縱或表示。而在 Julia 中,類型本身就是運行時的對象。並且,這些類型也可以被用來向編譯器傳達信息。

我們可以不顯式地使用類型標註或多重分派。但這些都是 Julia 最核心的特性,我還是建議你利用它們來改進你的代碼。這樣的話,你的代碼的表現力會更強,並且更容易閱讀。

1.1.3 更多的特性

Julia 的目標是成爲一門易用、強大和高效的編程語言。除了上述的優點之外,Julia 相對於其他類似系統的優勢還包括:

  • 可供免費使用,並且開源(遵從 MIT 協議)
  • 用戶自定義的類型與內置的類型一樣快速和緊湊
  • 無需擔心向量化代碼的性能,被拆解後的代碼速度會很快
  • 已爲並行計算和分佈式計算做了優化設計
  • 擁有輕量級的“綠色”線程(也就是協程)
  • 擁有可選擇使用但功能強大的類型系統
  • 擁有針對於數字和其他類型的轉換和提升方法,並且是優雅和可擴展的
  • 有效支持 Unicode,包括但不限於 UTF-8
  • 可以直接調用 C 程序中的函數,無需額外封裝或者特殊的 API
  • 可以管理其他進程,強大如 Shell
  • 擁有類似於 Lisp 的宏,以及其他的元編程工具

順便說一句,由於 Julia 的編譯器不同於 Python 和 R 中的解釋器,所以你在起初也許並不能體會到 Julia 的性能優勢。如果你發現一些程序運行起來比較慢,那麼我強烈建議你去閱讀官方文檔中的性能祕訣。一旦你理解了 Julia 是怎樣工作的,那麼寫出性能堪比 C 程序的代碼就比較容易了。

1.2 安裝和啓動

我們可以通過多種方式安裝 Julia。但我建議你使用二進制的形式安裝。你可以從官網的下載頁面中下載與你的計算機的計算架構和操作系統對應的安裝包。注意,本書講解的版本是v1.1.1。在下載之後,你還需要雙擊安裝包,並按照提示去安裝。

順便說一下,Julia 語言的版本一般會以vX.Y.Z的形式表示。其中的XYZ都只可能是正整數或者0。由此,X.Y.Z就組成了 Julia 語言的版本號(注意,不是版本)。這樣的版本號遵循了 Semantic Versioning 規範。簡單來說,X代表主版本號(或稱大版本號),Y代表次版本號(或稱小版本號),而Z則代表修訂版本號。另外,X.Y也可以被稱爲特性版本號。因爲它的遞進一般都代表着軟件在特性上的更新。而最後的Z的遞進,一般代表着軟件缺陷的修復。所以它也可以被叫做 BUG 修復版本號。

言歸正傳。在安裝完成後,你可以找到那個鮮豔的 Julia 三色圖標並雙擊(也可以在命令行中輸入julia並回車)。如果在當前界面中出現了類似於下圖的提示內容,那麼就說明你安裝成功了。


圖 1-1 Julia 的初始界面

順便說一下,如果你想退出這個界面,那麼同時按下Ctrld就可以了。

在使用julia命令的時候,我們可以追加一些啓動參數。一些常用的參數有:-e-E-p-i,以及-v-h。詳細說明如下。

  • -e:用於直接對跟在後面的表達式進行求值。例如,我們可以輸入julia -e 'a = 5 * 8; println(a)'並回車。這時,julia會對單引號內的表達式進行逐一求值。多個表達式之間需要以英文分號;分隔。第二個表達式println(a)在被求值時會在計算機的標準輸出上打印40。當所有求值都完成後,julia命令會直接退出(返回命令行提示符)。
  • -E:與-e的功能很類似。但不同的是,追加該參數的julia命令在退出之前還會在標準輸出上打印出最後一個表達式的求值結果。上面的第二個表達式println(a)的求值結果會是nothing,表示沒有結果值。
  • -p:指定用於處理並行任務的工作進程的數量。跟在它後面的值必須是一個大於 0 的整數,或者爲auto(指代當前計算機的 CPU 邏輯核心數)。例如,如果我們輸入的命令是julia -p 5,那麼工作進程的總數就會是6。這是因爲 REPL 環境本身還會佔用一個工作進程。如果不追加參數-p,那麼 Julia 就不會產生額外的工作進程。
  • -i:用於以交互模式運行命令。這意味着,命令執行後將進入 REPL(Read–eval–print loop)環境。簡單來說,這個 REPL 環境就是一個可以與 Julia 的運行時系統進行即時交互的界面。比如,你在這個環境中輸入println("abc")並回車,它立馬就會回顯獨佔一行的abc和一個空行。從字面上我們也可以瞭解到,該環境會讀取你輸入的表達式、求值讀到的表達式、顯示錶達式的求值結果,然後再次等待讀取。如此循環往復。如果我們在輸入julia命令的時候沒有追加任何源碼文件,那麼它就會以交互模式運行。
  • -v:僅用於顯示當前的 Julia 的版本。比如:julia version 1.1.1
  • -h:僅用於顯示julia命令的具體用法。其中包括了所有可用參數的說明。

到這裏,我們已經對 Julia 有了一個初步的認識。要想玩轉 Julia,我們首先就應該充分熟悉julia命令及其 REPL 環境。不過別擔心,我們後面要用到它們的地方還多着呢,你有的是機會熟悉。

1.3 編寫第一個程序

由於 Julia 程序可以作爲腳本程序來編輯和使用(就像 Shell 和 Python 那樣),所以 Julia 源碼文件的內容可以非常簡單。你可以把腳本程序看做是以普通文本的形式保存的、實現了一定邏輯的計算機指令片段。腳本程序一般存儲在一個單獨的文件中,並可以由特定的工具讀取和執行。比如,用 Bash(Bourne Again SHell)語言編寫的腳本程序可以由bash工具來執行。又比如,用 Python 語言編寫的腳本程序可以用python這個工具來執行。

腳本程序與普通程序的最大區別就是簡單。它既沒有高級的數據類型,也沒有複雜的組織結構和流程控制,更不支持能夠實現程序自我進化的元編程。而這些在 Julia 中都是存在的。因此,我們可以把 Julia 程序寫得很簡單,也可以把它寫得很複雜。這取決於程序要實現的功能和需求,以及編寫代碼的人的設計風格和思考能力。

當然了,我們在初學一門編程語言的時候肯定是要從最簡單的程序編寫開始的。然後由淺入深,逐步地掌握它的編寫方法、技巧和原理。本教程會專注於 Julia 程序的基本編寫方法和技巧,並在有必要時涉及一些原理。

好了,我們現在就來編寫我們的第一個 Julia 程序。我先呈現出代碼再來解釋。

文件 src/ch01/hello/main.jl

# 示例的主文件。
# - Julia version: 1.1.1
# - Author: haolin
# - Date: 2019-06-19

println("Hey, Julia!")

上述代碼被保存在了一個單獨的文件中。我們先來看這個文件的名字。main.jl顯然由兩個部分組成。這兩個部分由英文句號(或稱點號).分割。我們一般把第一部分稱爲主文件名,並把第二部分稱爲擴展文件名。我們一般只會將代表了程序入口(或者說可以由julia命令直接執行)的那個源碼文件命名爲main.jl

擴展名jl用於表示這個文件是一個 Julia 程序的源碼文件。你可能已經看出來了,jl就是 Julia 的縮寫。實際上,所有的 Julia 源碼文件的擴展名都必須是jl

這個源碼文件的內容也包含了兩個部分。第一部分爲程序註釋,第二部分爲程序代碼。

在 Julia 中,程序的註釋可以是單行的,也可以是多行的。如果是單行的註釋,我們需要以#作爲這一行的開始。比如:# 這是一個單行的註釋。如果是多行的註釋,我們就需要顯式地標示註釋的開頭和結尾。更具體地說,開頭的標示是#=,結尾的標示是=#。並且,這兩個標示通常都應該獨佔一行。上述源碼文件中的第一部分就是這樣的。

在這塊註釋中,我寫明瞭這個源碼文件在編寫時的 Julia 版本號、作者(也就是我)的代號,以及該文件被創建時的日期。這是一種比較標準的簡單寫法。當然,每一個工程化的軟件開發團隊都會有自己的代碼編寫規範,其中會包含對註釋風格的規定。因此,我在這裏展示的只是其中的一種寫法而已。不過,我建議你在編寫 Julia 源碼文件時一定要添加包含這幾條重要信息的頭部註釋。

另外,我們也可以用 Markdown 格式的文本作爲註釋。這是 Julia 所獨有的註釋方式。其做法簡單來說是,在一段代碼(比如完整的程序定義)的開始行的上一行,寫入 Markdown 格式的註釋,並用三聯的雙引號將其包裹起來。注意,前後的三聯雙引號都需要獨佔一行。一個簡單的示例如下:

"""
    get_parameter(key::String, first::Bool=true)

根據參數獲取指定的命令行參數值。
參數`key`代表命令行參數的名稱。參數`first`代表一種獲取策略。
如果參數`first`的值爲`true`,那麼無論有多少個同名的命令行參數,都只獲取第一個。否則只獲取最後一個。
"""
function get_parameter(key::String, first::Bool=true)

Julia 自有一套 Markdown 註釋規範。可參見 Julia 官方文檔及其源碼。

我們再來說文件中的第二部分。它只包含了一行代碼。我在本章的第一節中展示過與之類似的代碼。

這行代碼實際上是一個函數調用表達式。其中的println是函數的名稱。這個函數的功能是,向指定的輸出(設備)上輸送指定的內容。它常常被簡稱爲打印函數。緊跟在它後面的、由一對圓括號包裹的內容代表了一個動作。這個動作就是“調用”。圓括號中的內容就是我們在調用這個函數時傳給它的參數值。在這裏,這個參數值是字符串"Hey, Julia!"

你可能沒有發現,我在這裏並沒有爲它指定輸出(也就是輸送的目的地)。這時,指定的內容會被輸送到標準輸出(standard output)上。如果想指定輸出的話,那麼代表輸出的那個參數值就應該被放到圓括號中的最左邊,然後用英文逗號,與原先的參數值"Hey, Julia!"分隔開。如此就形成了一個參數值的序列,或者稱之爲參數值列表。例如,println(io, "Hey, Julia!")。其中的io是一個變量的名稱,用於表示代表了輸出的那個參數值。

好了,現在讓我們使用julia命令來執行這個簡單的程序。我們需要先進入這個源碼文件所在的目錄,然後這樣做:

$ julia main.jl 
Greetings! 你好! こんにちは? 안녕하세요?
Hey, Julia!

第一行最左邊的$代表命令行提示符。它表示我們是在命令行中執行julia命令的。我在後面的類似場景下都會帶上這個提示符。這也可以幫助你區分我們輸入的命令和命令回顯(或者說返回和顯示)的內容。

在這裏,我們輸入的命令是julia main.jl。顯然,我把上述源碼文件的路徑名作爲參數傳給了julia命令。該命令在收到這個參數後會立即讀取相應的源碼文件,並執行其中的代碼。

你可能會有疑問,第二行的內容好像與我們的程序並不相關啊。的確如此。實際上,julia命令在啓動時會先去執行一個名叫startup.jl的源碼文件。正是其中的代碼向標準輸出(在這裏是當前的命令行界面)輸送了第二行的內容。在默認情況下,startup.jl文件中的代碼如下所示:

println("Greetings! 你好! こんにちは? 안녕하세요?")

文件startup.jl也被稱爲 Julia 的啓動文件。它被保存在當前計算機的文件系統中的 Julia 配置目錄下。比如,在 macOS 操作系統中,它的存放路徑就是~/.julia/config/startup.jl

如果你不想看到這行多餘的內容,那麼有兩種方式可以達到目的。第一種方式,刪掉startup.jl文件中的那行代碼。第二種方式,在執行julia命令時追加參數--startup-file,並把參數值設置爲no。比如這樣做:

$ julia --startup-file=no main.jl
Hey, Julia!

這樣一來,命令回顯的就只剩下我們的程序所輸送的內容了。

到這裏,我們的第一個程序已經成功地執行了!我相信你已經對它有了足夠的瞭解。但這只是一個開始。我們馬上就要着手改進這個程序。

1.4 改進第一個程序

我們應該對上述程序的功能稍作改進。因爲它現在只能向 Julia 打招呼,不論執行它的人是誰。我們需要讓它根據執行人給定的參數值來自定義它打招呼的對象。順便說一句,我會把這一程序的改進版本放在 src/ch01/args 路徑下。

首先,我們要改變一下調用println函數時傳給它的那個參數。修改後的調用表達式如下:

println("Hey, $(name)!")

我只改動了幾個字符,即:把Julia改成了$(name)。後者代表了一個插值(interpolation)。對於插值來說,前綴$(和後綴)之間的內容可以是一個變量的名稱,也可以是一個表達式。在這裏,我放入的是變量name的名字。在println函數向目標輸送內容之前,它會把name替換成該變量在那一刻時的值。

當然,變量name現在還不存在。我們還需要在這行代碼的前面添加一些東西。

name, _ = MyArgs.get_parameter("name", true)
if name == "" 
    name = "handsome" 
end

上面這 4 行代碼的主要功能是定義name變量,併爲它賦予合適的值。第一行中的MyArgs代表了一個模塊。而表達式MyArgs.get_parameter("name", true)則代表了對這個模塊下的函數get_parameter的一次調用。在調用時,我們傳給了它兩個參數值,即:String類型(也稱字符串類型)的值"name"Bool類型(或者說布爾類型)的值true

這裏的模塊代表的是一塊程序。原則上講,這塊程序可以包含任意行的代碼。我一會兒再說怎麼定義一個模塊。現在你只需要知道,在我們調用MyArgs.get_parameter函數後,它會返回兩個結果值。在同一行中,我把這兩個結果值分別賦給了變量name_

符號=代表了“賦值”這個動作。這意味着,它右邊的表達式所產生的結果值會被賦給左邊的變量。注意,左邊的結果值的數量和右邊的變量的數量必須相同。在這裏,它們的數量都是兩個。

你可能會問:_是什麼?它實際上是一個佔位符。你可以把它想象成一個垃圾桶。當我們不再需要某個值的時候,就可以把它扔進(賦給)這個垃圾桶。這麼做一般有兩個原因。其一,讓 Julia 的垃圾收集器及時地回收這個不再被使用的值所佔用的內存空間。其二,保持程序的完整性和可讀性。換句話說,即使一個值或者一個變量不再有用了,我們也要進行妥善的處理。

所謂的垃圾收集器是指,用於自動地收集和清掃內存垃圾的內置程序。大多數現代編程語言都會提供垃圾收集器。簡單來說,當用戶程序中的一些代碼以及它們產生的數據不再有用的時候,垃圾收集器就會識別它們,並把它們所佔用的內存空間收回,以便重複利用或者還給操作系統。

再說回我們的程序。我們在第一行代碼中調用了MyArgs.get_parameter函數,並用它返回的第一個結果值爲name變量賦了值。緊隨其後的,是一個由if語句構成的代碼塊。

if name == "" 
    name = "handsome" 
end

if語句用於有條件地執行代碼。它以關鍵字if作爲起始,並以關鍵字end作爲末尾。在if右邊的就是一個條件,即:name == ""。這是一個判等表達式。它判斷的是變量name的值是否與空字符串""相等。

在我們的設計中,如果我們在執行這個程序的時候沒有通過參數指定一個問候對象的名字,那麼這裏的name變量的值就會是""。因此,這個if語句代表的邏輯就是,如果我們沒有指定問候對象的名字,那麼就把name變量的值設置成"handsome"

記住,在if關鍵字和條件代碼的下一行就是滿足條件時需要執行的代碼。當然,這樣的代碼可以有多行。但不管怎樣,這些代碼都需要被縮進。也就是說,它們要比包含了if關鍵字的那一行代碼更靠右一些。一般情況下,我們需要在這些代碼的左邊插入固定數量(比如 4 個)的空格或者一個製表位。

還要注意,關鍵字end與關鍵字if是左對齊的。並且end是需要獨佔一行的。這個關鍵字在 Julia 程序中是非常常用的。它通常作爲一個代碼塊的結束標識。比如,我們在寫模塊、函數、if語句、for語句、try...catch語句等等代碼塊時都會用到它。

再來說MyArgs模塊。我在改進版本的main.jl的開始處定義了這個模塊。代碼如下:

module MyArgs

include("args.jl")

end

它由關鍵字module、模塊名稱MyArgs、關鍵字end,以及被夾在中間的代碼組成。注意,在模塊的定義中,首尾兩行之間的那些代碼可以不縮進。這主要是考慮到整個文件的內容只包含一個(或寥寥幾個)模塊定義的情況。在這種情況下,如果中間的代碼都縮進,那麼就等於幾乎所有的代碼都要縮進了。這個工作量往往是沒必要的,而且這樣的代碼也是不美觀的。

在我們的MyArgs模塊的中間只有一行代碼。它也是一個表達式,代表了對include函數的調用。這個include函數接受一個字符串類型的參數,而且這個參數的值需要代表一個 Julia 源碼文件的路徑名。簡單來說,它會把參數值指定的源碼文件的內容複製、黏貼到調用它的代碼那裏。也可以說,它會用我們指定的源碼替換掉調用它的那行代碼。

我們在這裏傳給include函數的參數值是"args.jl"。這個源碼文件與改進版本的main.jl處在同一個目錄下,即:src/ch01/args。我在該文件中定義了一個名叫get_parameter的函數。這個函數的定義細節我就不講了,你可以自己去看。其中只包含了寥寥幾行代碼,用於變量定義、函數調用、條件求值等。

那裏有一些語法我們還沒有講,所以你看不懂也沒關係。但是,我希望你在看過之後記錄下自己有疑問的地方,並且帶着這些疑問閱讀本教程的後續部分。

現在讓我們執行一下改進版本的main.jl文件吧:

$ julia --startup-file=no main.jl --name=Robert 
Hey, Robert!

我在這次執行julia命令的時候給了它三個參數。這些參數之間是由空格分隔開的。第一個參數是我們已經熟知的--startup-file。而第二個參數是我要執行的源碼文件。在它之後就是我爲源碼文件提供的參數了。

在第三個參數中,我以--爲前綴。這主要是爲了遵從julia命令的參數形式。緊跟在--之後的就是參數名name、等於號=和參數值Robert。因此,程序回顯的內容就是Hey, Robert!。這是正確的。你可以改變一下第三個參數的值,然後看看效果。

1.5 小結

在本章,我們先講了一下 Julia 語言誕生的初衷。然後,我們對它的一些特性進行了略有側重的說明。Julia 語言中比較亮眼的特性有:可選的類型標註、多重分派機制、多種並行計算方式、元編程的支持、接近 C 語言的性能,等等。

在簡要介紹了 Julia 語言的安裝以及julia命令的使用之後,我們立即開始了第一個 Julia 程序的編寫。在經過一番改進之後,我們的第一個程序就成功實現了一個小功能,即:根據我們執行程序時給定的參數值,打印一句簡單的問候。

到這裏,作爲初識 Julia 語言的讀者,我覺得你已經知道的夠多了。在下一章,我將會着重介紹 Julia 程序的編程環境。這些也是我們在正式地編寫 Julia 程序之前很有必要了解的知識。

原文鏈接:

https://github.com/hyper0x/JuliaBasics/blob/master/book/ch01.md

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章