2024-2I プログラミング1 第28回 講義資料

2025年02月21日(金)1・2時限

1 連絡

1.1 2I実験の授業評価アンケート

昨日、回答していない学生は、このタイミング回答をお願いいたします。

1.2 作品共有1

1.3 作品共有2

2 例外処理

現在までの講義資料のなかで、何度か 例外処理 という言葉が登場しています (第11回講義第19回講義第23回講義)。

例外処理」とは、簡単にいえば「予期せぬエラーが発生した場合に プログラムがクラッシュすること (=停止・強制終了すること) を防ぎ、エラーを適切に処理してプログラムの実行を継続させる ための仕組み」を提供するものです。

ここでの「予期せぬエラー」とは、例えば、次のようなものを意味します。

2.1 例外とは

プログラミングの分野において 例外 (Exceptionエクセプション) という用語は、我々が日常的に使用する「例外」という言葉とは異なる意味合いで使用されます。プログラミング分野においての「例外」とは プログラムの実行中に予期せぬ事態やエラーやプログラムが正常に実行を続けることができない状況 を意味します。そして、例外処理 (Exception Handling) とは、それら例外に対して講じるべき処理や措置を指します。

具体的には次のように「例外」や「例外処理」という言葉が使用されます。

2.2 例外処理の記述

例外処理 (Exception Handling) とは、プログラム実行時に発生する「例外(=想定外の事態や、そのままではプログラムが続行不可能な状況)」を 捕捉 (Catch、キャッチ) して、それ対して適切な処理を講じるための枠組みを意味します。

例外処理を活用することで、いわゆる「通常のエラー処理」よりも、柔軟かつ効率的にプログラムの クラッシュを回避すること、結果的として強制終了やフリーズが起きずらい「安全性」と「堅牢性」を備えたアプリの提供が可能となります。

この「例外処理」という機能・仕組みは、Python言語に限らず、C++言語、JavaScript、Java などの多くのモダンなプログラミング言語でサポートされています。一方で、C言語 や Go言語 においては、例外処理はサポートされていません。

Pythonでの例外処理は、次のような try ブロックと except ブロックから構成されます。try ブロックには通常の実行コードを記述しておき、実行時に そのブロック内で何らかの例外 (=つまりエラーや想定外の事態) が検出・捕捉されるとその時点で処理 (制御フロー) が自動的に except ブロックに移行する という仕組みが提供されます。

一般に except ブロックには「エラーに対処する処理 (ユーザーへのメッセージ表示やログ出力)」や「プログラムを安全に終了するための処理」を記述しておきます。これにより「エラーを抱えたままプログラムが続行してクラッシュ (強制終了) に至る」というUX的に最悪の状態を回避することができます。UX: User Experience。

以下に、try - except ブロックを利用した「例外処理」の例を示します。

import os
import pickle

# 設定ファイルを読込む処理
def load_user_setting():

  fn = 'user-setting.pickle'

  # 通常のエラー処理 (事前検証)
  if not os.path.isfile(fn) :
    print(f'設定ファイル {fn} が存在しません。')
    return None
  
  # 例外処理 (try-except構文) によるエラー処理
  try : 
    with open(fn,'rb') as file:
      us = pickle.load(file)
    return us
  except Exception as e:
    print(f'設定ファイル {fn} の読み込みに失敗しました。')
    print(f'詳細 => {type(e)} {e}')
    return None

# 設定の読み込み
user_setting = load_user_setting()
print('プログラムの実行フローが最後まで到達しました。')

上記プログラムの 第09行目 から 第12行目 は「(例外処理ではない) 通常のエラー対策/処理」となります。指定したファイルパスに「ファイルが存在するか?」どうかを open 関数の 実行前に検証 して、もし、ファイルが存在しなければメッセージを表示して None を戻り値として関数を抜けています。

