静かなる名辞

pythonとプログラミングのこと


python使いのためのhy基本文法まとめ

はじめに

 hyの使い方の記事は一定の需要があると思い、まとめて公開する。

 hyはpythonの抽象構文木に変換されるlisp。lispなのでマクロなどの素敵な機能が使えるらしいが、私もよくわからないのでその辺には触れない。とりあえずpythonのコードをhyに書き写せる水準を目標にする。

 なお、公式ドキュメントはここ。
Welcome to Hy’s documentation! — hy 0.15.0 documentation

最初に覚えておくべきこと

 hyはlispなのでS式を扱う必要がある。S式は(e1 e2 e3 ...)という形式で、これはlispにおけるリストである。

 S式をそのまま書くと(一部特別な例外があるが)、e1を関数名、e2以下を引数として呼び出す。だから、

=> (+ 1 2)
3

 という結果が得られる。

 hyはlispであり、lispは関数型言語なので、考えられるほとんどのオブジェクト操作はこの関数呼び出しという形で実現することになる。こう書くとpythonのオブジェクト指向が使えなくて不便そうな気がするが、普通に使える。

=> (setv hoge "h o g e")
=> (hoge.split)
['h', 'o', 'g', 'e']

 setvはset variable・・・要するにpythonの代入文。そして二行目でpythonでいうhoge.split()をしている。なんてことはない。イメージとしては、インスタンスに属する関数オブジェクトへの参照をなんとかして作り、それをS式の形で関数として呼び出す感じである。

 このことがわかれば、ほぼpythonと同じように書けると思う。

hyのインストール

 pip install hyで入る。

import

 最も簡単なimportは次のように書ける。

(import os)

 これはpythonのimport osと等価である。

 もう少し複雑なimportをしたい場合、カッコが増える。しかもカクカッコである。たとえばfromに相当するものを書くときは、

(import [collections [defaultdict]])

 こんな感じである。これはfrom collections import defaultdictと等価である。もし複数階層あるモジュールからimportしたいときは、単にドットで繋げば良い。

 もちろんasも使える。

(import [numpy :as np])

 これでimport numpy as npと等価である。

 外側のカクカッコには一つのモジュールしか書けないが、内側の子モジュールのカッコには複数書けるようである。

=> (import [os [listdir name]])

 また、一つのimport関数で複数のモジュールからimportすることも可能。

(import [numpy :as np]
        [scipy :as sp])

代入

 最初に出したが、setvがpythonの代入と等価である。

=> (setv hoge 10)
=> hoge
10

 setv自身は値を返さない(強いて言えばNoneを返す)。pythonの代入文にそのままコンバートされる都合上そうなっている。なんだかlisp的には妥協した仕様に見える。

組み込みコレクション型

 pythonの組み込みコレクション型は使える(はずである)。表記もほぼpythonと同様だが、カンマがないのが違い。ただしreplはカンマ付きのpython表記を返してくる。ご愛嬌である。

=> [1 2 3 4]
[1, 2, 3, 4]
=> {1 2 3 4} # 恐ろしいことにsetのつもりで書くとdictになる
{1: 2, 3: 4}
=> (set [1 2 3 4]) # setにしたい場合
{1, 2, 3, 4}
=> (tuple [1 2 3 4]) # tupleも同様に対応するとこうなる
(1, 2, 3, 4)
=> (, 1 2 3 4) # 不思議なtupleの記法がある
(1, 2, 3, 4)

 記法がlispとpythonで混ざる欠点がある。一応問題にならないよう努力はしていると思うが、わかりやすい構文かと言われると微妙。hyの割と大きな弱点だと思う。

条件分岐

 条件分岐はifとcondがある。ifの書式は次の通り(公式チュートリアルから引用)。

(if (try-some-thing)
  (print "this is if true")
  (print "this is if false"))

 lispのifを知っている人には違和感がないと思う。知らない人はpythonの三項演算子だと思えば良い。というか、恐らく三項演算子そのものにコンバートされる。

 三項演算子なのでif関数自体に返り値がある。ということはpythonのif文より使いみちは多い。

 condもlispでは典型的なもの。これもチュートリアルから例を借用する。

