どうも、コンです。
以前 【シリアル通信】PySerialの基本的な使い方【Python】 という記事を書いたのですが、改めて読み直してみるとコードが少し古い書き方になっていました。動きはするものの、PEP 8 の慣習や Python 3.6 以降で標準になった書き方を踏まえて整理すると、もっと短く・意図が伝わりやすい書き方ができます。
この記事では、元記事のサンプルコードをベースに、PySerial のコードを今風に書き直す4つのポイントを「動くフルコード」で紹介します。各サンプルはそのままコピペして実行できる形です。
結論(先出し)
以下の4つを押さえると、PySerial のコードが短く、現代の Python らしい見た目になります。
with文でポートのオープン/クローズを自動化する- 関数に型ヒントを付けて引数と戻り値の意図を明示する
- 固定バイトの
read(n)ではなくreadline()やread_until()を使う - バイト列リテラル
b'...'と f-string で文字列処理を簡潔にする
元記事のコード(おさらい)
まず、元記事に出てきたサンプルを「ポート検知 → 開く → 送信 → 受信 → 閉じる」までひと続きにつなげたフルバージョンを示します。これがビフォアの基準になります。
import serial
import serial.tools.list_ports
# シリアルポートを自動検出する関数
def auto_detect_serial_port():
ports = list(serial.tools.list_ports.comports())
for p in ports:
print(p)
if 'USB Serial Port' in p.description:
return p.device
return None
# シリアルポートをオープンする
ser = serial.Serial(auto_detect_serial_port(), 9600, timeout=1)
# 送信
data = 'Hello, World!'
ser.write(data.encode())
# 受信
data = ser.read(10)
print(data.decode())
# シリアルポートをクローズする
ser.close()今風に書き直したコード(完成版)
4つのポイントを反映すると、同じ処理が以下のフルコードになります。これがアフターの最終形です。
import serial
import serial.tools.list_ports
def find_port(keyword: str = 'USB Serial') -> str:
for port in serial.tools.list_ports.comports():
if keyword in port.description:
return port.device
raise RuntimeError(f'シリアルポートが見つかりません: {keyword!r}')
def main() -> None:
with serial.Serial(find_port(), 9600, timeout=1) as ser:
ser.write(b'Hello, World!\n')
line = ser.readline().decode().rstrip()
print(f'受信: {line}')
if __name__ == '__main__':
main()元記事の同等処理が20行・クローズ忘れリスク付きだったのに対し、関数1つ・with ブロック1つで完結します。ここから何をどう変えたのか、4つのポイントに分けて順に見ていきます。
1. with 文で close() を自動化する
元記事は serial.Serial(...) でポートを開き、最後に ser.close() を呼ぶ書き方でした。送受信だけを抜き出すと以下のような流れになります。
import serial
ser = serial.Serial('COM3', 9600, timeout=1)
ser.write('Hello, World!'.encode())
data = ser.read(10)
print(data.decode())
ser.close()この書き方の弱点は、途中で例外が発生すると ser.close() まで到達せず、ポートが開きっぱなしになることです。try/finally で囲む手もありますが、PySerial の Serial クラスはコンテキストマネージャに対応しているので、with 文を使うのが一番素直な書き方です。
import serial
with serial.Serial('COM3', 9600, timeout=1) as ser:
ser.write('Hello, World!'.encode())
data = ser.read(10)
print(data.decode())with ブロックを抜けるときに、例外があってもなくても close() が呼ばれます。open() でファイルを扱うときと同じ感覚です。詳細は PySerial 公式 – Serial クラス のリファレンスを参照してください。
2. 関数に型ヒントを付ける
元記事ではシリアルポートを自動検知する関数を以下のように書いていました。
import serial
import serial.tools.list_ports
# シリアルポートを自動検出する関数
def auto_detect_serial_port():
ports = list(serial.tools.list_ports.comports())
for p in ports:
print(p)
if 'USB Serial Port' in p.description:
return p.device
return None
# シリアルポートをオープンする
ser = serial.Serial(auto_detect_serial_port(), 9600, timeout=1)
# シリアルポートをクローズする
ser.close()気になる点は2つあります。
- 引数も戻り値も型がわからず、IDE の補完が効きづらい
- 見つからないと
Noneを返すので、serial.Serial(None, ...)となり挙動が分かりにくい
型ヒントを付けて、検索ワードを引数化し、見つからない場合は例外を投げる形に書き直します。
import serial
import serial.tools.list_ports
def find_port(keyword: str = 'USB Serial') -> str:
for port in serial.tools.list_ports.comports():
if keyword in port.description:
return port.device
raise RuntimeError(f'シリアルポートが見つかりません: {keyword!r}')
with serial.Serial(find_port(), 9600, timeout=1) as ser:
pass # ここでシリアル通信を行うkeyword: str = 'USB Serial' と書くことで、Arduino を狙うなら 'Arduino'、Silicon Labs の USB-UART なら 'CP210x' のように呼び出し側で切り替えられます。戻り値が必ず str と決まっているので、serial.Serial() に渡す側も None チェックなしで扱えます。
3. read(n) より readline() / read_until() を使う
元記事では受信を ser.read(10) で書いていました。送信からの一連の流れは以下です。
import serial
with serial.Serial('COM3', 9600, timeout=1) as ser:
ser.write('PING\n'.encode())
data = ser.read(10)
print(data.decode())read(n) は最大 n バイトを読む API なので、相手が「9バイトしか送ってこなかった」「11バイト目以降を次の読み取りで処理したい」という場面で扱いづらくなります。多くのマイコン側プロトコルは改行区切り(\n)でメッセージを送ってくるので、readline() で行単位に読む方が素直です。
import serial
with serial.Serial('COM3', 9600, timeout=1) as ser:
ser.write(b'PING\n')
line = ser.readline().decode().rstrip()
print(f'受信: {line}')終端文字が \n 以外なら read_until(b'>') のように区切りを指定できます。固定バイトを読みたい用途も残るので read(n) 自体が悪いわけではなく、用途に応じて使い分けるイメージです。
4. b’…’ リテラルと f-string で文字列処理を簡潔に
元記事の送信コードでは、文字列を変数に入れてから encode() していました。
import serial
with serial.Serial('COM3', 9600, timeout=1) as ser:
data = 'Hello, World!'
ser.write(data.encode())定数の文字列を送るだけなら、最初からバイト列リテラル b'...' で書いておく方がシンプルです。動的な値を組み立てる場合は f-string と .encode() を組み合わせます。
import serial
with serial.Serial('COM3', 9600, timeout=1) as ser:
# 定数: バイト列リテラルそのまま
ser.write(b'Hello, World!\n')
# 動的: f-string で組み立てて encode
led_id = 42
brightness = 100
ser.write(f'LED{led_id}:{brightness}\n'.encode())受信したデータをログ出力する箇所も、% 記法や str.format() ではなく f-string で書くのが今は標準的です(PEP 498、Python 3.6 以降)。
まとめ
with文を使うとクローズ忘れと例外時のリーク両方を一度に防げる- 関数に型ヒントを付けると IDE 補完が効き、戻り値の契約が明確になる
- 受信は固定バイトの
read(n)よりreadline()/read_until()の方がプロトコルに合わせやすい - 定数バイト列は
b'...'、動的なら f-string +.encode()を使う
PySerial の API 自体はここ数年大きく変わっていないので、書き方を整えるだけで読みやすさはかなり改善できます。元記事のコードでも動作はするのですが、これから書く方には今回紹介した4ポイントを反映した形をおすすめします。
ここまで読んでいただきありがとうございました。参考になれば幸いです。
