coLinux日記

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

SimplePrograms で Python を学ぶ その13

2024-05-24 22:29:16 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の 13番目のプログラムは、 unittest を使った ユニットテスト です。

import unittest
def median(pool):
    copy = sorted(pool)
    size = len(copy)
    if size % 2 == 1:
       return copy[int((size - 1) / 2)]
    else:
       return (copy[int(size/2 - 1)] + copy[int(size/2)]) / 2
class TestMedian(unittest.TestCase):
    def testMedian(self):
       self.assertEqual(median([2, 9, 9, 7, 9, 2, 4, 5, 8]), 7)
if __name__ == '__main__':
    unittest.main()


13番目に出てくるということは、unittest って、 Python では有用なモジュールみたいですね。
しかも利用するためにクラスを使っているので、12番目がクラスだったのですね。

プログラムの見た目だけでは動きの想像が付かないので、例によってファイルにして実行してみます。

$ ./prog-013.py
.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
$

マニュアル、
https://docs.python.org/ja/3/library/unittest.html#module-unittest
を参考に unittest を調べましょう。最も参考になるのは、
https://docs.python.org/ja/3/library/unittest.html#basic-example
の基本的な例の部分ですね。これと、13番目のプログラムを対比すると、関数 median()
の定義の後のクラス TestMedian の定義は、object の代わりに unittest.TestCase を指定してるので、
「テストケースは、 unittest.TestCase のサブクラスとして作成します。メソッド名が test で始まる三つのメソッドがテストです。」
から、

テストケース TestMedian は、unittest.TestCase のサブクラスとして作成され、
テストランナーは、TestMedian の メソッドで test で始まる、 メソッド testMedian を検索してテストするようです。

「これらのテスト内では、予定の結果が得られていることを確かめるために assertEqual() を、条件のチェックに assertTrue() や assertFalse() を、例外が発生する事を確認するために assertRaises() をそれぞれ呼び出しています。」
なので、
assertEqual() の、第一引数 media関数の実行結果が、第二引数となっているかどうかを確かめるテストを実施するようです。

「最後のブロックは簡単なテストの実行方法を示しています。 unittest.main() は、テストスクリプトのコマンドライン用インターフェースを提供します。コマンドラインから起動された場合、上記のスクリプトは以下のような結果を出力します」
なので、以下のテストの実施方法は丸ごと覚えるのが正解のようです。

if __name__ == '__main__':
    unittest.main()

それで、unittest によって、プログラムの実行結果のような結果が表示されるのですね。

ちなみに定義したmedian()関数は、

>>> a = len([1, 2, 3, 4, 5])
>>> a
5
>>> a % 2
1
>>> a % 2 == 1
True
>>> b = len([1, 2, 3, 4])
>>> b
4
>>> b % 2
0
>>> b % 2 == 1
False
>>>

なので、関数 median() に渡される引数として数字要素のリストを渡すと、
それを sorted() でソートして、len() でリストの要素数を求めて、
条件式 size % 2 == 1 ( % はあまりを求める算術演算子で、そのあまりが 1 に等しいどうかを調べる)から、
要素数が奇数の場合は真ん中の要素を return で返し、
要素数が偶数の場合は真ん中に近い2つの要素を2 で割った平均を return で返す、
関数であることが分かります。

unittest で、 testMedian というテストを作成し、median([2, 9, 9, 7, 9, 2, 4, 5, 8])
が正しく 7 を返すかどうかをテストするのですね。

こんな単純なテストでは分かりませんが、かなり複雑な関数を作成し、色々な引数での返却値が正しいかどうかのテストを
unittest で実施しておいて、
その後、関数に機能を追加したり、バグがあったりした場合、その結果を更に unittest のクラスのメソッドに追加していけば、
作成した unittest プログラムを実行するだけで修正が正しい事を保証できるレポートが手に入るので、
unittest の利用価値は高そうです。

