Juia - print 入門 - Hello World から pretty-print まで -

この記事は Qiita に 2018/08/04 に投稿したものです。2020/3/26 移行しました。

目次

print や類似の関数の基本的な使い方から、一緒に使うと便利な関数について紹介したいと思います。 今回はREPLの表示をそれなりにきれいにすることを目標とします。 人の美への追求は際限なく 「IJulia 使えば Markdown も使えるからキレイに出力できる!」と考えるかもしれませんが、掛けた労力に対して得られる成果があまりに微々たるものなので今回は扱いません。1つの数字を MathJax で表示するくらいなら良いのですが配列が絡んでくると地獄です。

例えば "量子力学と言えば Dirac 表記でしょ!" といった軽いノリで 下記のように表示されていたものを

 0.7071067811865475
 0.0               
 0.0               
 0.7071067811865475

MathJaxを使って下記のように表示させようとするのはおすすめしません。地獄を見ます。 $$0.707 | 00 \rangle + 0.707 | 11 \rangle$$

たかが1行の出力のために何故600行以上もコードを書いているんだと虚無感が襲ってきます。

できるようになること

今まで↓のように表示させていたものが

Name Age Point
Carmen.Blanco 21 11.7303
Corrales.Cristobal 21 1.5121000000000002
Wendolin.Urías 30 1.1905999999999999
Nazario.Roberto 43 2.7626
zSantacruz 47 6.9193999999999996
Sisneros.Emilio 30 2.5084
Delia.Galarza 31 19.1949
Collado.Tomás 43 10.206900000000001
Abraham80 50 7.512499999999999
wAlonso 24 4.7661

↓のように表示させることができるようになります。

Name                   Age    Point     
Carmen.Blanco          21     11.7303
Corrales.Cristobal     21      1.5121
Wendolin.Urías         30      1.1906
Nazario.Roberto        43      2.7626
zSantacruz             47      6.9194
Sisneros.Emilio        30      2.5084
Delia.Galarza          31     19.1949
Collado.Tomás          43     10.2069
Abraham80              50      7.5125
wAlonso                24      4.7661


自作の型の表示が↓から

Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, [8, 8, 8, 8, 8, 8, 8, 8, 8, 8  …  8, 8, 8, 8, 8, 8, 8, 8, 8, 8])

↓になります。

Horse
 競走馬名: ハリボテエレジー
 騎手    : 手作好太郎
 父馬    : ダンボウルガクエン
 母馬    : ガムテイプマツリ
 馬齢    : 6
 着順
 8
 8
 8
 ⋮
 8
 8

一見すると簡単に出来そうですが、この記事の長さが物語っているようにこの程度の出力を得るだけでもすごーく面倒です。

環境

julia> versioninfo()
Julia Version 1.0.0
Commit 5d4eaca0c9 (2018-08-08 20:58 UTC)
Platform Info:
  OS: Linux (x86_64-pc-linux-gnu)
  CPU: Intel(R) Core(TM) i5-4460T CPU @ 1.90GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, haswell)
Environment:
  JULIA_SHELL = /usr/bin/zsh
  JULIA_EDITOR = nvim

今回使うJulia は安定版の v1.0 です。Julia v0.6 とは文法がほとんど違うので注意!

基本

他のプログラミング言語よろしく、print を使うと文字列や変数を画面に出力することができます。 ここで println を使うと最後に改行が入ります。

julia> print("Hello World")
Hello World
julia> println("Hello World"); println("Hello goropikari")
Hello World
Hello goropikari

各変数をカンマ区切りで渡すと連結されて出力されます。

julia> print("Hello", 1, "World")
Hello1World

printと似たもので show もありますが、こちらは print よりも変数について時として詳しく表示されます。

julia> x = 10
10

julia> println(x)
10

julia> show(x)
10
julia> x = Float16(1)
Float16(1.0)

julia> println(x)
1.0

julia> show(x)
Float16(1.0)

printshow はちょいちょい表示のされ方が違うので要注意。

julia> print("Hello\n")
Hello

julia> show("Hello\n")
"Hello\n"


出力先を明示的に指定しなかった場合、結果は標準出力に出力されます。出力先を指定したい場合は print の第一引数に出力先を指定します。

# 標準出力
julia> println(stdout, "Hello World")
Hello World

# 標準エラー出力
julia> println(stderr, "Hello World")
Hello World


JuliaにはC言語スタイルで print するためのマクロ @printf が用意されています。 これを使うとC言語と同じ様に print することができます。

julia> using Printf

julia> @printf("Hello%010d", 12345)
Hello0000012345


print@printf に似たものに sprint@sprintf があります。 違いは何かと言えば頭に s がついているほうは print したものを画面に表示するのではなく文字列として返します。

julia> using Printf

julia> @sprintf("Hello%04d", 3)
"Hello0003"

julia> @sprintf("Hello%04d", 3); # 標準出力に出しているわけではないので 
                                 # ; をつけると何も表示されない

@sprintf の方は比較的直感的です。一方で sprint の方はどうでしょう?

julia> sprint("Hello")
ERROR: MethodError: no method matching sprint(::String)
Closest candidates are:
  sprint(::Function, ::Any...; context, sizehint) at strings/io.jl:97
Stacktrace:
 [1] top-level scope at none:0

ありゃま、エラーが出ました。@sprintf と違い sprint には第一引数に関数を与えなければなりません。また、与える関数は第一引数に IO を受け取れるようなものに限ります。例えば printshow など。

julia> sprint(print, "Hello")
"Hello"

julia> sprint(print, 12345)
"12345"

IOContext

凝った出力をするため、また、出力結果を期待通りにするために IOContext の理解は避けては通れないのでここで紹介します。 IOContext は簡単に言えばどこどう出力するかをまとめたものです。

"どこ" には標準出力や標準エラー出力、書き込み可能なファイルなど。 "どう" はコンパクトに表示する、省略して表示する、色をつけて表示するなどなどです。

基本文法は

IOContext(io::IO, key1 => value1, key2 => value2, ...)

です。ここで key の部分は自分で決めることができますが、以下のよく使われているものに関しては同じ名前をつけると不具合が出る可能性があるので他の名前をつけましょう。

  • :compact 値をコンパクトに表示するか否か
  • :limit 表示を省略するか否か。Juliaでは配列は省略されて表示されますがそれはこれが true になっているから。
  • :displaysize 表示部分のサイズ。
  • :color 出力の文字に色をつけるか否か
  • :typeinfo 私は使ったことがないのでよくわかりません。すいません。show を使ったときには表示が変わるなぁということだけ確認しました。
julia> show(Float16(1))
Float16(1.0)
julia> show(IOContext(stdout, :typeinfo => Float16), Float16(1))
1.0

例えば、コンパクトに表示させたときとそうでないときの差は以下のとおりです。

julia> using Random; Random.seed!(2018); x = rand()
0.6545394330653942

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> println(io, x)
0.654539

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> println(io, x)
0.6545394330653942

IO が特定の key を持っているか否かを調べる時は haskeyを使います。 key を持っていたらその値を、持ってなかったら指定したの値を返すようにするには get を使います。

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> haskey(io, :compact)
true

julia> haskey(io, :hoge)
false

julia> get(io, :compact, true)
false

julia> get(io, :hoge, true)
true

普段遣いのときには IOContext はあまり使わないと思いますが、自分のパッケージを作って後述の pretty-print をするときは結構重要になってきます。 特に変数の値でなく、表示のされ方をテストしたいときは IOContext を知らないと、ちゃんとしたテストケースが書けません。

出力をきれいに魅せる

カラフルにする

printstyled を使うと色付きだったりボールド体で出力することができます。 色の指定方法は色の名前を指定するか、0〜255までの整数値で決めることができます。

julia> printstyled("Hello\n", color=:cyan)
julia> printstyled("Hello\n", color=:reverse) # 反転
julia> printstyled("Hello\n", color=:underline) # アンダーラインを引く
julia> printstyled("Hello\n", color=:light_blue, bold=true) # Julia v0.5 を使ったことがある人には懐かしのスタイル

f:id:goropikarikun:20181110181922p:plain

文字幅

数字を普通に出力すると以下の様に左寄せに出力されますが、時として位の位置を揃えて出力したいと思いますよね。

julia> for i in [1, 10, 100, 1000]
           println(i)
       end
1
10
100
1000

このように。

   1
  10
 100
1000

第一の方法は @printf を使う方法です。

julia> for i in [1, 10, 100, 1000]
           @printf("%4d\n", i)
       end
   1
  10
 100
1000

しかし私はC言語風の書き方に慣れていないので @printf は使いづらいです。 変数の数が増えてくるとどこがどこに対応しているのかパット見だとわからないので好きになれません。そのため私は lpad を使っています。

