{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [], "toc_visible": true }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "markdown", "source": [ "# [講義] Pythonプログラミングの勘所\n", "本講義では、書籍で学んだだけでは習得しづらい、Python機能の上手な使い方、心がけたい事柄をご紹介します。\n", "\n", "## 目次\n", "1. グローバル変数の使いどころ\n", "1. 関数の使いどころ\n", "1. クラスの注意事項\n", "1. エラー処理のポイント\n", "1. 命名規則について" ], "metadata": { "id": "X-0eVhgpXWD5" } }, { "cell_type": "markdown", "source": [ "# グローバル変数の使いどころ\n", "## ローカル変数とグローバル変数\n", "Pythonの変数には、ローカル変数とグローバル変数があります。\n", "\n", "ローカル変数は、関数の中で代入が行われた変数です。一方グローバル変数は、関数の外で代入が行われた変数です。\n", "\n", "次のソースコードにおいて、local_varがローカル変数、global_varがグローバル変数となります。\n", "\n", "~~~\n", "global_var = 10 # グローバル変数\n", "\n", "def func():\n", " local_var = 100 # ローカル変数\n", " if global_var == 10: # グローバル変数は通常、関数内では参照のみ可\n", " print(local_var)\n", "~~~\n", "\n", "グローバル変数は通常、関数内では参照しかできず、更新することはできません。しかしglobalキーワードを使えば、関数内で値の変更ができるようになります。\n", "\n", "~~~\n", "global_var = 100 # グローバル変数\n", "\n", "def func():\n", " global global_var # globalキーワードを使用\n", " local_var = 100 # ローカル変数\n", " print(local_var)\n", "\n", " global_var = True # 関数内での更新も可能になる\n", "\n", "~~~\n", "\n", "ここでは、グローバル変数について着目します。\n", "\n", "グローバル変数はどの関数からでも参照・更新ができるので、一見便利であるような気もします。しかし、グローバル変数は上手に使わないと、プログラムがとても複雑で分かりにくいものになってしまいます。\n", "\n", "## グローバル変数のデメリット\n", "* グローバル変数はどこでも参照・更新ができるため、プログラム全体を見ないと使い方が分かりません。\n", "\n", "* 新しく作った関数でグローバル変数を更新すると、同じグローバル変数を使った他の関数や、関数外の処理など、すべてに影響が生じてしまします。この影響が問題ないのか、プログラム全体を確認しなければいけません。\n", "\n", "また、次の例を見てみましょう。何が起きているか、分かるでしょうか。" ], "metadata": { "id": "XmMhZKdo-xh-" } }, { "cell_type": "code", "source": [ "global_var = 100\n", "\n", "def func1():\n", " print(global_var)\n", "\n", "def func2():\n", " print(global_var)\n", " global_var = 200\n", "\n", "print(\"func1を実行\")\n", "func1()\n", "\n", "print(\"func2を実行\")\n", "func2()" ], "metadata": { "id": "gA1AH_YGh-03" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "func1()では、グローバル変数を参照して値を画面に出力しています。これは特に問題ないでしょう。\n", "\n", "一方、func2()は、「ローカル変数global_varを、割り当てる前に参照している」としてエラーになっています。\n", "\n", "Pythonでは、関数の中で代入が行われた変数はローカル変数になります。やっかいなのは、「どこで代入したとしても、関数内の同名の変数はすべてローカル変数になる」ということです。func2()は、最後にglobal_varへ代入を行っていることから、print関数に指定しているglobal_varもローカル変数になってしまっているのです。\n", "\n", "このようなごく短い関数であれば間違いを見つけることは難しくありませんが、ある程度大きな関数でこのようなことが起こってしまったとき、問題を見つけるのに苦労してしまうかもしれません。" ], "metadata": { "id": "SdXwK7RQit9F" } }, { "cell_type": "markdown", "source": [ "## グローバル変数を使ってよい例\n", "これまでグローバル変数のネガティブな面を見てきましたが、絶対に使ってはいけないということはありません。特に「プログラム全体で横断的に使用する、不変の定数」は、グローバル変数を使うとよいです。\n", "\n", "なおそのような「プログラム全体で横断的に使用する、不変の定数」は、定数として使用するので、一度定義したら値を変更してはいけません。\n", "\n", "ただしPythonには、他のプログラム言語のように値の変更を防ぐ仕組みがありません。そのため慣例的に、定数として用いる変数は変数名をすべて大文字として、定数であることが分かりやすくなるようにします。例を次に示します。\n", "\n", "~~~\n", "FIGIRE_PATH = \"/data/figure\"\n", "~~~\n", "\n", "逆に、このような定数をハードコーディングしたり、関数の引数で受け渡しをして複雑な作りにしてしまったりといったことは、避けるようにしてください。" ], "metadata": { "id": "gjsvhPtCj0lB" } }, { "cell_type": "markdown", "source": [ "# 関数の使いどころ\n", "Pythonプログラムを作成するにあたり、長大な処理を、上から下へダラダラと記述していくようなプログラムは、好ましくありません。きちんと関数を作って、見通しの良いプログラムを作ることが必須となります。\n", "\n", "ですが、関数の構文を学んでも、それを上手に使うのにはコツがあります。ここでは、関数をどのような観点で作成すればよいのか、その勘所をご紹介します。\n", "\n", "## 何度も呼び出される処理を関数にする\n", "プログラム中で同じ処理が何度も出てくると、冗長で読みづらくなります。また、処理が間違っていて修正する際に、あちこちを修正する必要があり、修正漏れのリスクが生じてしまいます。\n", "\n", "## 機能ごとに関数にする\n", "それでは、1度しか行われない処理は関数にする必要がないかというと、そうとは限りません。ある程度大きなプログラムであれば、それが1度しか行われない処理であっても、機能ごとに関数を作成してください。これによるメリットは、次のとおりです。\n", "\n", "* 関数を作る人と使う人が異なる場合、関数を使う人は、入出力の仕様さえ知っていれば関数の中の処理を理解する必要がない。\n", "* 他のプログラムで再利用できる。\n", "\n", "また、一つの関数に複数の機能をまぜるのも避けてください。「1つの機能は1つの関数に」が原則です。\n", "\n", "## main処理は関数を列挙するだけにする\n", "きれいに機能ごとに関数を作ることができたら、プログラムスタート時のメイン処理は、関数が列挙されたのみの状態となることが理想です。\n", "\n", "イメージを次に示します。\n", "\n", "~~~\n", "def init():\n", " \"\"\" 初期化関数 \"\"\"\n", " :\n", "\n", "def read_data():\n", " \"\"\" データ読み込み関数 \"\"\"\n", " :\n", "\n", "df process_data()\n", " \"\"\" データ処理関数 \"\"\"\n", " :\n", "\n", "def output_data()\n", " \"\"\" データ出力関数 \"\"\"\n", " :\n", "\n", "# メイン処理\n", "init()\n", "read_data()\n", "process_data()\n", "output_data()\n", "~~~\n", "\n", "このような形になれば、メイン処理を見るとプログラムの大きな流れを容易につかむことができます。\n", "\n", "例えばデータ処理のソースコードを見たいと思ったときに、どこを見ればよいのかが一目で分かりますので、とても読みやすく、修正もしやすいコードとなります。\n", "\n", "ARIM事業でも、このような関数の作り方をコーディング規則として定めており、誰が見ても分かりやすいコードとなるようにしています。" ], "metadata": { "id": "zFBI5C_R45Xv" } }, { "cell_type": "markdown", "source": [ "# クラスの注意事項\n", "Pythonプログラムの少し高度な機能として、クラスがあります。\n", "\n", "クラスに限った話ではありませんが、その機能が何のために存在しているのか、どんなメリットがあるのかを理解せずに使用しても意味がありません。\n", "\n", "特にクラスについては、使いどころが少し難しく、適切な使い方ができていないことも多いです。よく見られる例としては、処理の共通化や汎用化のためだけに、クラスを使っている事例です。このような目的のみであれば、関数化や、モジュール(.pyファイル)の分割で十分であることがほとんどです。\n", "\n", "ここでは、クラスの特徴をごくごく簡単にご説明します。クラスを使おうと考えたとき、本当にクラスにするのが最適なのか、考える材料にしていただければと思います。\n", "\n", "## クラスとインスタンス\n", "クラスについて学ぶと、クラスとインスタンスという言葉が必ず登場します。クラスは製品の設計図、インスタンスは設計図を元に生産された製品、と例えられることが多いです。\n", "\n", "例えば、「車」クラスという設計図あって、その設計図をもとに「田中さんの赤い乗用車」インスタンスや、「鈴木さんの白いトラック」インスタンスという具体的な製品が作られます。これらは持ち主、色、モデルこそ違いますが、「ドアを開ける」「走る」といった共通の機能を持っています。\n", "\n", "実際のソースコードを見てみましょう。" ], "metadata": { "id": "0hXsQosq5GoJ" } }, { "cell_type": "code", "source": [ "class Car:\n", " def __init__(self, owner, color, model):\n", " self.owner = owner\n", " self.color = color\n", " self.model = model\n", " \n", " def open(self):\n", " print(f\"{self.owner}さんの{self.color}の{self.model}のドアが開いた!\")\n", "\n", " def run(self):\n", " print(f\"{self.owner}さんの{self.color}の{self.model}が走った!\")\n", "\n", "red_car = Car(\"田中\", \"赤\", \"乗用車\")\n", "red_car.open()\n", "red_car.run()\n", "\n", "print(\"-\"*50)\n", "\n", "white_car = Car(\"鈴木\", \"白\", \"トラック\")\n", "white_car.open()\n", "white_car.run()" ], "metadata": { "id": "FU7EAoGAUmTb" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "持ち主、色、モデルが違っても、車クラスのインスタンスにすれば「初期化する」「ドアを開ける」「走る」といった機能が共通化されるので、プログラムの理解・作成がとてもやりやすくなります。\n", "\n", "また、ドアを開ける機能を修正したいときに、openメソッドを変更すれば全ての車のインスタンスに変更が適用されるので、乗用車は修正されたけど、トラックは修正されなかった!といった修正ミスが少なくなります。" ], "metadata": { "id": "ZTDN8r4UUxbw" } }, { "cell_type": "markdown", "source": [ "## 継承とオーバーライド\n", "車だけでなく、同じ「乗り物」である「飛行機」も考えてみましょう。\n", "\n", "「持ち主」「色」「モデル」という属性は、車であっても飛行機であっても必要な情報です。「ドアを開ける」という機能も共通でしょう。\n", "\n", "一方、車は「走る」ですが、飛行機は「飛ぶ」という機能の方が適切です。\n", "\n", "このようなとき、「乗り物」というクラスを作って「持ち主」「色」「モデル」という属性や、「ドアを開ける」という機能を定義します。また、「走る」「飛ぶ」を抽象的に表す「動く」という機能を定義しておきます。\n", "\n", "この「乗り物」を継承して「車」「飛行機」というクラスを作り、「動く」が呼ばれたときには、車クラスであれば「走る」、飛行機クラスであれば「飛ぶ」ようにしてあげることができます。これには、メソッドのオーバーライドと呼ばれる機能を使います。\n", "\n", "実際のソースコードを見てみましょう。" ], "metadata": { "id": "pDpa1RT_VdyH" } }, { "cell_type": "code", "source": [ "class Machine:\n", " def __init__(self, owner, color, model):\n", " self.owner = owner\n", " self.color = color\n", " self.model = model\n", " \n", " def open(self):\n", " print(f\"{self.owner}さんの{self.color}の{self.model}のドアが開いた!\")\n", "\n", " def move(self):\n", " # 「走る」「飛ぶ」など様々な乗り物が動く機能を抽象化する\n", " print(f\"{self.owner}さんの{self.color}の{self.model}が動いた!\")\n", "\n", "class Car(Machine):\n", " def move(self):\n", " # Machineクラスのmoveを上書きして「走る」にする\n", " print(f\"{self.owner}さんの{self.color}の{self.model}が走った!\")\n", "\n", "class Airplane(Machine):\n", " def move(self):\n", " # Machineクラスのmoveを上書きして「飛ぶ」にする\n", " print(f\"{self.owner}さんの{self.color}の{self.model}が飛んだ!\")\n", "\n", "red_car = Car(\"田中\", \"赤\", \"乗用車\")\n", "red_car.open()\n", "red_car.move()\n", "\n", "print(\"-\"*50)\n", "\n", "blue_airplane = Airplane(\"佐藤\", \"青\", \"ヘリ\")\n", "blue_airplane.open()\n", "blue_airplane.move()" ], "metadata": { "id": "wmOMtJvhXIvA" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "このように、抽象的なクラスから具体的なクラスへと継承することで、「車」「飛行機」という異なる種類の乗り物であっても、共通の属性や操作は統合して一か所に定義することができます。" ], "metadata": { "id": "lD6OmnR_X4Zr" } }, { "cell_type": "markdown", "source": [ "## まとめ\n", "クラスの特徴・メリットは、「カプセル化」など他にもあります。\n", "\n", "クラスの説明はどうしても難しくなってしまうため、データ構造化よりも身近な例で、簡単に特徴をご紹介しました。\n", "\n", "これらの特徴を生かせる使い方であるかどうか、十分に検討して、クラスを乱用しないように気を付けてください。" ], "metadata": { "id": "muPnbkVdYVB1" } }, { "cell_type": "markdown", "source": [ "# エラー処理のポイント\n", "## 例外の上手な使い方\n", "Pythonのエラーには、構文エラーと例外があります。例外については、「この例外が起きたら、こうする」というのをプログラマが自由に決めることができます。例外は、次のようなtry~exceptを使って処理します。\n", "\n", "~~~\n", "try:\n", "    例外が発生する可能性のある処理\n", "except 発生しうる例外1 as 例外オブジェクト1\n", "    例外1が発生したときの処理\n", "except 発生しうる例外2 as 例外オブジェクト2\n", "    例外2が発生したときの処理\n", "              :\n", "else:\n", "   例外が発生しなかったときだけ動く処理\n", "finally:\n", "   例外が起きても起きなくても必ず動く処理\n", "~~~\n", "\n", "入門書などでは、次のようなプログラム例をよく見かけます。" ], "metadata": { "id": "J9N-Yp0VcsaH" } }, { "cell_type": "code", "source": [ "try:\n", " divval = 0\n", " print(100/divval)\n", "except ZeroDivisionError:\n", " print(\"ZeroDivisionErrorが発生しました\")" ], "metadata": { "id": "vi3jPj9SRGGs" }, "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "try:\n", " idx = 10\n", " mylist = [\"Tokyo\", \"Osaka\", \"Nagoya\"]\n", " print(mylist[idx])\n", "except IndexError:\n", " print(\"IndexErrorが発生しました\")" ], "metadata": { "id": "JqOQz1k8SkN0" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "try文の機能だけに着目して、分かりやすいように簡単なプログラムとしていると思われますが・・・。実際には、このような例外処理を書くことはお勧めしません。\n", "\n", "例外処理の上手な書き方として、次のような観点に着目してください。\n", "\n", "## 例外で処理すべきでない事例\n", "事前にif文でチェックできたり、そもそも例外が発生しないようプログラムを記述できたりするエラーは、「想定可能な事象」とえいます。このような事象は、例外で処理しないことをお勧めします。\n", "\n", "* ZeroDivisionErrorが想定されるケースでは、割る数が0でないかを事前にif文でチェックできます。0であったときは割り算せずに、結果を他の値で代替するなど、プログラムを止めずに続行できることも多いでしょう。\n", "\n", "* IndexErrorは、そもそもプログラムを正しく記述していれば、発生することはほぼないと言えるでしょう。\n", "\n", "## 例外で処理すべき事例\n", "例外処理は、if文でのチェックが難しかったり、プログラムの修正では対処できないような事象でのみ行うことをお勧めします。例えば、openでファイルを開くときのことを考えてみます。このとき、たとえば次のような例外が発生することが考えられます。\n", "\n", "* ファイルが存在しない (FileNotFoundError)\n", "* ファイルアクセス権限がない (PermissionError)\n", "* 新規作成しようとしたがすでに存在する (FileExistsError)\n", "\n", "これらすべてをif文でチェックするのは大変ですし、すべてのエラーに対応できているかはPythonに精通していないと判断が難しいでしょう。\n", "\n", "またそれ以前の問題として、仮にif文ではすべてのエラーをチェックできたとしても、if文でチェックしてからopenで開くまでの間に、ファイルを削除されたり、アクセス権を外されてしまうかもしれません。つまりそもそも、if文のチェックでは完全にこれらのエラーに対処できないのです。\n", "\n", "こういったケースでtry文を使えば、確実にエラーに対処できます。ファイルオープンに関連するエラーはOSErrorという例外1つで処理することができるため、エラー対処に漏れがなく、シンプルなプログラムになります。" ], "metadata": { "id": "ObRFhsD5R_5k" } }, { "cell_type": "code", "source": [ "try:\n", " with open(\"dummy\", \"r\") as f:\n", " print(type(f))\n", "except OSError as e:\n", " print(\"OSError: \" + str(e))" ], "metadata": { "id": "9S1rfiaOUJ5A" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "## やってはいけない例\n", "決してやってはいけない例外の使い方は、「何か例外が起きるけど、よく分からないからpassして動かしちゃえ!」という使い方です。極端なプログラムとして、下記のようなものがあります。\n", "\n", "~~~\n", "try:\n", " 例外が発生する可能性のある処理1\n", " 例外が発生する可能性のある処理2\n", " :\n", "except Exception:\n", " pass\n", "\n", "継続処理\n", "~~~\n", "\n", "このプログラムはどんな例外が起こっても、無視してプログラムが継続します。しかし、もし例外が起きていたら、プログラムが期待通りの動作をしたという保証はありません。\n", "\n", "上記のようなプログラムにはせず、例外が発生したらきちんと調査をして、exceptで適切な処理をしてください。もちろん、調査した結果、無視してよいと確認できたのであれば、passを使っても問題ありません。\n", "\n" ], "metadata": { "id": "sFoe4BxoWPOi" } }, { "cell_type": "markdown", "source": [ "## データ構造化プログラムでのポイント\n", "データ構造化プログラムにおいては、様々な計測データなどをファイルから入力し、構造化されたデータをファイルに出力することになります。そのため、まさに「例外で処理すべき事例」でご紹介したopenの例が該当します。\n", "\n", "また、入力ファイルが正しいという保証がないので、いざオープンに成功しても、読み込んでみないとどんなデータが来るのか分からず、どんな例外が発生するかもあらかじめ想定することは出来ないでしょう。\n", "\n", "データ構造化プログラムでは、ぜひ例外処理を活用してください。ARIM事業のデータ構造化プログラムでは、次のようにメインの処理全体をtry文で囲み、例外が起きたらトレースログを出力することにしています。(セミナー用に、細部は簡略化しています)" ], "metadata": { "id": "hZ2DBAKGdIWr" } }, { "cell_type": "code", "source": [ "import traceback\n", "\n", "try:\n", " #----------------------------------\n", " # データ構造化のメイン処理を記述 \n", " #----------------------------------\n", " # この例ではわざとFileNotFoundErrorを発生させる\n", " with open(\"dummy\") as f:\n", " print(type(f))\n", "except:\n", " print(traceback.format_exc())\n", " exit(1)" ], "metadata": { "id": "I-gPp-kHYdV0" }, "execution_count": null, "outputs": [] }, { "cell_type": "markdown", "source": [ "# 命名規則について\n", "Pythonでプログラムを作成していて、変数名や関数名をどのようにするか、悩んでしまうことがありませんか?\n", "\n", "変数名や関数名の付け方には、いくつかのコツやパターンがあります。\n", "\n", "上手に名前を付けることで、プログラムが非常に読みやすいものとなります。ここでは、変数名や関数名の付け方の勘所をご紹介します。\n", "\n", "## 変数名や関数名の付け方のポイント\n", "変数名や関数名を付ける際は、できる限り、次のようなことを心がけてください。\n", "\n", "* 名前を見ただけで、ある程度は意味が推測できる名前にする\n", "* 「l(エル)」「I(アイ)」「O(オー)」など、紛らわしい文字は単独で使わない\n", "* 長すぎる名前にしない\n", "\n", "### よい例\n", "~~~\n", "serial_number = 10\n", "update_date = \"2022-11-25\"\n", "~~~\n", "\n", "### 悪い例\n", "~~~\n", "xxx = 10 # 意味を推測できない\n", "value_l # 最後は「l(エル)」だが「I(アイ)」や「1(数字)」に見えて紛らわしい\n", "index_from_input_csv_file = 10 # 長すぎる\n", "~~~\n", "\n", "## 命名規則のパターン\n", "変数名や関数名は、1~3個程度の単語を組み合わせてつけることが多いです。\n", "\n", "2個以上の単語を組み合わせるとき、記述方法には次のようなお決まりのパターンがあります。\n", "\n", "|名称|ルール|例|\n", "|---|---|---|\n", "|ローワーキャメルケース|全ての単語の先頭を大文字とし、区切り文字なしでつなぐ|dataTextFile|\n", "|アッパーキャメルケース|最初の単語以外の先頭を大文字とし、区切り文字なしでつなぐ|DataTextFile|\n", "|スネークケース|単語を_(アンダーバー)でつなぐ|data_text_file|\n", "|ケバブケース|単語を-(ハイフン)でつなぐ Pythonでは使用不可|data-text-file|\n", "\n", "Pythonのプログラムにおいて、クラス名をアッパーキャメルケース、その他の変数名や関数名をスネークケースとすることが多いです。\n", "\n", "ARIM事業のコーディング規則でも、そのようにしています。\n", "\n", "また、Pythonは変数名に-(ハイフン)を使えませんので、ケバブケースにすることはできません。\n", "\n", "## ご参考:PEP8について\n", "Pythonの標準パッケージやモジュールは、PEP8というコーディング規約に基づいて作成されています。\n", "\n", "PEP8では、命名規則についても触れられています。\n", "\n", "https://pep8-ja.readthedocs.io/ja/latest/\n", "\n", "Pythonプログラマも、この規約を参考にすることが推奨されています。\n", "\n", "ARIM事業でもコーディング規則を定めており、PEP8をベースにしています。" ], "metadata": { "id": "3lj66WzO5c6p" } } ] }