Python
Clojure

Pythonで関数型言語してみた

最近とある理由でClojureに触りました。

8年近くCやPythonなどの手続き型言語を使ってきた者にとって,最初は「関数型言語」に戸惑いもありましたが,一方で,Pythonが関数型言語の良い点をたくさん取り入れていることにも気づきました。

そこで,Pythonでどこまで関数型言語ライクなことができるか,Clojureのサンプルを参考ししつつ試していきたいと思います。

今回はayato-pさんが公開されている「Clojure の日本語ガイド」の イディオム集 の内容をPythonで実装してみます。
手元のPythonのバージョンは3.6.1です。

複数のコレクションの要素を index ごとにまとめる

[0 1 2 3 4]
[:a :b :c :d :e]

Pythonには「キーワード」というものがないので文字列で代用します。zipを使えば簡単です。

a = [0, 1, 2, 3, 4]
b = [":a" ":b" ":c" ":d" ":e"]
zip(a, b)
# -> <zip at 0x111a14548>

おっと,Python 3はイテレーターを多用しているんでした。
そのままでは評価されないので,各要素を見たい場合はlistで括る必要があります。

list(zip(a, b))
# -> [(0, ':a'), (1, ':b'), (2, ':c'), (3, ':d'), (4, ':e')]

マップを平坦なシーケンスへと変換する

{:name    "ayato-p"
 :age     "24"
 :address "Japan"}

itertoolsモジュールのchainを使って辞書の (key, value) のペアを連結します。アスタリスク「*」はリストの要素を関数の引数として渡すための演算子です。

import itertools

d = {":name":    "ayato-p",
     ":age":     "24",
     ":address": "Japan"}

list(itertools.chain(*d.items()))
# -> [':name', 'ayato-p', ':age', '24', ':address', 'Japan']

可変長引数を受け取る関数にシーケンスのデータを渡したい

(def v ["foo" "bar" "baz"])

(defn f [& args]
  (clojure.string/join ", " args))

引数の渡し方は上でやったのと同じです。可変長引数を受け取る関数を定義する場合も,アスタリスクを使います。

v = ["foo", "bar", "baz"]

def f(*args):
    return ", ".join(args)

f(*v)
# -> 'foo, bar, baz'

辞書を渡す場合は,アスタリスクを2個使います。

m = {"name": "ayato-p", "age": 24}

def g(name, age):
    return "name:" + name + ", age:" + str(age)

g(**m)
# -> 'name:ayato-p, age:24'

シーケンスの全要素に関数を適用して nil を捨てる

(def people [{:name "ayato_p" :age 11}
             {:name "alea12" :age 10}
             {:name "zer0_u"}])

(remove nil? (map :age people)) ;(11 10)
(keep :age people) ;(11 10)

リスト内包表記を条件式とともに使うだけです。

people = [{":name": "ayato_p", ":age": 11},
          {":name": "alea12",  ":age": 10},
          {":name": "zer0_u"}]

[x[":age"] for x in people if ":age" in x]
# -> [11, 10]

ある値が boolean かどうかを知りたい

isinstanceを使います。

isinstance(True, bool) # -> True
isinstance(False, bool) # -> True
isinstance("", bool) # -> False
isinstance(None, bool) # -> False
isinstance(0, bool) # -> False
isinstance(1, bool) # -> False

複数の候補の中から nil でない値を見つけたら値を返す

これはClojureでもPythonでも同じ。

None or "ayato-p"
# -> "ayato-p"

しかし,bool化した際にFalseになるもの全部(None,0,False,空リスト等)が当てはまってしまうので,真面目にifを使ったほうがいいかと思います。

シーケンスが空かどうかを確かめたい

lenで長さを測るか,そのままifで評価しても良いです。boolで括れば中身の有無に応じてTrue/Falseが返ります。

ev = []
v = [1, 2]

if ev:
    print("not empty")
else:
    print("empty")
# -> empty

if v:
    print("not empty")
else:
    print("empty")
# -> not empty

bool(ev)
# -> False
bool(v)
# -> True

マップに対して条件を満すときだけ assoc/dissoc して、それ以外のときはそのまま返したい

(def m {:foo 1 :bar 2})

(cond-> m
  true (assoc :baz 3)) ;{:foo 1, :bar 2, :baz 3}

(cond-> m
  false (assoc :baz 3)) ;{:foo 1, :bar 2}

ifとdictを使って

m = {":foo": 1, ":bar": 2}

dict(m, **{":baz": 3}) if True else m
# -> {':bar': 2, ':baz': 3, ':foo': 1}