julia> for i in [1, 10, 100, 1000]
           println(lpad(i, 4))
       end
   1
  10
 100
1000

lpad は指定した文字数 $n$ よりも文字列 $s$ が短かったら、足りない分だけ $s$ の左に空白を入れた文字列 $s'$ を返します。右に空白を入れる場合は rpad を使います。 第3引数も指定すると空白以外の文字で埋めることができます。0埋めの連番ファイルを作りたいときなどにも便利です。

julia> lpad("Hello", 10)
"     Hello"

julia> lpad("Hello", 10, "-")
"-----Hello"

julia> lpad(1, 10, "hoge")
"hogehogeh1"

julia> rpad("Hello", 20)
"Hello               "

julia> rpad("Hello", 20, "-")
"Hello---------------"

julia> lpad(1, 6, "0")
"000001"

小数・複素数の表示をきれいにする Base.alignment

表示させたいものが整数ならば lpad を使えば位を揃えて表示することが出来ました。次は小数や複素数をきれいに表示させてみましょう。

配列を作った時、小数だったら位の位置を揃えて、複素数だったら実部と虚部を結ぶ符号の位置が揃って表示されますが、それを自力で実現するための方法を紹介します。

julia> x = randn(3) * 100
3-element Array{Float64,1}:
  -99.60800864461076 
   28.202047839771822
 -158.385848352147

julia> randn(ComplexF64, 3) * 100
3-element Array{Complex{Float64},1}:
 -11.13643420355874 + 21.75267947789508im  
 41.913378452353996 - 56.76768974241606im  
  83.73402073531838 + 0.15488964757358356im

完全に自力でやろうとすると整数部が何桁で小数部が何桁、実部が何桁で虚部が何桁・・・とやらないといけないわけですが、幸いそれらをやってくれる関数があります。それが Base.alignment です。

ちなみに Base.alignment はマニアックな関数なので Julia の公式ドキュメントに使い方は書いてありません。あまりに誰も使わないせいか docstring も2年近く間違ったまま放置されています。それくらいマニアックです。

Base.alignment の基本文法

Base.alignment(io::IO, x)

Base.alignmentIO と数字を入れると要素数2のタプルが返ってきます。もし入れた数字が小数ならば第1要素は整数部の桁数、第2要素は点を含めた小数部の桁数になります。 printshow と違い、 Base.alignment の場合は IO を省略できません。何故かと言えば context (compact とか) によって同じ数字でも表示される桁数が違うからです。

julia> x = rand()
0.6022015583915965

julia> println(IOContext(stdout, :compact => true), x)
0.602202

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.alignment(io, x)
(1, 7)

julia> println(IOContext(stdout, :compact => false), x)
0.6022015583915965

