Pythonで学ぶデザインパターン入門 Iteratorパターン
結城先生の Java言語で学ぶデザインパターン入門
のコードをPythonに書き直していくシリーズ第1段です。
Iteratorパターンとは
ざっくり言えばデータを順番にひとつひとつ数え上げていくパターンです。
具体的には、入れ物(Aggregate)に対して、反復して数える仕組み(Iterator)を構築すると行った仕組みです。
そんなの普通じゃんと言われればそれまでですが、ひとまず紹介させていただきます。
ソースコード
コードの概要
ここでは、入れ物は本棚(BookShelf
)で数え上げる仕組みはBookShelfIterator
と定義されます。
本棚の中に本を入れていき、最後にはすべてを順番にひとつずつ読み上げていくコードです。
具体的なソースコード
Aggragete(入れ物)
# coding: utf-8 from abc import ABCMeta, abstractmethod from Iterator.iterator import Iterator class Aggregate(metaclass=ABCMeta): @abstractmethod def iterator(self) -> Iterator: pass
こちらは入れ物の抽象クラスになります。
具体的な本棚などの入れ物は、こちらを継承します。
Iterator(数え上げる仕組み)
# coding: utf-8 from abc import ABCMeta, abstractmethod class Iterator(metaclass=ABCMeta): @abstractmethod def has_next(self) -> bool: pass @abstractmethod def next(self): pass
こちらは数え上げる仕組みの抽象クラスになります。
本棚を数え上げる仕組みなどの具体的な数え上げる仕組みはこちらを継承します。
Book(本)
# coding: utf-8 class Book(object): def __init__(self, name: str): self.__name = name def get_name(self) -> str: return self.__name
こちらは実際に数え上げるものになります。
BookShelf(本棚)
# coding: utf-8 from Iterator.Aggregate import Aggregate from Iterator.book import Book from Iterator.iterator import Iterator class BookShelf(Aggregate): def __init__(self, maxsize: int): self.__book = [Book('undefined')] * maxsize self.__last = 0 def get_book_at(self, index: int) -> Book: return self.__book[index] def append_book(self, book: Book): self.__book[self.__last] = book self.__last += 1 def get_length(self) -> int: return self.__last def iterator(self) -> Iterator: from Iterator.book_shelf_iterator import BookShelfIterator return BookShelfIterator(self)
こちらは数え上げるものを入れるために入れ物ですね。
Aggregateが継承されています。
写経のままだとどうしても循環インポートになっちゃうので、メソッドローカルに無理やりインポートしました。
Javaだとコンパイルエラーにならないんですね。。。
Pythonだとどれが最適解なのか、PHPみたいにnamespaceじみたことをするか、 BookShelfIterator
を同じファイルに書くかとかしか思いつかないですね。。。
BookShelfIterator(実際に本棚をひとつひとつ数えあげる仕組み)
# coding: utf-8 from Iterator.book_shelf import BookShelf from Iterator.iterator import Iterator class BookShelfIterator(Iterator): def __init__(self, bookshelf: BookShelf): self.__bookshelf = bookshelf self.__index = 0 def has_next(self) -> bool: if self.__index < self.__bookshelf.get_length(): return True else: return False def next(self): book = self.__bookshelf.get_book_at(self.__index) self.__index += 1 return book
こちら、本棚の中身をひとつひとつ数え上げるための仕組みですね。
Iteratorが継承されています。
実行ファイル
# coding: utf-8 from Iterator.book import Book from Iterator.book_shelf import BookShelf if __name__ == '__main__': bookshelf = BookShelf(4) bookshelf.append_book(Book('Around the World in 80 Days')) bookshelf.append_book(Book('Bible')) bookshelf.append_book(Book('Cinderella')) bookshelf.append_book(Book('Daddy-Long-Legs')) it = bookshelf.iterator() while it.has_next(): book = it.next() print(book.get_name())
実際にPythonに書いてみると、Javaみたいに変数に型をつけられなかったり、循環インポートの罠があったりで大変ですね。
実際どう便利か?
上記のソースコードの例を見てわかるとおり、入れ物と入れ物を数え上げる仕組みが別になっております。
つまり入れ物の中を変更せずとも、数え上げる仕組みに何かを追加することで数え上げにアレンジを加えることができます。
数え上げのための共通の仕組みがあることで、データベースからデータ一覧を抽出した時のデータの形式が共通だったりするので、設計の上ではとても大事なデザインパターンです。
この仕組みが考慮されていないと、ライブラリやフレームワークによって出力したデータのいじり方が変わってきたりしてとても大変でしょうね。
Pythonで学ぶデザインパターン入門
結城先生の名著である Java言語で学ぶデザインパターン入門
をPythonに書き直して勉強していこうコーナーを急にやりたくなりました。
以下に順番に掲載していきます。めざせ全部網羅
1章 Iteratorパターン
ひとつひとつ順番に走査して数え上げていくパターン saruhei1989.hatenablog.com
2章 Adapterパターン
既存の処理のラッパーパターン
3章 Template Methodパターン
抽象クラスに処理のテンプレートを記載してサブクラスで詳細実装
4章 Factory Methodパターン
Template Methodをインスタンス生成の箇所に特化させる
5章 Singletonパターン
必ず1個しかないことを保証する
6章 Prototyprパターン
複製する
7章 Builderパターン
別クラスで具体的な処理を記載してコントロールする
Python3.7入れる時に `No module named '_ctypes'` エラー
Python3.6までは問題なくビルドできたのに3.7で _ctypes
のエラーがでてビルドできないといった嘆きをよく見ます。
$ pipenv --python 3.7.2 Warning: Python 3.7.2 was not found on your system… Would you like us to install CPython 3.7.2 with pyenv? [Y/n]: Y Installing CPython 3.7.2 with pyenv (this may take a few minutes)… ✘ Failed... Something went wrong… Downloading Python-3.7.2.tar.xz... -> https://www.python.org/ftp/python/3.7.2/Python-3.7.2.tar.xz Installing Python-3.7.2... BUILD FAILED (CentOS Linux 7 using python-build 20180424) Inspect or clean up the working tree at /tmp/python-build.20190405065753.24551 Results logged to /tmp/python-build.20190405065753.24551.log Last 10 log lines: File "/tmp/tmpregj5qs2/pip-18.1-py2.py3-none-any.whl/pip/_internal/commands/__init__.py", line 6, in <module> File "/tmp/tmpregj5qs2/pip-18.1-py2.py3-none-any.whl/pip/_internal/commands/completion.py", line 6, in <module> File "/tmp/tmpregj5qs2/pip-18.1-py2.py3-none-any.whl/pip/_internal/cli/base_command.py", line 18, in <module> File "/tmp/tmpregj5qs2/pip-18.1-py2.py3-none-any.whl/pip/_internal/download.py", line 38, in <module> File "/tmp/tmpregj5qs2/pip-18.1-py2.py3-none-any.whl/pip/_internal/utils/glibc.py", line 3, in <module> File "/tmp/python-build.20190405065753.24551/Python-3.7.2/Lib/ctypes/__init__.py", line 7, in <module> from _ctypes import Union, Structure, Array ModuleNotFoundError: No module named '_ctypes' make[1]: *** [install] Error 1 make[1]: Leaving directory `/tmp/python-build.20190405065753.24551/Python-3.7.2' Warning: The Python you just installed is not available on your PATH, apparently. make: *** [setup] Error 1
libffi
をインストールすると良いようです。
CentOS系列だと以下のコマンドでインストールできます。
$ sudo yum install libffi-devel
理由は公式ドキュメントに載ってました。
A full copy of libffi is no longer bundled for use when building the ctypes module on non-OSX UNIX platforms. An installed copy of libffi is now required when building ctypes on such platforms. (Contributed by Zachary Ware in bpo-27979.)
今回から必要だよって感じの事を言ってます。
また、下記からOpenSSLのバージョンが古すぎてもダメそうですね。
The ssl module requires OpenSSL 1.0.2 or 1.1 compatible libssl. OpenSSL 1.0.1 has reached end of lifetime on 2016-12-31 and is no longer supported. LibreSSL is temporarily not supported as well. LibreSSL releases up to version 2.6.4 are missing required OpenSSL 1.0.2 APIs.
Python3.7は現行最新のPythonです。(執筆時2019/04/06)
アップデートして良いPythonライフを送ってください。
続報
やっぱりOpenSSLのエラーに遭遇しました。
AWSのEC2でT2インスタンスからT3インスタンスへの移行
AWSのEC2インスタンスをT2 -> T3に変更しましたが、その時の手順が非常にめんどくさかったので紹介します。
T3インスタンスとは?
公式の紹介ページはこちら
T2インスタンスも、低コストでバースト可能という意味では非常に優秀だったのですが、T3はそれに対して
- 最新のプロセッサ
- 無制限バーストモードがデフォで採用されてる(その分アドオンでお金が掛かる可能性あり)
- EBS最適化を利用可能
- T2より若干安い
というもので、まぁT2使ってるなら乗り換えない意味はそんなにないかな?と思えるようなインスタンスタイプです。
ただ、デメリットとしては
- T2だったら最初からいくらか付与されているクレジットがT3だと0から蓄積しなければいけない
といったものがあります。とはいえ最初の瞬間からいきなりネットワークにつなげて運用することはそうそうないので、準備中に勝手にクレジットが貯まるからいいんじゃないでしょうか。
急なスケールアウト大変じゃんっていうならまぁその時だけT2使うなりすれば良いでしょう。
T2 -> T3への移行
T3では、 Elastic Net Adapter
(移行ENA)の導入が必要です。
こちらの作業がちょっとめんどくさかったので紹介します。
最終的にawscliを使う必要があるので、それは注意してください。
LInuxのアップデート
まず、利用してすインスタンスのOSがENAに対応できるかをチェックする必要があります。
コンソール上で確認してみましょう。
$ modinfo ena modinfo: ERROR: Module ena not found.
T2インスタンス採用した当初のままのOSならば、こんな感じで対応してないよって出ると思います。 下記のような表示が出た場合は対応しているので、この手順はスキップして大丈夫です。
$ modinfo ena filename: /path/to/ena.ko.xz ... ... ... ...
さて、ENAに対応させるにはどうすればいいか?
それは非常に単純で、 OSアップデート
です。
CentOSなら以下のコマンドになります。
$ sudo yum update
OSアップデートは検証が難しいからやりたくないよって話なら諦めましょう。
その後、インスタンスを再起動してみてENA確認のコマンドで大丈夫そうだったらこの手順はOKです。
awscliでインスタンスのENAサポートをONに
awscliで対象のインスタンスIDのENAサポートをONにします。
この作業には、awscliのバージョンが古かったり、awscliからのアクセスでEC2へのアクセス権限がないとうまくいかないので事前にそこは対応しましょう。
また、この時にはインスタンスを停止しておくと後で面倒がないので良いです。
awscliを最新にする場合は以下コマンドでOKです。
$ pip install -U awscli
ENAサポートをONにするには以下コマンドです。何も出なかったら成功です。
$ aws ec2 modify-instance-attribute --instance-id インスタンスID --ena-support
その後インスタンスタイプを変えて起動できたら完了です。僕はこのインスタンスの起動にものすごい時間がかかったので注意してくださいね。多分仕様でしょう。
ちなみに、僕はENAサポートをONにする前になんとなくT3にインスタンスタイプを変更してみて、起動エラーが出たのですが、その場合そのインスタンスIDのインスタンスはその時変更したインスタンスタイプでは一生起動しなくなりました。
解決方法は、AMIをそのインスタンスから作り直してインスタンスIDを変えて起動です。中でエラー起動したものと同じものが指定されてるんですかね。。。ただでさえ起動に時間がかかるので非常にしんどかったです。
pipenvでPythonの開発環境を構築してみた
最近話題のPythonの環境構築ツールのpipenvですが、つかってみたので紹介します。
公式はこちら
pipenvとは?
pipenvとは、具体的には pip
と viertualenv
が融合して便利になったツールと考えると良いようです。
つまり、プロジェクト以外のシステムを汚さない独立した仮想環境と、パッケージ管理ができるツールといったことになります。
また、pyenvをシステムに導入していればローカルにインストールしていないversionのPythonも要求すれば勝手にインストールしてくれるといった特徴もあるようですね。
pipenvのインストール
下記のコマンド一発インストールが可能です。
$ curl https://raw.githubusercontent.com/kennethreitz/pipenv/master/get-pipenv.py | python
また、pipが入っていれば下記でもインストールが可能です。
$ pip install pipenv
構築
下記コマンドで、Pythonのversionを指定して仮想環境を構築可能です。
pyenvがあれば、ローカルにインストールしていなくても自動でインストールしてくれるのでいれることをおすすめします。
$ pipenv --python 3.7.2
そうすると、以下のようなPipfileというファイルが生成されます。
[[source]] name = "pypi" url = "https://pypi.org/simple" verify_ssl = true [dev-packages] [packages] [requires] python_version = "3.7"
上記をみてわかるように、 developpment環境とライブラリのインストール環境を分けることができます。
ここに、flaskというライブラリをインストールしたい場合は下記コマンドを実行します。
実行後にはPipfileに書き込まれます。
$ pipenv install flask
また、dev環境に導入したい場合は下記コマンドになります。
$ pipenv install --dev flask
過去のプロジェクトの活用
過去にrequirements.txtをつかって管理していたものを移行する場合は以下のコマンドで実行できます。
$ pipenv install -r ./requirements.txt
また、pipenvで管理した状態からrequirements.txtを抽出する場合は以下のコマンドで実行します。
$ pipenv lock -r
ライブラリのバージョンアップ/指定
ライブラリのバージョン指定やアップデートをする際はPipfileをそのまま修正します。
[packages] Flask = "==1.0.2"
上記のままだと1.02で固定してインストールされます。
[packages] Flask = "*"
上記に変更すると、現在の一番新しいものがインストールされます。
また、そうした場合、Pipfileで *
が自動的にインストールされたバージョンにかわるわけではないので以下のようにすると最新にしたあとにバージョンを固定できます。
$ pipenv lock -r > requirements.txt $ pipenv install -r ./requirements.txt
pipでインストールしてあるものを、一旦requirements.txtに落とし込んでその後PIpfileにまたコンバートする方法ですね。
pipenv環境の起動
作成したPythonの環境のシェルのサブプロセスを以下のコマンドで実行できます。
$ pipenv shell
こちらでPythonを実行すれば、設定したPythonが利用でき、でインストールしたライブラリがimportできます。
run scriptの実行
とはいえ、makeコマンドなどを設定していたり、コマンドを仮想環境外から実行したいことがありますよね。
その場合はPipfileに [scripts]
を設定してみましょう。
[scripts] start = "python run.py"
そして下記を実行します。
$ pipenv run start
これで、問題ありません。仮想環境でのPython実行ができます。
実行プロジェクト内での仮想環境作成
いままで紹介してきたpipenvですが、システムの全く別の箇所に仮想環境を作成してそれを実行するといった仕組みになります。
しかし、それよりもプロジェクト内のどこかにディレクトリを作成して仮想環境を作成したいといった要望はあると思います。
そんな方は以下の設定をしてみるとOKです。
export PIPENV_VENV_IN_PROJECT=1
たとえば、PyCharmなどでpipenv環境を利用する際に自動で作成された環境だと、複数作成したらどれがどれかわからないといったことも起こってしまいがちです。
そういったときには非常に便利なしくみです。
他にも公式ドキュメントを見ると様々な便利機能があるようです。モダンなPythonライフを過ごしましょう。
nginxでngx_http_uwsgi_moduleのキャッシュを導入した話
nginxで ngx_http_uwsgi_module
のキャッシュを触ったので設定方法を紹介します。
ngx_http_uwsgi_module
は、 nginxとuwsgi間のリクエストのやり取りを補助するものです。
nginxにはデフォルトで入っているのでインストールするために追加で何かをする必要はありません。
ngx_http_uwsgi_module
のドキュメントは以下です。
実際の設定
最低限必要な設定ファイルへの記載はざっくり以下のような感じです。
http { .... .... .... uwsgi_cache_path /data/nginx/cache levels=1:2 keys_zone=zone_name:1m inactive=1h max_size=1g; uwsgi_temp_path /data/nginx/tmp; .... .... } server { .... .... set $hw ''; if ($http_user_agent ~* '(iPhone|Android|hoge|fuga)') { set $hw "@mobile"; } .... .... location / { include uwsgi_params; uwsgi_pass unix:`uwsgiのパス`; uwsgi_cache zone_name; uwsgi_cache_key "$scheme://$host$request_uri$hw"; uwsgi_cache_valid 200 302 1m; uwsgi_cache_valid 404 10m; } .... .... }
uwsgi_cache_path
uwsgi_cache_path /data/nginx/cache levels=1:2 keys_zone=zone_name:1m inactive=1h max_size=1g;
キャッシュを設定する箇所を記載するところですね。
/data/nginx/cache
保存先のパス
- levels=1:2
キャッシュの保存先のディレクトリの深さの設定です。
1 -> 最初の1文字目をディレクトリの名前に
2 -> その次の2文字を次のディレクトリの名前に
つまり、この例では
/data/nginx/cache |-- 0 ← 1階層目:キーの末尾1字 | |-- 1a ← 2階層目:その次の2字 | | `-- 85764ac76sb670bc11a6a40ca251c1v0 ← キー
みたいな感じになります。
- keys_zone=zone_name:1m
こちらはキャッシュの名前空間とそのサイズの設定箇所です。
上記だと名前は zone_name
でサイズは1MBになります。
In addition, all active keys and information about data are stored in a shared memory zone, whose name and size are configured by the keys_zone parameter. One megabyte zone can store about 8 thousand keys.
とあるので、1MBで8000個のkeyを保存できるようです。
- inactive=1h
こちらはキャッシュの生存期間です。
上記だと1時間生存しますね。
- max_size=1g
こちらはキャッシュの最大サイズです。
上記だと1GBまで保存できるようになっています。正直1GBもいらないですけどね。。。
uwsgi_temp_path
こちらは、キャッシュの一時保存先の設定になります。
uwsgi_cache_path
で無効だと明示しない限りこちらは必要になります。
こちらを設定しないと、一時ファイルも uwsgi_cache_path
に入ってしまうので、設定したほうが良いでしょう。
tempファイルに一時ファイルとして保存されたのち、リネームするといった動作になるのでできるだけ uwsgi_cache_path
と同じファイルシステム上に設定するべきでしょう。
UAで分離
set $hw ''; if ($http_user_agent ~* '(iPhone|Android|hoge|fuga)') { set $hw "@mobile"; }
こちらは、UAが別の場合に変数に値をつっこんでいます。
こちらの値を利用して端末ごとにキャッシュを出し分けます。
端末ごとの設定がいらない場合はなしで良いでしょう。
include uwsgi_params
uwsgi使うぞって意味です(雑
これはキャッシュのパラメータじゃないですが、ないとなんか違和感があるので入れてます。
uwsgi_pass unix:uwsgiのパス
;
uwsgiへのパスを記載しましょう。 sockファイルでもURLでも記載方法に制限はありません。
これも上記同様キャッシュのパラメータじゃないですが、ないとなんか違和感があるので入れてます。
uwsgi_cache
どの名前空間のキャッシュを利用するか設定します。
前述の uwsgi_cache_path
から設定します。
uwsgi_cache_key
こちらは、キャッシュのキーを設定します。
紹介しているの設定だとフルパス + 前述のUAでの分離での分け方となります。
scheme
での分離がいらなかったりいろいろあると思うので、アプリの仕様に合わせると良いでしょう。
uwsgi_cache_valid
どれだけの時間キャッシュするかの設定です。
紹介している設定ではステータスコードごとにキャッシュ時間を分けています。
ステータスコードを省略することも可能です。その場合はすべての場合に適用されます。
これをnginxの設定に突っ込むだけでドカンと一発でキャッシュの設定ができます。
CDNを使うのはしんどいけど負荷軽減したいなという方は試してみても良いでしょう。
レスポンスを保存しているだけなので、サーバのメモリもそこまで消費することもなく、負荷がしっかり減るのでおすすめです。
RFC 1808、RFC 3986を見てきたらURLのQueryComponentというものが定義されていて面白かった話
いままでURLの文法をあまり深く考えたことがなかったのですが、よく調べてみると面白かったので紹介します。
ある時、PythonでURLをパースしていたんですが、ドキュメントを見ると
scheme://netloc/path;parameters?query#fragment
上記のようなRFC1808の形に沿ったURLを分解すると書いてあるんですね。
この、QueryComponent
と定義されている ;parameters
っていうパラメータですが、僕にとってはすごい初見の話でした。
Pythonのドキュメントには、とりあえずRFC1808に沿ってくれと書いてあるだけで詳細は記載されていません。
仕様を正しく理解してないとバグを埋め込みそうだったので仕様を少し深く調べてみました。
RFC1808の仕様を見てみると、たしかに定義されています。
ですがRFC1738を見てくれとのこと。
http://www5d.biglobe.ne.jp/stssk/nro/rfc1738_j.txt
見てみると、 type=<typecode>
といった形で使うと記載されています。
FTPでは僕らのよくつかっているgetパラメータなどが定義されていないように見受けられるので、その変わりですかね。
こちらはFTPのURLの形式のようで、それをRFC1808でも引き継いでいるようですね。
ちなみに現在は最新がRFC3989となっており、しっかりRFC1808の形を引き継いでいてQueryComponent
も現役のようです。