くりーむわーかー

プログラムとか。作ってて ・試しててハマった事など。誰かのお役に立てば幸いかと。 その他、いろいろエトセトラ。。。

Django

Python typeでクラス定義を書き換える

なんのこっちゃって感じ。rest_frameworkのソース読んでて、

あー、これはこういう事に使うのかってピンと来たので残しておく。

DjangoのModelもそうなんだけど、シリアライザを作る時に

class CommentSerializer(serializers.Serializer):
    email = serializers.EmailField()
    content = serializers.CharField(max_length=200)
    created = serializers.DateTimeField()

こんな感じで、「クラス変数」にモデルの定義を入れてる。

で、クラス変数ってstaticだから、インスタンス化して使おうが、

結局全インスタンスで共有されてる変数。

でも、DjangoもRestFrameworkもちゃんとインスタンス変数になってる。

なんでかなーって不思議だったんですが、ソース読んでると、

metaclassの定義使って、クラス定義読み込まれる時に、クラス変数を潰して、

インスタンス変数作ってるんですね。

RestFrameworkのソースだと↓の部分。

class SerializerMetaclass(type):
    """
    This metaclass sets a dictionary named `_declared_fields` on the class.
    Any instances of `Field` included as attributes on either the class
    or on any of its superclasses will be include in the
    `_declared_fields` dictionary.
    """

    @classmethod
    def _get_declared_fields(cls, bases, attrs):
        fields = [(field_name, attrs.pop(field_name))
                  for field_name, obj in list(attrs.items())
                  if isinstance(obj, Field)]
        fields.sort(key=lambda x: x[1]._creation_counter)

        # If this class is subclassing another Serializer, add that Serializer's
        # fields.  Note that we loop over the bases in *reverse*. This is necessary
        # in order to maintain the correct order of fields.
        for base in reversed(bases):
            if hasattr(base, '_declared_fields'):
                fields = [
                    (field_name, obj) for field_name, obj
                    in base._declared_fields.items()
                    if field_name not in attrs
                ] + fields

        return OrderedDict(fields)

    def __new__(cls, name, bases, attrs):
        attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
        return super().__new__(cls, name, bases, attrs)

で、Serializerの定義が↓

class Serializer(BaseSerializer, metaclass=SerializerMetaclass):

SerializerMetaclassの↓のところ

    @classmethod
    def _get_declared_fields(cls, bases, attrs):
        fields = [(field_name, attrs.pop(field_name))
                  for field_name, obj in list(attrs.items())
                  if isinstance(obj, Field)]
        fields.sort(key=lambda x: x[1]._creation_counter)

attrsにクラス定義でのクラス変数とか入ってて、これが最終的にクラス定義内に

展開されるっぽい。なので、attrs.popでクラス変数として消してる。

消すタイミングで、fieldsって変数に定義を退避してる感じ。

で、typeを継承してて、最後に「return super().__new__(cls, name, bases, attrs)」してる

という感じ。

なるほどー。ってなんか納得した。

pythonでメタプログラミングするならtype継承してクラス定義が作られてるって

知っとけみたいな話見てて「は?」って思ってたんだけど、

こう具体的なtypeの使い方見ると納得できますね。。。

で、↑のが何で便利なのかというと、MVCとMVVMとかで作ってる時って、

View層とのやり取りは専用のViewModel作ると思うんですよ。

バリデーションとか統一しやすいし。

で、ViewModelの定義する時のベースにこれがすごく使える。

一つ一つのフィールドのデータ型を個別に作れるので。

一つ賢くなりましたというお話。。。

PythonでSingletonの実装に一言

PythonでSingleton(シングルトン)使いたくなったんで、

色々調べてみたんですが、見てく中でちょっと、ん?って感じた。

PythonにはC#とかで言う、privateがないので、いろいろ工夫が必要。ってのは分かる。

例えば、

# まずとりえず
class SingletonClass:
    __instance = None
    def __init__(self):
        self.val = "value"
    
    @classmethod
    def get_model(cls):
        if cls.__instance is None:
            cls.__instance = SingletonClass()
        return cls.__instance

if __name__ == "__main__":
    x = SingletonClass.get_model()
    y = SingletonClass.get_model()
    print(f"x = {id(x)} , y = {id(y)}")  # x = 140550875600936 , y = 140550875600936

