coLinux日記

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

SimplePrograms で Python を学ぶ その10

2024-05-02 23:32:20 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の 10番目のプログラムは、Time, 条件, from..import, for..else です。

from time import localtime

activities = {8: 'Sleeping',
              9: 'Commuting',
              17: 'Working',
              18: 'Commuting',
              20: 'Eating',
              22: 'Resting' }

time_now = localtime()
hour = time_now.tm_hour

for activity_time in sorted(activities.keys()):
   if hour < activity_time:
      print (activities[activity_time])
      break
else:
   print ('Unknown, AFK or sleeping!')


これも、10行ではないですが、activities という辞書を定義するときに、
インデントして継続行になっているところを1行と数えると10行です。

もう長くなったので、
丸ごとファイルに入れて、先頭行に #!/usr/bin/python3 を付加して、chmod u+x で実行可能にします。
そのファイル名を、prog-010.py とすると実行結果はこうなります。
先にdate コマンドを使ったのは、時間を扱うプログラム?だから現在の時間を知るためです。

$ date
2024年 4月 28日 日曜日 07:11:05 JST
$ ./prog-010.py
Sleeping
$

実行時刻を替えてもう一つ実行結果を記録しておきます。

$ date
2024年 4月 29日 月曜日 22:20:34 JST
$ ./prog-010.py
Unknown, AFK or sleeping!
$

さっそくプログラムの中身を見てみましょう。

まず from ですが、
https://docs.python.org/ja/3/reference/simple_stmts.html#import
で、import文の一つの形式として説明されています。

「from 節で指定されたモジュールを見付け出し、必要であればロードし初期化する;
import 節で指定されたそれぞれの識別子に対し以下の処理を行う:」


要するに、

from foo(モジュール名とします) import attr(識別子とします。)

によって、 foo.attr を省略して attr として表せるという指定ですね。

from節でロードするモジュール time は「時刻に関するさまざまな関数を提供」するものですね。
https://docs.python.org/ja/3/library/time.html?highlight=time

これによって、time.localtime() を省略して localtime() として使えて、
「エポックからの経過時間で表現された時刻を、」「ローカル時間に変換します。」
なので、現在の時刻が得られるようです。早速試して見ましょう。

>>> from time import localtime
>>> c = localtime()
>>> c # localtime()の出力(戻り値)
time.struct_time(tm_year=2024, tm_mon=5, tm_mday=2, tm_hour=23, tm_min=42, tm_sec=41, tm_wday=3, tm_yday=123, tm_isdst=0)
>>>
>>> c.tm_hour   # c から tm_hour の値を得る
23
>>> c.tm_year   # c から tm_year の値を得る
2024
>>> c.tm_year + 3  # c.tm_year の値は 文字列ではなくて数値
2027
>>> localtime().tm_hour  # 8時になって localtime() を使うと 8 になっている。
8
>>>

というわけで、プログラムの hour は今の時刻を数値で表したものです。
辞書 activities の キーはこの時刻を表しているのですね。キーが24時間表記なので hour も0 ~ 23 ですね。
ここで、辞書に対する keys() メソッドを調べてみましょう。

>>> a = {5: 'test5', 2: 'test2', 9: 'test9' }  # 辞書 a を定義
>>> a          # a を表示する。
{9: 'test9', 2: 'test2', 5: 'test5'}
>>> a.keys()        # a.keys() を表示する。
dict_keys([9, 2, 5])
>>> sorted(a.keys())    # これをソートする。
[2, 5, 9]
>>>

dict_keys() を sorted()の引数として渡すと、キーのソートされた配列が得られることが分かりました。

つまり、for文で activity_time にソートされたキーから生成された配列から一つづつキーを代入して、ループするのですね。

10番目のプログラムで再び条件式がでてきました。といっても、他のプログラムとほぼ同じだと思うので、
if 文の中の条件式は、hour(現在の時刻) が activity_time より小さかったら 真 となって、
下にインデントされた部分を実行するのですね。
真なら print()で辞書からそのキーに対応する値を表示したあと、break がでてきました。
https://docs.python.org/ja/3/reference/simple_stmts.html#the-break-statement

「break 文は、文を囲う最も内側のループを終了させ、ループにオプションの else 節がある場合にはそれをスキップします。」

なので、
for ループを終了して、その activity_time は保持されるわけですね。

さて、for ループの後ろに インデントのない else: が出てくるのは何でしょうか。
それが、プログラムの説明にある for..else だと思います。
https://docs.python.org/ja/3/reference/compound_stmts.html#the-for-statement
の定義では、
for target_list in expression_list : suite [ else : suite ]
となっているので、
for ループには else: 節を付けることができて、今までは省略されたいたということが分かりました。Python 以外はあまりでてこない?形式です。

「その後、スイートが実行されます。 全ての要素を使い切ったとき (シーケンスが空であったり、イテレータが StopIteration 例外を送出したときは、即座に)、 else 節があればそれが実行され、ループは終了します。」
「最初のスイートの中で break 文が実行されると、 else 節のスイートを実行することなくループを終了します。 」


なので、break 文を使っているので else節をスキップしてループを終了するのですね。

条件を満たさず、すべての要素を使い切ると for ループが終了して、最後にelse節が実行されるわけです。
試してみます。