(setv somevar 33)
(cond
 [(> somevar 50)
  (print "That variable is too big!")]
 [(< somevar 10)
  (print "That variable is too small!")]
 [True
  (print "That variable is jussssst right!")])

 ま、lisp知ってる人には説明するまでもないし、知らない人はこんなページだけで勉強するなんて無謀なことはしてないはずなので、これといって説明はしない。あと、condも当然値を返す。

ループ構文

 pythonのforとwhileがそのまま使える。まずfor。

=> (for [c "hoge"] (print c))
h
o
g
e

 カクカッコに囲まれている部分がpythonで言うところのfor c in "hoge"のc in "hoge"に相当する。その次がbodyで、bodyは複数の関数を書けば逐次実行される。

=> (for [c "hoge"] (print c) (print (+ c "!")))
h
h!
o
o!
g
g!
e
e!

 zipなどもpythonと同様に使える。構文がちょっとだけ変わるけど。

=> (for [[c1 c2] (zip "hoge" "fuga")] (print c1 c2))
h f
o u
g g
e a

 組み込み関数などはだいたい使える。hyはlispの皮をかぶったpythonに過ぎない。

 whileも直感的なものである。(while condition *body)というフォーマットで書けば良い。

=> (setv i 0)
=> (while (< i 10) (print i) (+= i 1))
0
1
2
3
4
5
6
7
8
9

 新しくインクリメント演算子が登場した。このインクリメント演算子はNoneを返す。どうやらhyはpythonの文を片っ端からNoneを返す関数に変換して作ったlispらしい(for, whileともに返り値はNone)。やはりlispの皮をかぶったpythonに過ぎないという気がする。

関数定義

 defnを使う。書式は(defn function-name [*args] *body)。

=> (defn str_hoge [string] (setv s (+ string "hoge")) (print s))
=> (str_hoge "fuga")
fugahoge

 ちなみにlisp的にはアンダーバーではなくハイフンを使うのが正しいらしい。また、hyはアンダーバー区切りとハイフン区切りを勝手に相互変換するので、str-hogeでも同じ関数が呼べる。python使い的には気持ち悪いので、とりあえずアンダーバー区切りで説明している。

 また、関数オブジェクトはfnで作れる。これを利用してsetvで関数定義することも可能(pythonのlambda文を変数に代入することに相当。ただしhyでは複数行書いても良い)。

=> (setv str_hoge2 (fn [string] (setv s (+ string "hoge")) (print s)))
=> (str_hoge2 "fuga")
fugahoge

 キーワード引数なども指定できる。lispでいうラムダリスト。

=> (defn piyo [str1 &optional [str2 "piyo"]] (print (+ str1 str2)))
=> (piyo "hoge")
hogepiyo
=> (piyo "hoge" :str2 "fuga")
hogefuga

 他に可変長引数、可変キーワード引数に対応した&restや&kwargsもあるが、使う機会は少ないので説明は省く。

オブジェクト指向

 pythonはオブジェクト指向言語である。よってlispの皮を被ったpythonであるhyもオブジェクト指向言語である。

 hyのオブジェクト指向はあまり深く考えずとも、直感的に使える。たとえばクラス定義してクラスメソッドを呼ぶというよくある処理。

=> (defclass X []
... (defn __init__ [self x] (setv self.x x))
... (defn print [self] (print self.x)))
=> (setv x (X "xxx"))
=> (x.print)
xxx

 簡単そうである。クラスのインスタンス化がちょっと気持ち悪いと思うかもしれないが、クラス名は実質的にコンストラクタへの参照だと思えばそんなにキモくない。

 ただし、python風の(普通のオブジェクト指向言語風の)ドット記法が使えるのは変数に束縛されたオブジェクトに対してだけである。

=> ("hoge hoge".split)
  File "<input>", line 1, column 13

  ("hoge hoge".split)
              ^-----^
