Python (パイソン) であそぼう 7
〜 プログラムを使って会話 〜
2022-12-12 作成 福島
TOP > asob > python-talk
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob ]

情報交換にはルールが必要

最近のスマートフォンや PC にはネットワークが必要で、これに繋がっていないと何もできないと言っても過言ではないでしょう。
何か作業をするとき、ひとりでそのすべてを処理することは稀で、ともすると「ぼっち」と揶揄されてしまいます。
これを解決する道具がコンピュータネットワークです。

例えば、誰かが文章を考え、それを違う誰かが校正したり、感想を述べたりして分業、協調します。
昔は紙に印刷したり、フロッピーディスクを渡したりしましたが、21 世紀の今はコンピュータネットワークを使って、
効率的かつ迅速に情報をやり取りします。

ひとりで作業をする場合は気にしなくて良いのですが、何人か集まるとルールが必要になります。
みんなが自分勝手に行動していたのでは、物事がうまく進まず混沌としてしまいます。
特にコンピュータネットワークは動作速度が速いので、あっという間に情報が混乱するのです。

ルールは多くありません。多くするとルールが守れなくなります。具体的には以下の 2 つです。
  1. 誰かの発言が相手に届くまで、他の人は発言しない。
  2. 自分が発言するときは相手を指名する。
他に発言中の人がいるのに自分がかぶせて発言するのは下品なだけでなく、情報の伝達が崩れてしまいます。
相手を指名しないで発言するのは、受容を他人に忖度させることに他ならず、これは即ち傲慢な行動です。
他人を困らせる人が会話に参加してはいけません。

本稿のアルゴリズムは、バス共有型に代表される非同期半二重通信方式で、これを機能制限して*1実装しています。
*1機能制限: データが自動的に消滅するように作ると、学習目的を大きく逸脱してしまうため。

参加者全員がひとつの場所を共有してメッセージをやり取りします。
周波数 (チャンネル) がひとつだけの無線機と考えると良いかもしれません。
(無線機と異なり、置かれたメッセージが自動的に消滅することはないので、自分宛てのメッセージも出せます)

便宜上この場所を「伝言台」と呼称します。
伝言台ではデータを頻繁に書き換えるので SSD のシステムの場合は RAM ドライブを利用するようにしてください。
Linux は tmpfs、Windows は RAM ディスクが RAM ドライブです。
ハードディスクなら特に気にする必要はありません。


0. 事前準備

0-1. この章では、python を使います。
インストールがまだなら、ここ (Windows10) を参考にしてインストールしておいてください。
Rocky なら既に OS と一緒に入っています。
0-2. この章では、テキストエディタを使います。
Linux なら付属の「Vim」や「gedit」で十分です。
(Vim の初心者向け使い方はここにあります)

Windows なら「メモ帳」でも構いませんが、本格的なテキストエディタをお勧めします。
(強力なアンドゥや、キーマクロが使えるようになります)
インストールがまだなら、ここ (秀丸エディタ) を参考にしてインストールしておいてください。
また、プログラムを起動するには拡張子が重要となるので、エクスプローラーで拡張子が表示されるようにしておいてください。
0-3. 未経験者向けの情報を省いています。
プログラミング未経験の方は、予めこちら (Python であそぼう 1) を学習しておくことを推奨します。
0-4. ひとつの PC で Python を複数起動します。
狭い画面でも実行できますが、一度に複数のプログラムを起動するので、見易くするためには広い画面を用意してください。


1. 自分と会話

1-1. ユーザプログラムの作成
<テキスト20> ファイル名「user1.py」
import time
import comm

comm.Init('user1', './cardstand/')     # 自分の識別名*2と伝言台*3を指定。
comm.Quiet()

