名古屋で数学するプログラマ(仮)

@antimon2 が趣味兼一部本職の数学で何かするときのブログ。

「yieldのお勉強 Lv.1」解説(Python編) #CodeIQ

CodeIQ 出題者デビュー問題、公開終了となりました!
たくさんの挑戦、ありがとうございます。

(すでに問題の公開は終了していますので、上記 URL で問題詳細を見ることはできません)

問題文は省略します(挑戦者だけの特典♪)が、問題は、一部が未実装のプログラムを実装してテストを全て通るようにする、というもの。
以下に、問題プログラムと解答例を示します。

【2014/08/20 23:45 追記:解説補足記事 公開しました】

問題プログラムと解答例

まずは、問題プログラム。

問題プログラム:

drop 関数が空っぽの定義(pass のみ)になっています。
これを、テストが通るように実装する、というわけです。
テストで要求しているのは、大体次の通り:

  • generator 関数の定義。
  • 第1引数 iterable で列挙される要素のうち、先頭の n 個(第2引数)だけ除去する。

私の用意していた解答例を示します。

解答例1:

解答例2:

解答例1 は、言語依存度の少ない、シンプル・丁寧でかつパフォーマンスも十分なコード。
解答例2 は、Lv.2問題 で紹介している組込関数 next()を利用し、コンパクトにまとめたコード。

他にも方法はあります。例えば、何名かに以下のような解答が見られました(drop 関数の定義部分のみ抽出):

# Koshi様の解答(抜粋)
def drop(iterable, n):
  for i, v in enumerate(iterable):
    if i >= n : yield v

組込関数 enumerate() を利用し、要素のインデックス値とその要素を同時に列挙しながら処理をしています。
また if 文のインライン記法を使って1行にまとめることでさらにコンパクトにまとめています。

また他には、next(it) の代わりに、iterator(generator) オブジェクトのメソッド it.next() を利用している解答もよく見られました。
ただし、これは Python2 でしか使えないバージョン依存の解答となります(ちなみに Python3 では .__next__() に変更されました)(もちろん、「使用した Python のバージョン」の申告と一致していれば Python2/Python3 依存のコードでも正解にしています)。

解説

改めて説明しますと、求めていたのは、「第1引数で与えられた iterable オブジェクトが列挙する要素のうち、最初の n 個(第2引数)を除去して残りを列挙する generator関数 を定義すること」でした。

なお、Python に慣れ親しんでいる人の中には、標準ライブラリにこの仕様を満たす関数が用意されているのをご存じの方もいらっしゃるかもしれません。
今回は「import 文を使用しないこと。」という条件を付けることで、これが利用されることを回避しています。
つまり「よくある処理(だから標準ライブラリにも用意されている関数)を、自分で実装してみよう」という問題だったわけです。

解答例はいずれも、非常にシンプルです。
ループを回して、必要な箇所で yield 式を使用している。ただそれだけです。
特に難しいことはありませんね(^-^)

別解

何名かの方から、「yield を使わなくても解ける」というご指摘を受けました。
はい、その通りです。例えば、以下のようにすればOK:

def drop(iterable, n):
  it = iter(iterable)
  for _ in range(n): next(it)
  return it

引数 iterable を明示的に iterator オブジェクトに変換した後、引数 n の回数だけ next(it) して、その it をそのまま返却してしまっても動作してしまいます。
(今回は「yield 式を必ず利用すること。」という条件を付けたので、実際にこのような解答を送ってきた挑戦者様には「正解ですが不完全」とフィードバックしています)

なぜこれがOKなのかというと。
Pythoniterator (generator 含む)は、(Lv.2問題 で桜先生が解説している通り)next() でどんどん次の値を取得することは出来ても、「現在の値を取得」とか「列挙を巻き戻す」とかいったメソッド・関数は用意されておらず、途中まで列挙したらその後は for 〜 in 〜 に渡してもその続きから列挙するようになっているためです。

考察・出題者としての反省(Python編)

まず、テストケース少なすぎた(>_<)
これは出題者として大いに反省すべき点です。
ま、「全然考えなしにただテストケース通ったから提出した」っていう「全くこちらの意図・想定していなかった別解」というのは全くなかったので、それだけは救われた感じです。

しかし、全く想定していなかったわけではないけれど、こんな感じの解答を寄せていただいた挑戦者様が数名いらっしゃいました:
「iterable で列挙した要素をいくつか(5個 もしくは n個 がほとんどでした)リストに格納して、そのリストの内容(だけ)を yield で列挙する」

