utamaro’s blog

誰かの役に立つ情報を発信するブログ

pythonを使ってファクトリメソッドパターンを実装する方法について

python を使ってファクトリメソッドパターンの実装をした際に試した内容です。

ファクトリメソッドパターンの作り方としては二通り知っていますが、そのうちの「カテゴリによって実行するインスタンスを決める」方法の実装を紹介します。

今回はデコレータを使ったパターンと、使わなかったときのパターンを比較して、どちらを選択したほうが良いか考えます。

結論としては、デコレータを使ったパターンの方がコード量が少なく、メンテ時の修正範囲も少なくなると思いました。

環境について

  • python3.9.0

題材について

以下の条件を満たすクラス設計をします。

  • 従業員が存在する
  • 従業員は2つのカテゴリで分類される
    • エンジニア
    • 営業
  • 各従業員には名前がある
  • 各従業員はカテゴリ別に通勤方法が決まっている
    • エンジニアは徒歩で通勤する
    • 営業は電車で通勤する
  • 従業員の名前と通勤方法を出力できる

デコレータを使ったパターン

デコレータを使うことで、どのクラスがファクトリーの対象となっているのかがわかりやすくなっています。

from enum import Enum


class Employee:
    def __init__(self, name):
        self._name = name

    def commute_method(self):
        # 本来は出社方法を集約かコンポジションで管理するのが良いです。
        # 簡易的な実装にするために関数を実装するようにします。
        # もしくは、出社するためのクラスを別に用意するのが良いと思います。
        raise NotImplementedError

    @property
    def name(self):
        return self._name


class CategoryTypes:
    engineer = 'engineer'
    sales = 'sales'


class EmployeeFactory:
    class_ = {}

    @classmethod
    def register(cls, category):
        def wrapper(cls_):
            if not issubclass(cls_, Employee):
                raise TypeError
            cls.class_[category] = cls_
            return cls_
        return wrapper

    @classmethod
    def create(cls, category):
        cls_ = cls.class_[category]
        return cls_


@EmployeeFactory.register(CategoryTypes.engineer)
class Engineer(Employee):

    def commute_method(self):
        return '歩いて出社'


@EmployeeFactory.register(CategoryTypes.sales)
class Sales(Employee):
    def commute_method(self):
        return '電車で出社'


class EmployeeInfo:
    def __init__(self, employee_):
        self._employee = employee_

    def print(self):
        print('\n'.join([
            f'名前: {self._employee.name}',
            f'通勤方法: {self._employee.commute_method()}',
        ]))


if __name__ == '__main__':
    employee = EmployeeFactory.create(CategoryTypes.engineer)
    sales = EmployeeFactory.create(CategoryTypes.sales)
    EmployeeInfo(employee('Aさん')).print()
    EmployeeInfo(sales('Bさん')).print()

実行結果

名前: Aさん
通勤方法: 歩いて出社
名前: Bさん
通勤方法: 電車で出社

デコレータを使わない例

デコレータを使った例を基に、デコレータを使わなかったときのコードを考えてみます。

from enum import Enum


class Employee:
    def __init__(self, name):
        self._name = name

    def commute_method(self):
        # 本来は出社方法を集約かコンポジションで管理するのが良いです。
        # 簡易的な実装にするために関数を実装するようにします。
        # もしくは、出社するためのクラスを別に用意するのが良いと思います。
        raise NotImplementedError

    @property
    def name(self):
        return self._name


class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'


class Engineer(Employee):

    def commute_method(self):
        return '歩いて出社'


class Sales(Employee):
    def commute_method(self):
        return '電車で出社'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
    }

    @classmethod
    def create(cls, category_type):
        if category_type in cls.FACTORY_MAP:
            return cls.FACTORY_MAP[category_type]
        raise TypeError


class EmployeeInfo:
    def __init__(self, employee_):
        self._employee = employee_

    def print(self):
        print('\n'.join([
            f'名前: {self._employee.name}',
            f'通勤方法: {self._employee.commute_method()}',
        ]))


if __name__ == '__main__':
    employee = EmployeeFactory.create(CategoryTypes.engineer)
    sales = EmployeeFactory.create(CategoryTypes.sales)
    EmployeeInfo(employee('Aさん')).print()
    EmployeeInfo(sales('Bさん')).print()

デコレータを使った場合と異なるのは、ファクトリを作成するクラス(EmployeeFactory)です。

また、カテゴリを管理するクラスがEnumになっています。