Python を学んでいる者としては、Python でどのくらい unittest が利用されているか興味深いです。
コメント (2)
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

SimplePrograms で Python を学ぶ その12

2024-05-16 20:54:00 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の 12番目のプログラムは、クラス です。

class BankAccount(object):
   def __init__(self, initial_balance=0):
      self.balance = initial_balance
   def deposit(self, amount):
      self.balance += amount
   def withdraw(self, amount):
      self.balance -= amount
   def overdrawn(self):
      return self.balance < 0
my_account = BankAccount(15)
my_account.withdraw(50)
print (my_account.balance, my_account.overdrawn())


Python のクラス定義ですね。
https://docs.python.org/ja/3/reference/compound_stmts.html#class-definitions
クラス定義は、複合文「複合文には、他の文 (のグループ) が入ります」の中で説明されているので、インデントによって中身を記述するわけです。説明によれば、

class classname(argment_list):

なので、 (argment_list) が継承リストで
「継承リストは通常、基底クラスリストを与えます」
だそうです。プログラムでは classname が 「BankAccount」 で、argment_list が 「object」 ですね。
「継承リストのないクラスは、デフォルトで、基底クラス object を継承する」
なので、(object) は省略できるようです。

さらに、クラス定義の中身の先頭にある __init__() は、
https://docs.python.org/ja/3/reference/datamodel.html#object.__init__
によると、

object.__init__(self[....])

なので、このクラスのインスタンス(クラスから作られたオブジェクト)を作るときに初期化するものを指定するための基底クラス object のメソッドですね。
この部分は丸ごとまねするのが良さそうです。

プログラムの見た目から、Python では、クラス定義内の self の部分を生成したインスタンスの名前に置き換えて使うようです。 

クラスを使うときにはオブジェクト指向を理解しておくのが前提なので、
オブジェクト指向超入門
http://www.rsch.tuis.ac.jp/~ohmi/software-intro/objectoriented.html
あたりで復習するとして、ここでは、クラスのインスタンスを生成すると、クラスで定義されているデータとメソッドが使えるようになるので、データとメソッドという用語を使います。(ということは、SimplePrograms が示すように、Pythonを学ぶ場合、同時にオブジェクト指向を理解する必要があるということらしいです。import でそれを感じましたが常識なんですね。)

ここで言っているデータは、Python では、
「インスタンス属性は、メソッドの中で self.name = value とすることで設定できます」
なので、「インスタンス属性(instance attribute)」と言うみたいです。

まず、クラスのデータから確かめて見ましょう。

>>> class Atest(object):
...      def __init__(self,initial_adata=0):
...         self.adata = initial_adata
...
>>>

クラス Atest を定義して、__init__ でデータを設定するのがねらいです。
このクラスのインスタンスを生成するには他の言語で一般的な new とか使わずに Python では classname そのもの(「( )」は必要?)
を変数に代入すれば良いみたいです。

>>> obj_01 = Atest() # クラス Atest のインスタンスを生成
>>> obj_01.adata
0

引数を指定しないと生成されたインスタンス obj_01 の データ adata が 0 となりますね。
つまり、__init__() の initial_adata=0 は、デフォルトで、0 を initial_adata に代入するということです。

>>> obj_02 = Atest(100)
>>> obj_02.adata
100
>>>

引数 100 を指定してインスタンス obj_02 を生成すると、そのデータ adata が 100 になりました。
つまり、__init__() の initial_adata に 指定した引数がデフォルト値の代わりに代入されて使用されるわけです。 ちなみに存在しないデータに対しては、
>>> obj_02.bdata
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Atest' object has no attribute 'bdata'. Did you mean: 'adata'?
>>>
「bdata なんて無いよ、adata じゃないの」と言ってくれるんですね。

また、インスタンスだけなら、
>>> obj_02
<__main__.Atest object at 0xf788eb10>
>>>
なので、「クラスAtest の object」と理解されているようです。