dict(m, **{":baz": 3}) if False else m
# -> {':bar': 2, ':foo': 1}

dict関数のこのような使い方は初めてです。実際にPythonでプログラムを書く際はdictの内容を書き換えてから利用することのほうが多いかと思います。

reduce を途中で止めたい

(reduce (fn [acc x]
          (if (zero? x)
            (reduced 0)
            (* acc x)))
        1
        (cycle [9 8 7 6 5 4 3 2 1 0]))

reducedはPythonに無いので,例外を使って似た機能を実装します。

import functools

class Reduced(Exception):
    def __init__(self, data):
        super().__init__()
        self.data = data

def myreduce(f, it):
    try:
        return functools.reduce(f, it)
    except Reduced as e:
        return e.data

def mymultiply(acc, x):
    if x == 0:
        raise Reduced(0)
    return acc * x

myreduce(mymultiply, [9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
# -> 0

マップのキー(バリュー)すべてに対して関数を適用( map )したい

辞書内包表記を使えば簡単。

m = {"key1": 1,
     "key2": 2,
     "key3": 3}

{":" + k: v for k, v in m.items()}
# -> {':key1': 1, ':key2': 2, ':key3': 3}

ベクターからインデックスを元に要素を落としたい

リストのデータを変更して良いならpopを使えば良いです。enumerateとリスト内包表記を使って新たなリストを作っても良いです。

l = [9, 8, 7, 6, 5, 4, 3, 2, 1]
l.pop(5)
l
# -> [9, 8, 7, 6, 5, 3, 2, 1]

l = [9, 8, 7, 6, 5, 4, 3, 2, 1]
[x for i, x in enumerate(l) if i != 5]
# -> [9, 8, 7, 6, 5, 3, 2, 1]

java.util.LinkedList のインスタンスをベクターにしたい

普通のPythonではJavaのインスタンスを扱えません。JythonっていうJavaで実装されたPythonがあるみたいですが,開発が停滞してるようなのでスキップ。

ループの間で何度か更新する値を保持していたい

これはふつうにforループで。

プログラム全体で参照できるような簡易データベースが欲しい

引用元に「あまり推奨するわけではない」って書いてあるのでスキップ。

falsy な値をリストから除去する

(filter identity [nil false true 1 "hello" [1 2] {:foo 1} :hoge])
;; (true 1 "hello" [1 2] {:foo 1} :hoge)

これもリスト内包表記で

[x for x in [None, False, True, 1, "hello", [1, 2], {":foo": 1}] if x]
# -> [True, 1, 'hello', [1, 2], {':foo': 1}]

オブジェクトの一覧にインデックスを付ける

上でも既に出ていますが,enumerateを使います。

list(enumerate(["a", "b", "c", "d", "e"]))
# -> [(0, 'a'), (1, 'b'), (2, 'c'), (3, 'd'), (4, 'e')]

こうすることで次のように利用できます。

[str(i) + " is " + x for i, x in enumerate(["a", "b", "c", "d", "e"])]
# -> ['0 is a', '1 is b', '2 is c', '3 is d', '4 is e']

シーケンスから最初に条件に合致するものを取得する

(defn find-first [pred coll]
  (first (drop-while (complement pred) coll)))

いい加減forループ書けよって思われそうだけど,意地でも書かないぞ!!
ジェネレーターを作って,nextで最初の要素を取り出します。

l = [1, 2, 3, 4, 5, 6, 7, 8, 9]
next(x for x in l if x > 4)
# -> 5

ある自然数以上の最も小さな素数を試し割りで取り出す例は

import itertools

def isprime(x):
    return next((0 for n in range(2, int(x ** 0.5) + 1)
                                         if x % n == 0), 1)

next(x for x in itertools.count(1000) if isprime(x))
# -> 1009

(2017年7月20日追記)
素数判定で,iterableの全要素が真のときに真を返す「all」を使ったほうがより簡潔に書けます。

def isprime(x):
    return all(x % n for n in range(2, int(x ** 0.5) + 1))

まとめ

ClojureとPythonを簡単に比較していただけたでしょうか。
筆者はClojureの初心者なので,本当はClojureにできてPythonにできないことがたくさんあるのかもしれません。

しかし,このように見てみると,Pythonではリスト内包表記やジェネレーター式を使うことで,余計な関数名を書かずに関数型言語ライクな処理を非常にコンパクトに書けることが分かるかと思います。

最後の例のように,無理にジェネレーターばかり使う必要もありませんが,「このようなこともできるよ」程度に見ていただければ,いつかコードを書く際の手助けになるかもしれません。

ではでは〜。