get_modelを使ってる間は大丈夫。ただ、SingletonClass()の普通のコンストラクタに

制限かけてるわけじゃないので、1つである事を保証はしてない。

なので、まず__new__を使ってみる。

#__new__使う場合
class SingletonClass:
    __instance = None

    def __init__(self):
        print("__init__")
        self.val = "value"

    def __new__(cls):
        print("__new__")
        if cls.__instance is None:
            cls.__instance = super().__new__(cls)
        return cls.__instance
    
if __name__ == "__main__":
    x = SingletonClass()
    print(x.__dict__)
    y = SingletonClass()
    print(x.__dict__)

    print(f"x = {id(x)} , y = {id(y)}")

↓実行結果
__new__
__init__
{'val': 'value'}
__new__
__init__
{'val': 'value'}
x = 140367603169544 , y = 140367603169544

公式のドキュメント的には

__init__は、インスタンスが (__new__() によって) 生成された後、それが呼び出し元に返される前に呼び出されます。引数はクラスのコンストラクタ式に渡したものです。

__new__() が cls のインスタンスを返した場合、 __init__(self[, ...]) のようにしてインスタンスの __init__() が呼び出されます。このとき、 self は新たに生成されたインスタンスで、残りの引数は __new__() に渡された引数と同じになります。

__new__() が cls のインスタンスを返さない場合、インスタンスの __init__() メソッドは呼び出されません。

__new__() の主な目的は、変更不能な型 (int, str, tuple など) のサブクラスでインスタンス生成をカスタマイズすることにあります。また、クラス生成をカスタマイズするために、カスタムのメタクラスでよくオーバーライドされます。

って事らしいので、__new__でオブジェクトの実態が作られてるから、__new__の中で既にインスタンスがあるかどうか見る感じ。

で、この実装だと、

    x = SingletonClass()
    y = SingletonClass()

    x.val = "x-value"
    print(f"y.val = {y.val}")
    x.val = "y-value"
    print(f"x.val = {x.val}")


↓結果
y.val = x-value
x.val = y-value

みたいに書けるので、シングルトンを意識しないで使えて、値も共有出来て便利ー。みたいな感じ?

正直、「え?それってどうなん?」って思た。

つか、これってシングルトンっていうか、

ただのグローバル変数 だよね?

確かに、オブジェクトのIDは一致してるんだけど、上で行くと、2回目の__new__の中で

__init__呼び出されてるから、self.valの値変わっちゃうよね?↓の感じ。

    x = SingletonClass()
    x.val = "x-value"
    print(f"x.val = {x.val}")
    y = SingletonClass()
    print(f"x.val = {x.val}")

↓結果
x.val = x-value
__new__
__init__
x.val = value

んーと、これだいぶヤベー気がするのですが、こういうモンなの?

あと、シングルトン意識しなくていいとか、どうなんだろ。

シングルトンは意識して使って欲しいと自分は思う。

つか、シングルトンって何のために使うんだろ。

グローバル変数の変わり?それは違うよね?

オブジェクト指向自体、グローバル変数との闘いの果てに出来たものだと思うので、

そういう使い方は違うように思う。

シングルトンって、不変な何かのマスタとか設定とか、オブジェクト指向だと、

どうしてもインスタンスを毎回作る感じになってしまうので、

そういう不要なインスタンスを作らないでパフォーマンスとかメモリ効率を

上げるためのものだと思ってます。

ただ、Pythonだとやっぱりどうしてもオブジェクト内の値すらも、自由に書き換え出来ちゃうから、

こういうの強制させるものは作るの無理なんかな。。。

一人で作ってる時は正直どーでもいいんだけど、10人とか開発者がいると、

色々意識して欲しいので、とりあえずこんな感じにしてみた。

class SingletonClass:
    def __new__(cls):
        raise NotImplementedError("コンストラクタ直呼びは禁止。SingletonFactory.get_model()を使用。")

    @classmethod
    def __private_init__(cls, self):
        print("__init__")
        self.val = "value"
        return self

    @classmethod
    def __private_new__(cls):
        print("__new__")
        return cls.__private_init__(super().__new__(cls))

    def inst_method(self):
        return True