ついでに、2つの引数を指定してインスタンスを生成してみます。

>>> class Btest(object):
...      def __init__(self,initial_adata=0,initial_bdata=0):
...         self.adata = initial_adata
...         self.bdata = initial_bdata
...         self.tdata = self.adata + self.bdata
...
>>> obj_03 = Btest(10,100)
>>> obj_03.adata
10
>>> obj_03.bdata
100
>>> obj_03.tdata
110
>>>

複数引数も大丈夫ですね。

続いて、メソッドの定義を見てみましょう。プログラムをまねしてメソッドを定義してみます。
Plus というメソッドを作って、そのメソッドに引数を渡してみます。
対話型の Pyton は、矢印キーで入力履歴が再生できるので積極的に利用するとチェックも簡単です。

>>> class Ctest(object):
...      def __init__(self,initial_adata=0,initial_bdata=0):
...         self.adata = initial_adata
...         self.bdata = initial_bdata
...      def Plus(self, amount):
...         self.pdata1 = self.adata + self.bdata
...         self.pdata2 = self.pdata1 + amount
...
>>> obj_04 = Ctest(10,100)
>>> obj_04.pdata1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Ctest' object has no attribute 'pdata1'. Did you mean: 'adata'?
>>> obj_04.Plus(10000)
>>> obj_04.pdata1
110
>>> obj_04.pdata2
10110
>>>

インスタンス obj_04 を作成し、最初のエラーは、まだ Plus メソッドを実行していなかったので、データ pdata1 が作成されていなかったからですね。
obj_04.Plus(10000)
で、引数に 10000 を与えて Plus を実行すると、pdata1 と pdata2 が生成されて、
obj_04 のデータとして参照できるようになることが分かりました。メソッドの引数は、
def Plus(self, amount): の amount に代入されてその中で使えるようになるのですね。
インスタンスのデータは、そのメソッド等で値が代わったりすると、その後それが参照できるわけです。

クラスの簡単なデータ生成はこれの応用でできそうなので、この形式は丸ごと覚えた方がよさそうです。

いままで試してみたことで、このプログラムが理解できるようになりました。
BankAccount クラスのインスタンス my_account を生成するときに引数 15 を与えたので、
my_account のデータ balance は 15 なります。
my_account のメソッド withdraw に引数 50 を与えて実行したので、
そのデータ balance は -35 (15 - 50 だから) になります。
最後に print 文で、そのデータ balance と メソッド overdrawn() の実行結果が表示されるのですね。
ということは、メソッド定義内で、
reture 戻り値
を使うと メソッドの戻り値とできるわけです。
overdrawn() の戻り値は、条件式で、 balance が 負なら True 、そうでなければ False? ですね。
確認のため、プログラムに最後の3行を複製して修正し、条件式が偽になるものを追加して実行してみます。

$ ./prog-012-02.py
-35 True
10 False
$

(10 False は追加した3行で出力されました。) やはり条件が正しくないと、 False でした。

おまけで、チュートリアルのクラスの説明の中で、クラスのデータがリストの場合があったので追記しておきます。

class Dog:

   def __init__(self, name):
      self.name = name
      self.tricks = [] # creates a new empty list for each dog

   def add_trick(self, trick):
      self.tricks.append(trick)

__init__() でリストを生成しておかないと、複数のインスタンスで共通のリストになっちゃうようです。注意したいところですね。
コメント
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

SimplePrograms で Python を学ぶ その11

2024-05-10 09:11:05 | Python
SimplePrograms - Python Wiki
https://wiki.python.org/moin/SimplePrograms

の 11番目のプログラムは、三重クオートの文字列 と whileループ です。