なぜEnumになっているのかというと、デコレータを使った場合の実装だと、Enum.field.valueのようにvalueを書く必要があったからです。 valueを書くのを忘れてエラーになってしまうことが多いため、あえてEnumでの実装を避けています。

class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
    }

    @classmethod
    def create(cls, category_type):
        if category_type in cls.FACTORY_MAP:
            return cls.FACTORY_MAP[category_type]
        raise TypeError

比較する

デコレータを使った場合と、デコレータを使わなかった場合を比較すると、デコレータを使った場合の方がカテゴリの追加時(更新時)に修正範囲が小さいです。

例えば、新しく「カスタマーサポート」を追加する際を考えてみます。

デコレータを使った実装の場合は以下の修正が必要になります。

class CategoryTypes:
    engineer = 'engineer'
    sales = 'sales'
    customer_support = 'customer_support'


@EmployeeFactory.register(CategoryTypes.customer_support)
class CustomerSupport(Employee):
    def commute_method(self):
        return '自転車で出社'


customer_support = EmployeeFactory.create(CategoryTypes.customer_support)
EmployeeInfo(customer_support('Cさん')).print()

このように、カテゴリを追加して、そのクラスを用意するだけで修正が完了します。

デコレータを使わない場合についても同様に考えてみます。

class CategoryTypes(Enum):
    engineer = 'engineer'
    sales = 'sales'
    customer_support = 'customer_support'


class CustomerSupport(Employee):
    def commute_method(self):
        return '自転車で出社'


class EmployeeFactory:
    FACTORY_MAP = {
        CategoryTypes.engineer: Engineer,
        CategoryTypes.sales: Sales,
        CategoryTypes.customer_support: CustomerSupport,
    }
    # 略


customer_support = EmployeeFactory.create(CategoryTypes.customer_support)
EmployeeInfo(customer_support('Cさん')).print()

このように、FACTORY_MAPを修正するひと手間が必要になってしまいます。

今回はとてもシンプルな例で比較したため、1箇所のみの差しかありませんでしたが複雑になれば大きな差になるため気をつけたほうが良いです。

結論

ファクトリメソッドパターンを python で実装する場合は、デコレータを積極的に使うことで、修正箇所が少なくなります。

指定したキーをdictから再帰的に検索して削除する方法

Djangoを使っているなかで、apiのテストをする際に特定のkeyを取り除きたいということがありました。

例えば、レスポンスにcreated_atが含まれている場合、レコードが作成された日が入ると、assertDictEqualでは期待値と一致しないためエラーとなってしまいます。

その際にcreated_atを取り除くことが目的です。

これによって、一つ一つのkeyを取り出して期待値と一致するか検証する必要がなくなり、assertDictEqualを使って検証することができるようになります。

※ 値を取り除くよりも、freezegunを利用してdatetime.nowにパッチするのが良いと思われます。

コード

remove_keysを実行することでdictから指定したキーを取り除けます。

また、valueがlistやdictであった場合も対応できます。

def remove_keys(d: Dict, _remove_keys: List[str] = None):
    if _remove_keys is None:
        raise ValueError('keys must be required.')
    for k in list(d.keys()):
        v = d[k]
        if k in _remove_keys:
            del d[k]
        if isinstance(v, dict):
            remove_keys(v, _remove_keys)
        elif isinstance(v, list):
            for vd in v:
                remove_keys(vd, _remove_keys)

使い方

sample = {
    'id': 1,
    'name': 'sample',
    'description': 'sample',
    'created_at': int(datetime.now().timestamp()),
    'modified_at': int(datetime.now().timestamp()),
    'obj': {
        'id': 1,
        'name': 'sample',
        'description': 'sample',
        'created_at': int(datetime.now().timestamp()),
        'modified_at': int(datetime.now().timestamp()),
    },
    'list_obj': [
        {
            'id': 1,
            'name': 'sample',
            'description': 'sample',
            'created_at': int(datetime.now().timestamp()),
            'modified_at': int(datetime.now().timestamp()),
        },
        {
            'id': 2,
            'name': 'sample',
            'description': 'sample',
            'created_at': int(datetime.now().timestamp()),
            'modified_at': int(datetime.now().timestamp()),
        },
    ]
}
remove_keys(sample, ['created_at', 'modified_at'])
assert sample == {
    'id': 1,
    'name': 'sample',
    'description': 'sample',
    'obj': {
        'id': 1,
        'name': 'sample',
        'description': 'sample',
    },
    'list_obj': [
        {
            'id': 1,
            'name': 'sample',
            'description': 'sample',
        },
        {
            'id': 2,
            'name': 'sample',
            'description': 'sample',
        },
    ]
}

