Edited at

素人の言語処理100本ノック:02

More than 1 year has passed since last update.

言語処理100本ノック 2015の挑戦記録です。一覧はこちらからどうぞ。


再び環境変更

まだ2問しかやっていないのですが、再び環境を変更することにしました。Pythonを3.xに変更します。


  • Ubuntu 16.04 LTS

  • Python 2.7.12 → 3.5.2 :: Anaconda 4.1.1 (64-bit)

Python 3.xを入れるとトラブりそうで躊躇していたのですが、前問で_ha1fさんにAnacondaを教えていただき、すごく簡単に3.5.2にできました。ありがとうございます!

Anacondaは、よく使われるパッケージの詰め合わせみたいなものなんですね。ググるとたくさん解説が見つかりますが、インストールについてはAnaconda を利用した Python のインストール (Ubuntu Linux)が分かりやすかったです。

(2016/09/11追記)

その後、コメントでpython_ufoさんに教えていただいたのですが、Ubuntu 16.04 LTSには最初からPython 3.5.2が入っていて、python3で使うことができます。そのため、Python 3.5.2を使いたいだけならAnacondaのインストールは不要です。


第1章: 準備運動


02.「パトカー」+「タクシー」=「パタトクカシーー」


「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.


出来上がったコード:


main.py

# coding: utf-8

target1 = 'パトカー'
target2 = 'タクシー'
result = ''
for (a, b) in zip(target1, target2):
result += a + b
print(result)

(2018/02/04追記)

gimoさんにfunctools.reduce()を教えていただいたので、回答例を追加します。


main2.py

# coding: utf-8

from functools import reduce

target1 = 'パトカー'
target2 = 'タクシー'
result = ''.join(reduce(lambda x, y: x + y, zip(target1, target2)))
print(result)


実行結果:


端末

パタトクカシーー



zip()itertools.zip_longest()について

zip()は複数のイテラブル(リストやタプルなど)から要素を集めたリストを作るとのこと。こんな便利な関数があるとはPythonすごい!

いろいろ試してみます。


インタプリタ

>>> target1 = 'abcde'

>>> target2 = '12345'
>>> zip(target1, target2)
<zip object at 0x7f1294d76108>

あれ?zip objectができた?

調べてみたら、Python 2.xではリストを返していたのが、3.xではイテレータを返すように変わったそうです。前方から少しずつ処理するようになって効率的になったのかも知れません(こういうのをジェネレータと呼ぶようですね)。Pythonのドキュメントや解説を見る時は、常にバージョンの意識が必須!

イテレータなのでリストに変換すれば中身が確認できます。


インタプリタ1

>>> target1 = 'abcde'

>>> target2 = '12345'
>>> res_zip = zip(target1, target2)
>>> list(res_zip)
[('a', '1'), ('b', '2'), ('c', '3'), ('d', '4'), ('e', '5')]

個々の要素が1つずつ取得されて、タプルに詰め込まれています。これが1つずつ取り出される流れです。

元の要素数が異なる場合は、少ない方に合わせられます。


インタプリタ2

>>> target1 = 'abc'

>>> target2 = '12345'
>>> res_zip = zip(target1, target2)
>>> list(res_zip)
[('a', '1'), ('b', '2'), ('c', '3')]

長い方に合わせたい時はitertools.zip_longest()が使えます。


インタプリタ3

>>> import itertools

>>> res_zip = itertools.zip_longest(target1, target2)
>>> list(res_zip)
[('a', '1'), ('b', '2'), ('c', '3'), (None, '4'), (None, '5')]

要素が足りない方はNoneが入りますが、指定することもできます。


インタプリタ4

>>> res_zip = itertools.zip_longest(target1, target2, fillvalue = 'zzz')

>>> list(res_zip)
[('a', '1'), ('b', '2'), ('c', '3'), ('zzz', '4'), ('zzz', '5')]

いたせりつくせり。

zip()に戻って、今度は3つ以上をくっつけてみます。


インタプリタ5

>>> target1 = 'abcde'

>>> target2 = '12345'
>>> target3 = 'あいうえお'
>>> res_zip = zip(target1, target2, target3)
>>> list(res_zip)
[('a', '1', 'あ'), ('b', '2', 'い'), ('c', '3', 'う'), ('d', '4', 'え'), ('e', '5', 'お')]