class SingletonFactory:

    __instance = None

    @classmethod
    def get_model(cls):
        if cls.__instance is None:
            cls.__instance = SingletonClass.__private_new__()
        return cls.__instance

完全に自前でコンストラクタ用意した感じ。

いちを、普通に__new__と__init__で作ったオブジェクトとdir()の結果は

一致してたから大丈夫だと思うんだけど、ちょっと心配。

ただ、まー不変な設定とかマスタをキャッシュ的に使いたいだけなのでこれでもいいか。

で、これだとスレッドセーフじゃないので、ロックをつける。

import threading

class SingletonFactory:

    __instance = None
    _lock = threading.Lock()

    @classmethod
    def get_model(cls):
        with cls._lock: 
            if cls.__instance is None:
                cls.__instance = SingletonClass.__private_new__()
        
        return cls.__instance

あと、起動中は不変なデータなハズなんだけど、もしかしたら、稼働中に

値の更新をしたくなるかもしれないので、いちを更新用のメソッドをつけておく。

最終系。。。

import threading

class SingletonClass:
    def __new__(cls):
        raise NotImplementedError("コンストラクタ直呼びは禁止。SingletonFactory.get_model()を使用。")

    @classmethod
    def __private_init__(cls, self):
        print("__init__")
        self.val = "value"
        return self

    @classmethod
    def __private_new__(cls):
        print("__new__")
        return cls.__private_init__(super().__new__(cls))

    def inst_method(self):
        return True

class SingletonFactory:

    __instance = None
    _lock = threading.Lock()

    @classmethod
    def get_model(cls):
        with cls._lock: 
            if cls.__instance is None:
                cls.__instance = SingletonClass.__private_new__()
        
        return cls.__instance

    @classmethod
    def recreate(cls):
        with cls._lock: 
            cls.__instance = SingletonClass.__private_new__()
        return cls.__instance

もちろん、Pythonのコードなので、正直、取得した後に値とかオブジェクトの内容を変更したりは

余裕で出来る。ただ、意識はしてくれるんじゃないだろうかと期待。

StackOverFlowだと、デコレータ使ったりして定義してるっぽいけど、どうなんでしょ。

サクッと作るならこの辺りかなーとか思いました。

この辺も参考になる。

ついでに、Djangoのソース読んでるとロックのところって下で書いてる。

_lock = threading.RLock()

なんで、RLock()使ってるんだろ?

理由が思いつかない。知ってる人誰か教えてください。

DjangoとCeleryのジョブキューについて

Pythonでジョブキュー的に処理を外に投げたい場合に

Django+Celeryが良く見るやつ。

公式の通りにやれば特に問題なく動く。

で、一つ気になったところ。

これって、Djangoのアプリの中で下の感じで処理を投げるじゃないですか。

def aaction(request):
    res = some_task.delay()
    return HttpResponse(f"Delay Task!!!")

そしたら、Workerサーバというかサービスが↑でジョブキューに入った

処理依頼を処理する流れじゃないですか。

で、その時のWorkerプロセスの動きが、どう見ても、毎回Djangoアプリを初期起動してるんだよね。

そういうもん?バッチ処理的に使うなら、起動時間は全体の処理時間からみて

問題にならないって事なんかな?

2~3秒かかる程度の処理は投げない方が良い感じ?

それなりに大きいアプリだと起動処理もそこそこオーバーヘッドあると思うんだけど。。。

多分中で、django.setup()してると思うんだけど、

どうなんでしょ。何となく、起動時にsetup()はやって使いまわして欲しいのですが。。。

つか、それやったら、Djnagoの普通のWebサーバだろみたいな感じかしらね。

何とも釈然としないモヤモヤ感。

何かいいやり方ないかなー。もうちょい検討。。。

Python Djangoをローカルのスクリプトから読み込んで使う

Djangoのアプリの中でゴニョゴニョするんじゃなく、

別のスクリプトからDjangoアプリを呼び出して操作したい場合。

テストとかで。

公式的にはこの辺。

で、現状ちゃんと動くのは↓

import os
import sys
import django

#sys.path.append('/home/hogeuser/sandbox/mysite')
sys.path.append(os.getcwd())
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
django.setup()

#↑の後ならモデルとか読み込んで使える
from myapp import models

a = SampleTable()
a.save()

