来世から頑張る!!

技術ブログを目指して

Python入門のためtoolz入れてみた

pythonは文法が単純で覚えやすいらしい。

class MyClass:
  def __init__(self, init_value):
    self.value = init_value
  def my_method(self, value):
    return self.value + value

instance = MyClass(5)
print instance.my_method(3) # => 8
print MyClass.my_method(instance, 3) # => 8

あと、なんか特別な意味を持つメソッドは全部 __???__ みたいに _ 2つで囲まれてるらしい。

ということで入門。
細かいことを気にせずtoolzというライブラリを入れる。

・・・ために準備

pyenvのインストール

pythonはシステムに食い込んでいるらしいので、別のpythonを気軽にいじれるツールを導入。

> git checkout https://github.com/yyuu/pyenv.git ~/.pyenv
> echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
> echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
> echo 'eval "$(pyenv init -)"' >> ~/.bash_profile

if文とかは適当に。

> git clone https://github.com/yyuu/pyenv-virtualenv.git ~/.pyenv/plugins/pyenv-virtualenv
> echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bash_profile

ローカルpythonのインストール

好きなバージョンのpythonをインストールできる。 今回はやむにやまれぬ事情で2系

> pyenv install 2.7.9
> pyenv virtualenv 2.7.9 my-env-name-2.7.9

上がpythonのインストールで、下が依存ライブラリが入る環境のインストール。 プロジェクト毎に依存関係を分けるためのやつ。

で、あとは自分のプロジェクトフォルダを作って、バージョン指定

> mkdir /my/project/root/dir
> cd /my/project/root/dir
> pyenv local my-env-name-2.7.9

最後の指定を行うことで、このディレクトリでのpythonが自動的に指定したバージョンになる。

pythonを使ってみる。

何はともあれtoolzをインストール。

> pip install toolz
> pip freeze > freeze

これで my-env-name-2.7.9 にだけtoolzが入ったはず。 python コマンドでpythonインタープリターを起動し試してみる。

>>> import toolz
>>> toolz.map(lambda x: x + 1, [1, 3, 5, 7, 9])
<itertools.imap object at 0x7f3cfdf1af90>
>>> for a in toolz.map(lambda x: x + 1, [1, 3, 5, 7, 9]):
...   print a
...
2
4
6
8
10
>>> exit()

ひとつ上のディレクトリーに移動して、同じく実行。

>>> import toolz
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named toolz
>>> exit()

いい感じ。

あと、ファイルにコードを書くときのshebangはenvを使わないといけないみたいなので注意。

#! /usr/bin/env python
# coding: utf-8

toolzを使ってみる。

toolzは難しいプログラミングを簡単にしてくれるためのツールで、swiftzみたいなものらしい。

さっき使ったmapなど、遅延実行してくれるのででっかいファイルの処理するのに使用した。

>>> infile = open('inflie.txt', 'r')
>>> lines = toolz.map(some_function, infile)
>>> outfile1 = open('outfile1.txt', 'w')
>>> outfile2 = open('outfile2.txt', 'w')
>>> for line in lines:
...   if some_condition(line):
...     outfile1.write(line)
...   else:
...     outfile2.write(line)
...
>>> outfile2.close()
>>> outfile1.close()
>>> infile.close()

まあtryとかは適時使用するとして、こんな感じでやった時に、for式のlineを取り出した段階で初めて some_function が実行される。

toolz.map で帰ってくる itertools.imap object ってやつは1回しか評価できない。やっててこれでハマった。 list(map(f, [1, 2, 3])) とかすればlist化できるけど、これだと普通の組み込みのmapでいい気がするし、どうするのがベストなんだろう?

で、ここで問題はコードに出てきた some_functionmap の第1引数は引数を1個とって戻り値も1個の関数なので、複雑な処理をしようと思うと長い複雑な関数が必要になる。

関数合成

それは困るので、toolzを使って関数の合成。

>>> a = lambda x: x + 1
>>> b = lambda x: x * 3
>>> c = lambda x: x - 2
>>> x = toolz.compose(a, b, c)
>>> x(3) # == a(b(c(3)))
4

これで複数の関数をつなげて実行できるんだけど、どうも使いづらい。 だって、aから並べたらaから実行してほしいじゃない?

これに近い関数にpipeがある。

>>> toolz.pipe(2, a, b, c) # == c(b(a(2)))
7

でもこれは可変長引数の関係からか評価する値を先に書かなくちゃダメ。 仕方がないので新しい関数を定義してみた。

def chainf(*f):
  from toolz import pipe
  return lambda x: pipe(x, *f)
>>> x = chainf(a, b, c)
>>> x(2) # == c(b(a(2)))
7

これ、元からあるなら誰か教えて。。。

複数の関数がつなげられるようになったので、次の問題に挑戦。

部分適用

map は引数1の戻り値1なので、二つの引数を受け取るような関数が使えない。

これを解決するためには、部分適用を使用する。 そうすればテストするときは関数単位でテストできるし、合成もできるので便利なはず。

def with_contains(values, target):
  if target in values:
    return (target, True)
  else:
    return (target, False)

この例だとif文いらないけど、まあもう少し複雑な前提で。

これを toolz.map で使って各値をtupleにしたいと。 まあ、lambdaとか使ってvaluesを固定してしまえばいいんだけど、toolzには便利な curry があるので使おう。

>>> w = toolz.curry(with_contains) # w(values)(target) == with_contains(values, target)
>>> x = w([1, 2, 3])
>>> x(2)
(2, True)
>>> x(4)
(4, False)

curry複数引数の関数を1引数で1戻り値(関数が帰ってくる)の関数に変えた関数を返してくれる。

>>> uc = lambda a, b, c, d: a + b + c + d
>>> cd = curry(uc) # uc(a, b, c, d) == cd(a)(b)(c)(d)

これで1引数まで持っていければ、小さなテスト済みの関数の組み合わせで最初の map が実行できる。

もちろんoutputのファイルに書き込む部分も関数化できるんだけど、テスト方法がよくわからないからループしてログに書くだけにした。

toolz.do とかを使えば処理自体は難しくないんだけど、ファイル出力のテストってどうやるのかがわからない。

テストの書き方勉強会とかないかなぁ。