REFRAIN = '''
%d bottles of beer on the wall,
%d bottles of beer,
take one down, pass it around,
%d bottles of beer on the wall!
'''
bottles_of_beer = 9
while bottles_of_beer > 1:
  print (REFRAIN % (bottles_of_beer, bottles_of_beer,
    bottles_of_beer - 1))
  bottles_of_beer -= 1


つまり、 「'''文字列'''」 のことですね。
https://docs.python.org/ja/3/reference/lexical_analysis.html#literals
によると、Python では、

「リテラル (literal) とは、いくつかの組み込み型の定数を表記したものです。」

であり、その中の文字列リテラルの字句定義で、

「longstring ::= "'''" longstringitem* "'''" | '"""' longstringitem* '"""' 」

の、longstring に該当するものですね。 """ もあるようです。次の説明があります。

「三重クオートリテラル中には、三連のエスケープされないクオート文字でリテラルを終端してしまわないかぎり、エスケープされていない改行やクオートを書くことができます (さらに、それらはそのまま文字列中に残ります)。(ここでいう "クオート" とは、文字列の囲みを開始するときに使った文字を示し、' か " のいずれかです。)」

改行もそのまま文字列にできるので、print()での長い文章等の表示に使えるのですね。試してみましょう。

>>> a = '''one
... two
... three
... go'''
>>> print(a)
one
two
three
go
>>>

こうやって見た目通りに書けるので、プログラムが見やすくなるわけです。ところで、

>>> a
'one\ntwo\nthree\ngo'
>>>

普通の文字列ですね。改行は改行文字(\n)として入っています。同じ文字列を b に代入します。

>>> b = 'one\ntwo\nthree\ngo'
>>> b
'one\ntwo\nthree\ngo'
>>>

print()で表示してみると、三重クォートの文字列とまったく同じです。

>>> print(b)
one
two
three
go
>>>

さて、while ループの部分を簡単にするとこんな感じです。

>>> a = 3
>>> while a > 1:
...       print('Number %d' % a )
...       a -= 1
...
Number 3
Number 2
>>>

while ループは、while 横の条件を満たす間はループするという、一般的な使い方ですね。
-= は、aの値を一つ減らす一般的な形式で、
a = a - 1
と等価でしょう。これが無いとループを終了できないわけです。
while ループも forと一緒で、break や else が使えるのが python らしいところですね。

このプログラムも 例によって prog-011.py ファイルとして実行してみました。
$ ./prog-011.py

9 bottles of beer on the wall,
9 bottles of beer,
take one down, pass it around,
8 bottles of beer on the wall!


8 bottles of beer on the wall,
8 bottles of beer,
take one down, pass it around,
7 bottles of beer on the wall!


7 bottles of beer on the wall,
・・・・・・・・・・・・・・・・省略・・・・・・・・・・・・・・・・・

2 bottles of beer on the wall,
2 bottles of beer,
take one down, pass it around,
1 bottles of beer on the wall!



''' の最初の改行と、最後の改行も含んで、最初と最後に空行、結果的に途中2行空行になっているわけです。
ちなみに、whileループ内の print() の途中は、インデントによって継続行と見なされていますね。

今回出てきた三重クオートの文字列は改行文字(\n)を見た目通りに設定できるので便利そうですが、タブ文字(\t)はどうでしょうか。
確かめるプログラムはこんな感じです。
$ cat test-011.py
a = 'abc\ndef123456\nghi\t\tjkl'
print(a)

b = '''abc
def123456
ghi          jkl'''
print(b)

$
( b に代入する三重クオートの途中の空白はタブ文字2つです。)  

タブ間隔を3文字にするには、tabs コマンドを使い、その後にプログラムを実行してみます。
$ tabs -3
$
$ python3 test-011.py
abc
def123456
ghi           jkl
abc
def123456
ghi           jkl
$

タブ文字も問題なく使用できました。(このブログで表示は乱れることがあります。)
コメント
  • X
  • Facebookでシェアする
  • はてなブックマークに追加する
  • LINEでシェアする

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でシェアする