coLinux日記

coLinuxはフリーソフトを種として、よろずのシステムとぞなれりける。

SimplePrograms で Python を学ぶ その18

2024-08-02 22:41:20 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の 18番目のプログラムは、 20行の素数のふるいです。

import itertools

def iter_primes():
    # an iterator of all numbers between 2 and +infinity
    numbers = itertools.count(2)

    # generate primes forever
    while True:
        # get the first number from the iterator (always a prime)
        prime = next(numbers)
        yield prime

        # this code iteratively builds up a chain of
        # filters...slightly tricky, but ponder it a bit
        numbers = filter(prime.__rmod__, numbers)

for p in iter_primes():
    if p > 1000:
        break
    print (p)


15 lines プログラムで使った itertools モジュールのイテレータ count() を使うのですね。
https://docs.python.org/ja/3/library/itertools.html
によれば、

count()、 引数:[start,[step]]、 結果:start,start+step,start+2*step,...

となる、無限イテレータ(Infinite iterators)だそうです。
つまり、itertools.count(2) は 2以上の 1づつ増加する数列を生成するのですね。
そして、この数列は、next()で1つづつ取り出せるわけです。

>>> n = itertools.count(2)
>>> p = next(n)
>>> print(p)
2
>>> p = next(n)
>>> print(p)
3
>>> p = next(n)
>>> print(p)
4
>>>

iter_primes()関数の動きを見てみます。
メインのwhile ループの中で、 yield を無視して動作を調べると、

>>> import itertools
>>> numbers = itertools.count(2)
>>> # ここから while ループ
>>> prime = next(numbers)
>>> numbers = filter(prime.__rmod__, numbers)
>>> print(prime)
2
>>> prime = next(numbers)
>>> numbers = filter(prime.__rmod__, numbers)
>>> print(prime)
3
....................
>>> prime = next(numbers)
>>> numbers = filter(prime.__rmod__, numbers)
>>> print(prime)
31
>>> prime = next(numbers)
>>> numbers = filter(prime.__rmod__, numbers)
>>> print(prime)
37
>>> prime = next(numbers)
>>> numbers = filter(prime.__rmod__, numbers)
>>> print(prime)
41
>>>
....................

このように、毎回素数が得られるわけですね。filter() は numbers からprime で割り切れるものを除いて、新しい numbers を生成するメソッドのようです。

その後、for 文を使って、iter_primes() を1回だけ呼び出して、生成された素数のイテレータオブジェクトから1つづつ取り出して(つまり next() のように動作する?) p に代入し、その素数が1000 を越えるまで print(p) で出力するのがこのプログラムです。

しかし、普通のプログラミング言語だと、このような関数を作り出すのは難しいように思います。それを Python では、 yield 文(式?)を使ってこれを実現するようです。

関数 iter_prime() は、中で yield prime つまり、 yield 文を使っているのでジェネレータ関数になります。

最初にこのジェネレータ関数を呼び出すと、
yield 式まで実行して prime の値を返して処理を「一時停止」し、
次にこの関数の一時停止を解除すると、yield の次を実行して while ループで繰り返して yield 式の処理をして、
また一時停止します、
のようになるらしいです。

つまり、
>>> import itertools
>>>
>>> # 1回目
>>> numbers = itertools.count(2)
>>>
>>> prime = next(numbers)
>>> print(prime)
2
>>>
>>> # 2回目
>>> numbers = filter(prime.__rmod__, numbers)
>>> prime = next(numbers)
>>> print(prime)
3
>>>
>>> # 3回目
>>> numbers = filter(prime.__rmod__, numbers)
>>> prime = next(numbers)
>>> print(prime)
5
>>>
===== 以下省略 ====

となるので、ここで生成される関数iter_primes()は、素数の無限イテレータ・オブジェクトを生成し、 for 文の in で指定され最初に1回だけこの関数が実行され、生成されたジェネレータオブジェクトから次々と素数が p に代入されるみたいです。