なんだけど、環境変数とか使うのどうにかならないかなーと思い、試してみた。

#公式に乗ってるやつ ⇒ これは無理
import django
from django.conf import settings
import mysite.settings as mysettings
settings.configure(default_settings=mysettings, DEBUG=True)
django.setup()

# 下のエラーが出る
# AttributeError: module 'mysite.settings' has no attribute 'LOGGING_CONFIG'

どうもsettingsを入れるだけだと、djangoのデフォルトのsettingsを丸ごと上書きしてるらしく、

必要な定義が消えちゃうっぽい。

やるなら、自分のsettingsにDjangoのデフォルトのsettingsを全部書かないとダメっぽ。

そして、それがかなり量があるのでしんどい。

つか、公式でもデフォルトはdjangoでやってるから原則変えるんじゃねー

みたいな感じっぽい。

で、次に試したのは↓

#これは動くけど、自分のアプリが読み込まれてない
import django
from django.conf import settings
from django.conf import global_settings
settings.configure(default_settings=global_settings, DEBUG=True)
django.setup()

Djangoのデフォルトを読み込ませてみたらいいんじゃないか的な。

これだとうまくいくけど、自分のINSTALL_APPSが読み込まれてないので、色々ダメ。

まー当たり前か。

という事で、デフォルトは上書きせずにマージするようなやり方がないか調べてたけど、

良く分かりませんでした。。。

どうも環境変数に入れる以外に良い方法が出回ってなさそうですね。

つか、manage.pyの中でこの方法でやってるからこれしかないのかしらね。

誰かいい方法知ってたら教えてほしい。

CentOS7でApacheでDjango動かすまで

まーはまったので、残しとく。ApacheでDjango動かすまでのまとめ。Python3.6とDjango入れるのはこっちがメイン

まず、Python3.6入れる。

#RPM持ってくる
sudo yum -y install https://centos7.iuscommunity.org/ius-release.rpm
#インスト
sudo yum -y install python36u
sudo yum -y install python36u-pip
sudo yum -y install python36u-devel

Apache入れる

sudo yum install -y httpd
sudo yum install -y httpd httpd-devel

で、ここから人によると思うんだけど、自分はユーザのホームに「public_html」を作ってそこにDjangoのプロジェクト入れる感じでやる。

#ユーザのホームで
mkdir public_html
#ホームディレクトリのパーミッションを変える
chmod 701 /home/hogeusr

で、ホームディレクト内のディレクトリ使う場合は、ホームディレクトリそのものの、パーミッションで他ユーザの実行権限つけないと動かない。これのやり方はなんか他にないか今度調べよ。

そしたら、とりあえずDjangoのプロジェクト作って確認しておく。

cd ~/public_html/
#仮想環境作る
python3.6 -m venv pyenv
#仮想環境に切り替え
cd pyenv
source bin/activate
#pipの更新
pip install --upgrade pip
#Djangoのインスト
pip install django
#プロジェクト作る
django-admin startproject mysite
#とりあえず実行
cd mysite
python manage.py runserver
#↓にブラウザでアクセス
http://127.0.0.1:8000/

そしたら、mod_wsgiをインストしておく。

#pipで入れないとダメらしい。あと、仮想環境上でやらないとダメ。
pip install mod_wsgi
#入ったモジュールのパスを表示→メモっとく(後で使う)
find /home/hogeusr/public_html/ -name 'mod_*.so'
#仮想環境抜ける
deactivate

設定ファイルを作る

#mod_wsgiの読み込み設定(上でfindしたやつ)
sudo vi /etc/httpd/conf.modules.d/mod_wsgi.conf
LoadModule wsgi_module /home/hogeusr/public_html/pyenv/lib/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so

#wsgiに読ませるDjangoの設定
sudo vi /etc/httpd/conf.d/django.conf
#ファイルの中身は↓(パスは適宜変更)
WSGIPythonHome /home/hogeusr/public_html/pyenv
WSGIScriptAlias / /home/hogeusr/public_html/pyenv/mysite/mysite/wsgi.py
WSGIPythonPath /home/hogeusr/public_html/pyenv/mysite:/home/hogeusr/public_html/pyenv/lib/python3.6/site-packages