このように通常は、そのまま続行すればエラーが生じる可能性がある要因について 事前チェックをかける という方法でエラー対策を講じます。例えば、除算においては「除数が 0 以外であること」を検証し、math.sin() を使用する場合では「引数として与える値が数値型であること」を検証し、もし問題があれば、その処理 (=除算 や math.sin() ) を「実行しない」ことで、つまり「エラーが起こる処理を実行しないこと」でプログラムのクラッシュを回避します。

しかし、全てのエラーを事前に想定・予測して対策を講じること が常に可能であるとは限りません。例えば、上記の 第10行目 ではファイルの存在を確認していますが、その直後に ファイルが削除される可能性、ファイルが破損している可能性、排他的なアクセス制御により ファイルが読み取りが不可能になっている可能性などあります。これらの状況をすべて想定してプログラム上で事前に対処することは事実上不可能となります。

このようなケースに対して try-except による「例外処理」が有効となります。

例外処理を使用すると try ブロック内でエラー (例外) が発生した場合、そこでの処理は中断され、制御フローが except ブロックに移行して処理が続行されます。具体的には、ファイルが存在しなければ 第16行目open 関数で例外が発生し、それが検出・捕捉されることで 第20行目 に制御が移行します。また、ファイルが壊れていれば (適切な pickle 形式でなければ) 第17行目pickle.load 関数で例外が発生し、それが検出・捕捉されることで 第20行目 に制御が移行します。

いわゆる通常のエラー対策では、エラーが発生する可能性がある処理に先立って、その処理が安全に実行可能かを事前検証することで (つまり、エラーが発生しない条件下でのみ処理を実行することで) プログラムのクラッシュを回避します。一方で、「例外処理」ではエラーの発生自体は許容し、発生したエラーに対して適切な処理を施すことで、プログラムのクラッシュを回避するという「考え方の違い」があります。

実際のプログラム設計においては、事前検証を基本とする通常のエラー対策と、例外処理の 両方を適切に組合わせること で、堅牢性のあるアプリを実現します。

プログラムの途中で「クラッシュが発生してアプリが強制終了 (異常終了) すること」と「エラーの発生に基づき意図的にプログラムの終了処理すること」は、アプリの品質上、大きな違いがあるので十分に注意してください。


演習1: 次のプログラムを 'user-setting.pickle' が存在しない状況で実行し「プログラムの途中でクラッシュすること」を確認せよ。つまり、プログラムの途中で強制終了が発生して プログラムの最後まで実行されず「プログラムの実行フローが最後まで到達しました。」というメッセージが出力されないこと を確認せよ。.

import pickle

def load_user_setting():

  fn = 'user-setting.pickle'
  with open(fn, 'rb') as file:
    us = pickle.load(file)
  return us

user_setting = load_user_setting()
print('プログラムの実行フローが最後まで到達しました。')

(実行結果の一例)

> python crash.py 
Traceback (most recent call last):
  File "C:\Users\xxxx\Documents\xxxx\ex.py", line 11, in <module>
    user_setting = load_user_setting()
                   ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\xxxx\Documents\xxxx\ex.py", line 6, in load_user_setting
    with open(fn, 'rb') as file:
         ^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'user-setting.pickle' 

 

演習2: exception-handling-01.py第10行目 から 第12行目 をコメントアウトして、同フォルダに user-setting.pickle が存在しない状態でプログラムを実行せよ。そして、例外処理により、次のメッセージが表示されること (つまり try ブロックから except に実行フローが移行していること)、また、プログラムがクラッシュすることなく最後まで実行されていることを確認せよ。

設定ファイル user-setting.pickle の読み込みに失敗しました。
詳細 => <class ‘FileNotFoundError’> [Errno 2] No such file or directory: ‘user-setting.pickle1’
プログラムが最後まで到達しました。正常終了します。

 

