Python School 2.0.0 documentation

クラスを定義する

«  組込み関数 (zip / dict) を使う   ::   Contents   ::   O/R マッパー経由でデータベースを使う  »

クラスを定義する

ここまでの章では、基本的なデータ構造である tuple, list, dict を使ってきました。 データを保持するだけの場合はこれらで十分ですが、データを使って何らかの処理を実現するには 「クラス」としてカプセル化しておくと便利なことが多々あります。 たとえば、1日の相場の動きを計算する処理もデータ構造とセットになっていると使いやすくなります。

Python の場合は class キーワードを使ってクラスを定義します。 クラスを実装するときは self というキーワードが欠かせません。 初めてクラスを実装するときは、いわゆるおまじないのようなものという理解でも構いませんが、 何度か実装を繰り返すとその必要性や意味について疑問が出てくる場合がほとんどでしょう。 詳しく知りたい場合はこちらのブログ記事を読んでみましょう。

なお、 self を使う必要のないメソッドは、そもそもそのクラスに定義すべきメソッドではないかもしれません。 Python はトップレベルに関数を定義できますので、無理にクラスを定義する必要はないことを忘れないでください。 (Java の場合はすべての関数を何らかのクラスに実装する必要があります)

簡単な構文と定義

StockPrice というクラスを定義してみます。 コンストラクタとして「日付,始値,高値,安値,終値」を受け付けます。 内部的に値を保持するときは小数に変換します。 1日の動き値は diff() というメソッドで呼び出せるようにします。

class StockPrice(object):

    def __init__(self, day, price_begin, price_max, price_min, price_end):
        self.day = day
        self.price_begin = float(price_begin)
        self.price_max = float(price_max)
        self.price_min = float(price_min)
        self.price_end = float(price_end)

    def diff(self):
        return self.price_end - self.price_begin

使うときは以下のように呼び出します。 引数の tuple, list にアスタリスクを付けると変数展開されますので、覚えておくと便利です。 dict の場合はアスタリスクを2つ付けると変数名と値に展開されます。

r = "2014-06-06,15138.75,15144.34,15042.59,15077.24".split(",")
p1 = StockPrice(r[0], r[1], r[2], r[3], r[4])
print(p1)
p2 = StockPrice(*r)
print(p2)

実装の改修

csv-2.py を改修して csv-5.py とします。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Parse daily Tokyo stock prices.
"""

import argparse
import csv  # import standard "csv" module
import logging


class StockPrice(object):

    def __init__(self, day, price_begin, price_max, price_min, price_end):
        self.day = day
        self.price_begin = float(price_begin)
        self.price_max = float(price_max)
        self.price_min = float(price_min)
        self.price_end = float(price_end)

    def diff(self):
        return self.price_end - self.price_begin


def parse_args():
    """Parse arguments and set up logging verbosity.

    :rtype: parsed arguments as Namespace object.
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("-f", "--file", dest="filename",
                        help="setting file", metavar="FILE")
    parser.add_argument("-o", "--output", dest="output",
                        help="output file", metavar="FILE")
    parser.add_argument("-n", "--dryrun", dest="dryrun",
                        help="dry run", default=False, action="store_true")
    parser.add_argument("-v", "--verbose", dest="verbose", default=False,
                        action="store_true", help="verbose mode")
    parser.add_argument("-q", "--quiet", dest="quiet", default=False,
                        action="store_true", help="quiet mode")
    # Add this line from boilerplate.
    parser.add_argument("filename", nargs=1, help="CSV file path")

    args = parser.parse_args()

    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    elif not args.quiet:
        logging.basicConfig(level=logging.INFO)

    return args


def process(args):
    """Parse daily Tokyo stock prices, and calculate up/down.
    """
    with open(args.filename[0]) as fp:
        reader = csv.reader(fp)  # Instantiate CSV reader with file pointer.
        for t in reader:
            p = StockPrice(*t)
            diff = p.diff()
            if diff > 0:
                message = 'up'
            elif diff < 0:
                message = 'down'
            else:
                message = 'same'
            # Write out day, up/down/same, and diff.
            print('{}\t{:5}\t{}'.format(p.day, message, round(diff, 2)))


def main():
    args = parse_args()
    process(args)


def test():
    pass

if __name__ == '__main__':
    main()

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

csv-2.py からの差分は以下の通りです。

@@ -9,6 +9,19 @@
 import logging