<Directory /home/hogeusr/public_html/pyenv/mysite>
<Files wsgi.py>
Require all granted
</Files>
</Directory>

で、ここまでやった状態で「sudo systemctl restart httpd」を実行するとSELinuxに拒否られる。なので、この辺の設定を次にする。

sudo setsebool -P httpd_enable_homedirs 1
sudo setsebool -P httpd_unified 1
sudo systemctl restart httpd
#↓にブラウザでアクセス
http://localhost

これでDjangoの初画面がでるはず。

WSGIPythonHomeは仮想環境のディレクトリを指定。WSGIScriptAliasはここで指定したURLに来た時にどのWsgiスクリプト動かすか。WSGIPythonPathはWSGIで動かした時に読ませるPythonPath。色んなモジュール読み込みで必要らしい。PythonPathについてはココのブログが分かりやすい。公式はこっち

Apacheで動かすときのDjangoの公式はコッチ

やっててエラーが出たら「/etc/httpd/logs/error_log」にヒントが出るので見ながらアレコレやるしかあるまい。見たエラー的には↓

Fatal Python error: Py_Initialize: Unable to get the locale encoding.
→ WSGIPythonPath のパスが違う。homeティレクトリのパーミッション。

Target WSGI script not found or unable to stat: /home/hogeusr/public_html/pyenv/mysite/wsgi.py
→ パスが違う

自分は他のセキュリティ系の設定はやらなくてもlocalhostで動いたけど、ほんとはやらないとダメなのかな?下の内容。

#↓のファイルでALLOWED_HOSTS=['*']にしておく。
#全OKの設定なのでホントは色々考えないとダメだろね。
vi ~/public_html/pyenv/mysite/mysite/settings.py

あとはファイヤーウォール。面倒なので、一回切ればいいじゃないだろか。。。

systemctl stop firewalld

CentOS 7でPython3.6入れるのとDjangoを動かすまで

タイトル通り。Python3.6の入れ方が色々あってよくわかんないのでまとめ的に。ApacheでDjango動かうまではコッチ

CentOSで現状入ってるPythonのバージョンは2.7。最新は3.7っぽいけど、3.6用のRPMしかないっぽいので3.6を入れる。ソースからビルドすれば3.7も行けるらし。

参考にしたのはココ。RPM配布してるのはココ

まず、Python3.6入れる。


#現状のバージョン確認
python -V

#RPM持ってくる
sudo yum -y install https://centos7.iuscommunity.org/ius-release.rpm

#使えるやつの確認
yum list available | grep python3

#インスト
sudo yum -y install python36u
sudo yum -y install python36u-pip
sudo yum -y install python36u-devel

#バージョン確認(これで3.6とか出てればOKかな)
python3.6 -V

そしたら、Django動かす。python3.6で仮想環境作れば、仮想環境上のpythonは3.6見る様子。pipも。

#適当に作業フォルダ作る
mkdir mypy
cd mypy

#venv作る
python3.6 -m venv testenv
cd testenv

#仮想環境に変える
source ./bin/activate

#仮想環境のバージョン確認 ⇒ 3.6のはず
python -V

#ついでにpipのアップグレード
pip install --upgrade pip

#Djangoをインスト(仮想環境にしてからやる)
pip install django

#Djangoのバージョン確認
python -m django --version

#Djangoのプロジェクトを作る
django-admin startproject mysite
cd mysite

#とりあえずサーバを動かす
python manage.py runserver

#↓でアクセスしてみる
http://127.0.0.1:8000/

#仮想環境抜けるとき
deactivate

ホントは元から入ってるPythonも3.6にならないか、↓みたいにコマンドのリンクつけ直してみたんだけど、やってみたらyumが動かなくなった。yumでシンタックスエラーが出る。バージョン変わったからなんか直さないとダメなんかね。てかyumってPythonだったのね。。。

#pythonコマンドの見先を変えたいので確認
which python
which python3.6

#リンク消してつけ直し
unlink /bin/python
ln -s /bin/python3.6 /bin/python

DjangoとApacheとWSGIとWindows

Windows上でDjangoで作ったアプリをApacheからWSGIで呼べるようにする。

まーWindowsはいばらの道じゃ。

とりあえず必要な資源をダウンロードしておく。ApacheとかPythonとか。PythonはPIPとVirtualEnvも。