pythonを使ってツイートをlikeした人の一覧を取得する方法

twitter apiを利用して自身のツイートをいいねしたユーザの一覧を取得します。

このapiは2021年7月6日段階でtwitter-pythonライブラリには実装されていませんでした。

なのでrequestsライブラリとrequests_oauthlibを利用してAPIを実行します。

環境

  • ProductName: macOS
  • ProductVersion: 11.2.3
  • python: 3.9.4

requirements.txt

certifi==2021.5.30
chardet==4.0.0
future==0.18.2
idna==2.10
oauthlib==3.1.1
python-twitter==3.5
requests==2.25.1
requests-oauthlib==1.3.0
urllib3==1.26.6

コードについて

結構簡単に実装できるので、解説はせずにサンプルコードを示します。

サンプルコード

import requests
from requests_oauthlib import OAuth1

import settings


def _make_url(tweet_id):
    return f'https://api.twitter.com/2/tweets/{tweet_id}/liking_users'


if __name__ == '__main__':
    url = _make_url('xxxxx')
    auth = OAuth1(
        'CONSUMER_KEY',
        'CONSUMER_SECRET',
        'ACCESS_TOKEN_KEY',
        'ACCESS_TOKEN_SECRET',
    )
    r = requests.get(url, auth=auth)
    print(r.text)

コードの中のOAuth1に渡している値については、twitterでプロジェクトを作成したときに作った値を入れてください。

_make_urlに渡しているxxxxxについては、ツイートのIDを指定します。

このIDはツイッターをブラウザで開き、ツイートをクリックすることで確認できます。 クリックしたあとのアドレスバーが以下のようになります。

https://twitter.com/{Sample}/status/{tweet_id}

このときの{tweet_id}に入っている値を指定します。

このコードを実行すると、以下のような結果を取得できます。 ※ 例として取得した値は編集されています。

{
    "data": [
        {
            "id": "1",
            "name": "サンプルA",
            "username": "sample_a"
        },
        {
            "id": "2",
            "name": "サンプルB",
            "username": "sample_b"
        }
    ],
    "meta": {
        "result_count": 2
    }
}

あとはこれを使うだけです。

今回はテキストで出力したかったのでr.textを使いましたが、以下の例のようにr.json()を使うことでdict形式で取得できます。

liking_users = r.json()
users = liking_users.get('data')
for user in users:
    print(user.get('username'))

注意

今回紹介したAPIは15分あたり75回の実行制限があります。 https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/get-tweets-id-liking_users

1分あたり5回実行できるので、12秒に1回実行できる計算です。

一度エラー制限に引っかかるように使用し、その際に起きるエラーやステータスコードを確認した上で実装することをおすすめします。

2021年版 pythonの開発環境を整える

概要

pythonの開発環境を整えて、スクリプトを実行できるのを目標にします。

pythonの開発環境ですが、2021年7月現在で私が使用している構成を基にご紹介します。

私の場合は、pyenvで任意のバージョンをインストールし、venvを使ってローカル環境を作成しています。

以下その環境を導入するまでの目次です。

  • 作業環境について
  • pyenv をインストールする
  • pyenv の使い方
    • pythonをインストールする
    • 使用するバージョンを設定する
    • 仮想環境を使う
  • 注意すること

作業環境について

  • ProductName: macOS
  • ProductVersion: 11.2.3
  • BuildVersion: 20D91
  • Homebrew 3.2.0
  • zsh 5.8 (x86_64-apple-darwin20.0)

pyenv をインストールする

(公式のドキュメント)https://github.com/pyenv/pyenv通り、brewを使ってインストールします。

brew install pyenv

git を使った方法もありますが、管理が若干面倒なのでおすすめしません。 ただし、最新のバージョンを使用することができるので、バージョンを切り替えたりしたい方にはおすすめです。 一般的にwebの開発をしている方にとっては不要だと思います。

pyenv をインストールできたら、~/.zshrc に以下の記述を追加します。

######################################################
# python settings
######################################################
# pyenv

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"

if command -v pyenv 1>/dev/null 2>&1; then
  eval "$(pyenv init -)"
fi

(見やすくするためにpython用の設定とわかるようにしています)

pyenv の使い方