while True:
    # メッセージ送信内容の入力。
    # 送りたいメッセージがなければ受信だけを行う。
    to = input('誰へ?')
    if to == '.':   # プログラムを終了するときは「.」を入力する。
        print('終了します。')
        break
    if to != '':
        msg = input('内容?')
    else:
        msg = ''

    result, msgFrom, msgBody = comm.Talk(to, msg)
    if   result == -1:  # 発言なし。
        print('問題なし。')
    elif result ==  0:
        print('発言失敗。')
    elif result ==  1:
        print('発言成功。')
    elif result ==  2:  # 発言の前にメッセージが到着していた。
        print('メッセージ受信。From:' + msgFrom + ' ' + msgBody)
        if to != '':
            print('発言は失敗。')
*2自分の識別名はメッセージを送受信するときに使われます。プログラムを複数起動するときは、他と重ならない名前に変更してください。
*3伝言台にすべてのファイル (ロックファイル, from ファイル, to ファイル, メッセージファイル) を置きます。

<テキスト21> ファイル名「comm.py」(シー・オー・エム・エム・ドット・ピー・ワイ)
·「user1.py」と同じフォルダーに置くこと。
import exLock   # 排他ロッククラス。交通整理にはこれが必須。*4
import os, time

myName = ''
talkRetry = 0
talkEnv = ['','','','']


# 環境条件を保持する。 def Init(mName, baseDir): global myName global talkRetry global talkEnv myName = mName # 自分の識別名 talkRetry = 5 # リトライ回数 # 作業用ディレクトリを作成する。 try: os.mkdir(baseDir) except: pass talkEnv[0] = baseDir + '/speak.LCK' # 発言ロック用ディレクトリ talkEnv[1] = baseDir + '/from.txt' # 発言者名用ファイル talkEnv[2] = baseDir + '/to.txt' # 宛名用ファイル talkEnv[3] = baseDir + '/message.txt' # 本文用ファイル
# メッセージ置き場をクリアにする。 def Quiet(): WipeMessage(talkEnv, talkRetry)
# 可能ならメッセージを書き込む。 # 自分宛てのメッセージがある場合は読み込む。 # # メッセージの送受信状況を返す。 # -1: 自分宛てのメッセージが無かった / メッセージを取得できなかった # 0: メッセージを発言できなかった # 1: メッセージを発言できた # 2: 自分宛てのメッセージが到着していた (自分の発言は取り消し) def Talk(talkTo, msg): result = -1 # 問いかけを取り出す。 fromUser, toUser, message = ReceiveMessage(talkEnv, talkRetry) if toUser != '': # 誰かの発言がある。 # 自分宛てのメッセージなら受領する。 if toUser == myName: # メッセージを受領したら、メッセージ用ファイルを削除する。 WipeMessage(talkEnv, talkRetry) talkFrom = fromUser msg = message result = 2 else: # 他人へのメッセージなので、自分の発言は控える。 if talkTo != '': result = 0 if result == -1: # 発言できそう。 if talkTo != '': # 発言したいことがある。 # 発言する。 result = 0 stat = SendMessage(talkEnv, myName, talkTo, msg, talkRetry) if stat == True: result = 1 return result, fromUser, msg
# メッセージを送信し、その結果を返す。 # False: 送信失敗 # True: 送信成功 def SendMessage(TalkEnv, fromUser, toUser, msg, count): lockDir = TalkEnv[0] fromFilePath = TalkEnv[1] toFilePath = TalkEnv[2] messagePath = TalkEnv[3] for i in range(1 + count): lock = exLock.exLock(lockDir) lockResult = lock.lock() if lockResult == True: break time.sleep(0.2) # 0.2 秒待つ。 said = False if lockResult == True: # 自分を名乗る。 # 符号は UTF-8 を明示する。 with open(fromFilePath, 'w', encoding='utf-8') as f: f.write(fromUser) # 相手を指名する。 with open(toFilePath, 'w', encoding='utf-8') as f: f.write(toUser) # 本文を設置する。 with open(messagePath, 'w', encoding='utf-8') as f: f.write(msg) said = True lock.unlock() return said
# メッセージを受信する。 def ReceiveMessage(TalkEnv, count): lockDir = TalkEnv[0] fromFilePath = TalkEnv[1] toFilePath = TalkEnv[2] messagePath = TalkEnv[3] fromUser = '' toUser = '' msg = '' for i in range(1 + count): lock = exLock.exLock(lockDir) lockResult = lock.lock() if lockResult == True: break time.sleep(0.2) # 0.2 秒待つ。 if lockResult == True: try: # メッセージを読み込む。 # 符号は UTF-8 を明示する。 with open(fromFilePath, 'r', encoding='utf-8') as f: fromUser = f.read() with open(toFilePath, 'r', encoding='utf-8') as f: toUser = f.read() with open(messagePath, 'r', encoding='utf-8') as f: msg = f.read() except: pass # ファイルが無くてもエラーにしない。 lock.unlock() return fromUser, toUser, msg
# 名乗り用、宛名用、本文用ファイルをすべて削除する。 def WipeMessage(TalkEnv, count): lockDir = TalkEnv[0] fromFilePath = TalkEnv[1] toFilePath = TalkEnv[2] messagePath = TalkEnv[3] for i in range(1 + count): lock = exLock.exLock(lockDir) lockResult = lock.lock() if lockResult == True: break time.sleep(0.2) # 0.2 秒待つ。 if lockResult == True: if os.path.isfile(fromFilePath): os.remove(fromFilePath) if os.path.isfile(toFilePath): os.remove(toFilePath) if os.path.isfile(messagePath): os.remove(messagePath) lock.unlock()
*4exLock.py はこちらと同じもの。comm.py と同じディレクトリに置くこと。
comm.py はクラス化していないプログラムです。クラス化しない理由は、以下の 2 点です。
  • 他の処理系と合わせる
  • アルゴリズムを強調する