①Djangoで適当にアプリを作る。

仮想環境つくる
virtualenv venv

仮想環境での作業に変える
cd venv
Scripts\activate

Djangoをインスト
pip install django

Djangoのバージョン ⇒ 今回は2.0.6
python -m django --version

アプリを作る
django-admin startproject mysite

とりあえず動かして確認 ⇒ http://127.0.0.1:8000/
cd mysite
python manage.py runserver

②Apacheの準備

1.ダウンロードしたZipを適当なフォルダに展開しておく
2.confファイルの変更(Apache24\conf\httpd.conf)
 ⇒ デフォでC:\Apache24みたいになってるところを格納したフォルダのパスに全部変える。
3.動作確認 : コマンドプロンプト上でbin/httpd.exeで動かしたら「http://localhost」にアクセス
 ⇒ IT WORKS!!って出ればOK。ポート変えてる時はURLは適宜変える。

そしたら、胆のmod_wsgiのインスト。仮想環境上でやってもいいかもだけど、自分は本体(って言い方であってんのかな)の方にインストした。windowsでインストールするやり方は昔はアレコレ大変だったようですが、今は割と楽。調べてて、昔のやり方ではまりそうになったんだけど、mod_wsgiのGitのReadmeにちゃんと書いてあった。やっぱ一次情報をちゃんと読まないとダメですね。これには大部分が昔のやり方で載ってるんだけど、一番上にここに載ってる事はもうやんなって書いてありましたね。

で、今のやり方は↓。

pip install mod_wsgi

これだけ。随分楽になったようで。・・・が、ApacheをC:の直下に展開してないとエラーが出ると言う罠。「No Apache installation can be found. Set the MOD_WSGI_APACHE_ROOTDIR environment to its location.」っていうエラーが出る。C:直下以外にある場合は、この環境変数setしてからじゃないとダメっぽいですね。stackoverflowに質問もあった。 という事で、C:直下以外にある場合は↓の感じ。

set "MOD_WSGI_APACHE_ROOTDIR=F:\Apache32\Apache24"
pip install mod_wsgi

で、Apacheには32bit版と64bit版があるんですが、64bit版だとこれでもまだ変なエラーがでる。あきらめて32bit版にしましたとさ。。。

あと、自分のPCにはVisualStudio2017も入ってるんですが、上記のStackOverflow見てると、VisualStudio入ってないとダメそう?mod_wsgiのインストールの中で、コンパイルしてるっぽい?

インストール出来たら↓。

mod_wsgi-express module-config

上記のコマンド実行すると↓感じの設定文字列が出てくるので、これをApacheのhttpd.confにコピペする。

LoadFile "f:/python36/python36.dll"
LoadModule wsgi_module "f:/python36/lib/site-packages/mod_wsgi/server/mod_wsgi.cp36-win32.pyd"
WSGIPythonHome "f:/python36"

そしたら、あとはDjangoの公式に載ってる設定をhttopd.confに追加しておしまい。最終的にhttpd.confに追記するのは↓。

LoadFile "f:/python36/python36.dll"
LoadModule wsgi_module "f:/python36/lib/site-packages/mod_wsgi/server/mod_wsgi.cp36-win32.pyd"

WSGIPythonHome "f:/py/venv"
WSGIScriptAlias /mysite "f:/py/venv/mysite/mysite/wsgi.py"
WSGIPythonPath "f:/py/venv/mysite"

<Directory f:/py/venv/mysite/mysite>
<Files wsgi.py>
Require all granted
</Files>
</Directory>

WSGIPythonHomeはDjangoを使ってる仮想環境のルートを指定するっぽいですよ。これで、WSGIScriptAliasで指定してるURLにアクセスすればDjangoが動くという寸法です。ただ、なんだかデフォのままだとトップページが出ないぽいので、「/mysite/admin/」みたいに中のURLまで行かないとダメな感じ?あんま調べてない。まー動いたから良しとしよう。

あーあと、MariaDB使ってるんですが、開発環境上だと「pip install PyMySQL」だけで動くんだけど、Apache通すと動かない。エラーログに「Did you install mysqlclient?」って出てくるから「pip install mysqlclient」でインストールしたら動くようになった。なんだろね。。。

問合せ