julia> io = IOContext(stdout, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.alignment(io, x)
(1, 17)

それでは、実際に Base.alignment を使って小数をきれいに表示させてみましょう。 方針は配列の要素全てについて Base.alignment で調べ、整数部の桁数の最大値と小数部の桁数の最大値の和を文字幅とするように表示させます。整数部、小数部の足りない分の桁は repeat をつかって空白で埋めます。

julia> x = randn(5) * 10
5-element Array{Float64,1}:
  -9.212777882398562 
  -0.6891109738516901
  -2.6882962369764787
 -36.9082211306946   
   4.914965667105987 

julia> l = r = 0
0

julia> a= []
0-element Array{Any,1}

julia> io = IOContext(stdout, :compact => true)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> for i in x
          atmp = Base.alignment(io, i)
          global l = max(l, atmp[1])
          global r = max(r, atmp[2])
          push!(a, atmp)
       end

julia> for (al, item) in zip(a, x)
          ls = repeat(" ", l - al[1])
          rs = repeat(" ", r - al[2])
          println(io, ls, item, rs)
       end
 -9.21278 
 -0.689111
 -2.6883  
-36.9082  
  4.91497

1つ目の例を再現

ここまでで冒頭の1つ目の例を再現することができます。

julia> # using Pkg; Pkg.add("Faker")

julia> using Faker, Random

julia> Random.seed!(0);

julia> un = Faker.user_name
user_name (generic function with 1 method)

julia> age() = rand(20:50)
age (generic function with 1 method)

julia> point() = round(abs(randn()), digits=5)
point (generic function with 1 method)

julia> n = 10
10

julia> data = Matrix{Any}(undef,n,3);

julia> for i in 1:n
           data[i,1] = un()
           data[i,2] = age()
           data[i,3] = point() * 10
       end

julia> begin
           name = "Name"
           a = "Age"
           p = "Point"
           io = IOContext(stdout, :compact => true)
           wn = max(length(name), maximum(length.(data[:,1]))) + 5
           wa = 7
           l = r = 0
           for i in 1:n
               aij = Base.alignment(io, data[i,3])
               global l = max(l, aij[1])
               global r = max(r, aij[2])
           end
           println( rpad(name, wn) * rpad(a, wa) * rpad(p, max(10,l+r)))
           for i in 1:10
               aij = Base.alignment(io, data[i,3])
               ls = repeat(" ", l - aij[1])
               rs = repeat(" ", r - aij[2])
               println(io, rpad(data[i,1], wn), rpad(data[i,2], wa), ls, data[i,3], rs)
           end
       end
Name                   Age    Point     
Valladares.Cynthia     43      4.7313
Rolando20              49      1.5648
Gabino16               45      4.4951
Gabino.Cepeda          25      9.6989
Isabela.Pulido         35     13.5565
Eloy25                 43     25.1382
KGuillen               35      5.1041
Sonia54                39     18.9942
Anabel.Mesa            32     28.9205
nGracia                50      6.3489

配列 Base.print_array

続いて配列の表示についてです。

配列を print してみた時、「思ってたのと違う」と感じたことはないでしょうか?

julia> x = rand(5)
5-element Array{Float64,1}:
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

julia> println(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]

julia> y = rand(5,5)
5×5 Array{Float64,2}:
 0.528446  0.948737  0.996994  0.862299  0.675343
 0.633248  0.637487  0.34968   0.767471  0.960373
 0.868802  0.253233  0.235227  0.709841  0.134395
 0.587811  0.637926  0.33784   0.455068  0.180991
 0.912758  0.550064  0.916355  0.58957   0.386093

julia> println(y)
[0.528446 0.948737 0.996994 0.862299 0.675343; 0.633248 0.637487 0.34968 0.767471 0.960373; 0.868802 0.253233 0.235227 0.709841 0.134395; 0.587811 0.637926 0.33784 0.455068 0.180991; 0.912758 0.550064 0.916355 0.58957 0.386093]

変数を定義した時に表示される方法で出力してほしいのであって、一行にずらずら要素を出力してほしいわけじゃないんだ!!!と思ったことはないでしょうか?

そんな時は show を使います。しかし、そのまま使うと print と大差ありません。 IOMIME を指定すれば望みの結果が得られます。 REPL のときの MIME は "text/plain" と覚えとけば、とりあえず良いと思います。

julia> println(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]

julia> show(x)
[0.856423, 0.395794, 0.0803914, 0.919405, 0.376928]
julia> io = IOContext(stdout, :limit => true, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> show(io, "text/plain", x)
5-element Array{Float64,1}:
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

さてさて、無事に望みの結果を得ることが出来ましたが、人によってはヘッダー部( 5-element Array{Float64,1}: など)を表示させたくないと望むことでしょう。そんなときは Base.print_array を使います。

ちなみに Base.alignment同様、Base.print_array も公式ドキュメントには使い方が載っていないマニアックな関数です。ただ、alignment と比べて実用性高いのでドキュメントに載せて良いのではと個人的には思います。

Base.print_array の基本文法

Base.print_array(io::IO, x)
julia> io = IOContext(stdout, :limit => true, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.print_array(io, x)
 0.8564231118379442
 0.3957938354126007
 0.0803914002136299
 0.919405100907382 
 0.3769282818406279

julia> Base.print_array(io, rand(30))
 0.826103394472415   
 0.12252942018471757 
 0.2578897738095447  
 0.9962502442037657  
 0.7355190804809946  
 0.7585607760377631  
 0.9151773834286789  
 0.3929985249254522  
 0.6641753306579214  
 0.331754333274941970.7833300985202769  
 0.18328052862651867 
 0.23765181941856928 
 0.023377483952839784
 0.09753998271209197 
 0.5774230030542189  
 0.8859464218457049  
 0.6723385022029065  
 0.47453039665141294 
julia> io = IOContext(stdout, :limit => false, :compact => false)
IOContext(Base.TTY(RawFD(0x0000000d) open, 0 bytes waiting))

julia> Base.print_array(io, rand(30))
 0.7545909477271988  
 0.04426425050940841 
 0.8374874234291312  
 0.1487924398894196  
 0.9042352827314581  
 0.6163873731935219  
 0.8876188540406658  
 0.941831168132194   
 0.5473667368995183  
 0.06389212057729132 
 0.31826618585326116 
 0.5844167820961697  
 0.2901193343171964  
 0.04053208788398743 
 0.12097218515312957 
 0.008122564658366693
 0.26497805735815083 
 0.6564738355733903  
 0.058178777741022536
 0.3444346924786441  
 0.3245624908138902  
 0.8014413904453013  
 0.9545200342276534  
 0.154475460973849   
 0.7799280363802161  
 0.5400078688551904  
 0.8594465295979896  
 0.881281761468725   
 0.8220828940691287  
 0.9481617822907571 

~~ここでは扱いませんが Markdown で配列を表示させたい人は Base.print_matrix_row の使い方を覚えると良いかもしれません。*1

pretty-print

最後に pretty-print です。pretty-printの正確な定義がよくわかりませんが、垢抜けない野暮ったい出力をプリティーにすることでしょう(多分)。

下図の「ここ」とくくった部分をなんて呼ぶのか、または、「ここ」の部分を表示させる機能をなんて呼ぶのかわからないので説明しづらいのですが、「ここ」とくくった部分が何故このように表示されるかといったら、それはもちろんそのように定義されているからです。

f:id:goropikarikun:20181110182037p:plain

そしてこの「ここ」の部分の表示のさせ方は型によって特徴付けられています。

この「ここ」の部分の表示方法を変えたい場合は Base.show のメソッドを上書きすることで実現できます。

基本的な書き方

function Base.show(io::IO,  ::MIME"text/plain", x::MyType) # MyType の部分を好みの型に変える
    println(io, "A is ", x.a) # print に io を入れることを忘れないように!
    println(io, "B is ", x.b)
    ...
end

2つ目の例を再現

新たに作った Horse 型の表示方法を定義してみます。

julia> struct Horse
           name::String
           jockey::String
           sire::String
           dam::String
           age::Int
           finpos::Vector{Int}
       end

julia> x = Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, fill(8,100))
Horse("ハリボテエレジー", "手作好太郎", "ダンボウルガクエン", "ガムテイプマツリ", 6, [8, 8, 8, 8, 8, 8, 8, 8, 8, 88, 8, 8, 8, 8, 8, 8, 8, 8, 8])

julia> function Base.show(io::IO, ::MIME"text/plain", x::Horse)
           summary(io, x)
           println(io)
           println(io, " 競走馬名: ", x.name)
           println(io, " 騎手    : ", x.jockey)
           println(io, " 父馬    : ", x.sire)
           println(io, " 母馬    : ", x.dam)
           println(io, " 馬齢    : ", x.age)
           println(io, " 着順")
           Base.print_array(io, x.finpos)
       end

julia> x
Horse
 競走馬名: ハリボテエレジー
 騎手    : 手作好太郎
 父馬    : ダンボウルガクエン
 母馬    : ガムテイプマツリ
 馬齢    : 6
 着順
 8
 8
 8
 8
 8
 8
 8
 8
 8
 88
 8
 8
 8
 8
 8
 8
 8
 8

MIME を "text/markdown" で定義すると、 Jupyter Notebook を使っている場合 "text/markdown" で定義した方法が優先されます。 私がやるとしたら下記のように配列を含んでいなかったら Markdown への対応を考えるかもしれません。

function Base.show(io::IO, ::MIME"text/markdown", x::Number)
    println(io, "\$", x, "\$")
end
rand()

f:id:goropikarikun:20181110182114p:plain

配列を含んでいるとどう面倒なのかは下記の例を見ていただくと納得いただけるのではないかと思います。以下は先程の例を "text/markdown" に変えただけです。

function Base.show(io::IO, ::MIME"text/markdown", x::Horse)
   summary(io, x)
   println(io)
   println(io, " 競走馬名: ", x.name)
   println(io, " 騎手    : ", x.jockey)
   println(io, " 父馬    : ", x.sire)
   println(io, " 母馬    : ", x.dam)
   println(io, " 馬齢    : ", x.age)
   println(io, " 着順")
   Base.print_array(io, x.finpos)
end
x

f:id:goropikarikun:20181110182146p:plain

まとめ

  • 凝りだすと際限がないので程々のところでやめましょう。
  • print を凝りたい場合、公式ドキュメントは非力なので Julia のソースコードを読んでください。

参考

*1:Base.print_matrix をいじれば Markdown にすぐに対応できそうな気がしたのですがうまく行きませんでした。