演習3: exception-handling-01.py と同フォルダに user-setting.pickle というファイルが存在する状態 (ただし、ファイルの内容はデタラメな文字列となっている) でプログラムを実行せよ。そして、例外処理により、次のメッセージが表示されることを確認せよ。

設定ファイル user-setting.pickle の読み込みに失敗しました。
詳細 => <class ’_pickle.UnpicklingError’> pickle data was truncated
プログラムが最後まで到達しました。正常終了します。

2.3 エラー処理の方法

次に示す exception-handling-02.pyexception-handling-03.py は、次のように標準入力から「区分番号」に相当する文字列を受け取って、それに基づき「小学生」「中学生」・・・「大学生」の区分を選択するプログラムになっています。

0:小学生  1:中学生  2:高校生  3:高専生  4:大学生  
区分番号(0~4)を入力してください => AAA

不正な値が入力されました。やり直してください。
区分番号(0~4)を入力してください => 8 

不正な値が入力されました。やり直してください。
区分番号(0~4)を入力してください => 6.9

不正な値が入力されました。やり直してください。
区分番号(0~4)を入力してください => 3

区分「高専生」が選択されました。

exception-handling-02.py は「事前検証を使って「不正な入力値」に対処しているプログラム」になっています。つまり、第10行目x.isdigit() メソッドで、標準入力から得られた文字列が数値から構成されているかを検証し、それがクリアできれば 第11行目int() 関数で整数値に変換し、さらに数値範囲が リスト s_arr のインデックスとして適切なものかどうかを確認しています。これらの「事前検証」を行なうことによって、標準入力から AAA86.8 のような文字列が入力されたときも、プログラムがクラッシュすることなく動作する仕組みを提供しています。

def get_student_type():
  s_arr = ['小学生', '中学生', '高校生', '高専生', '大学生']
  for i, v in enumerate(s_arr):
    print(f'{i}:{v}  ', end='')
  print()

  while True:
    x = input(f'区分番号(0~{len(s_arr)-1})を入力してください => ')

    if x.isdigit():
      n = int(x)
      if 0 <= n <= len(s_arr) - 1:
        return s_arr[n]

    print('\n不正な値が入力されました。やり直してください。')

st = get_student_type()
print(f'\n区分「{st}」が選択されました。')

一方で exception-handling-03.py は「例外処理を使って不正な入力値に対処しているプログラム」となっています。こちらのプログラムでは、標準入力から得られた文字列に対して事前検証はせずに、try 内部の 第10行目s_arr[int(x)] を実行してみて、例外が発生すれば 第12行目 に処理を飛ばすことで、不適切な文字列が入力されてもプログラムがクラッシュしない仕組みを提供しています。

def get_student_type():
  s_arr = ['小学生', '中学生', '高校生', '高専生', '大学生']
  for i, v in enumerate(s_arr):
    print(f'{i}:{v}  ', end='')
  print()

  while True:
    x = input(f'区分番号(0~{len(s_arr)-1})を入力してください => ')
    try:
      return s_arr[int(x)]
    except Exception as e:
      print('\n不正な値が入力されました。やり直してください。')

前述したように「事前検証と例外処理のどちらでエラー対策/処理をするべきか」は状況によって変わります。ただし、基本的には事前検証と例外処理を併用することが一般的には望まれます。


演習1 : 上記の2つのプログラムを実行して、その動作を確認せよ。

演習2 : 上記の例外処理を使用したプログラム (exception-handling-03.py) では、一部の不正な値を検出することができない。どのようなケースにおいてその問題が発生するか考え、検証せよ。答え: 「-1」や「-2」のような不正入力に対応することができない。

2.4 例外「KeyboardInterrupt」

Pythonプログラム開発や実行において頻繁に遭遇する例外には TypeErrorValueErrorIndexErrorImportErrorなどがあります。加えて、CLIプログラム (Command Line Inteface) においてキーボードから Ctrl+C を入力した際に発生する KeyboardInterrupt という例外も重要となります。