これは for文の復習として、以下のリファレンスを参考に説明してみました。

------------------------
for 文は、シーケンス (文字列、タプルまたはリスト) や、その他の反復可能なオブジェクト (iterable object) 内の要素に渡って反復処理を行うために使われます:

for_stmt ::= "for" target_list "in" starred_list ":" suite
            ["else" ":" suite]

starred_list 式は一度だけ評価され、評価結果として イテラブル オブジェクトを返す必要があります。そのイテラブルから イテレータ が作られます。

------------------------

つまり、
>>> a = iter_primes() # 一回しか関数を呼び出さないことを強調します。
>>> a
<generator object iter_primes at 0xf7027db8>
>>> for p in a:
...       if p > 6:
...          break
...       print(p)
...
2
3
5
>>>

です。 この動きはとても興味深いです。

ちょっと理解が怪しくなっているように感じているので、次回以降に yield や filter() を調べて見たいと思います。

コメント (3)    この記事についてブログを書く
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする
« Raspberry Pi に最新の Pytho... | トップ | SimplePrograms で Python を... »
最新の画像もっと見る

3 コメント

コメント日が  古い順  |   新しい順
手前味噌ですが。 (cametan_42)
2024-08-02 22:49:14
手前味噌ですが、これら記事も参考にしてみてください。

Pythonによる素数列の現代的実装法:
https://blog.goo.ne.jp/cametan_42/e/043fa475cd72e087db804f9ea6b181cf

Pythonで学ぶ斜方投射:
https://blog.goo.ne.jp/cametan_42/e/57084fbeb4556fda4039d084b248a5de
返信する
yield (espiya)
2024-08-06 07:46:14
cametan_42様、コメントありがとうございます。
早速拝見し、yield の奥深さが分かりました。

関数で yield を使うと、生成されたオブジェクトの中にその関数の機能を部品みたいに取り込んで値を生成していくようなイメージを感じました。

最初、その関数を何度も呼び出して値を得るのかと勘違いしていましたが、関数呼び出しは1度だけで生成されたオブジェクトから部品を使って値を得ていくような感じらしいので驚きました。
返信する
その解釈でいいと思います。 (cametan_42)
2024-08-08 23:16:26
> 関数で yield を使うと、生成されたオブジェクトの中にその関数の機能を部品みたいに取り込んで値を生成していくようなイメージを感じました。

> 最初、その関数を何度も呼び出して値を得るのかと勘違いしていましたが、関数呼び出しは1度だけで生成されたオブジェクトから部品を使って値を得ていくような感じらしいので驚きました。

はい、Pythonではそのように解釈して構わないと思います。
厳密には、いわゆる「遅延評価」と違って、Pythonの場合は「値を消費する」ってのが特徴です。
これも手前味噌ですが、この記事を紹介しておきます。

PythonのmapとLisp他関数型言語のmap:
https://blog.goo.ne.jp/cametan_42/e/5753b0be11bd051094bb07bbfbed6dd8

Pythonのイテレータは上手く使えば「無限長」を扱える強力な機能ですが、本物の「遅延評価」と違ってデータが永続的ではない、と言う特徴があります。つまりイテレータは「更新される」。
よって変数に代入とかして「一回でも値を取り出せば」元のデータには戻りません。その辺が本物の遅延評価と違うトコです。
イテレータを何かに代入して使いまわししたい場合は、無引数ラムダ式を使ってサンクを作るのが手です。

## 無引数ラムダ式内にイテレータを置く
var = lambda : イテレータ

そうすれば、

for i in var():
 実行

みたいにすれば「一つづつ値を取り出せる」けど、大元のvarはlambdaで保護されてるので、イテレータが「消費」される事がありません。
この辺がPythonのイテレータを再利用したい際のピットフォールなんで、Tipsとして提示しておきます。
返信する

コメントを投稿

Python」カテゴリの最新記事