coLinux日記

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

SimplePrograms で Python を学ぶ その07

2024-04-12 23:14:12 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の、 7番目のプログラムは、表題から「データ構造の辞書型」 と 「ジェネレータ式」です。

prices = {'apple': 0.40, 'banana': 0.50}
my_purchase = {
   'apple': 1,
   'banana': 6}
grocery_bill = sum(prices[fruit] * my_purchase[fruit]
      for fruit in my_purchase)
print ('I owe the grocer $%.2f' % grocery_bill)


7番目にして Python らしい?機能が紹介されています。5行で表せそうですが7行になっています。つまり、長くなってしまった行を複数行にする方法が分かりそうです。

ここからは、

Pythonチュートリアル
https://docs.python.org/ja/3/tutorial/index.html

も、仕様を調べるのに参照したいと思います。Pythonリファレンスマニュアルは、表記法が、
https://docs.python.org/ja/3/reference/introduction.html#notation

で、規定されている「modified Backus–Naur form (BNF バッカス・ナウア記法) grammar notation」を使っているので、
今更ですが入門には向かない気がしたからです。早い話が、Python自身を作成するのに向いている表記方法ですから。

試しに、リスト型について見てみると、

「Pythonは多くの 複合 (compound) データ型を備えており、複数の値をまとめるのに使われます。最も汎用性が高いのは リスト (list) で、コンマ区切りの値 (要素) の並びを角括弧で囲んだものとして書き表されます。リストは異なる型の要素を含むこともありますが、通常は同じ型の要素のみを持ちます。」

と、より分かりやすそうです。Pythonでは、配列とは言わずに 「リスト」 と言った方が良さそうですね。

さて、Python は、データ構造の型の一つとして「辞書型」が組み込まれているそうです。bashとかで言う連想配列ですね。

「辞書は キー(key): 値(value) のペアの集合であり、キーが (辞書の中で)一意でなければならない、と考えるとよいでしょう。」

だそうです。最初の1行がそれを表しており、以下の様になるわけです。

  {キー1:値, キー2:値, ..... }

1行目は、キー「apple」に対して値「0.40」、キー「banana」に対して値「0.50」となる辞書を prices に代入し、
2から4行目は、キー「apple」に対して値「1」、キー「banana」に対して値「6」となる辞書を my_purchase へ代入することになります。1行で表せるのに3行にしているのは、辞書型の定義は長くなる可能性があるのでとても1行では収まらないので 「複数行」 で定義したくなるからでしょう。そこでプログラムのようにインデントで継続行として、続けることができるようになっているわけですね。試してみます。

>>> a = {
...      'apple': 1,
...      'banana': 2,
...      'orange': 3 }
>>>

for文やwhile文などと同じプロンプトがでてきましたが、最後の行に '}'があると終了とみなすようです。

同様に、5、6行もつながっていて継続行はインデントで指示するようです。そこで、わざと5行目を分割して、全体を8行にして実行結果をみてみましょう。

>> prices = {'apple': 0.40, 'banana': 0.50}
>>> my_purchase = {
...      'apple': 1,
...      'banana': 6}
>>> grocery_bill = sum(prices[fruit] *
...      my_purchase[fruit]
...      for fruit in my_purchase)
>>> print ('I owe the grocer $%.2f' % grocery_bill)
I owe the grocer $3.40
>>>

ちゃんと継続していますね。表示された結果から、りんごとバナナの価格と個数から合計金額を求めていることが分かります。
詳しく見ていきましょう。

まず、辞書型の各キーに対応する値はどのようにして参照できるのでしょうか。連想配列と似てるので、
>>> prices['apple']
0.4
>>> prices['banana']
0.5
>>> prices['orange']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'orange'
>>>

キーに合致すると対応する値を返し、合致しないと KeyError になるのですね。

次に sum()を調べます。

>>> b = sum(1,2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable
>>>

なので、表計算で使うようなsum関数とは違うようです。最初の引数はリストと予想して試します。また、関数の出力はそのまま表示されるのが分かったのでそうします。
もちろんこの出力は変数に代入できます。

>>> sum([1,2,3,4])
10
>>> sum([1,2,3,4],5)
15
>>> sum([1,2,3,4],5,10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: sum expected at most 2 arguments, got 3
>>> sum([1,2,3,4],[1,2,3,4])
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate list (not "int") to list
>>> sum([1,2,3,4],sum([1,2,3,4]))
20
>>>

2つめの引数は、数値のみ許すようです。複雑なのはその内出てくるでしょう。

今回のプログラムは二つの辞書から同じキーの値をかけ算して、結果をすべてたすのが目的ですが、リストの場合、

>>> [1,2]*2
[1, 2, 1, 2]
>>> [1,2]*[1,2]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't multiply sequence by non-int of type 'list'
>>>

となってしまうので、5,6行目のジェネレータ式を使うようです。用語集、
https://docs.python.org/ja/3/glossary.html
から、ジェネレータ式は、以下の様なものです。

「イテレータを返す式です。 普通の式に、ループ変数を定義する for 節、範囲、そして省略可能な if 節がつづいているように見えます。 こうして構成された式は、外側の関数に向けて値を生成します:」

sum()の中身(ジェネレータ式)をみると以下の様になっています。

prices[fruit] * my_purchase[fruit] for fruit in my_purchase

ここの for は for節 ですね。本来ジェネレータ式はカッコでくくるのが正しいのですが、

「関数の唯一の引数として渡す場合には、丸括弧を省略できます。」

だそうです。そこで、このジェネレータ式を丸括弧で囲むと、変数に代入できてこのように表示されます。

>>> c = (prices[fruit] * my_purchase[fruit] for fruit in my_purchase)
>>> print(c)
<generator object <genexpr> at 0x7691be40>
>>>

ジェネレータ式は、ジェネレータオブジェクト?が生成されるようです。前に使ったenumerate()に似ています。
このジェネレータオブジェクトをsum() の引数に与えると要素が加算されるようです。

>>> sum(c)
3.4
>>>

つまり今回の式では、
( forの後ろの変数を使った算術式 for 変数 in 辞書 )
と考えると、
算術式が prices[fruit] * my_purchase[fruit]、
変数が、 fruit、
辞書が、 my_purcharse
ですね。
このジェネレータ式の意味は、辞書の中からキーを取りだして変数に代入し算術式で使って計算する、
を辞書のキーの数だけ行って、ジェネレータオブジェクトを生成する、ということらしいです。

ここで、再び算術式がでてきました。やはり他のプログラミング言語と同様な四則演算の形式ですね。
今回は * を使った「乗算」ですが、「足し算」に代えて試してみます。辞書の代わりに配列にします。

>>> sum( i + 1 for i in [1,2,3,4])
14
>>>

これで使い方が分かりました。辞書なら、各キーが変数に代入されて算術式が実行されるわけです。

ところで、

>>> c = (prices[fruit] * my_purchase[fruit] for fruit in my_purchase)
>>> d = c
>>> sum(d)
3.4
>>> sum(d)
0
>>> sum(c)
0
>>>

このジェネレータオブジェクトですが、どうやら1回しか使えないようです。
しかも、別の変数にコピーしてもジェネレータオブジェクトはコピーが生成されず
2つの変数とも1回しか使えなくなるようです。

更に、ジェネレータ式で生成された変数(ここでは fruit )は、for文とかと異なり参照できません。
>>> print(fruit)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'fruit' is not defined
>>>

興味深いですね。
7番目にして、かなり複雑な話になりました。これでPython入門しようとしても無理があるかもしれませんが、このまま続けます。
コメント (3)
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする