さるへい備忘録

さるへいのやったことを綴っているブログです。基本的にテクノロジーの話題です。

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だとコンパイルエラーにならないんですね。。。

vermeer.hatenablog.jp

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に書き直して勉強していこうコーナーを急にやりたくなりました。

www.hyuki.com

以下に順番に掲載していきます。めざせ全部網羅

1章 Iteratorパターン

ひとつひとつ順番に走査して数え上げていくパターン saruhei1989.hatenablog.com

2章 Adapterパターン

既存の処理のラッパーパターン

saruhei1989.hatenablog.com

3章 Template Methodパターン

抽象クラスに処理のテンプレートを記載してサブクラスで詳細実装

saruhei1989.hatenablog.com

4章 Factory Methodパターン

Template Methodをインスタンス生成の箇所に特化させる

saruhei1989.hatenablog.com

5章 Singletonパターン

必ず1個しかないことを保証する

saruhei1989.hatenablog.com

6章 Prototyprパターン

複製する

saruhei1989.hatenablog.com

7章 Builderパターン

別クラスで具体的な処理を記載してコントロールする

saruhei1989.hatenablog.com

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

理由は公式ドキュメントに載ってました。

docs.python.org

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のエラーに遭遇しました。

saruhei1989.hatenablog.com

AWSのEC2でT2インスタンスからT3インスタンスへの移行

AWSのEC2インスタンスをT2 -> T3に変更しましたが、その時の手順が非常にめんどくさかったので紹介します。

T3インスタンスとは?

公式の紹介ページはこちら

aws.amazon.com

T2インスタンスも、低コストでバースト可能という意味では非常に優秀だったのですが、T3はそれに対して

  • 最新のプロセッサ
  • 無制限バーストモードがデフォで採用されてる(その分アドオンでお金が掛かる可能性あり)
  • EBS最適化を利用可能
  • T2より若干安い

というもので、まぁT2使ってるなら乗り換えない意味はそんなにないかな?と思えるようなインスタンスタイプです。
ただ、デメリットとしては

  • T2だったら最初からいくらか付与されているクレジットがT3だと0から蓄積しなければいけない

といったものがあります。とはいえ最初の瞬間からいきなりネットワークにつなげて運用することはそうそうないので、準備中に勝手にクレジットが貯まるからいいんじゃないでしょうか。
急なスケールアウト大変じゃんっていうならまぁその時だけT2使うなりすれば良いでしょう。

T2 -> T3への移行

T3では、 Elastic Net Adapter (移行ENA)の導入が必要です。

aws.amazon.com

こちらの作業がちょっとめんどくさかったので紹介します。
最終的に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-ja.readthedocs.io

pipenvとは?

pipenvとは、具体的には pipviertualenv が融合して便利になったツールと考えると良いようです。
つまり、プロジェクト以外のシステムを汚さない独立した仮想環境と、パッケージ管理ができるツールといったことになります。

また、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環境を利用する際に自動で作成された環境だと、複数作成したらどれがどれかわからないといったことも起こってしまいがちです。
そういったときには非常に便利なしくみです。

pleiades.io

他にも公式ドキュメントを見ると様々な便利機能があるようです。モダンなPythonライフを過ごしましょう。

nginxでngx_http_uwsgi_moduleのキャッシュを導入した話

nginxで ngx_http_uwsgi_module のキャッシュを触ったので設定方法を紹介します。
ngx_http_uwsgi_module は、 nginxとuwsgi間のリクエストのやり取りを補助するものです。
nginxにはデフォルトで入っているのでインストールするために追加で何かをする必要はありません。

ngx_http_uwsgi_module のドキュメントは以下です。

pp.nginx.com

実際の設定

最低限必要な設定ファイルへの記載はざっくり以下のような感じです。

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を分解すると書いてあるんですね。

docs.python.org

この、QueryComponentと定義されている ;parameters っていうパラメータですが、僕にとってはすごい初見の話でした。
Pythonのドキュメントには、とりあえずRFC1808に沿ってくれと書いてあるだけで詳細は記載されていません。
仕様を正しく理解してないとバグを埋め込みそうだったので仕様を少し深く調べてみました。

www5d.biglobe.ne.jp

RFC1808の仕様を見てみると、たしかに定義されています。
ですがRFC1738を見てくれとのこと。

http://www5d.biglobe.ne.jp/stssk/nro/rfc1738_j.txt

見てみると、 type=<typecode> といった形で使うと記載されています。
FTPでは僕らのよくつかっているgetパラメータなどが定義されていないように見受けられるので、その変わりですかね。
こちらはFTPのURLの形式のようで、それをRFC1808でも引き継いでいるようですね。

ちなみに現在は最新がRFC3989となっており、しっかりRFC1808の形を引き継いでいてQueryComponentも現役のようです。

tools.ietf.org