pyenvのヘルプを確認する場合は、以下のコマンドを実行すると確認できます。

pyenv --help

pythonをインストールする

python3.9.4をインストールする

pyenv install 3.9.4

python2環境用に2.7.18をインストールする

pyenv install 2.7.18

使用するバージョンを設定する

pyenvのグローバルにデフォルトで3.9.4を使用するようにし、python2用に2.7.18を使うようにする

pyenv global 3.9.4 2.7.18

カレントディレクトリで使用するバージョンを指定する

pyenv local 3.9.4

仮想環境を使う

pythonコマンドを利用して、仮想環境を用意する

python -m venv venv

上記コマンドを実行すると、venvというディレクトリが作成されます。

仮想環境を使用する

source ./venv/bin/activate

すると、ターミナルが以下の様になります。

(venv) ➜  ~/sample ✗

この状態でpipコマンドを利用してライブラリをインストールできます。 ためしに、requestsをインストールするコマンドを例として出します。

pip install requests

仮想環境の利用をやめる

deactivate

deactivateと打つと仮想環境の利用をやめることができます。

注意すること

venvを利用した開発の場合、作成されるバージョンはグローバルのpythonのバージョンと一致します。

今回の場合だと3.9.4で仮想環境が作成されます。

例えば、以下のコマンドを実行すると、バージョンが3.9.1の環境を用意できます。

pyenv install 3.9.1
pyenv local 3.9.1
python -m venv venv_391
source ./venv_391/bin/activate
python --version

--versionの結果

Python 3.9.1

これを利用すると、1つのプロジェクトに複数のバージョンの環境を用意できます。

virtualenvを使った場合、特定のバージョンを利用して名前をつけて環境を用意する必要がありました。 これがローカルファイル(venv等)で用意できるので、環境の切り替えが簡単になります。

Bootstrap5を使ったレビュー一覧画面を作りました。

概要

2019年に作成したプログラムを改修し、bootstrap5を利用したレビューの一覧画面を作成しました。

テンプレートはWordPress用ではなく、自ら開発する方向けとなっています。

例えば、現在作成しているサービスのレビューサイトへ追加するといったことが可能です。

開発時にvuejsやreactjsに移行しやすいようにjavascriptの実装は最低限としているため、動きのあるテンプレートではありません。

ご利用方法や詳細については下記を参考にお願いいたします。

ビルド済みのファイルについては含まれません。

理由としては、webpackを利用してファイルをビルドしているため、ビルド済みのファイルを含めると参照パスが正しく設定されないためです。

動作を確認する場合、後述のビルドコマンドを実行してください。

サンプル画像

f:id:miyaji-y26:20210704130729p:plain
通常の画面サイズで表示した場合のイメージ
f:id:miyaji-y26:20210704130807p:plain
スマホを想定した画面のイメージ

購入はこちらからできます。
https://samplecodes.uc.r.appspot.com/checkout/start/3

レビュー一覧テンプレートについて

以下の機能が含まれています。

  • レビュー評価のプログレスバー
  • レビューの一覧
  • レビューの一覧をソートするセレクトボックス
  • レビューを投稿するためのモーダルウィンドウ
  • ページング用のリンク
  • mobile向けのレスポンシブ機能
  • 表示しているレビュー位置 (例: 1 - 10 of 20 review)

レビューの内容は以下の要素が含まれています。

  • タイトル
  • 投稿日時 (例: 2019-07-25 post)
  • レビューの内容
  • レート (星の数を表示)

利用する際に必要なスキルについて

  1. htmlを書けること
  2. javascript or typescriptを書けること
  3. webpackによるビルド方法がわかること
  4. css or scssを書けること

購入ファイルについて

.
├── package-lock.json
├── package.json
├── readme.md
├── src
│   ├── assets
│   │   ├── reset
│   │   │   ├── normalize.scss
│   │   │   └── scripts.ts
│   │   └── review
│   │       ├── scripts.ts
│   │       └── styles.scss
│   └── html
│       └── index.html
├── tsconfig.dev.json
├── tsconfig.json
├── tsconfig.prod.json
├── webpack.base.config.js
├── webpack.dev.config.js
└── webpack.prod.config.js

動作環境について

  • node: v16.1.0
  • npm: 7.11.2

利用方法について

利用する際は以下のように利用してください。

  1. npm install
  2. npm run webpack-dev
  3. webpackによってビルドが行われ、distフォルダが作成されます。
  4. src/html/index.htmlを開きます。