CLIプログラムにおいて、Ctrl+C は「コピー」のショートカットではなく、プロセスに割り込み信号(SIGINT)を送信し、処理を中断させるための操作 として機能します。この操作は、プログラムが暴走したり、無限ループに陥った際など、プログラムを強制終了させる必要がある場合 に使用します。

例えば、次のプログラム loop-01.py を実行したとき (VSCodeの F5 ではなく、ターミナルから python loop-01.py コマンドで実行したとき)、このプログラムは Ctrl+C でターミナルに割り込み信号 (SIGINT: Interrupt Signal) を送信し、強制終了することができます。

import time
print('処理中', end='')
while True:
  time.sleep(0.5)
  print('.', end='')

このプログラムを実際に実行し、Ctrl+C を使って強制終了できることを確認してください。なお、Ctrl+C は Python プログラムに限らず、その他の CLIプログラムにおいても同様に機能します。

この Ctrl+C によって発生する KeyboardInterrupt は、try - except で例外処理することができます。例えば、次のように例外処理すれば、強制終了ではなく正常終了させることができます。

import time
print('処理中', end='')
while True:
  try:
    time.sleep(0.5)
    print('.', end='')
  except KeyboardInterrupt:
    print('処理を中断しますか? Y/[N] => ', end='')
    if input() == 'Y':
      break
print('プログラムの実行フローが最後まで到達しました。')

演習 : loop-02.py を実行して、その動作について確認せよ。

2.5 例外処理についてさらに理解を深める

ここまで例外処理の概念を掴むための解説をしてきました。しかし、ここで触れた内容は例外処理の一端にしか過ぎません。実務プログラミングでは、ここでは紹介していない raisefinally というキーワードを使った例外処理のテクニックが使われます。「Python 例外処理 入門」などキーワード検索すると丁寧な解説記事が見つかるので、それらを参照してください。

3 デバッグ支援ライブラリ icecream

プログラムのデバッグに便利なライブラリとしてicecreamというライブラリを紹介しておきます。このライブラリは、「変数名」と「変数に格納された値」を同時に表示する機能を持ちます。

icecreamは標準の組み込みライブラリに含まれていないので、pip を使ってインストールする必要があります。

pip install icecream

例えば、次のように使用します。

from icecream import ic

a = 10
b = 40

ic(a)
ic(a*b)
ic(type(a))

実行結果は、次のようになります。

ic| a: 10
ic| a * b: 400
ic| type(a): <class 'int'>

もし、同様に print 関数を使って a: 10 のように表示するとすれば、print('a: {a}') のように記述する必要があります。これが、このライブラリを使用すれば ic(a) のように非常に短い文で記述可能になります。

また、引数を与えずに、次のように ic() だけを記述すると、それが実行された行数と時刻が出力されます。例えば、途中で強制終了してしまうプログラムがあったとき「どこまで (何行目まで) プログラムが正常に動作しているか?」などを確認したい場合に利用できます。

from icecream import ic

a = 10

ic()
ic(a)

a += 10

ic()
ic(a)

a *= 5

ic()
ic(a)

実行結果は、次のようになります (変数 a の値が、何行目時点でどのような値になっているかが把握できます)。

ic| ic-02.py:5 in <module> at 15:51:11.157
ic| a: 10
ic| ic-02.py:10 in <module> at 15:51:11.191
ic| a: 20
ic| ic-02.py:15 in <module> at 15:51:11.195
ic| a: 100

また ic.disable() を呼び出せば、以降の ic()一括して無効化すること ができます。プログラムのなかの様々な場所で ic() を記述しているとき、完成版としてリリースするために、それらを個別にコメントアウトしていくことは非常に面倒なので、ic.disable() を利用してください。

その他、詳しくは公式リファレンスを参照してください。例えば ic.configureOutput(prefix='[DEBUG] ') のようにすれば ic| の部分を [DEBUG] に置き換えるようなこと もできます。