1-2. ユーザプログラムの実行
プログラム user1.py を起動して、自分に話しかけてください
親しい友人が見ていたら精神を心配されるかもしれませんが、動作確認のためなので仕方がありません。

自分の識別名は user1*2 です。

存在しない識別名へも発言出来てしまいます。
そうなると発言ファイルがクリアされないので、プログラムを起動しなおしてください。

読みやすくするために行間を広げています。実際は広がっていません。

 >_ Windows PowerShell 
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShell をお試しください https://aka.ms/pscore6

PS C:\Users\who> python3 user1.py 

誰へ?user1             -- (1) 発言 : 伝言台に伝言を置く。
内容?お元気ですか?
発言成功。

誰へ?                  -- (2) 受信 : 伝言台に自分宛ての伝言があるかチェックする。
メッセージ受信。From:user1 お元気ですか?

誰へ?user1             -- (3) 発言
内容?はい元気です
発言成功。

誰へ?user1             -- (4) 発言
内容?あなたは元気ですか?
メッセージ受信。From:user1 はい元気です -- (5) 受信 : 自分宛ての伝言があった。
発言は失敗。

誰へ?user1             -- (6) 発言
内容?あなたは元気ですか?
発言成功。

誰へ?                  -- (7) 受信
メッセージ受信。From:user1 あなたは元気ですか?

誰へ?user1             -- (8) 発言
内容?私も元気です。
発言成功。

誰へ?                  -- (9) 受信
メッセージ受信。From:user1 私も元気です。

誰へ?.                 -- (10) 会話を終了する。
終了します。

PS C:\Users\who> exit 
1-3. ユーザプログラムの説明
1-3-1. メッセージの衝突回避
冒頭で少し述べましたが、伝言台に伝言ファイルを置き、それを読むことで情報のやり取りを行います。

伝言ファイルは、送り主、宛先、メッセージの 3 ファイルです。
(分けずに 1 つのファイルでも実現できますが、プログラムを簡潔にするため分けています)