ただそのような解答を寄せていただいた挑戦者さまには漏れなく、「ではこういう場合はどうでしょう?」と追加のコード(テストコードの場合もあり)を示して、「これも期待通りに動いて且つもっとコンパクトに書けますよ」という旨のアドバイスをお送りしています。
ちなみに追加コード(テストコード)の例は、例えば以下:

  # 11番目から110番目までの(100個の)フィボナッチ数列を取得するテスト
  def test_fib_11_110(self):
    expected = [89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, 196418, 317811, 514229, 832040, 1346269, 2178309, 3524578, 5702887, 9227465, 14930352, 24157817, 39088169, 63245986, 102334155, 165580141, 267914296, 433494437, 701408733, 1134903170, 1836311903, 2971215073, 4807526976, 7778742049, 12586269025, 20365011074, 32951280099, 53316291173, 86267571272, 139583862445, 225851433717, 365435296162, 591286729879, 956722026041, 1548008755920, 2504730781961, 4052739537881, 6557470319842, 10610209857723, 17167680177565, 27777890035288, 44945570212853, 72723460248141, 117669030460994, 190392490709135, 308061521170129, 498454011879264, 806515533049393, 1304969544928657, 2111485077978050, 3416454622906707, 5527939700884757, 8944394323791464, 14472334024676221, 23416728348467685, 37889062373143906, 61305790721611591, 99194853094755497, 160500643816367088, 259695496911122585, 420196140727489673, 679891637638612258, 1100087778366101931, 1779979416004714189, 2880067194370816120, 4660046610375530309, 7540113804746346429, 12200160415121876738, 19740274219868223167, 31940434634990099905, 51680708854858323072, 83621143489848422977, 135301852344706746049, 218922995834555169026, 354224848179261915075, 573147844013817084101, 927372692193078999176, 1500520536206896083277, 2427893228399975082453, 3928413764606871165730, 6356306993006846248183, 10284720757613717413913, 16641027750620563662096, 26925748508234281076009, 43566776258854844738105]
    result = []
    for n in drop(self.fib):
      result.append(n)
      if len(result) == 100: break
    self.assertEqual(result, expected)

実際にはこれに加えて、例えば以下のようなヒントを与えています:
「(ヒント:リストに格納しないでどんどん yield しちゃえば良いんです!)」
これでほぼ こちらの想定していた解答 に辿り着いた方がほとんどでした。一安心でした(^-^)

あと、generator 関数内で while True: 〜 等で無限ループするのに抵抗のある方がいらっしゃるようでした。
この件に関しては、こんな感じで覚えてください。

generator 関数内に終了条件を書く必要はありません。
それを利用する側が「もうこれ以上要らない!」と思ったときに中断すれば良いのです。
例えば for 〜 in に渡したときは、そのループ内で break して中断したり。
例えば他のメソッドに渡したときは、そのメソッドの引数で調整したり(例省略)。
それだけで、あとは Python が内部で適切に処理を中断してくれるのです。

Lv.1 なのに、けっこう高度な技を要求されていた、ような気がしてきましたか?
でも、簡単な記述で実は割と高度なことができちゃう、この書き方を覚えておけば、けっこう世界が拡がると思います(^-^)

コメント紹介(抜粋)

お寄せいただいたコメントを一部紹介、およびこの場を借りて返信いたします。

(ryosy383様)
私は普段yieldを使わないので、こういうふうにに使うとパズルみたいで面白いです☆

(Surume_様)
とても楽しかったです。楽しい問題をありがとうございました!

⇒楽しみながら学んでいただけたら、と思って問題作りしたので、こちらとしてもウレシイです(^-^)
 こちらこそ、ありがとうございます!

(marsbar様)
Rubyのyieldにも似ているが、Rubyは関数の固まりというような概念ととらえていたので、pythonのyieldと若干差があるかと思いました。

⇒一言で言えば、「RubyyieldPythonyield は別物です」。
 詳しくは…この記事でも書きたかったのですが長くなるので日を改めて記事に起こしますm(_ _)m

(みの@高槻様)
unittestを初めて使用したので戸惑いました。

⇒今回、何気に unittest の使い方の練習にもなった、という方が他にもいらっしゃいました。
 こういった単機能の関数のテスト等には手軽で有効だと思います(^-^)
 どんどん活用してみてください。

(tsuji_hide様)
コメントがないのでそれぞれの関数が何をしてるのか理解に時間がかかった

⇒ちょっと不親切な部分がありました。ゴメンナサイ。

(mursts様)
ジェネレータの入門にちょうどいい問題でした。

(petit-lycee様)
余り使えてなかったgenerator。これを機会に勉強できました。ありがとうございました。

⇒まさにそのような問題を目指していたので、こちらとしても嬉しいです(^-^)
 こちらこそ、ありがとうございます!

(そらまめ様)
インデントが2桁でちょっと驚きました

(はちべー様)
pep8を守ってほしかった

⇒スミマセン。ちょっと自己流過ぎるコードになっていた部分がありました。
 そうですね、出題という観点からは、できる限り推奨されている書式で書いた方が良かったですね。
 今後に活かしたいと思いますm(_ _)m

(suppy193様)
丁寧なフィードバックありがとうございます。iterable の理解が深まりました。
簡潔なプログラムになってビックリしております。

⇒2回目の再挑戦時のコメントです。
 こちらこそ、丁寧なお返事、ありがとうございます(^-^)励みになりますo(^▽^)o

(ウッキー竹脇様)
yieldまったく知らなかったのですが、桜先生のおかげで良く解りました。

(tagomago様)
yieldを使い慣れていないので勉強になりました。

(cb250f様)
unittestやIterator型をはじめて知りました。python使用者が周りにいないため勉強になりました。

⇒o(^▽^)o