webpack-prodを実行すると、プロダクション環境へデプロイするためのコードを生成できます。 具体的な設定については、以下のファイルを確認してください。

  • tsconfig.prod.json
  • webpack.prod.config.js

購入はこちらからできます。
https://samplecodes.uc.r.appspot.com/checkout/start/3

「会社はムダが9割」という本を読んでみた感想

本のタイトルから気になって読んでみました。

エンジニアの立場からするとムダなことを簡略化して、無くすということを考えることがあります。

その参考にしようと思って読んでみました。

感想

1回読んだときは内容が頭に入ってきませんでした。

ISO?なにそれ。そんなことよりも、ムダの改善方法や事例を教えてくれと。

「○○はムダ」という章に対して、それがなぜムダなのかがわかりやすく書かれていないこと、どのような基準で「ムダ」といっているのかが分からなかったからです。

例えば「社員同士で遠慮するムダ」という章があるのですが、最後の方でようやく「遠慮を無くすことで仕事が上手く回り始めた」といった記載がありました。

ただ、それも結局「仕事をどんどん振って現場に連れて行く」ということをやるようにアドバイスをしたからと読み取れました。

定価で買うだけの価値はないかな?
という感想がはじめに来ました。中古本かKindleUnlimitedで0円で読むのが良いと思います。

分かりづらい部分もありますが、参考になる箇所もあります。

話の中には実は自分もやっているかなと思うものがありました。

例えば「実行しないムダ」「自分のことがわからないムダ」という章があります。

前者は計画を立てただけでは意味が無く、走りながら修正する。

実際に運用しながら微調整を加えて精度を上げるというものです。

後者は自分の性質を知るということ。

何をしたいのか、何が好きなのか、何が嫌いなのか。

現状分析を行い、ゴールから逆算して計画を見直すことで結果が出せるというものです。

まとめ

私が「会社はムダが9割」という本で得られたことは、会社内のムダを知るというよりも、自分のための気づきでした。

今の自分が何を改善すればよいのかという指標を得るためのツールとして、この本を使うのが良いと思います。

DjangoでPageNotFound時にjsonを返す方法

Djangoapiを作っている際に、自分で定義していないpathをリクエストしたところ404ページが表示されました。

apiを使うため、これをjsonで返すように変更したいと考えて、実現する方法を調べてみました。

参考にしたページ一覧

Error handling
https://docs.djangoproject.com/en/3.0/topics/http/urls/#error-handling

handler404
https://docs.djangoproject.com/en/3.0/ref/urls/#handler404

Customizing error views
https://docs.djangoproject.com/en/3.0/topics/http/views/#customizing-error-views

実現方法

まずはproject/urls.pyを以下のようにします。

from django.http import JsonResponse
from django.urls import path, include


def handle_404(request, exception):
    return JsonResponse({"status": "page not found."})

def handle_403(request, exception):
    return JsonResponse({"status": "forbidden."})

def handle_500(request):
    return JsonResponse({"status": "server error."})


urlpatterns = [
    path('api/', include('api.urls')),
]

handler403 = 'project.urls.handle_403'
handler404 = 'project.urls.handle_404'
handler500 = 'project.urls.handle_500'

この記述の中のhandlerXXXが重要な部分です。

この記述はurls.pyに書くようにドキュメントにかかれています。 また、参考にできるコードはdjango.views.defaults.page_not_foundから確認できます。

import用のパスをproject.urls.handle_403のようにしていますが、これは以下のように関数を代入しても良いです。

handler403 = handle_403
handler404 = handle_404
handler500 = handle_500

handle_404関数についてはurls.pyに書く必要はなく、別のファイルに書いても大丈夫です。

例えば、restframework用の例外ハンドラと同じ場所に書くのが良いと思います。

検証

検証する際にDEBUG = TRUEではできないのでFALSEにします。

curlを利用してリクエストします。

curl http://localhost:8000/api/hoge/piyo

結果

{"status": "page not found."}

このときのexception変数のインスタンスResolver404でした。 404でハンドリングしているため、Http404で例外が出ていると予想していたのですが間違っていました。

というかdocstringに以下のように書かれていたのでわかっていませんでした。

The message from the exception which triggered the 404 (if one was supplied), or the exception class name

なんのインスタンスなのか書いておいてほしいです。

Resolver404だったので、api内部でraise Http404としてもハンドラでキャッチされませんでした。

私の場合はdjangorestframework用の例外ハンドラを用意していたので、そちらでキャッチされました。