+class StockPrice(object):
+
+    def __init__(self, day, price_begin, price_max, price_min, price_end):
+        self.day = day
+        self.price_begin = float(price_begin)
+        self.price_max = float(price_max)
+        self.price_min = float(price_min)
+        self.price_end = float(price_end)
+
+    def diff(self):
+        return self.price_end - self.price_begin
+
+
 def parse_args():
     """Parse arguments and set up logging verbosity.

@@ -44,14 +57,8 @@
     with open(args.filename[0]) as fp:
         reader = csv.reader(fp)  # Instantiate CSV reader with file pointer.
         for t in reader:
-            # Assign each field on individual variables.
-            day = t[0]
-            price_begin = float(t[1])
-            price_max = float(t[2])
-            price_min = float(t[3])
-            price_end = float(t[4])
-            # Calculate the differenciate of the day.
-            diff = price_end - price_begin
+            p = StockPrice(*t)
+            diff = p.diff()
             if diff > 0:
                 message = 'up'
             elif diff < 0:
@@ -59,7 +66,7 @@
             else:
                 message = 'same'
             # Write out day, up/down/same, and diff.
-            print('{}\t{:5}\t{}'.format(day, message, round(diff, 2)))
+            print('{}\t{:5}\t{}'.format(p.day, message, round(diff, 2)))


 def main():

パッケージング

スクリプトをたくさん書いていくと、似たような実装が様々な場所に散り散りになってしまいます。 全てをフラットに配置すると依存関係の把握が難しくなってしまいますので、パッケージとしてまとめます。 ここでは、引数処理の関数をパッケージとして共有しておきましょう。 プロジェクト内で共通となるデータ構造やクラス定義もまとめておくと良いでしょう。

適切にパッケージを作成することで次のようなメリットがあります。

  • 関数やクラスを再利用できるようになる。
  • 全体の見通しが立ちやすくなる。
  • 部分的にテストできるようになる。

Python のパッケージは基本的にディレクトリ構造に一致します。 あるディレクトリを Python のパッケージとして有効にするためには、次のふたつの条件を満たす必要があります。

  • パッケージのトップディレクトリが sys.path に含まれる。
  • それぞれのディレクトリは __init__.py を持つ。

__init__.py は特別な意味を持つファイルで、パッケージに関するメタ情報などを記述します。 単なる Python ファイルですから、普通に Python スクリプトを記述しても構いません。 中身は空っぽでも構いませんが、コメントに概要くらいは書いておくと親切ですね。

たとえば、引数処理の関数を cmdline.py ファイルに記述して pyschool パッケージに入れると、次の構造になります。

pyschool
        __init__.py
        cmdline.py

スクリプトから使う場合は import を変更します。 importfrom と合わせて使うこともできます。 この記法を使うと、既存のコードをほとんど変更せずにモジュールを導入できます。

from pyschool.cmdline import parse_args

これまでに記述した csv-*.py スクリプトは共通処理ばかりですので、試してみましょう。

Note

インポートするモジュールのパス解決

Python スクリプトは、それだけでモジュールとしてインポートできます。 インポートするディレクトリの指定方法はいくつかあります。

  • 標準ライブラリ置き場 (pyvenv/virtualenv 環境の Lib など)
  • カレントディレクトリ
  • 環境変数 PYTHONPATH で指定
  • Python スクリプト内で sys.path に追加

実際には sys.path のリストが全てですが、その実現方法は色々ある、と覚えておいてください。

上記の csv-5.py は以下のように変更できます。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Parse daily Tokyo stock prices.
"""

import csv  # import standard "csv" module

from pyschool.cmdline import parse_args


class StockPrice(object):

    def __init__(self, day, price_begin, price_max, price_min, price_end):
        self.day = day
        self.price_begin = float(price_begin)
        self.price_max = float(price_max)
        self.price_min = float(price_min)
        self.price_end = float(price_end)

    def diff(self):
        return self.price_end - self.price_begin


def process(args):
    """Parse daily Tokyo stock prices, and calculate up/down.
    """
    with open(args.filename[0]) as fp:
        reader = csv.reader(fp)  # Instantiate CSV reader with file pointer.
        for t in reader:
            p = StockPrice(*t)
            diff = p.diff()
            if diff > 0:
                message = 'up'
            elif diff < 0:
                message = 'down'
            else:
                message = 'same'
            # Write out day, up/down/same, and diff.
            print('{}\t{:5}\t{}'.format(p.day, message, round(diff, 2)))


def main():
    args = parse_args()
    process(args)


def test():
    pass

if __name__ == '__main__':
    main()

# vim: set et ts=4 sw=4 cindent fileencoding=utf-8 :

次の章では、オブジェクト定義に O/R マッパーを使ってみましょう。

«  組込み関数 (zip / dict) を使う   ::   Contents   ::   O/R マッパー経由でデータベースを使う  »