予想通りの結果です。


zip()の結果を戻す

zip()の結果をもう一度zip()に渡すと、元に戻せるそうです。

まず、zip()で3つのリストを渡して5つのタプルを作ります。


インタプリタ6

>>> target1 = 'abcde'

>>> target2 = '12345'
>>> target3 = 'あいうえお'
>>> res_zip = zip(target1, target2, target3)
>>> list(res_zip)
[('a', '1', 'あ'), ('b', '2', 'い'), ('c', '3', 'う'), ('d', '4', 'え'), ('e', '5', 'お')]

この5つのタプルをもう1度zip()に渡せば、今度はその5つのタプルから1要素ずつ取り出してタプルを作るので、元の3つに戻ります。なるほど!


インタプリタ7

>>> res_zip = zip(target1, target2, target3)

>>> res_list = list(res_zip)
>>> res2_zip = zip(res_list[0], res_list[1], res_list[2], res_list[3], res_list[4])
>>> list(res2_zip)
[('a', 'b', 'c', 'd', 'e'), ('1', '2', '3', '4', '5'), ('あ', 'い', 'う', 'え', 'お')]

戻りました!

文字列ではなくタプルになっていますが、戻し方は後述します。

なお、zip()は引数可変ですが、イテラブルの前に*を付けると引数にバラしてくれる機能があります。そのため、res_list[0], res_list[1]...といちいち書かないでも、*res_listでOKです。便利!


インタプリタ8

>>> res_zip = zip(target1, target2, target3)

>>> res_list = list(res_zip)
>>> res2_zip = zip(*res_list)
>>> list(res2_zip)
[('a', 'b', 'c', 'd', 'e'), ('1', '2', '3', '4', '5'), ('あ', 'い', 'う', 'え', 'お')]


タプルを文字列に戻す

(2018/02/04追記)

zip()を文字列の処理で使うと、タプルの中に1文字ずつバラバラで詰め込まれます。これを文字列に戻したい時は、文字列のイテラブルを連結してくれるstr.join()が便利です。


インタプリタ9

>>> chars_tuple = ('a', 'b', 'c', 'd', 'e')

>>> ''.join(chars_tuple)
'abcde'


functools.reduce()について

(2018/02/04追記)

functools.reduce()という便利な関数をgimoさんに教えていただきました。これは指定したイテラブルに対して指定した関数を累積的に実行し、結果を1つにまとめてくれるものです。まずは例を。


インタプリタ10

>>> def add(x, y):

... return x + y
...
>>> import functools
>>> functools.reduce(add, [1, 2, 3, 4, 5])
15

functools.reduce()は、直前の結果とイテラブルから取り出した値の2つを関数に渡して新たな結果を作り、これをイテラブルの終わりまで順次繰り返してくれます。上の例のfunctools.reduce(add, [1, 2, 3, 4, 5])で、次のコードのような処理をやってくれます。


インタプリタ11

>>> result = 0

>>> for x in [1, 2, 3, 4, 5]:
... result = add(result, x)
...
>>> result
15

result = 0に相当する結果の初期値はfunctools.reduce()の3番目の引数で指定できます。省略した場合は初回だけイテラブルから2つ取り出して、それを関数に渡した結果を初期値にしてくれます。

なお、2つ目の解答例main2.pyでは、関数の部分にラムダ式lambda x, y: x + yを指定しています。ラムダ式については素人の言語処理100本ノック:18で少し解説していますので、そちらを参照してください。


イテレータの状態遷移にハマりました

今回試していてハマったのは、イテレータを一度リストに変換したりすると、もう使えなくなってしまう(進んでしまったイテレータはリセットできない)ことです。

例えば、前述のインタプリタ6の実行直後にインタプリタ7を実行する場合、インタプリタ7の最初の行res_zip = zip(target1, target2, target3)は一見不要に見えます。インタプリタ6の4行目ですでに実行していますから。

でも、インタプリタ6の5行目list(res_zip)でリストに変換する際、res_zipのイテレータが進んでしまうので、もう一度list(res_zip)しても空のリストになってしまうのです。

Pythonに慣れている方は、イテレータの状態遷移を自然に気にできるかと思いますが、私はしばらく気づかずハマってました。

 

3本目のノックは以上です。誤りなどありましたら、ご指摘いただけますと幸いです。