演習1 : ic-02.py第09行目ic.disable() を記述して、その動作を確認せよ。

演習2 : 演習1に引き続き、第14行目ic.enable() を記述して、その動作を確認せよ。

演習3 : ic-01.py第02行目ic.configureOutput(prefix='[DEBUG] ') を記述して、その動作を確認せよ。

f文字列を使ったデバッグの小技

f文字列では、次のように変数名のあとに「イコール」を付けると、変数名と内容を同時に出力してくれます。

x=10
print(f'x={x}') # このようにしなくても 
print(f'{x=}')  # このようにすれば `x=10` と出力される 

実行結果

x=10
x=10

4 総括

本科目についての「総括」です。

本授業は通年科目として約1年間をかけて約30回の講義・演習 (=90min×30=45hr) を通じて「Pythonプログラミング」について学んできました。

しかしながら、4月の第01回講義で伝えてきたように ジュニアレベルのプログラマ(=ソフトウェアエンジニアのタマゴとして就職ができるレベル)に到達するために必要な学習時間は 1,000~2,000 時間と言われおり、また、プログラミングの「超初心者」が「初心者」になるまでに必要な学習時間は 250 時間と言われています。その意味で、学校で受ける90分×30回の授業の学習時間などは誤差レベルでしかありません。

実質的に意味を持ってくるのは「授業で学んだことを基礎/きっかけとして自発的な「学び」をしている時間」です。事実、皆さんの肌感覚としても 高専祭展示や自由課題などを通してプログラミングしている ときが、授業よりも何倍も「学び」や「成長」が得られる有意義な時間という実感があると思います。

また、学校の授業は様々な制約で「週1回」というペースでしか進みませんが、仮に自発的に「週5回」のペースで勉強を進めていれば、わずか2カ月程度で本授業30回分の学習は可能です。つまり、2年生の前期中間試験の頃 (6月頃) には PySide を使ったプログラムがつくれるレベルに到達可能なのです。事実、本校にも、全国の高専生にも、そのようなペースで学習をガンガンと進めている人が少なからず存在します。

プログラミングをはじめとして情報系分野のエンジニアは、学歴や学位ではなく「経験・実績」が大きく評価される領域です(その経験・実績を示すためのひとつの手段がポートフォリオです)。学校で総合成績の上位をとっていれば、どこでも希望のところに就職できる・・・というわけではありません。

自身の「人生設計」や「キャリア計画」に基づき、興味関心のある分野については、授業など待たず、また授業の枠を超えて貪欲に「学び」を深めていって欲しいと思います。授業は、そのための「チュートリアル」と考えてください。

4.1 ぜひ視聴してほしい動画

第01回講義でも見てもらったら動画です。いま、1年間の学習を終えたいま、改めて視聴してほしいと思います。

4.2 達成目標の確認

本科目のシラバスに示した【科目の達成目標】からの抜粋です。

  1. Pythonの開発環境および実行環境を構築できる。
  2. エディタやバージョン管理システムなどの各種開発ツールを効果的に利用できる。
  3. 関数、条件分岐、繰り返し処理を使用して基本的な構造化プログラミングができる。
  4. Pythonの多様なライブラリを活用して日常生活や学習に有用な小規模アプリを開発できる。
  5. エラーや意図せぬ結果が生じたとき、その原因特定と解決に向けて適切なアプローチができる。

さて、上記について「成長」を冷静に客観的に評価してみましょう。

なお、本科目では、プログラミングの授業ですが、CPUやメモリなどのハードウェア、OSとの関係においてプログラムはどのようにして動くのかといったことアルゴリズムやデータ構造に関すること については、意図的に扱っていません。これらについては、来年度以降の「アルゴリズムとデータ構造1・2」「コンピュータシステム」「コンピュータアーキテクチャ」「情報理論」などの授業のなかで学習していくことになります。