>>> a
{9: 'test9', 2: 'test2', 5: 'test5'}
>>> for i in sorted(a.keys()):
...       if i > 4 :
...         print(a[i])
... else:
...       print('Finish!')
...
test5
test9
Finish!
>>>
for文の中にbreke文がないのですべてのキーを調べたあとに else: 節を実行しています。

>>> for i in sorted(a.keys()):
...       if 6 < i:
...         print(a[i])
...         break
... else:
...       print('Finish!')
...
test9
>>>
サンプルプログラムのように break文を入れると else: 節はスキップしているのが分かります。

>>> for i in sorted(a.keys()):
...       if 100 < i:
...         print(a[i])
...         break
... else:
...       print('Finish!')
...
Finish!
>>>

該当するキーがないと、ループの中はなにもせずに else: 節を実行するのがわかります。
通常のプログラミング言語では else:節がないので、for ループで break してもfor ループの次の行を実行します。

Python では、 for文の else:節は break と併用すると便利と覚えておきます。

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

3 コメント

コメント日が  古い順  |   新しい順
breakはなかなか悩ましい (cametan_42)
2024-05-03 15:18:51
僕とか関数型言語界隈のモノだとそのお題は多分こう書きますね。

#!/usr/bin/env python3

from time import localtime

activities = {8: 'Sleeping',
      9: 'Commuting',
      17: 'Working',
      18: 'Commuting',
      20: 'Eating',
      22: 'Resting' }

time_now = localtime()
hour = time_now.tm_hour

if __name__ == '__main__':
 res = [activities[activity_time]\
    for activity_time in sorted(activities.keys())\
    if hour < activity_time]
 print('Unknown, AFK or sleeping!' if res == [] else res[0])

やっぱリスト内包表記の出番で、かつ、条件に合わないブツをフィルタリングしちゃう。
そうすれば得られたリストの第0要素が求めるモノとなるんで、そいつを印字、にするんじゃないかな。
ただし、リスト内包表記なんかは「必ずリストを最後まで走査する」わけで、元々のコードの「条件に見合ったブツが見つかった時点で計算から脱出する」、つまり後続の計算をスキップする辺りがより効率的だろう、と言うのはその通り、だとは思います。リスト内包表記だと「計算を途中で中断する」トリックが仕込みづらい。
やっぱこの辺は悩みドコロにはなりますね。
返信する
goto (espiya)
2024-05-04 10:20:28
cametan_42様、コメントありがとうございます。

コメントから、「必ずリストを最後まで走査する」ではなくて、途中で break すれば効率的なので”必要”という狙いが for ~ else にあると思いました。

ちょっと検索したら、Pythonプログラムを高速化するための Cython だと、この break を goto に変換しているみたいです。
Python は 「goto」 が無い!いや、拒否する!その代わりこれね、と言うのがこの仕様の示す折衷案でしょうか。興味深いです。

もっとも、「必ずリストを最後まで走査する」ことは、CUDA等が一般的になってきているのでハードによってはむしろ効率的となっている?みたいなので、当方で扱っているRaspberry Pi のような低速なハードの時は、依然として有用な仕様と捉えるべきかもしれませんね。
返信する
ジェネレータ式も煩わしい (cametan_42)
2024-05-04 17:11:28
> break すれば効率的なので”必要”という狙いが for ~ else にあると思いました。

うん、僕もそう思います。

> Python は 「goto」 が無い!いや、拒否する!その代わりこれね、と言うのがこの仕様の示す折衷案でしょうか。

と言うか、基本的に例えばC言語なんかでも、gotoあっても使わせない為に(笑)、breakなんかが入ってる、ってのはありますからね。
breakは代表的な構文糖衣(Syntactic Sugar)だとは思います。

> 依然として有用な仕様と捉えるべきかもしれませんね。

そうかもしれません。

ところで、別解として、いつぞやのジェネレータ式を使う、ってのも考えられるんですが・・・。

#!/usr/bin/env python3

from time import localtime

activities = {8: 'Sleepint',
      9: 'Commuting',
      17: 'Working',
      18: 'Commuting',
      20: 'Eating',
      22: 'Rasting'}

time_now = localtime()
hour = time_now.tm_hour

if __name__ == '__main__':
 res = lambda : (activities[activity_time] for activity_time\
        in sorted(activities.keys()) if hour < activity_time)
 print(next(res()) if any(res()) else 'Unknown, AFK or sleeping!')

ジェネレータ式はそのままだと「未評価」なんで、anyで「真」の条件を発見した次第で以降の計算をスキップします。
だから題意的には「計算をスキップする」ので意図は意図通り動くんですが・・・。
一方、ジェネレータには以前見た通り、「要素を消費する」性質がある。anyで「真」を発見した時点でその「真」の要素を消費しちまうんですね。
結果、上のコードだと、仮に変数resでラムダ式を被してサンクにしなかった場合、例えば(18で)'Commuting'が真、って発見された時点でnext(res)は必ず'Eating'になってしまうと言う・・・・・・。
結果、やっぱりサンクにしておいて、ジェネレータ式が返す「一番最初の要素」を得る時、nextで同じ値を得る為に再計算せなアカン、と言う非常に美味しくない状況になります(笑)。
やっぱ悩ましいです、この問題は(笑)。考え方が「その通り」だとしてもどうもスッキリしません(笑)。コードと言うか「考え方」がコンピュータサイエンス寄りになって複雑化してきます。
返信する

コメントを投稿

Python」カテゴリの最新記事