自分の都合で好き勝手に伝言ファイルを置くと、他の誰かがファイルを置くタイミングで自分も同じファイルを置いてしまうかもしれません。
交差点で起きる「出合い頭であいがしら事故」のような事象ですが、英単語で「コリジョン」と呼びます。

コリジョンを防止するための仕組みを「排他制御はいたせいぎょ」と呼び、本稿では exLock.py を使います。
現実の世界では「押しボタン式信号機」がそれに近い概念です。

以下を繰り返すことにより、データの衝突を避けます。
  1. ファイルを置く前にまず一本の旗を奪い合う。
  2. 旗を手に入れた人だけがファイルを伝言台に置く。
  3. ファイルを置いたら、旗を元に戻す。
  4. 旗を持っていない人は、ファイルを置くのを諦める。または旗が元に戻されるまで一定時間待つ。
  5. 1 へ戻る。
1-3-2. プログラムの階層化
プログラムの大きさを見てわかる通り comm.py が最も重要で、処理の多くを担っています。
プログラムの構成図 [1]
主プログラムuser1.py
comm.py抽象化ブロックInit, Quiet, Talk
実務ブロックSendMessage, RecvMessage, WipeMessage ↔ メッセージ伝言台
ライブラリexLock.lock, exLock.unlock ↔ 排他制御
comm.py のプロシージャは大きく分けて 2 つのブロックに分類できます。

Init, Quiet, Talk の抽象化ブロックと、
SendMessage, ReceiveMessage, WipeMessage の実務ブロックです。

主プログラムである user1.py から呼び出すのは抽象化ブロックのプロシージャです。
実務ブロックのプロシージャを主プログラムから直接呼び出すことはありません。
(呼び出しても構わないが、プログラムの構造が汚くなる)

実務ブロックのプロシージャは抽象化ブロックから呼び出され、ライブラリを呼び出します。

このように、プログラムにレベルを設け、種類別に保守できるようにすることを「階層化」と言います。

分かりやすい構造、分かりやすいコーディングは重要です。
あなたのプログラムを他人が理解できないのはアルゴリズムが高度なわけでなく、単純にコードが汚いからです。
「プログラムは動けば良い」という時代は 1999 年に終わりました。汚いコードはバグを引き寄せます。
自分が書いたプログラムを他人が理解できなかったら、優越感に浸るのではなく自分のプログラムを改良しなければなりません。

最初は難しいと思いますが、階層を考えてプログラムを設計できるように心がけてください。
階層化するとプログラムが格段に分かりやすくなります。


2. 他人と会話

2-1. 複数で会話するプログラムの作成
user1.py をコピーしてもうひとつ user2.py を作成し、その識別名を user2 へ変更します。
user1.py との違いは「自分の識別名」だけで、他はすべて同じです。

<テキスト22> ファイル名「user2.py」
import time
import comm

comm.Init('user2', './cardstand/')     # 自分の識別名*5と伝言台*6を指定。
comm.Quiet()

while True:
    # メッセージ送信内容の入力。
    # 送りたいメッセージがなければ受信だけを行う。
    to = input('誰へ?')
    if to == '.':   # プログラムを終了するときは「.」を入力する。
        print('終了します。')
        break
    if to != '':
        msg = input('内容?')
    else:
        msg = ''

    result, msgFrom, msgBody = comm.Talk(to, msg)
    if   result == -1:  # 発言なし。
        print('問題なし。')
    elif result ==  0:
        print('発言失敗。')
    elif result ==  1:
        print('発言成功。')
    elif result ==  2:  # 発言の前にメッセージが到着していた。
        print('メッセージ受信。From:' + msgFrom + ' ' + msgBody)
        if to != '':
            print('発言は失敗。')
