KATOエンジニヤリング開発日誌

「アウトプット無きエンジニアにインプットもチャンスも無い」の精神で書いています

Software-Design 2018年2月号のPythonライブラリの記事 2

2018年1月に発売された「Software Design 2018年2月号」のPythonライブラリの記事の第2章を読み終わりました。

www.kato-eng.info

第2章 Pythonの基礎力を高めよう

優雅にファイル操作をする

過去に仕事でPythonスクリプトを作成するときにファイルパスを指定することは毎回のようにありました。その際にファイルパスの指定は下記のように記載することが多かったです。

import os
# 実行しているPythonスクリプトファイルが格納されているディレクトリを取得
base_dir = os.path.dirname(os.path.abspath(__file__))

しかし、Python3.6からはpathlibモジュールが使いやすくなったそうで、今後はpathlibモジュールを利用していくといいそうです。

import pathlib
# 実行PythonファイルのPathオブジェクトの作成
base_file = pathlib.Path(__file__)
# resolve関数でPathオブジェクトから絶対パスに変換
abs_base_file = base_file.resolve()
# parentプロパティでディレクトリのパスを取得
base_dir = abs_base_file.parent
# dataファイルが格納されているディレクトリのパスを取得
data_dir = base_dir / '..' / 'data'

他にもディレクトリを再帰的に検索したり、特定のパターンを持ったファイルのみを取得することもできる。

辞書を拡張して便利に使う

文字列から各文字が何回ずつ出現するかをカウントする機能としてcollectionsというライブラリがある。

import collections

data = "Beautiful is better than ugly."
counter = collections.Counter(data)
print(counter)

# 要素数トップ3のデータを抽出する
print(counter.most_common(3))

# キーの値をカウントアップする
counter['t'] += 10
print(counter['t'])
# 割り当てのないキーにアクセスする
print(counter['A'])

好きなデータ構造で値を初期化する

下記のような人の名前と所属するチーム番号を持ったデータ構造があるとする。

member = [
    ('Alice', 1),
    ('Guido', 3),
    ('Monty', 2),
    ('Tim', 3)
]

このデータからチーム番号をキーとしたリストを作りたい。

{1: ['Alice'], 3: ['Guido', 'Tim'], 2: ['Monty']}

この場合Pythonではcollectionsモジュールにあるdefaultdictという、存在しないキーが参照されると新しいキーと値を自動的に生成するクラスがある。

import collections

member = [
    ('Alice', 1),
    ('Guido', 3),
    ('Monty', 2),
    ('Tim', 3)
]

team_members = collections.defaultdict(list)
for name, team in member:
    team_members[team].append(name)

print(team_members)

出力結果

defaultdict(<class 'list'>, {1: ['Alice'], 3: ['Guido', 'Tim'], 2: ['Monty']})

defaultdictはlist以外にintやdictで作成することもできる。

ライブラリの自作

ライブラリを自作することもできる。例として国民の祝日情報を提供するライブラリを作成する。

事前準備

ライブラリ用のディレクトリを作成し、日本の祝日情報を用意する。

# ライブラリ用のディレクトリ作成
Python $mkdir national_holiday
Python $mkdir national_holiday/holiday_data
# 日本の祝日情報を取得
Python $python3
>>> import requests
>>> import pathlib
>>> res = requests.get('https://raw.githubusercontent.com/holiday-jp/holiday_jp/master/holidays.yml')
>>> pathlib.Path('national_holiday/holiday_data/jp.yml').write_text(res.text)

スクリプトの作成

「national_holiday/jp.py」として、日本の祝日判定クラスを実装する。

import pathlib
import datetime

class NationalHoliday():
    def __init__(self):
        self._holidays = None

    @property
    def holidays(self):
        '''holiday情報がself._holidaysに入っていなければ
           load_holidayを読み込んでデータを挿入し、self._holidaysを返す'''
        if self._holidays is None:
            self._holidays = self.load_holidays()
        return self._holidays

    def load_holidays(self):
        '''祝日情報を返す'''
        holiday_dict = {}

        current_dir = pathlib.Path(__file__).resolve().parent
        holiday_file = current_dir / 'holiday_data' / 'jp.yml'

        with open(holiday_file, 'r') as f:
            for line in f:
                day, holiday_name = line.split(': ')
                datetime_ = datetime.datetime.strptime(day, '%Y-%m-%d')
                date = datetime_.date()
                holiday_dict[date] = holiday_name

        return holiday_dict

    def is_holiday(self, date):
        '''引数の日にちが休日・祝日かどうかを返す'''
        return date in self.holidays

このままではnational_holidayをimport時にjpモジュールが見れないので、national_holidayディレクトリに「__init__.py」ファイルを作成する。

Python $echo "from . import jp" > national_holiday/__init__.py

pythonインタプリタを開いて作成したnational_holidayパッケージを利用してみる。

Python $python3
>>> import national_holiday
>>> import datetime
# national_holidayの中身を確認して「jp」が見えているのがわかる
>>> dir(national_holiday)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', 'jp']
>>> holiday = national_holiday.jp.NationalHoliday()
>>> d = datetime.datetime.strptime('2018-05-03', '%Y-%m-%d').date()
>>> holiday.is_holiday(d)
True
>>> d2 = datetime.datetime.strptime('2018-02-05', '%Y-%m-%d').date()
>>> holiday.is_holiday(d2)
False

別ディレクトリのスクリプトからパッケージを参照する

national_holidayパッケージと同じ階層にhelloというディレクトリを作成し、main.pyというファイルを作成する。

Python $mkdir hello
Python $vim hello/main.py
# national_holiday/main.py

import national_holiday
import datetime

jp_holidays = national_holiday.jp.NationalHoliday()
d = datetime.datetime.strptime('2018-05-03', '%Y-%m-%d').date()
print(d, jp_holidays.is_holiday(d))

main.pyからnational_holidayのjpモジュールを利用してみる内容になっている。

これを実行すると、national_holidayモジュールがシステムから見えていないという内容のエラーが発生する。

Python $python3 hello/main.py
Traceback (most recent call last):
  File "hello/main.py", line 1, in <module>
    import national_holiday
ModuleNotFoundError: No module named 'national_holiday'

実行時のディレクトリを移動すると作成したモジュールはimportできないということがわかる。どの環境でも使えるようにするためには「national_holiday」を環境にインストールする必要がある。環境にインストールするとPythonパスが通った場所にスクリプトが配置されるので、どこで実行しても呼び出しが可能となる。

パッケージを環境にインストールする

インストールするための設定ファイルである「setup.py」ファイルをhelloディレクトリと同じ階層に作成する。

Python $vim setup.py
# coding:utf-8
from setuptools import setup

setup(
    name='MyLibrary',
    version=1.0,
    # 使えるようにしたいパッケージを記載する
    packages=['national_holiday'],
    # パッケージが依存しているデータがあれば記載する
    package_data={
        'national_holiday': ['holiday_data/*'],
    },
)

作成したsetup.pyを実行する。

Python $python3 setup.py install
running install
...省略...

Installed /usr/local/lib/python3.6/site-packages/MyLibrary-1.0-py3.6.egg
Processing dependencies for MyLibrary==1.0
Finished processing dependencies for MyLibrary==1.0
Python $

環境にインストール後、別ディレクトリからnational_holidayパッケージが使えることを確認するため、再度「hello/main.py」を実行する。

Python $python3 hello/main.py
2018-05-03 True