本文轉自羅秀哲知乎專欄,經授權轉載。
原文:
https://www.zhihu.com/people/luo-xiu-zhe/activities
沒有銀彈,Julia的特性更多不會有Python好學。但是不用擔心,上手是非常容易的。之前因為準備用戶見面會(Julia User Meet Up)準備了一個教程,這個教程很大一部分來自於:
https://github.com/Roger-luo/TutorialZH.jl
但是因為不是所有的依賴都支持了1.0,所以這個教程會在下月更新一次。這裡僅僅是介紹一些基本語法,幫助大家快速上手。這個教程以及集智俱樂部的視頻:
https://www.bilibili.com/video/av28248187/
都可以任意轉載和修改。希望註明出處,也希望可以幫忙宣傳一下中文社區(當然這也並不是強制的)。
一些資料
知乎上的話Julia有很多專欄,這個專欄更新的比較勤可以看看,也歡迎大家給這個專欄投稿:
https://zhuanlan.zhihu.com/julia
然後書的話,我有意願之後等中文社區人多一些組織一個開源圖書的項目,但是這個可能最近還實現不了,一個是我精力有限,第二是人手不足。我自己的專欄Half Integer是當blog用的,因為我也使用很多別的語言和技術,不會單獨分享Julia的內容,此外我也更傾向於寫一些更幹的東西。
然後如果你英文可以的話,有很多很棒的英文資料還有課程,我推薦你到這裡看看:
https://julialang.org/learning/
有問題去哪裡問
我首先推薦的是中文和英文的discourse論壇,它們的連結也都可以在官網找到,地址分別如下:
中文論壇:
https://discourse.juliacn.com/
英文論壇(這個人比較多):
https://discourse.julialang.org/
然後你可以加Julia的其他社區:
https://julialang.org/community/
也甚至可以直接去GitHub提交issue,整個Julia語言的開發都是在GitHub上的:
https://github.com/julialang/julia/issues
然後你也可以在知乎上提問,知乎上也有很多Julia的深度用戶(雖然還很少)。
最後你也可以加我們中文社區的QQ群,但是這是最不推薦的,因為QQ沒有代碼高亮,也不能像slack一樣支持各種方便技術討論的特性。一般QQ主要用於討論文檔翻譯工作的一些問題和社區建設。對於比較緊急的問題希望你可以在discourse(中英文皆可)上發了帖子之後把連結發到QQ群裡來。
中文論壇的訪問問題
中文論壇目前由於域名備案還沒有完成(將會暫時掛靠在集智俱樂部名下)也還沒有配置CDN,將會從香港跳轉,可能第一次訪問速度較慢,之後就好了,這是正常的我們也在慢慢解決這個問題。
Julia官方團隊也在尋找怎麼加速國內訪問官網 julialang.org 的問題,我們日後也會考慮直接使用julialang.org 。包括在境內維護鏡像下載等。但是因為目前真的非常缺乏人手,也希望大家可以參與到社區建設裡來。
關於中文社區
中文社區是受到Julia開發團隊支持的眾多本地化組織之一,非盈利是基本準則。值得自豪的是,在Julia只有0.3的時候在24個貢獻者的努力下,就有了0.3的中文文檔。1.0的中文文檔也正在進行中,我們也利用Julia的文檔系統嘗試支持doc string。早期的成員裡有Jiahao Chen,Yichao Yu,i2300等Julia團隊的成員。
GitHub地址:JuliaCN
網址:http://juliacn.com/
論壇地址:http://discourse.juliacn.com/
JuliaCN目前暫時不接受任何個人捐贈(因為這可能並不合法),但是如果你願意資助Julia語言的發展,可以通過官網的捐贈按鈕進行捐贈官網的地址在這裡,但是也希望對Julia語言感興趣的公司和機構能夠幫助這樣一個真正開源的,由社區成員自發組織起來的組織成長起來,雖然發起人已經不知道是誰了(並不是我),但是目前具體合作事宜都可以聯繫我。也非常希望有更多的機構可以贊助我們的(甚至是接下來的)活動和伺服器等開支。如果有Julia的招聘崗位也歡迎來社區做廣告。
寫在前面
這兩天的媒體報導可能讓一些人有了恐慌,但是我現在有一個誠懇的建議就是如果你完全沒有編程基礎,時間也不多的話(時間多了不是想學啥學啥),我建議你先學一下Python,這並不衝突,因為Julia的語法本書和Python很像,1.0之後也專門增加了一些feature幫助你更好地從Python轉向Julia。此外因為Julia剛剛有了第一個長期支持版本,這還不意味著這個語言已經完全成熟,我想此時的Julia更像是彼時的Python 2.0,還有很長一段路要走,但是已經非常的有前景。
那麼什麼人我會建議學習Julia呢?或者Julia在什麼場景下也許能夠有優勢呢?我個人的體驗是以下這裡一類:
之前使用Python但是因為性能問題,經常需要使用numba/Cython/C API/ctypes/etc.等方式進行優化的人。Julia或許能夠幫助你解決兩語言問題,並且獲得可讀性更好,更容易維護的代碼。
之前使用MATLAB,但是被一些付費功能困擾的用戶(MATLAB 2018也是不錯的,但是要支持正版哈)
之前使用Fortran和R的用戶,強烈建議使用Julia(可以結合著用也,FFI是很不錯的)
之前使用Sage/Octave的用戶,不妨嘗試一下這個新玩意兒
之前使用Mathematica但是想開始做一些數值的用戶,Mathematica不是不能做數值,也可以調用C/C++但是Julia不妨是相比其它工具更平滑的選擇。
如果你之前的工作僅僅使用Python就足以勝任,那麼不必著急,也不必恐慌,不妨在感興趣的時候試試這個新東西,但是也完全可以等到Julia被大規模使用的時候再跟進。實際上從一開始像MXNet這樣的深度學習框架就官方支持了,這些框架的Python用戶轉移過來也並不是什麼難事,但是如果你無須高度定製自己的程序,那麼其實不會體會到什麼不同和優勢。
此外,也要再三聲明,雖然Julia可以寫出高性能的代碼,但是寫出高性能的代碼這件事情本身就很困難。雖然寫起來像Python,運行速度像C是我們的夢想,但是在現在這個階段,並不是隨便寫一段Julia代碼就真的能達到C的。Julia只是給你提供了充分優化的空間,和(達到高性能代碼的)相對容易的編程體驗。
Julia目前因為官網的伺服器只有AWS s3(他們也很窮)。所以國內的一些地區下載速度很慢:
https://julialang.org/downloads/
大家可以試一試,然後也可以去Julia Computing公司提供的Julia全家桶(你可以把它理解為Julia版本的Anaconda),最左邊的JuliaPro是免費的:
https://juliacomputing.com/
之前浙大的LUG搭建了一個鏡像,但是維護的同學最近有一些忙,所以目前還沒有更新到1.0。但是其實你如果無法從以上途徑下載,那麼從境內的源裡下載Julia 0.6也其實並不影響你先熟悉一些基本語法(這是這個教程的主要目的),境內的源的下載地址在這裡:
http://juliacn.com/downloads/
我們也會儘快更新。
然後還有一個叫做Julia Box的雲服務很方便可以使用,裡面有很多教程,都是jupyter notebook,打開即用,全部都是在線的不用安裝。但是唯一的缺點就是國內可能不一定能夠正常訪問到。
http://juliabox.com
Julia語言的社區不夠大,此外由於不是像rust這樣的靜態編譯語言,也不是像CPython這樣的解釋型編譯器,在啟動的時候有比較明顯的overhead,這個問題一直在優化(REPL的啟動時間已經從曾經的1.0s到了現在的0.2s,依然和IPython這樣的有明顯差距),有PL的朋友私下和我說是LLVM的JIT不是那麼好(像nodejs的V8這個問題就不是很明顯)
所以在這個階段選擇一個合適的開發工具是非常必要的。目前支持最好,bug最少的是Atom上的Juno插件,如果你下載Julia Pro那麼會自帶這個編輯器。如果你想選擇手動安裝,那麼可以在這裡下載Atom:
https://atom.io/
安裝方法:
http://docs.junolab.org/latest/man/installation.html
或者我也推薦你安裝IJulia之後,使用jupyter notebook和jupyter lab進行開發。
其它的平臺也有支持,例如Jetbrain的各個IDE都可以使用由 @考古學家千裡冰封等開發的插件。VS code也有Julia插件,以及Vim也是有支持的。但是他們都還沒有支持逐行執行和單獨執行某一塊代碼的功能,這對於本身被設計得很像Mathematica的Julia來說沒有執行一個cell的支持開發起來會時常被JIT的預熱時間所困擾。
然後為了克服JIT的預熱,避免重複啟動編譯器。如果你不重新定義(re-define)類型的話,可以試試 Revise.jl :
https://github.com/timholy/Revise.jl
這是一個用於熱加載Julia代碼的工具,1.0已經支持方法(method)的刪除了。所以也能夠方便你的開發。
其實和Python一樣,在我日常使用中,作為動態語言,以及因為語法本身適合分塊執行,我其實很少會用到斷點和專門的debugger,此外雖然有相關的包,在1.0的編譯器裡也為未來加入debugger提供了相關功能,但是目前還沒有完善,你也許可以試試(但是我不推薦):
https://github.com/Keno/Gallium.jl
https://github.com/timholy/Rebugger.jl
Julia有一個由社區維護的網站用來幫助你從1900多個包裡找出符合你需求的Julia包:
https://juliaobserver.com/
一般來說用比較新的,star比較多的包會好一些。然後如果你覺得某個包不錯,也請在GitHub上給一個star。
當你下載好了Julia之後,不論是Julia Pro還是單獨的Julia編譯器,你都可以先打開一個REPL(交互式編程環境),類似於IPython之於Python,Julia的REPL支持了基本的代碼高亮,文檔查看等功能。但是Julia的REPL更強大(這件事稍後再說)。
Windows/Mac用戶:
雙擊Julia的三色圖標,就能打開REPL。在Atom裡面的話在左上角有Julia一欄,點擊裡面的open terminal即可。
Linux用戶:
下載好以後去找到bin文件夾,然後把它加入你的 PATH 環境變量裡去,以後就可以在命令行裡直接通過 `julia` 命令啟動REPL。
樹莓派用戶和超算用戶:
我相信你們是專業的,請閱讀官網的教程吧。注意超算用戶不用要求管理員安裝也可以安裝到自己的用戶目錄下的,設置好環境變量即可。然後有一些超算(比如中國科學技術大學的超算中心)Julia編譯器是很早就裝好的,但是可能使用module load加載。
運行Julia的程序總的來說可以有三種方式(其實原理上它們都基本是等價的)
執行一個Julia腳本,和其它Julia語言一樣,你可以用如下命令執行Julia腳本,一般來說Julia腳本都以 `.jl` 作為擴展名。
julia script.jl
這個執行出來是沒有報錯高亮的,需要顏色請用以下命令執行
julia --color=yes script.jl
2. 如果直接啟動Julia會進入到REPL裡去
julia
你會看到
_ _ _ _(_)_ | Documentation: https://docs.julialang.org (_) | (_) (_) | _ _ _| |_ __ _ | Type "?" for help, "]?" for Pkg help. | | | | | | |/ _` | | | | |_| | | | (_| | | Version 1.0.0 (2018-08-08) _/ |\__'_|_|_|\__'_| ||__/ |julia>
也可以在這裡運行Julia命令。
在REPL裡面可以直接查文檔,按 ?就會跳到help模式,在0.7之後(包括1.0),按 ] 就會進入pkg模式,在這個模式下按 ?就會顯示相關文檔
(v1.0) pkg> ? Welcome to the Pkg REPL-mode. To return to the julia> prompt, either press backspace when the input line is empty or press Ctrl+C. Synopsis pkg> [--env=...] cmd [opts] [args] Multiple commands can be given on the same line by interleaving a ; between the commands. Environment The --env meta option determines which project environment to manipulate. By default, this looks for a git repo in the parents directories of the current working directory, and if it finds one, it uses that as an environment. Otherwise, it uses a named environment (typically found in ~/.julia/environments) looking for environments named v$(VERSION.major).$(VERSION.minor).$(VERSION.patch), v$(VERSION.major).$(VERSION.minor), v$(VERSION.major) or default in order. Commands What action you want the package manager to take: help: show this message status: summarize contents of and changes to environment add: add packages to project develop: clone the full package repo locally for development rm: remove packages from project or manifest up: update packages in manifest test: run tests for packages build: run the build script for packages pin: pins the version of packages free: undoes a pin, develop, or stops tracking a repo. instantiate: downloads all the dependencies for the project resolve: resolves to update the manifest from changes in dependencies of developed packages generate: generate files for a new project preview: previews a subsequent command without affecting the current state precompile: precompile all the project dependencies gc: garbage collect packages not used for a significant time activate: set the primary environment the package manager manipulates
查看具體某個命令的文檔可以
(v1.0) pkg> ?add add pkg[=uuid] [@version] [#rev] ... Add package pkg to the current project file. If pkg could refer to multiple different packages, specifying uuid allows you to disambiguate. @version optionally allows specifying which versions of packages. Versions may be specified by @1, @1.2, @1.2.3, allowing any version with a prefix that matches, or ranges thereof, such as @1.2-3.4.5. A git-revision can be specified by #branch or #commit. If a local path is used as an argument to add, the path needs to be a git repository. The project will then track that git repository just like if it is was tracking a remote repository online. Examples pkg> add Example pkg> add Example@0.5 pkg> add Example#master pkg> add Example#c37b675 pkg> add https://github.com/JuliaLang/Example.jl#master pkg> add git@github.com:JuliaLang/Example.jl.git pkg> add Example=7876af07-990d-54b4-ab0e-23690620f79a
安裝包在0.7之後都用pkg模式來安裝,因為這個更方便,但是和0.6一樣,如果你想使用代碼來安裝也是可以的,但是在0.7之後需要加載 Pkg 模塊(0.6不用)
using Pkg
然後安裝你想要的包
Pkg.add("Example")
Julia的REPL擴展性很強,比較有名的比如OhMyREPL
甚至還可以在Julia的REPL裡把C++當成動態語言來寫,按 < 鍵進入C++模式(Julia的C++ FFI:Cxx.jl,暫時還沒更新到1.0,需要0.6)
3. 第三種方式就是在Atom這樣支持cell的編輯器裡(notebook也是類似的),在Atom中在某一行按 shift+enter 會單獨執行這一行,結果會列印在這一行的後面。如果有多行的結果你可以用滑鼠點擊以下,就會展開。如果你選中了很多行,那麼就會執行你選中的部分,結果顯示在選中的部分最後。
notebook裡面的使用方法也是shift + enter和Python等其它語言類似。
下面的部分你可以在以上三種方式裡的任意一種裡執行。
本教程只是幫助熟悉語法,想要掌握Julia還請認真閱讀手冊(中文手冊還在翻譯中):
https://docs.julialang.org/en/stable/manual/getting-started/
正如所有的經典教程一樣,我們先來學習怎麼寫hello world:
在Julia裡面寫hello world可以這樣寫
> println("Hello World")
注意 在Julia裡為了保證聲明可以幫助你區分類型,String是需要雙引號的,字符使用單引號。
Julia的字符串繼承了Perl的字符串差值,正則表達式等,Stefan的評價是他覺得Perl的字符串處理是最漂亮的,於是就抄了過來。
> name = "Roger"> println("Hello $name")
這裡name是一個變量,Julia和Python一樣,不需要聲明變量,因為所有的變量都只是給值綁定了一個名字而已。然後對於變量插入,可以直接使用 $ 符號。
這將列印出
Hello Roger
當然對於比較長的表達式你也可以使用括號
> println("1 + 1 = $(1 + 1)")
這將列印出
1 + 1 = 2
我們上面提到了怎麼綁定一個變量名:
> x = "Roger"
Julia的變量名除了ASCII字符以外,還可以使用包括但不限於UTF-8的unicode,比如甚至是中文
> 你好 = "Hello!"
還可以是Emoji,輸入 `\:smile` 然後再按 `tab`
> 😄 = "smile"
別忘了這是一個為科學家們打造的語言,還可以利用LaTeX來輸入特別的數學符號,在notebook或者REPL裡輸入 `\` + `epsilon` 按 `tab` 鍵
> ϵ = 2.2
Julia還利用了LLVM的一些常數(無限精度):
> ππ = 3.1415926535897...
我們寫一個非常簡單的求和函數,它會對一個向量 A 求和
function mysum(A) s = 0.0 # s = zero(eltype(A)) for a in A s += a end send
函數聲明使用 function 關鍵字開頭搭配 end 關鍵字,也許一開始你對這個 end 不是很喜歡,或許會問為什麼不像Python一樣呢?為什麼不用 {} 呢?別著急後面在元編程的部分告訴你 end 的好處。
然後 for 循環也是一樣的,使用for關鍵字,然後可以搭配 in 來遍歷一個數組(是不是幾乎和Python一樣?),但是別忘記了所有的代碼塊都最後要寫一個end
當然 in 關鍵字可以單獨使用,用於判斷某個集合類(collection,例如一個數組)裡面是否包含某個元素
> 1 in [1, 2, 3]true
注釋方式和Python一樣,也使用 #,而多行注釋使用
#= xxx=#
但是除此之外,Julia是有類型的,也可以標註類型(而不是聲明),而對於短小的函數聲明也可以更加的貼近數學語言。例如我們想要判斷某個數字是奇數還是偶數
is_even(x::Int) = x % 2 == 0
Julia使用 :: 來標註類型 (學過Python3的朋友可能知道Python也有類似的類型標註但是是:)
這個時候如果輸入了,例如浮點數那麼就會報錯
> is_even(2.0)MethodError: no method matching is_even(::Float64)Closest candidates are: is_even(!Matched::Int64) at In[3]:1Stacktrace: [1] top-level scope at none:0
然後多寫文檔是個好習慣,讓我們給is_even和mysum加上文檔,對於已經定義過的東西,可以直接這樣加文檔
""" is_even(x::Int) -> Bool判斷一個整數 `x` 是否是偶數"""is_even""" mysum(A) -> Number對 `A` 求和。"""mysum
但是也可以在聲明的時候加
""" is_even(x::Int) -> Bool判斷一個整數 `x` 是否是偶數"""is_even(x::Int) = x % 2 == 0""" mysum(A) -> Number對 `A` 求和。"""function mysum(A) s = 0.0 # s = zero(eltype(A)) for a in A s += a end send
Julia的文檔系統使用 Documenter.jl ,所有文檔都用markdown編寫,這種markdown是Julia flavor的,具體細則非常簡單還請參見:
https://docs.julialang.org/en/stable/manual/documentation/#Markdown-syntax-1
Julia裡的分支預測也很簡單,和很多語言都非常像
if cond1# blablaelseif cond2# blablaelse# blablaend
Julia也有原生支持的多維數組(而不是List)甚至有非常完善的Array Interface。這表現為Julia擁有大量的針對不同情況設計的數組類型,例如:可共享數組,供並行計算使用;靜態數組,適合給小的數組加速;稀疏數組,實現上目前只有稀疏矩陣;分布式數組,用於分布式計算;CUDA數組CuArray,用於在N卡上計算,等等,就不一一列舉了它們之中除了自帶的數組(類似於numpy的多維數組)以外都在外部支持的包裡,而所有的這些數組都適用了同樣的Interface。他們在使用體驗上幾乎沒有差別。
比如可以產生一個隨機數組
> rand(10)
這將得到一個向量,裡面有10個元素,每個元素的類型是默認的Float64類型。
產生一個隨機矩陣(跟隨你的直覺就好)
> rand(10, 20)
產生一個三維的張量
> rand(10, 20, 30)
...
那麼如果要聲明Int類型的數組呢?
> rand(Int, 10)> rand(Int, 10, 20)> rand(Int, 10, 20, 30)
那麼如何聲明初始化為0的數組呢?現在告訴你函數名稱是 zeros 和MATLAB以及numpy完全一樣,看了上面 rand 的用法,猜猜這個怎麼用?
那麼如果我需要更複雜的構造呢?Python的List有一個很著名的List Comprehension方法,Julia也有。
> [i for I in 1:10]
這將獲得
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
和Python的List Comprehension是不是一樣?但是等等,還不止如此,Julia對多維數組的支持是非常好的,Comprehension對於多維數組也可以用,用逗號分隔即可
[(i, j) for i in 1:5, j in 1:6]5×6 Array{Tuple{Int64,Int64},2}: (1, 1) (1, 2) (1, 3) (1, 4) (1, 5) (1, 6) (2, 1) (2, 2) (2, 3) (2, 4) (2, 5) (2, 6) (3, 1) (3, 2) (3, 3) (3, 4) (3, 5) (3, 6) (4, 1) (4, 2) (4, 3) (4, 4) (4, 5) (4, 6) (5, 1) (5, 2) (5, 3) (5, 4) (5, 5) (5, 6)
這將得到一個由Tuple構成的矩陣,那你已經看到了Julia裡面元組使用括號來聲明。而除了這種元組還有一種稱為命名元組(NamedTuple)的東西,這是1.0的新特性
info = (name="Roger", age="0", wechat="不告訴你")
你可以直接通過 . 算符來訪問
> info.name"Roger"> info.age"0"> info.wechat"不告訴你"
廣播(broadcast)
多維數組的廣播是一個很重要的特性,也是Julia多維數組的標準接口(Interface)任何Julia的數組都可以使用廣播。Julia從MATLAB學來了 . 算符。任何被點算符作用的函數和其它算符都可以被廣播。例如
> sin.(A)
將廣播sin函數到A的每一個元素。什麼是廣播簡單來說就是將一個函數作用在多維數組,元組,標量的每一個元素上去。這有點像是函數式編程裡map的概念,但是不完全一樣。
廣播運算對任何函數都是支持的,比如
> foo.(A, B, c)
這裡A和B時兩個數組,c是一個標量那麼foo將會以 foo(a, b, c) 的形式作用在每一個A,B的元素a, b 上。
和很多其它的面向對象語言一樣,Julia裡所有的東西都是對象,或者說是某個類型的實例,但非 `class` 的實例,Julia沒有 `class`,但是有更輕量級的類型。
定義一個複數類型 複數是很多數值計算和數據分析算法都會用到的類型,然而一般的處理器上並不是自帶複數類型的,我們往往使用兩倍精度的浮點數來模擬一個複數,例如在 `C` 語言裡,雙精度的複數可以寫作:
double _Complex a;
或者在 `C++` 中,雙精度的複數可以寫作:
std::complex<double> a;
在 `Python` 裡,沒有顯示類型,但是我們可以使用 `j`:
In [1]: 1 + 1.jOut[1]: (1+1j)
Julia裡有自帶的 Complex類型,和Python類似,用 im 表示虛數單位
1 + 1im
Complex是純Julia實現的數值類型。所以我們用這個作為例子來看看怎麼定義一個類型
struct MyComplex real::Float64 imag::Float64end# 一個複數就是 MyComplex 類型的一個實例,也就是一種對象a = MyComplex(1.0, 2.0)
而實際上和C/C++一樣,Julia的複數類型也是純Julia實現的,我們這裡會簡單地實現一個複數類型MyComplex。Julia的類型使用struct關鍵字,然後用end表示這一段表達式的結束。每個Julia類型有一個默認的構造函數,這個構造函數的變量即為類型聲明的成員。
但是僅僅聲明了類型還遠遠不夠,我們還需要對複數定義複數運算,方便起見我們這裡僅重載 *算符:
(a+b·i)(c+d·i)=(ac-bd)+(ad+bc)·i
首先我們需要將要重載的東西從 Base 模塊中拿出來(而不是自己聲明一個新的,為什麼?留給大家之後思考,這對你理解什麼是Julia的多重派發很有幫助),在Julia裡,運算符 只是一種特殊的函數,並不會被特別對待
import Base: **(a::MyComplex, b::MyComplex) = MyComplex(a.real * b.real - a.imag * b.imag, a.real * b.imag + a.imag * b.real)
然後試試?
b = MyComplex(1.0, 3.0)a * b
現在輸出不是很好看,我們重載一下show方法把默認列印出來的字符串修改一下,這裡字符串裡的$是字符串插入,可以將一個Julia表達式的輸出轉換為字符串(String類型)然後插入到字符串裡。
import Base: showshow(io::IO, ::MIME"text/plain", x::MyComplex) = print(io, "$(x.real) + $(x.imag)im")
我們已經有了一個簡單的複數類型,但是實際上真正在用的時候,我們可能會需要不同精度的複數類型,例如使用 32位浮點精度的複數類型。為了能夠使用不同的複數類型,我們需要使用參數類型,而複數的實部和虛部都是實數,我們還需要限制其類型範圍
struct MyComplex2{T <: Real} real::T imag::Tend
這裡Real是自帶的抽象類型,我們之後會講什麼是抽象類型。而T則是一個參數。參數類型也有默認的構造函數。
MyComplex2{Float32}(1.0f0, 2.0f0)
但是實際上你還可以定義一些自己的構造函數,在 Julia 裡因為是沒有class的,除了構造函數以外的方法都不能寫在類型聲明內部。而一旦你在類型聲明中聲明了一個自己的構造函數,默認的構造將被覆蓋。比如試試下面這個例子
struct MyComplex3{T <: Real} real::T imag::T MyComplex3(real::T) where {T <: Real} = new{T}(real, 0)endMyComplex3(1.0)
內部構造函數往往是為了在生成這個實例前做一些預處理(例如判斷輸入變量是否符合要求等)更詳細的例子請參見文檔
https://docs.julialang.org/en/latest/manual/constructors/
Julia語言是沒有 class 的,但這並不意味著 Julia 無法面向對象,Julia對象的方法(method)通過 多重派發 和 類型樹 進行分配,而不依賴於 class 這種結構。下面這一部分不會影響你使用Julia的多重派發特性,因為它非常的直覺化,但是如果你可以通過下面Python和C++的例子了解到到底什麼是多重派發和單重派發,那麼也將是非常有益的。
想了解什麼是多重派發,我們要先從單重派發(single dispatch) 說起,大部分支持 class 的語言都是單重派發,我再用 Python 舉個例子,Python 3.4有一個的提案(PEP 443)裡有一個對單重派發通用函數的提案:
from functools import singledispatch@singledispatchdef fun(arg, verbose=Falase): print(arg) @fun.register(int)def _(arg, verbose=False): print(arg)
所謂單重派發就是只能按照函數參數的一個類型進行派發方法,從而實現多態。
顧名思義,多重派發就是根據所有參數的類型進行派發方法。C++的函數重載其實就是一種靜態的多重派發(static multiple dispatch)。但是到了動態類型就不行了,比如下面這個來自StackOverflow的例子
#include <iostream>struct A {};struct B : public A {};class Foo {public: virtual void wooo(A *a, A *b) { std::cout << "A/A" << std::endl; }; virtual void wooo(A *a, B *b) { std::cout << "A/B" << std::endl; };};void CallMyFn(Foo *p, A *arg1, A *arg2) { p->wooo(arg1, arg2);}int main(int argc, char const *argv[]) { Foo *f = new Foo(); A *a = new A(); B *b = new B(); CallMyFn(f, a, b); return 0;}
運行上面的 C++ 代碼將會得到
A/A
而我們預期的是
A/B
這是因為 C++ 只支持 Single Dispatch (多重派發在提案中:Report on language support for Multi-Methods and Open-Methods for C++),對於動態類型,編譯器只能通過 class 名稱決定調用的方法,當需要根據參數等信息決定方法的時候就無能為力了。注意,多重派發是一個動態類型的特性,這裡 A,B都是做成了動態類型,於函數重載不同,一些類似於多重派發在C++ 中實際上是函數重載,出現歧義(ambiguous)是由於 C++ 的隱式類型轉換。
顧名思義,就是會根據所有的參數來進行派發。例如讓我們在Julia裡重新實現C++裡的例子,注意由於 Julia 沒有繼承,我們在這裡用抽象類型代替。Julia會匹配參數類型最符合的方法,然後調用。在Julia裡,由於Julia本身是動態語言,函數的重載(overload)與多重派發是一個意思,但是實際上Julia的派發會發生在運行時和編譯時,而這在很少的情況下有可能影響性能。
abstract type TypeA endstruct TypeB <: TypeA endstruct TypeC <: TypeA endwooo(a1::TypeA, a2::TypeA) = println("A/A")wooo(a::TypeA, b::TypeB) = println("A/B")callme(a1::TypeA, a2::TypeA) = wooo(a1, a2)b = TypeB(); c = TypeC();callme(c, b)
在Julia裡,上面的 wooo稱為一個通用函數(generic function)而對某些類型的具體實現,例如
wooo(a1::TypeA, a2::TypeA) = println("A/A")
稱為 method。
下面來說說Julia的類型系統。
Julia的類型主要分為抽象類型(Abstract Type)和實體類型(Concrete Type),實體類型主要分為可變類型(Mutable Type)和不可變類型(Immutable Type)
abstract type AbstractType endstruct ImmutableType <: AbstractTypeendmutable struct MutableType <: AbstractTypeend
抽象類型使用 abstract type 關鍵字 匹配 end聲明。默認的合成類型都是不可變類型,使用 struct 搭配 end 聲明。而可變類型在 struct 前面增加 mutable 關鍵字即可。某個實體類型(concrete type)是另外一個抽象類型(abstract type)或者抽象類型是另外一個抽象類型的子類,這樣的關係使用 <: 來聲明。
一個抽象類型的所有子類型會構成一顆樹,其中實體類型一定在樹的葉子結點。
下面這個 view_tree 函數會對一顆類型樹進行深度優先遍歷(DFS)
using InteractiveUtils # 0.7 之後需要調用這個標準庫function view_tree(T, depth=0) println(" "^depth, T) for each in subtypes(T) view_tree(each, depth+1) endendview_tree(AbstractType)
運行會得到AbstractType作為父節點的類型樹
AbstractType ImmutableType MutableType
再看一個複雜一些的例子(自己運行一下試試):
abstract type AbstractAnimal endabstract type AbstractBird <: AbstractAnimal endabstract type AbstractDog <: AbstractAnimal endabstract type AbstractCat <: AbstractAnimal endstruct Sparrow <: AbstractBird endstruct Kitty <: AbstractCat endstruct Snoope <: AbstractDog endview_tree(AbstractAnimal)
而Julia在分發一個函數的方法的時候,會儘可能匹配最具體的類型,也就是儘可能匹配這顆樹的葉子結點。思考一下下面這段代碼的運行結果
combine(a::AbstractAnimal, b::AbstractAnimal, c::AbstractAnimal) = "three animals get together!" # method 1combine(a::Sparrow, b::AbstractCat, c::AbstractAnimal) = "a sparrow, a cat and some animal" # method 2combine(Sparrow(), Kitty(), Sparrow()) # 這個會匹配方法2
類型在 Julia 裡是非常廉價的,利用多重派發和廉價的類型,我們可以針對數學對象實現更詳細的優化,例如對於滿足不同性質的矩陣,我們有對它們之間互相乘積的優化方法,我們可以將部分操作作為懶惰求值(Lazy Evaluation)加入運算中,然後再為滿足不同性質的矩陣派發精細的優化方法:
實際上Julia在標準庫裡已經這麼做了 (雖然實際上依然還有更多的特殊矩陣,你也許可以在JuliaArrays 裡找到你需要的矩陣類型),不同類型的矩陣會被派發到不同類型的方法上去。
試試對Julia自帶的抽象類型 AbstractMatrix 運行這個代碼
view_tree(AbstractMatrix)
Julia有這樣的特點:廉價的類型和多重派發+類型樹的結構,我們可以繼承類型的行為(behavior)而不能繼承類型的成員,而多重派發讓所有Julia類型很自然地變成了鴨子類型(Duck Type),我們只要定義好不同的接口/interface就足以定義類型的行為。
實際上由於嚴格保持了樹的結構,Julia也不允許多重繼承,也不存在混入(mixin)這樣的設計模式,這避免了鑽石繼承的問題。
需要說明的是,以上僅僅是Julia的特點,它帶來了一些好處,也同時帶來了一些缺點。限於篇幅暫且不表。
問題:想想這幾種 rand 的接口,例如 rand(1:10),rand(Bool), rand(Bool, 2, 2) 等是怎麼實現的?
在當下,如果有人說放棄 Python 那一定是一個很愚蠢的決定,正如開頭所說,Python 和 Julia 各自有其優缺點,而我們在遷移到 Julia 之後依然可以調用我們的一些歷史依賴,並且也依然可以將新寫的更快更簡單的代碼作為 Python 的包提供給 Python 社區。所以你可以選擇
這主要需要依賴於兩個包:PyCall.jl 和 pyjulia。這一部分我們主要講 PyCall.jl
目前PyCall還沒有更新到1.0但是在0.6和0.7都是沒有問題的。如果你沒有安裝 PyCall 模塊,請使用Julia的包管理器安裝 PyCall,如果你的環境裡沒有安裝 python 或者不在標準路徑中,那麼Julia將會下載一個 miniconda 來安裝 python 環境。如果你想使用你已經安裝的 python,請在Julia的環境變量 ENV 中設置 python 路徑:
julia> ENV["PYTHON"] = "... python 路徑 ..."julia> Pkg.build("PyCall")
安裝好之後 PyCall 的使用方法和原生 Python 的語法很接近 (多虧了Julia的宏!)
using PyCall@pyimport numpy as npnp.zeros(10)
Julia自帶的多維數組類型 Array 和 numpy 可以共享一塊內存,所以當使用 numpy 在 Python 中得到了一個 numpy.ndarray 後在Julia裡會看到一個新的 Array。
除了像 @pyimport, @pydef 這樣的宏以外,和其它FFI(外部函數接口)的模塊一樣,PyCall也有python的字符串字面量,它將會執行一行python代碼/或者在 __main__ 模塊裡執行一段 Python 代碼,然後將其轉換為Julia對象。試試這個
py"sum([1, 2, 3])"
Julia的元編程(看不懂也沒關係)作為一個具有Lisp血統的語言,元編程是繞不過去的話題。但是Julia在宏上的繼承其實是相對克制的。元編程屬於比較深入的內容,這裡我只會簡單地介紹一下,想要深入了解可以看看文檔和Lazy.jl這個非常簡單的庫是怎麼實現的,這部分看不懂不會影響你的使用。
在Julia裡,所有的東西不僅僅可以是對象,他們也都是表達式,當然表達式也是對象。也許你一開始還在討厭Julia的 end 不夠簡潔,但是學了這一部分,你就會發現 end 關鍵字的妙處了。
在 Julia 裡我們可以使用語言本身的語法來處理 Julia 自己的表達式,這被稱為元編程(Meta Programming),那么元編程有什麼用呢?
字符串的字面量是區分不同類型的字符串的一種非常方便的方法,在Python裡,我們有正則表達式字面量 r"(.*)",格式化字面量 f"hello {who}"。而在Julia裡,則可以通過宏定義自己的字符串字面量,只需聲明以 _str 為結尾的宏即可。
試試這段代碼
struct MyStr <: AbstractString data::StringendBase.show(io::IO, s::MyStr) = print(io, "My String is: ", s.data)macro my_str(s) MyStr(s) endmy"hello!"
這大大方便了需要做大量字符串處理的任務,例如生物信息等。此外這也使得Julia很容易在文檔裡支持LaTeX,Markdown等多種不同的格式,並且按照統一的接口(Interface)給它們派發不同的方法。
試試自帶的markdown string literal (markdown字符串字面量)
import Base.Markdown: @md_strmd"""# Header 1## Header 2### Header 3#### Header 4"""
在Julia裡獲得表達式的方法被稱為引用(quote),可以有兩種方法進行引用
對於短小的表達式使用
進行引用:
ex = :(a + b)
對於大段的表達式,使用quote關鍵字進行引用
quote a + b b + cend
到了這裡你也許已經發現了,實際上任何一部分Julia代碼都是表達式,而不同的表達式有不同的tag而正是因為使用了end才能夠和各種不同的代碼塊進行匹配。例如實際上函數關鍵字function和end本身定義了一個代碼塊
ex = quote function foo() println() endendex.args
所有的表達式都是Expr,QuoteNode,Symbol三種類型之一。
當我們有很多個函數嵌套調用的時候會需要打很多括號,現在想要比較方便地用空格表示函數合成例如:g f k l ⇒ g(f(k(l(x)))),我們將在這裡實現一個非常簡單的(單變量)函數合成的宏 @>
fa(x) = (println("call a"); x)fb(x) = (println("call b"); x)fc(x) = (println("call c"); x)macro >(fs...) fsend
使用@macroexpand查看你的宏所產生的表達式
@macroexpand @> fa fb fc # => x->fa(fb(fc(x)))
然後想想這個代碼是什麼意思?這裡的 $ 也是插入另外一個表達式的意思
macro >(fs...) ex = :($(last(fs))(x)) for f in reverse(fs[1:end-1]) ex = :($f($ex)) end :(x->$ex)endf = @> fa fb fcf(2)
看看是不是成功了!
實際上,作為一個多範式的程式語言,Julia本身是支持函數式編程的,而函數式編程常常會使用Lazy這個包,裡面寫好了更加強大的用於函數合成的宏@>,它支持多變量函數的合成。
https://campus.swarma.org/gcou=388
該教程視頻同時發布在嗶哩嗶哩彈幕網:
視頻地址一:https://www.bilibili.com/video/av28248187
視頻地址二:https://www.bilibili.com/video/av28178443
全網首發Julia中文視頻教程:1小時上手機器學習的明日之星
用Julia把π玩出花來
編輯:Yiri
關注集智AI學園公眾號
獲取更多更有趣的AI教程吧!
搜索微信公眾號:swarmAI
集智AI學園QQ群:426390994
學園網站:campus.swarma.org
商務合作和投稿轉載|swarma@swarma.org