*5自分の識別名はメッセージを送受信するときに使われます。user1 と異なる名前を指定します。
*6伝言台は user1 と同じ場所に合わせます。
comm.py と exLock.py も上記 1-1 と同様に user2.py と同じ場所へ置いてください。
2-2. 複数で会話するプログラムの実行
user1.py, user2.py を両方とも起動してから会話を開始します。
それぞれのプログラムから user1 と user2 で互いにメッセージを送り合います。
  • user1 ‐ メッセージ → user2
  • user1 ← メッセージ ‐ user2
例では user1 から会話を開始していますが、どちらが先でも構いません。
読みやすくするために行間を広げています。実際は広がっていません。

user1 を起動
 >_ Windows PowerShell 
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShellをお試しください
 https://aka.ms/pscore6

PS C:\Users\who> python3 user1.py 

誰へ?user2
内容?お元気ですか?
発言成功。








誰へ?user2
内容?あなたは元気ですか?
メッセージ受信。From:user2 はい元気です
発言は失敗。





誰へ?
メッセージ受信。From:user2 あなたは元気ですか?

誰へ?user2
内容?私も元気です。
発言成功。




誰へ?.
終了します。

PS C:\Users\who> exit 
  user2 を起動
 >_ Windows PowerShell 
Windows PowerShell
Copyright (C) Microsoft Corporation. All rights reserved.

新しいクロスプラットフォームの PowerShellをお試しください
 https://aka.ms/pscore6

PS C:\Users\who> python3 user2.py 





誰へ?
メッセージ受信。From:user1 お元気ですか?

誰へ?user1
内容?はい元気です
発言成功。






誰へ?user1
内容?あなたは元気ですか?
発言成功。








誰へ?
メッセージ受信。From:user1 私も元気です。

誰へ?.
終了します。

PS C:\Users\who> exit 
2-3. 複数で会話するプログラムの説明
user1 と user2 のプログラムは同じで、識別名だけが異なっていますがうまく動作します。
「そうなるように作ってある」と言ったほうが分かりやすいかもしれません。

上記 1-3-2 のように、構成図にするとこうなります。
プログラムの構成図 [2]
主プログラムuser1.pyuser2.py
comm.py抽象化ブロックInit, Quiet, Talk
実務ブロックSendMessage, RecvMessage, WipeMessage ↔ メッセージ伝言台
ライブラリexLock.lock, exLock.unlock ↔ 排他制御
冒頭のルール「自分が発言するときは相手を指名する」は、以下の動作に要約可能で、
識別名が異なってもアルゴリズムは同一のため、同じプログラムを使うことができます。
  • 相手の識別名を指定して発言する。
  • 他人宛ての発言は無視する。
  • 自分宛ての発言を取り込む。
comm.py はこれを守ってプログラミングされています。


3. さらに会話を実験

3-1. 他の実装と会話する。
ルールさえ守られていれば、Python ではない他のプログラムと会話することができます。
こちらを参照して、異なる言語で作られた同じプログラムとの会話を実験してください。
(固有の識別名にすることを忘れずに)
3-2. ネットワーク越しに会話する。
プログラム exLock.py は、排他制御にディレクトリを使用しているため
SMB (Windows で構成するファイル共有) や、NFS (Linux のファイル共有) でも動作できます。

他の PC やファイルサーバ (NAS とも呼ばれます) に共有フォルダーを設定し、
そこを伝言台にすることにより、自分以外の PC との会話を実験してください。
(共有フォルダーの設定は、PC の持ち主や LAN の管理者に相談してください)

伝言台にする PC やファイルサーバで必ずしもユーザプログラムが動作する必要はありません。
また、会話する PC の数は 2 台とは限りません。理論上は無制限に会話できます。
3-3. さらに多くのプログラムと会話する。
user1, user2 とプログラムを増やしたように、user3, user4, ··· と PC が許す限りいくつでもプログラムを増やすことができます。
好きなだけプログラムを増やして実験してください。
(固有の識別名にすることを忘れずに)

プログラムを増やしすぎると自分の手足が足りなくなるので、友人に手伝ってもらってください。
この章は、ここで終了です。