HyTypeError: cannot access attribute on anything other than a name (in order to get attributes of expressions, use `(. <expression> split)` or `(.split <expression>)`)

 親切なことにエラーメッセージがなすべきことを教えてくれる。

=> ((. "hoge hoge" split))
['hoge', 'hoge']
=> (.split "hoge hoge")
['hoge', 'hoge']

 上の書き方だとsplitの関数オブジェクトが得られるので、呼び出すにはもう一回カッコでくくってやる必要がある。下の書き方は直接呼べる。

スライス・辞書などのキー参照

 getという関数があり、(get a 0)はpythonのa[0]と等価である。次のように使うことができる。

=> (setv l [1 2 3])
=> (get l 0)
1
=> (setv d {1 10 2 20})
=> d
{1: 10, 2: 20}
=> (get d 1)
10
=> (setv (get d 3) 30)
=> d
{1: 10, 2: 20, 3: 30}

リスト内包表記

 pythonのリスト内包表記がちゃんとhyにもある。安心して欲しい。次のように書く(チュートリアルの例)。

(setv odds-squared
  (list-comp
    (pow num 2)
    (num (range 100))
    (= (% num 2) 1)))
; And, an example stolen shamelessly from a Clojure page:
; Let's list all the blocks of a Chessboard:

(list-comp
  (, x y)
  (x (range 8)
   y "ABCDEFGH"))

; [(0, 'A'), (0, 'B'), (0, 'C'), (0, 'D'), (0, 'E'), (0, 'F'), (0, 'G'), (0, 'H'),
;  (1, 'A'), (1, 'B'), (1, 'C'), (1, 'D'), (1, 'E'), (1, 'F'), (1, 'G'), (1, 'H'),
;  (2, 'A'), (2, 'B'), (2, 'C'), (2, 'D'), (2, 'E'), (2, 'F'), (2, 'G'), (2, 'H'),
;  (3, 'A'), (3, 'B'), (3, 'C'), (3, 'D'), (3, 'E'), (3, 'F'), (3, 'G'), (3, 'H'),
;  (4, 'A'), (4, 'B'), (4, 'C'), (4, 'D'), (4, 'E'), (4, 'F'), (4, 'G'), (4, 'H'),
;  (5, 'A'), (5, 'B'), (5, 'C'), (5, 'D'), (5, 'E'), (5, 'F'), (5, 'G'), (5, 'H'),
;  (6, 'A'), (6, 'B'), (6, 'C'), (6, 'D'), (6, 'E'), (6, 'F'), (6, 'G'), (6, 'H'),
;  (7, 'A'), (7, 'B'), (7, 'C'), (7, 'D'), (7, 'E'), (7, 'F'), (7, 'G'), (7, 'H')]

 私はチュートリアルを読んでもよくわからなかったので、次の内包表記を自分でhyに変換してみることにした。

>>> [[x*y for y in range(1,10)] for x in range(1,10)]

 何の事はない掛け算九九である。

 まず外側を作る。

=> (list-comp x (x (range 1 10)))
[1, 2, 3, 4, 5, 6, 7, 8, 9]

 次に内側を書く。

=> (list-comp (list-comp (* x y) (y (range 1 10))) (x (range 1 10)))
[[1, 2, 3, 4, 5, 6, 7, 8, 9], [2, 4, 6, 8, 10, 12, 14, 16, 18], [3, 6, 9, 12, 15, 18, 21, 24, 27], [4, 8, 12, 16, 20, 24, 28, 32, 36], [5, 10, 15, 20, 25, 30, 35, 40, 45], [6, 12, 18, 24, 30, 36, 42, 48, 54], [7, 14, 21, 28, 35, 42, 49, 56, 63], [8, 16, 24, 32, 40, 48, 56, 64, 72], [9, 18, 27, 36, 45, 54, 63, 72, 81]]

 仕組みはわかった。わかりやすいかと言われると微妙だが、pythonのリスト内包表記も最初は読みづらく感じたし、慣れればなんとかなるだろう。

まとめ

 内容は必要を認めれば追加する。

 hyはけっこう楽しい(小並感)。