はじめに
タイトルにある通り「言語処理100本ノック 2015」の問題を解きながらPythonを学ぶことが目的です。
なので、ただ問題を解いて終りにならないよう、理解に至るまでの過程、リファクタリングの過程、異なるアプローチからの解き方があるケースでは複数の回答を、また、関連する内容についても言及していこうと思います。
100問やり遂げた暁には、自分はパイソニスタ?パイソニスト?パイソニアン?だと胸を張って言えることを目標に。(なぜ通り名がこんなにたくさん・・・?)
環境
- Python 3.7.2
- Win10
02. 「パトカー」+「タクシー」=「パタトクカシーー」
「パトカー」+「タクシー」の文字を先頭から交互に連結して文字列「パタトクカシーー」を得よ.
【作成したコード】
gnr = (x + y for x, y in zip("パトカー", "タクシー"))
ans = "".join(gnr)
print(ans) # => パタトクカシーー
試行錯誤の過程で得た知識
サラッと書きましたが、このコードに至るまでには色々と試行錯誤がありましたので、その過程で理解した内容に触れていきたいと思います。
zip関数とzipオブジェクト
今回、zip関数を使いzipオブジェクトを取得しています。
この部分が何をしているのか確認のために、zipオブジェクトの中身を確認したいのですが、
#zipオブジェクトの中身を確認
zip_obj = zip("パトカー", "タクシー")
print(zip_obj) # => <zip object at 0x7fc8dfa9bf48>
このままでは、中身がどういう状態なのかよく分かりません。
なので、zipオブジェクトの属性を調べてみると、
zip_obj = zip("パトカー", "タクシー")
for att in dir(zip_obj):
print(att)
#__class__
# -- 中略 --
#__iter__ <= イテラブルオブジェクトである。(つまり、forでループできる)
# -- 中略 --
#__next__ <= というか、そもそもイテレータである。
# --以下略--
どうやらfor
でループを回すことが出来そうです。(そもそもジェネレータ式を使っといて今更何言ってるんだ、という話ではあるのですが・・・)
ということで、for
でループを回してzipオブジェクトの要素を確認してみます。
zip_obj = zip("パトカー", "タクシー")
for elem in zip_obj:
print(elem)
#('パ', 'タ')
#('ト', 'ク')
#('カ', 'シ')
#('ー', 'ー')
なるほど。今回の例でzip関数の挙動を説明すると、
- zip関数に「パトカー」「タクシー」2つの文字列を引数として渡すことで
- 同じ位置の文字、つまり、('パ', 'タ')、('ト', 'ク')・・・のタプルを要素とするイテレータを返してくれている
ということが分かりました。
zip関数の補足を何点か
#引数として渡すことが出来るのはイテラブルオブジェクト
#引数の数は2つ以上でも問題ない。
#異なる種類の引数を渡しても大丈夫。
# リスト、タプル、文字列の3つの引数を渡した例
nums = zip([1, 2, 3], ("one", "two", "three"), "一二三")
for elem in nums:
print(elem)
#(1, 'one', '一')
#(2, 'two', '二')
#(3, 'three', '三')
#要素数の数が異なる値をzip関数に渡した場合、多い方の要素は省略される
alf = zip("abc", "ABCDEFG")
for elem in alf:
print(elem)
#('a', 'A')
#('b', 'B')
#('c', 'C')
#zipオブジェクトはイテレータので、アクセス済みの要素に再度アクセスすることは出来ない。
boolean = zip((1, 0), (True, False))
#forでループするよりも、リスト化した方が結果が分かりやすいので、今回はリスト化して検証
# 1回目
lst1 = list(boolean)
print(lst1) # => [(1, True), (0, False)]
# 2回目
lst2 = list(boolean)
print(lst2) # => []
#zipオブジェクトに添え字でアクセスすることは出来ない
zip_obj = zip("パトカー", "タクシー")
zip_obj[1] # => TypeError: 'zip' object is not subscriptable
zip関数の挙動の確認は、以下のページを参考にさせて頂きました。
アンパック
gnr = (x + y for x, y in zip("パトカー", "タクシー"))
# ↑
# この部分の話
zip関数の所で触れた通り、zipオブジェクトはforでループする際、毎回タプルを返すことになりますので、上記「この部分」で触れた所は
#1回目
x, y = ("パ", "タ")
#2回目
x, y = ("ト", "ク")
#以下省略
という挙動になると思います。
今回のケースではタプルの2つの要素(1回目であれば「パ」と「タ」)をそれぞれ別の変数(「x」と「y」)に代入していますが、Pythonでは
x, y = ("パ", "タ")
とすることで、リストやタプル(あと文字列も)の各要素(文字列の場合は1文字ずつ)をそれぞれ別の変数に代入することが可能です。
この操作を、アンパックと呼びます。
今回の例では、「左辺の変数の数」=「右辺の値の数」という単純な状態で左辺の変数に代入していますが、左辺の変数の1つにアンパックの補足
*
を付けて、「左辺の変数の数」<「右辺の値の数」の状態でアンパックすることも可能です。#「左辺の変数の数」<「右辺の値の数」 のケース
#左辺の変数の1つに「*」を付けることで、溢れた値をリストにして、1つの変数にまとめて格納
cars = ("パトカー", "タクシー", "覆面パトカー", "機動隊指揮車両", "救急車")
police_car, taxi, *others = cars
print(police_car) # => パトカー
print(taxi) # => タクシー
print(others) # => ['覆面パトカー', '機動隊指揮車両', '救急車']
#文字列もアンパック可能
one, two, three = "123"
print(one) # => 1
print(two) # => 2
print(three) # => 3
アンパックの理解には、下記のページが参考になりました。
内包表記(ジェネレータ式も含む)
pythonにおける、for文でのループ処理
nums = [1, 2, 3]
for n in nums: # nums内の要素を1つずつ順番に2倍
n * 2 # つまり、順番に、2, 4, 6
このループ処理全体を[]
や()
や{}
で囲って、処理後の要素(このケースではn * 2
の結果)から成る別の新たな集まり(リストとか、辞書とか)を作るのが、内包表記です。
nums = [1, 2, 3]
new_lst = [n * 2 for n in nums] # => [2, 4, 6]
上記の例はfor文
を[]
で囲んだリスト内包表記になりますが、内包表記には、(ジェネレータイテレータを作成するジェネレータ式を含めると)全部で4種類あります。
- リスト内包表記 =>
[]
で囲み、リストを作成 - セット内包表記 =>
{}
で囲み、集合を作成 - 辞書内包表記 =>
{key: value}
の形を取り、辞書を作成 - ジェネレータ式 =>
()
で囲む。ジェネレータイテレータを返す式
なお、今回作成したコードは、ジェネレータ式に該当します。それぞれの内包表記の例
nums = [1, 2, 3, 2, 1]
lst_nums_double = [n*2 for n in nums] # リスト内包表記
set_nums_double = {n*2 for n in nums} # セット内包表記
dic_nums_double = {i: n*2 for i, n in enumerate(nums)} # 辞書内包表記
gnr_nums_double = (n*2 for n in nums) # ジェネレータ式
print(lst_nums_double) # => [2, 4, 6, 4, 2]
print(set_nums_double) # => {2, 4, 6}
print(dic_nums_double) # => {0: 2, 1: 4, 2: 6, 3: 4, 4: 2}
print(gnr_nums_double) # => <generator object <genexpr> at 0x7f2ec1000a40>
gnr = (x + y for x, y in zip("パトカー", "タクシー"))
4種類ありますが、「結果として何が作成されるのか」以外の部分は、概ね同じです。
内包表記については、分かりやすい解説のページが多数あり、また、詳しく触れてゆくと凄いボリュームになりそうなので、今回のコードに関係する内容に絞って、簡単に。
内包表記の記述方法と挙動
書き方はいたって簡単で、リスト内包表記を例にとると、
nums = [1, 2, 3]
[ n * 2 for n in nums ]
#[ 変数に対する処理 for 変数 in イテラブルオブジェクト ]
# ↑
# 直後のイテラブルオブジェクト内の各要素を示す任意の変数
試しにfor文
と比較してみると、
for n in nums:
n * 2 # <= forの2行目が前方
# ↓
[ n * 2 for n in nums ]
# ↑
# forの1行目が後方(ダブルコロン「:」を省略)
とするだけです。これだけで、
nums = [1, 2, 3]
new_lst = [n*2 for n in nums] # => numsの各要素を2倍する
print(new_lst) # => [2, 4, 6]
の結果が得られます。
今回作成したコードの内包表記部分の挙動の確認
挙動を確認しやすいように、zipオブジェクトをリストに変換し、ジェネレータ式をリスト内包表記に変更した上で、動きを観察してみます。
lst1 = list(zip("パトカー", "タクシー")) # => [('パ', 'タ'), ('ト', 'ク'), ('カ', 'シ'), ('ー', 'ー')]
lst2 = [x + y for x, y in lst1] # => ['パタ' ,'トク' , 'カシ' , 'ーー' ]
lst1内の各要素('パ', 'タ')、('ト', 'ク')、・・・
に対して、内包表記の先頭でx + y
の処理を指定しており、これによってパタ、トク、・・・
の新しい要素からなる別のリストを作成していることが分かります。
なお、内包表記の解説は、下記のページがとても分かりやすかったです。
ジェネレータ
ジェネレータとは何なのか?
これは、今回の問題で、最も頭を悩ませた内容です。
そして、正直な話を先にすると、まだよく理解出来てはいないのですが、ひとまず、現段階で分かったところまで。
「ジェネレータ」という言葉が関連する3つの用語
Pythonには、ジェネレータという言葉が関連する用語が3つがあります。
- ジェネレータ関数 => ジェネレータイテレータを返す関数(
return
ではなくyield
を使う) - ジェネレータイテレータ => ジェネレータ関数で生成されるオブジェクト
- ジェネレータ式 => ジェネレータイテレータを返す式(1の簡易的な書き方)
ジェネレータ — Python 3.7.4 ドキュメント
ジェネレータ式 — Python 3.7.4 ドキュメント
単にジェネレータという場合、多くのケースではジェネレータ関数を指しているのですが、文脈によってはジェネレータイテレータを指すこともあるようです。(ややこしい・・・)
ただ、確かにややこしいのですが、要は、
- ジェネレータ関数 or ジェネレータ式により、ジェネレータイテレータが生成される。
- ジェネレータ式はジェネレータ関数の簡易版
ということだと思います。
ジェネレータイテレータ
ジェネレータイテレータ — Python 3.7.4 ドキュメント
generator iterator
(ジェネレータイテレータ) generator 関数で生成されるオブジェクトです。
yield のたびに局所実行状態 (局所変数や未処理の try 文などを含む) を記憶して、処理は一時的に中断されます。 ジェネレータイテレータ が再開されると、中断した位置を取得します (通常の関数が実行のたびに新しい状態から開始するのと対照的です)。
ちょっと何言ってるか分からないので、リスト内包表記とジェネレータ式を使って、リストとジェネレータイテレータを比較してみます。
【リスト】
nums = (1, 2, 3)
#この時点で全ての要素が生成される。
lst = [n*2 for n in nums]
#そして、ずっと状態を保持している(メモリを確保している)ので、何度でもアクセス可能。
print(lst) # => [2, 4, 6]
print(lst) # => [2, 4, 6]
【ジェネレータイテレータ】
nums = (1, 2, 3)
#この時点では、まだ要素が生成されていない。要素に対するアクセスがあったら順に要素を返してゆく状態を作るだけ。
gnr = (n*2 for n in nums)
print(gnr) # => <generator object <genexpr> at 0x7f3b291c0e60>
#ジェネレータイテレータはイテレータの一種なのでnext()でアクセス可能
print(next(gnr)) # => 2 この時点で、1つ目の要素が作られる
#リストに変換して残りの全ての要素にアクセス
print(list(gnr)) # => [4, 6] この時点で、2つ目、3つ目の要素が作られる(1つ目の要素へはもうアクセスできない)
print(list(gnr)) # => [] 既に全ての要素を取得後なので、もう何も作られない
ジェネレータイテレータは、処理を実行した時点では要素が生成されておらず、各要素を返すことが出来る状態が作られているだけです。
要素は、その要素に対しての呼び出しがかかった時に初めて生成されます。
これにより、メモリの消費を抑えています。
ジェネレータイテレータとイテレータの違いが分からない・・・
知った風で色々と述べてきましたが、ここまでに記載してきたジェネレータイテレータの特徴は、「それは、イテレータでも全く同じことが言えるんじゃないのかな・・・?」と思っています。
#この時点では、まだ要素が生成されていない。要素に対するアクセスがあったら順に要素を返してゆく状態を作るだけ。
itr = iter("246")
print(itr) # => <str_iterator object at 0x7f6f0d5f8a20>
#next()でアクセス可能
print(next(itr)) # => 2 この時点で、1つ目の要素が作られる
#リストに変換して残りの全ての要素にアクセス
print(list(itr)) # => [4, 6] この時点で、2つ目、3つ目の要素が作られる(1つ目の要素へはもうアクセスできない)
print(list(itr)) # => [] 既に全ての要素を取得後なので、もう何も作られない
はやり、「ジェネレータイテレータ」と「イテレータ」の挙動は、全く同じのように見えます。
もちろん、「ジェネレータイテレータ」は「イテレータ」の一種なので、この部分の挙動が同じであること自体は当然なのですが、ただ、「ジェネレータイテレータ」と敢えて別でオブジェクトが用意されているので、「何か別の特徴があるんじゃないのかな・・・?」と思っています。
が、それが何なのか分からない・・・
ということで、ジェネレータイテレータについては、まだ疑問点が残っているのですが、この点は、また別で考えて、分かったら別で記事を書こうと思います。
なお、ジェネレータの理解には、以下のページがとても参考になりました。
joinメソッド
gnr = (x + y for x, y in zip("パトカー", "タクシー"))
#作成したジェネレータイテレータをリスト化して、中身の要素を確認してみる
lst = list(gnr)
print(lst) # => ['パタ', 'トク', 'カシ', 'ーー']
長い道のりでしたが、ようやく、あとは生成したジェネレータイテレータの中身を繋げるだけで完成、という状態まで来ました。
ということで、joinメソッドを使って、中身を繋げて完了です。
【作成したコード(再掲)】
gnr = (x + y for x, y in zip("パトカー", "タクシー"))
ans = "".join(gnr)
print(ans) # => パタトクカシーー
雑感
シンプルで一見簡単そうに見える問題でしたが、今回、大項目で上げていない知識や、あと、直接は問題の解法に関係ない知識も含め、本当に多くのコトを学びました。
- zip関数
- アンパック
- 内包表記
- ジェネレータ
- リスト、タプル、セット、辞書
- イテレータ、イテラブルオブジェクト
- イテラブルオブジェクトからindexも取得するenumerate関数
- 辞書からkeyやValueを取得する、items、keys、values
とんでもないボリュームですね・・・
まだ、準備運動で、3問目なのに、よくもまぁ、こんなに・・・
先が思いやられますが、本当に多くの方が、丁寧で分かりやすいサイトを運営されていて、調べて、新しい知識を知り、検証して納得し、知識が定着してゆく、というのは、凄く楽しく感じられました。
ま、大変であることは間違いないのですが・・・
余談になりますが、当初、タプル、イテレータ、内包表記、などなど、今回初めて知った知識に関して、もう少し詳しく検証して触れていこう、と思っていたのですが、全部詰め込んだら度を越して記事が長くなりそうだと途中で気付き、それらは、100本ノックの記事とは別で切り出して書くことにしました。
書いたらこの記事からもリンクを貼ろうと思うのですが、この作業、「大きな関数を、小さな関数に分割する作業みたいだな」と思って、個人的にちょっと面白かったです。
とんでもなく長くなりそうなので外に切り出した記事
内容に誤りやご意見、もっと良い解法がある、等ありましたら、ぜひコメント頂ければ幸いです。