- 投稿日:2021-06-15T23:49:31+09:00
残プロ 第-14回 ~pythonでcsvをgui表示~
今回すること csvを読込,gui表示 pyhtonに標準で入っているtkinterモジュールを利用 表の作成にはttk.treeviewを使用する .py tkinterの流れとしては, ガワ(ウィンドウやフレーム)をつくる 中身(ラベルやボタン)をつくる 配置 となります.もちろん配置した後から中身をつくることもできます. ガワと中身は作っておいて配置せず,任意のタイミング(ボタン押下等)で表示するといったことができます.今回はやってませんが... 以下,サンプルプログラムになります. import tkinter as tk import tkinter.ttk as ttk import pandas as pd def selected(event): for item in tree.selection(): print(item, tree.item(item)) if __name__ == '__main__': # Load .csv df = pd.read_csv("tasks.csv", encoding='shift-jis') # create Window and Treeview root = tk.Tk() tree = ttk.Treeview(root, show='headings') # set Treeview columns tree['column'] = ("No",) + tuple(df) # set header tree.heading("No", text="No") for c in df: tree.heading(c, text=c) # set cells for i, row in enumerate(df.itertuples()): tree.insert("", "end", tags=i, values=row) # set layout tree.pack() # bind action tree.bind('<<TreeviewSelect>>', selected) # set loop root.mainloop() 実行結果 各行にselected関数をbindメソッドによって紐づけています.内容はクリックされた行のitemを表示するものです.今回の例ではvaluesに要素の配列が格納されていることが分かりますね! 選択された行のvaluesを操作すればgui上で表示の変更が,tagsを利用すれば.csvの編集ができそうです. terminal I006 {'text': '', 'image': '', 'values': [5, 'ペットボトル,缶', '2021年06月22日', '8:00:00', 'SecondTuesday,FourthTuesday', 'nan'], 'open': 0, 'tags': [5]}
- 投稿日:2021-06-15T23:42:49+09:00
Seaborn定量比較-散布図
散布図とは 定義はMatplotlib定量比較-散布図をご参考ください。 簡単な散布図 Seabornで散布図を描くには、scatterplotとrelplot(relation plot)の2つの方法があります。 scatterplotの場合: import matplotlib.pyplot as plt import seaborn as sns sns.scatterplot(x='col01', y='col02', data=test_data) plt.show() relplotの場合: import matplotlib.pyplot as plt import seaborn as sns sns.relplot(x='col01', y='col02', data=test_data, kind='scatter') plt.show() サブプロットで複数の散布図を描く relplotのcolとrowパラメータで、横と縦で複数の散布図を描けます。 sns.relplot(x="G1", y="G3", data=student_data, kind="scatter", col="schoolsup", col_order=["yes", "no"], row='famsup', row_order=["yes", "no"]) plt.show() 常用オプション size: 点のサイズ hue: 点の色 sns.relplot(x='col01', y='col02', data=test_data, kind='scatter', size='cylinders', hue='cylinders') plt.show() style: 点のスタイル sns.relplot(x='col01', y='col02', data=test_data, kind='scatter', hue='origin', style='origin') plt.show()
- 投稿日:2021-06-15T23:42:42+09:00
Djangoの旅 ~Part2~ アプリ作成
今回の目標 Webブラウザに文字を表示させる!!!! 手順 1・プロジェクト内にアプリを作成 2・アプリの登録 3・views.pyを変更してブラウザに文字を出力できるようにする 4・プロジェクトのurls.pyを変更 5・アプリ内にulrs.pyを自作、3で作成したviews.pyを起動できるようにする コード解説 $ cd プロジェクトディレクトリ $ python manage.py startapp testapp(任意のアプリ名) プロジェクト内に移動(1行目) アプリ作成(2行目)、今回はtestappだがアプリ名は任意可 settings.py # Application definition INSTALLED_APPS = [ 'testapp.apps.TestappConfig',#アプリ登録 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] 1で作成したアプリをプロジェクトに登録する やり方: プロジェクトを管理しているディレクトリ内にあるsettings.pyを変更 ex)(APP/APP/settings.py) 記述する内容はアプリ内のapps.pyのクラスを呼びだすコードを記載 'testapp.apps.TestappConfig' →testapp/apps.pyのTestappConfigクラスのこと views.py from django.shortcuts import render from django.http import HttpResponse#記述 # Create your views here. def index(request): return HttpResponse('Djangoの旅') このアプリがどのように動作するのかはviews.pyで指定する Webサーバーへのリクエストに応じてindex関数を呼び出して文字列Djangoの旅を返す このプログラムを呼び出すためにルーティングを設定 ルーティングはWebサーバーのURLとプログラムを紐付けする役割 Djangoのルーティングにはプロジェクトのルーティング設定とアプリのルーティング設定の2段階がある!!! APP/APP/urls.py from django.contrib import admin from django.urls import include,path#include追加 urlpatterns = [ path('testapp/',include('testapp.urls')), path('admin/', admin.site.urls), ] 1段階目としてまずはプロジェクトを管理しているディレクトリのurls.pyを変更 サーバーアドレスの後にtestapp/が続いていればtestappアプリケーションのurls.pyを利用することを表している。 path()の第1引数がアドレスの文字列で,第2引数がそのアドレスがリクエストされたときに起動するview関数,nameはこのアドレスを逆引きするための名前!!! APP/testapp/urls.py from django.urls import path from . import views urlpatterns = [ path('',views.index,name = 'index') ] 2段階目でアプリディレクトリにurls.pyを作成 アドレスの最後が何もなしだったらviews.pyのindex関数を起動 $ python manage.py runserver サーバー起動 ブラウザで http://localhost:8000/testapp/ ブラウザ上にDjangoの旅が出力されていればOK!!! 参考文献 path()について https://qiita.com/j54854/items/201ecbe55017fd2a7996 include()について https://qiita.com/miler0528/items/f5f22db2141ec7cd7198
- 投稿日:2021-06-15T23:42:42+09:00
Djangoの旅~Part2~ アプリ作成
今回の目標 Webブラウザに文字を表示させる!!!! 手順 1・プロジェクト内にアプリを作成 2・アプリの登録 3・views.pyを変更してブラウザに文字を出力できるようにする 4・プロジェクトのurls.pyを変更 5・アプリ内にulrs.pyを自作、3で作成したviews.pyを起動できるようにする コード解説 $ cd プロジェクトディレクトリ $ python manage.py startapp testapp(任意のアプリ名) プロジェクト内に移動(1行目) アプリ作成(2行目)、今回はtestappだがアプリ名は任意可 settings.py # Application definition INSTALLED_APPS = [ 'testapp.apps.TestappConfig',#アプリ登録 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ] 1で作成したアプリをプロジェクトに登録する やり方: プロジェクトを管理しているディレクトリ内にあるsettings.pyを変更 ex)(APP/APP/settings.py) 記述する内容はアプリ内のapps.pyのクラスを呼びだすコードを記載 'testapp.apps.TestappConfig' →testapp/apps.pyのTestappConfigクラスのこと views.py from django.shortcuts import render from django.http import HttpResponse#記述 # Create your views here. def index(request): return HttpResponse('Djangoの旅') このアプリがどのように動作するのかはviews.pyで指定する Webサーバーへのリクエストに応じてindex関数を呼び出して文字列Djangoの旅を返す このプログラムを呼び出すためにルーティングを設定 ルーティングはWebサーバーのURLとプログラムを紐付けする役割 Djangoのルーティングにはプロジェクトのルーティング設定とアプリのルーティング設定の2段階がある!!! APP/APP/urls.py from django.contrib import admin from django.urls import include,path#include追加 urlpatterns = [ path('testapp/',include('testapp.urls')), path('admin/', admin.site.urls), ] 1段階目としてまずはプロジェクトを管理しているディレクトリのurls.pyを変更 サーバーアドレスの後にtestapp/が続いていればtestappアプリケーションのurls.pyを利用することを表している。 詳しくは、プロジェクトを管理しているディレクトリのurls.pyでアプリケーションのurls.pyをincludeを用いてマッピングし、アプリケーションのurls.pyで詳細をアプリケーションの中のviewの定義のマッピングを行う。まず、プロジェクトのurls.pyには、includeメソッドを利用して、アプリケーションのurls.pyと紐づける。 path()の第1引数がアドレスの文字列で,第2引数がそのアドレスがリクエストされたときに起動するview関数,nameはこのアドレスを逆引きするための名前!!! APP/testapp/urls.py from django.urls import path from . import views urlpatterns = [ path('',views.index,name = 'index') ] 2段階目でアプリディレクトリにurls.pyを作成 アドレスの最後が何もなしだったらviews.pyのindex関数を起動 $ python manage.py runserver サーバー起動 ブラウザで http://localhost:8000/testapp/ ブラウザ上にDjangoの旅が出力されていればOK!!! 参考文献 path()について https://qiita.com/j54854/items/201ecbe55017fd2a7996 include()について https://qiita.com/miler0528/items/f5f22db2141ec7cd7198 https://www.nblog09.com/w/2019/01/20/django-mapping/
- 投稿日:2021-06-15T23:33:02+09:00
CPU環境でも動画ファイルの物体検知ができる環境を簡単に作る
はじめに 色々と目的があり、仕事ではなく家の環境で物体検知をやってみたくなったんですが 動画ファイルを入力して検知した結果の動画ファイルを出力する CPU環境でも動く 簡単に構築できる ような環境構築の情報が思いのほかピンポイントに無くてちょっと苦労したので、 この機会にまとめてみることにしました ※ブログの内容を、環境構築に内容を絞ってまとめなおしています 元々の動機は「格安の監視カメラで撮影した動画ファイルを自動で分析したいから」です ※本記事で情報不足だと感じる方は、こちらもご覧ください。もう少し詳細に書いています 環境 Windows10 Anaconda 4.10.1 10年前のVAIOをSSDに換装して使い続けており、 スペックは中々に貧弱です… 環境構築手順 早速、手順をまとめていきます Anacondaの仮想環境を作る 仮想環境の作成 Anaconda Powershell(Anaconda Prompt)を立ち上げ、 yolov3(名前はなんでもいいんですが)用の仮想環境を作っておき、 作成した仮想環境に切り替えておきます conda create --name yolov3 conda activate yolov3 必要なライブラリのインストール 必要なライブラリをインストールしておきます tensorflow、kerasのバージョンによる影響は確認していませんが、 バージョン違いによるエラーも多々起こるため、 理由が無ければ同じバージョンをインストールすることをお勧めします conda install tensorflow==1.14.0 keras==2.2.4 pillow matplotlib ここで、h5pyというライブラリをダウングレードしておかないと、 後で実行したときにエラーを吐きます こちらも、理由が無ければ同じバージョンをインストールすることをお勧めします conda install h5py==2.10.0 さらに、動画を入出力とする場合はOpenCVを使用するので、そちらもインストールします デフォルトのリポジトリではopencvはインストールできないので、 conda-forgeのリポジトリを指定してインストールします conda install -c conda-forge opencv==4.0.1 これで環境が整いました! ソースコードを準備する こちらのYOLOv3実装ソースコードを使用します https://github.com/qqwweee/keras-yolo3 gitでクローンしてもいいですし、ダウンロードしてきても良いと思います 学習済モデルを準備する 学習済モデルのダウンロード 上記githubのREADMEに書いてくれていますが、 学習済モデルをダウンロードすることができます https://pjreddie.com/media/files/yolov3.weights こちらからダウンロードしておきます ファイルをkeras用に変換する ダウンロードしたファイルは、keras用のファイル(.h5)に変換する必要があります 変換スクリプトも準備してくれているので、そちらを使います python convert.py yolov3.cfg yolov3.weights model_data/yolo.h5 すると、model_dataの配下にyolo.h5というファイルが作成されているはずです ここまでで、準備OKです! 実行 python yolo_video.py --input <入力ファイル名> --output <出力ファイル名> これで、動画を入力すれば物体検知した動画が出力されるはず! 実行のサンプル 無料の動画を使って、物体検知を実際にやってみました こちらからダウンロードした動画を使っています https://www.motionelements.com/ 実際に検知した結果がこちらです(画像です) Qiitaって手軽に動画は貼れないんですね… 動画を見てみたい方はブログの方をご覧ください https://ai-mechatro.com/analyze-security-camera-3/
- 投稿日:2021-06-15T22:50:13+09:00
SeleniumでCookieを再利用する
はじめに Seleniumでスクレイピングする際にログインセッションを引き継ぎたいと思い、いろいろ調べた結果をここに備忘録的に残しておくことにしました。 方法 まず、driver.get_cookies()でクッキーを取得します。 次に、例えばjson.dump()などで適当に保存しておきます。 再度取り出したCookieの適用は、下記のように行います。 ※Cookie適用前に、対象のドメイン配下のページ(例えばルートURL)にアクセスしておく必要があるようです。 driver.get(url_root) for cookie in cookies: driver.add_cookie(cookie) driver.get(url_target) これらを用いて、条件分岐でいい感じにログイン処理を実装すればよさそうです。 参考
- 投稿日:2021-06-15T22:14:07+09:00
テスト
初めまして。大学生でpythonやctfを勉強している"さいず"と申します。 ここでは私が学んだことを徒然なるままに書き記していきます。 他にも興味が湧いたこともここに書いていくかもしれないです。 よろしくお願いします。
- 投稿日:2021-06-15T21:49:35+09:00
test
最初に 前回のチュートリアルで作成した投票アプリに対してテストコードを書く所から始める。内容はチュートリアル5〜7をカバーする予定になる。
- 投稿日:2021-06-15T21:29:58+09:00
test
最初に 前回のチュートリアルで作成した投票アプリに対してテストコードを書く所から始める。内容はチュートリアル5〜7をカバーする予定になる。
- 投稿日:2021-06-15T20:42:42+09:00
Django公式チュートリアル(1~4)で分からない所、徹底的に調べた。
最初に 本格的なWebアプリケーションを作成したいのでPythonのフレームワークDjango(読み方:ジャンゴらしい。ディーどこ行った。)についてチュートリアルをこなしながら学んで行こうと思う。実際に作成するアプリは質問に対して回答して投票を表示するアプリになる。 この記事は公式チュートリアルの1〜4までに気になった事躓いた事をまとめていく。全部で1〜7まであるが4までにアプリは完成する。 5からはテストコード等を書いていくのでボリュームが多くなるため、前編として今回の記事を投稿する。後編も必ず書こうと思う。 今回のチュートリアルで作成したもの 質問一覧のページがあって、そこから質問に対して投票を行う。その後今まで投票された数を表示するページリダイレクトされる。 VSCodeのPython用の拡張機能をインストールする。 コードを書くにあたって構文エラーは事前に無くしたいので、拡張機能をインストールする。マイクロソフトがPython用に提供しているものがあるのでそちらをインストールする。 自分はanaconda環境での使用をしているので下記の通知が出てきた。 terminal.integrated.inheritEnv を false にした方が良いらしい。 We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. VSCodeの code > preference > settings に terminal.integrated.inheritEnv と入力して出てきたチェックを外す。 【Mac/Python】VSCodeターミナル動作が通常ターミナルと違う時 | ゆうきのせかい それだけだと Import "django.contrib" could not be resolved from source という警告がでたままなので、赤枠の箇所をクリックしてDjangoをインストールした環境を選択すると警告文が消えます。 インストール 好きなフォルダーを作成して、そこに開発していく。Pythonの環境構築は自分は下記のように行っている。 https://techblog-pink.vercel.app/posts/cc111706c3167 anacondaで仮想環境を作成して、Djangoをインストールしていく。 pip install Django 開発を始めたいフォルダに移動して下記を実行する。 django-admin startproject mysite mysiteというディレクトリが生成される。 mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py フォルダはこのような構成になっている。 mysite/mysite となっているのが不思議だ。外側の mysite はDjangoのシステム的には何でも良いらしい。 urls.py はプロジェクトのURLを宣言する。目次のような機能を提供する。 サーバを起動する。 実行すると http://127.0.0.1:8000/ でサイトにアクセス出来るようになる。 最初はデータベース等の設定をしていないので、ターミナルに警告が表示されるが他に問題がなければアクセス出来る。 ctr + c でサーバを終了することが出来る。 python manage.py runserver GitHubで管理する これから開発していくので、Githubにコードをあげて進捗を管理したい。しかし、このままだと settings.py に書かれた SECRET_KEY も一緒にアップロードしてしまうので別ファイル local_settings.py に SECRET_KEY を書く。ちなみに私は気付かずに一度GitHubに、そのままアップしてしまった。そのため新たに SECRET_KEY を作成する手間が掛かる。 local_settings.py SECRET_KEY = 'settings.pyにあったシークレットキーまたは、新たに作成したもの' この変数を settings.py で読み込んでいく。下記のコードを追加する。 try: # 同じ階層のlocal_settingsファイルからSECRET_KEYをkeyとして読み込む。 # 参考記事等では .local_settingsとなっているが、local_settingsはファイルなので必要ない # 将来的に複雑にファイルを分ける必要が出てフォルダにする場合は .が必要となる。 # ダメだった .local_settingsが正しかった。Django環境だとパッケージとして見なされるのか... from .local_settings import SECRET_KEY as key except ImportError: pass # シークレットキーの部分を読み込んだ変数に置き換える。 SECRET_KEY = key 実際に test.py と test2.py を同じ階層に作成してどのように読み込まれるか調べてみた。 test.py hello = "hello" test2.py # .testとするとImportError: attempted relative import with no known parent package # と表示される。 # asを付けない場合は helloで読み込まれる。 from test import hello as A print(A) settings.py と同じ環境に出来ていると思ったが、全然違った。 local_settings.py は パッケージとして認識されるが、 test.py はパッケージとして認識されないので . を使用するとエラーになる。そもそも python test2.py と直接実行しているので settings.py とは違う実行状況になる。 詳しくはここで回答を頂いている。 SECRET_KEYの作成 自分は知らずにシークレットキーをGitHubにあげてしまったので新たに作り直す必要があるのでシークレットキーを生成してくれるプログラムを実行する。これを直接ターミナルで実行するとシークレットキーが生成されるのでそれを local_settings.py に貼り付ける。 get_random_secret_key.py from django.core.management.utils import get_random_secret_key secret_key = get_random_secret_key() text = 'SECRET_KEY = \'{0}\''.format(secret_key) print(text) これでようやくチュートリアルに専念してコードを書き進める事が出来る。 プロジェクトとアプリ Djangoではプロジェクトの中にアプリが含まれる。なので特定のDjangoで作成されたWebサイト全体をプロジェクトと呼び、その中に含まれる小規模な投票アプリ、ログシステムをアプリと呼ぶ。 アプリを作成する。 manage.py と同じ階層に移動して python manage.py startapp polls を実行すると polls というフォルダが生成される。 polls/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py これでプロジェクトにpollsというアプリが作成された事になる。 Viewを作成する。 URLからパスにアクセスがあって、その際に実行する関数がViewになるここでHTMLファイルを返したりと処理を決めることが出来る。 views.py に記述する。 def detail(request, question_id): return HttpResponse("You're looking at question %s." % question_id) def results(request, question_id): response = "You're looking at the results of question %s." return HttpResponse(response % question_id) def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) viewメソッドの第一引数には必ずHttpRequestクラスを受け取る。 引数 request には今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってくる。そして戻り値としては HttpResponseクラスを返す必要がある。 実際にHttpResponseとHttpRequestの中身がどんな感じになっているのか気になる人は下記のページで確認できます。 ざっくりですが、 views.py があって実行されるとクラスの中身はこんな感じに格納されているみたいです。 def sample(request): print(request) response = HttpResponse('') print(response) return response # 実行結果 # <WSGIRequest: GET '/hello/'> # <HttpResponse status_code=200, "text/html; charset=utf-8"> Djangoはリクエストを受け取ってレスポンスを返しているだけです【詳しく解説】 views.py だけではURLと紐づいていないので URLconfを作成する。 URLconf urls.py を作成する URLconfを作成するには urls.py というファイルを views.py と同じ階層に作成する。mysiteフォルダ内には urls.py がすでにあるので pollsフォルダ内に作成する。中身はこんな感じになる。 include()を使ってアプリのURLを結び付ける。 polls/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] <int:question_id> が views.py 関数の引数として渡される。この <> を使用すると、URLの一部がキャプチャされ渡される。文字列 :quesiton_id> は一致するパターンを定義し、 <int: の部分はURLパスに当てはまる値の型を指定している。なので <str:, <slug: などもある。 そして、これをmysiteフォルダ内の urls.py に結びつけてあげる必要がある。一応こっちが最初に読み込まれるのでここに後から追加したアプリのURLを include() を使って追加していくイメージになる。 mysite/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path('polls/', include('polls.urls')), path('admin/', admin.site.urls), ] これでpollsアプリのURLを結び付けることができた。サーバを起動して http://localhost:8000/polls/ にアクセスするとViewが返されるようになる。 path()の引数 4つの引数を受け取ることが出来る。そのうち route と view の2つは必須で残り kwargs と name は省略出来る。 route:URLパターンを含む文字列が入る。リクエストを処理する際に urlpatterns を順番にみて最初にマッチしたものを取り出す。このパターンはGET、POSTのパラメータに影響は受けないあくまでURLパスだけを見る。 view:URLがマッチしたら、そこに付随するView関数を返す仕組みになっている。 kwargs:任意のキーワード引数を辞書としてView関数に渡せる。 name:URLに名前を付ける事で reverse() を使って呼び出せるようになり、htmlのテンプレートでformに指定するURLを変更する際に動的にURLが変更されるようになる。 polls/urls.py がこんな感じだとして from django.urls import path from . import views import re urlpatterns = [ path('', views.index, name='index'), path('<int:number>', views.subView, name='suburl'), ] 試しに下記の views.py を実行してみる。 from django.http import HttpResponse from django.urls import reverse def index(request): urlName = reverse('index') print(urlName) return HttpResponse("Hello, world. You're at the polls index.{0}".format(urlName)) # 実行結果 # /polls/ こうすると例えば pollllllls/urls.py とURLを変更してもコードを変更する必要がないので、変更に強いコードになります。 ※pathのnameについて python、djangoのurls.pyで設定するnameってなんやねん?? - Qiita DjangoのURL ユーザがDjangoで作られたサイトにアクセスした際にどのような処理が走るのか。 ROOT_URLCONFに設定されているURLを確認する。(HttpRequestオブジェクトにurlconfという属性が設定されている場合はその値をROOT_URLCONFとする。) urlpatternsという名前の変数を探す。この変数値は django.urls.path() または django.urls.re_path() インスタンスのsequenceでなければならない。 urlpatternsから順番に要求されたURLパターンを探す。 マッチしたらViewを返す。 マッチしなかったらエラーハンドリングビューを返す。 URLconfのサンプル pathの左側がマッチするURL(route), 右側がマッチしたら呼び出される関数(view) from django.urls import path from . import views urlpatterns = [ # /articles/2003/にアクセスした場合 # Views.special_case_2003(request)を呼び出す。最後の/もしっかりないとマッチしない。 # 引数としてrequestが関数に渡る。 path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive), path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail), ] /articles/2005/03/ というアクセスがあった場合上記のパターンの中からviews.month_archive(request, year=2005, month=3) という views.py (上記で記したviews.pyとは別で例としてあげてるurls.pyに対応するviews.pyがあったらという話で見てもらいたい。)に書かれたviewメソッドに引数を渡して呼び出す事になる。引数 request にはHttpRequestクラスが入り 今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってる。 /articles/2003/03/building-a-django-site/ なら最後のパターンにマッチして、このように views.article_detail(request, year=2003, month=3, slug="building-a-django-site") 関数を呼び出す。 タイムゾーンの設定 デフォルトではUTCと世界標準時間になっているので、ここで日本時間に変更しておきたいと思う。 settings.py の TIME_ZONE = 'UTC' を TIME_ZONE = 'Asia/Tokyo' に変更する。 データベースの作成 ここでデータベース用語についてまとめて置く。 カラム:縦列の事を指し、別名では列と呼ばれる。 カラム名:列全体に付けられた名前 フィールド:データが入っている場所。 フィールド名:そのデータが入っているカラム名を指す。 項目名:フィールド名を指す。 テーブル:Excelでいうシートのようなもの。 レコード:データそのものを指す言葉になる。もう一つは横列の事を指し、行と呼ばれる。そして行をロウと呼ぶこともある。 これからデータベースを設定していく、チュートリアルの段階なのでまずは複雑な設定の必要がないSQLiteを使用していく。 python manage.py migrate 実行すると settings.py に書かれた INSTALLED_APPS の設定を参照して mysite/settings.py ファイルのデータベース設定に従って必要な全てのデータベーステーブルを作成する。 migrate(マイグレート)するとは データベースを削除してから作り直すと、DBに保存されている情報が全て削除されてしまう。こういった事態を回避する方法として、データベースマイグレーションを行う方法が生まれた。マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うことが出来る。 モデルの作成 models.py models.py はデータを保存したり、取り出したりするときの設定を記録するファイルになる。データベースの取扱説明書と書かれることが多い。 models.py からデータベースが作成される流れ models.pyファイルでモデル(データベースの型)を作成する。 migrationファイルを作成する。 migrate(最終的にはおそらくSQL文に変換されて、データベースに命令を出してデータベースを作成する。)する。ここで上記のファイルを元にデータベースが作成される。 つまりモデルはデータベースのレイアウトとそれに付随するメタデータになる。 簡単な例を示す。(チュートリアルとは関係ないモデル) # modelsモジュールを読み込むこれはデータベースを作成するのに必要な機能が格納されている。 from django.db import models # データベース作成機能を継承してモデルを書き込んでいく。 class BookModel(models.Model): # booknameという文字を入力することが出来るフィールドを作成する命令をだす。 # bookname = models.CharField(max_length = 50)とすると50文字までと制限をかけれる。 # CharFieldには必須の引数がありmax_lengthを設定しないといけない。 bookname = models.CharField(max_length = 50) # CharFieldとほぼ同じで文字列を扱うがTextFieldの方がデータの読み出し等にコストがかかるらしい。 summary = models.TextField() # 整数値を入れるフィールドを作成する。 rating = models.IntegerField() ForeignKey(外部制約キー) これを使ってこの後2つのモデルを双方向に参照できるようにするのだが、その前にデータベースにおいて外部制約キーまたは外部キーとも呼ばれるがどのような役割を果たしているのかみていこうと思う。 外部キーとは関係データベースにおいてデータの整合性を保つための制約(参照整合性制約) 外部キーに設定されている列(子テーブルのカラム)には、参照先となるテーブルの列内(親テーブルのカラム内)に存在している値しか設定できない。 そのため、外部キーに設定されている子テーブルの列内に親テーブルの列内に存在しない値を追加しようとするとエラーになる。 なので新しく値を追加したい場合は一度、親テーブルで追加する必要がある。 このように制約を結ぶ事でデータの整合性を保つ事ができる。 Djangoでは foreign keyが設定されている方が子テーブルになる。 今度は少し複雑なモデルを見ていく。 図のようなQuestionとChoiceという2つのモデルを作成する。 ChoiceにQuestionが ForeignKey を使って紐ずけられている。 on_delete=models.CASCADE は紐づけられたモデルが削除される際にどのような動作をするかを決める事が出来る。削除された後そのモデルだった部分をNullで埋めたり、そもそも削除できないようにしたりと出来る。 CASCADE は紐づけられた側のモデルで関連するオブジェクトも削除するという動きになる。なので Questionが削除されたら、Questionと関連のあるChoice側のオブジェクトも削除するような動作を取る。 詳しくはここの記事が分かりやすい。 Django2.0から必須になったon_deleteの使い方 - DjangoBrothers from django.db import models class Question(models.Model): # Question textというフィードを作成してそこに入る文字列は200文字までと制限している。 question_text = models.CharField(max_length=200) # 基本的には変数名がフィールド名として使用されるが、引数で文字列を渡す事でフィールド名設定する事が出来る。 pub_date = models.DateTimeField('date published') class Choice(models.Model): # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) # Votesフィールドは整数値を受け付ける。最初は0が入る。 votes = models.IntegerField(default=0) モデル間の双方向やりとりについて 【Django】1対多の関係( related_name, _set.all() )について - Qiita モデルを作成したのでデータベースにマイグレート(モデルを元にデータベースのレイアウトを作成するデータを追加するわけではない。)していく migrationファイルを作成する前に、新たに作成したアプリpollsを伝える必要がある。 settings.py の INSTALLED_APPS の配列に 'polls.apps.PollsConfig', を追加する。 次にmigrationファイルを作成する。 新たにファイルを作成する必要はなくターミナルでコマンド実行する。 python manage.py makemigrations polls 実行すると models.py を元にmigrationファイルが生成される。 python manage.py migrate 実行するとmigrationファイルを元にデータベースが作成される。 試しにコマンドラインからDjango shellを通してデータベースを変更したりしてみる。 シェルに入るには下記のコマンドを実行する。 python manage.py shell するとPythonコード >>> を書ける状態になるのでここからデータベースにアクセスするコードを書く。 # 作成したモデルを読み込む from polls.models import Choice, Question # 格納されたデータを確認する。まだ追加していないので、空になっている。 Question.objects.all() from django.utils import timezone # モデルにデータを追加する。 q = Question(question_text="What's new?", pub_date=timezone.now()) # データベースに保存する。 q.save() q.id q.question_text q.pub_date # データの上書き q.question_text = "What's up?" q.save() # データが格納されているのが確認できる。 Question.objects.all() # 実行結果:<QuerySet [<Question: Question object (1)>]> このままではadminページでオブジェクト名が Question object (1) と表示され分かりにくいので 特殊メソッド __str__() をモデルに追加する。 あと追加で was_published_recently(self) メソッドを書きました。 データが最近追加されたかどうかを判定するメソッドで True or False で返します。 class Question(models.Model): # クラス変数を定義する。データベースフィールドを表現している。 # Charフィールドは文字のフィールド question_text = models.CharField(max_length=200) # 日時のフィールド pub_date = models.DateTimeField('date published') def __str__(self): # インスタンスを生成して、printした際にここが実行される。 # シェルで表示されるオブジェクトに質問名が使われるだけでなく # adminでオブジェクトを表現する際にも使用されるので追加する必要がある。 return self.question_text def was_published_recently(self): now = timezone.now() # now - datetime.timedelta(days=1)は今の時間から一日引いた日付を出す。 # 2021-05-19 23:29:56.216634こんな感じの値になる。 # pub_dateが現在時刻より過去で現在時刻から一日以内の場合はTrueを返すメソッド return now - datetime.timedelta(days=1) <= self.pub_date <= now class Choice(models.Model): # これはChoiceがQuestionに関連付けられている事を伝えている。 # データベースの多対一、多対多、一対一のようなデータベースリレーションシップに対応する。 # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text すると下記のように表示されるので、 何のデータが入っているのか分かり易くなった。 Djangoのshell内でも下記のようにオブジェクトの中身が分かり易くなった。 from polls.models import Choice, Question Question.objects.all() # 実行結果:<QuerySet [<Question: What's up?>]> # 続けて色々な関数を試してデータベースから # データを取得してみる。 # filterをかけてデータを取得する。 # idはデータベースにデータを追加した際に1から順番に自動で割り振られる。 Question.objects.filter(id=1) # 実行結果:<QuerySet [<Question: What's up?>]> # Questionオブジェクトのquestion_textフィールドで"What"から # 始まるデータを取得する。 Question.objects.filter(question_text__startswith="What") # 実行結果:<QuerySet [<Question: What's up?>]> from django.utils import timezone # 今年作成されたデータを取得する。 current_year = timezone.now().year Question.objects.get(pub_date__year=current_year) # id 2のデータを取得する。 # filterで指定しなくてもgetでも取得できる。 Question.objects.get(id=2) # 実行結果はない場合はエラーになります。 # プライマリーキーと呼ばれるものでいまいちidとの違いが分からない。 # 取得するデータはidの場合と同じになる。 q = Question.objects.get(pk=1) q.was_published_recently() # 実行結果:True # ChoiseはQuestionと関連付けられてるのでQuestionからも # データにアクセスする事ができる。 # Choice側にはまだデータを入れてないので結果は何も表示されない。 # Choicecは質問に対する回答の選択肢をデータとして持つ。 # qには今What's upという質問が入っているので、それに対しての # 選択肢を作成した。 q.choice_set.all() q.choice_set.create(choice_text="Not much", votes=0) q.choice_set.create(choice_text="The sky", votes=0) c = q.choice_set.create(choice_text='Just hacking again', votes=0) # 選択肢が関連づけられている質問を返す。 c.question # 実行結果:<Question: What's up?> q.choice_set.all() # 選択肢が何個あるか数える。 q.choice_set.count() Choice.objects.filter(question__pub_date__year=current_year) # 実行結果:<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]> # just hackingの選択肢だけ削除する。 c = q.choice_set.filter(choice_text__startswith="Just hacking") c.delete() pkとidの違い pkは primary key の略で、データベースでは 主キー と呼ばれている。主キーはテーブルで一意の値を取る。 どのレコードを主キーにするかはフィールド名を定義する時に primary_key=True を付ければ設定できる。 codeというフィールドに入る値を主キーとする。 code = models.CharField(max_length=10, primary_key=True) djangoの場合はModel(=テーブル)には必ず1つの主キー用のフィールドが必要になる。ユーザが定義しない場合はidという名前のAutoFiekd(int型の連番1~n)が作成される。 そのため、pkキーを定義しない場合はpkはidのショートカットになる。 pkキーを上記の code のように定義した場合はidは作成されない。 SQLに直接アクセスする。(おまけ) sqllite3を使用してデータベースを作成している場合はmysiteフォルダ内にデータベースのファイルが生成されていると思うので、下記のコマンドからSQLで操作するシェルに入る事ができる。 sqlite3 db.sqlite3 # シェル内での操作 # 作成されたテーブル一覧を確認できる。 >>> .table auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session # 先ほどモデルから作成されたテーブル auth_user_groups polls_choice auth_user_user_permissions polls_question # 構造を確認できるみたいだけど、見てもよく分からなかった。 >>> .schema # シェルから抜ける。 >>> .quit モデルからデータベースを操作する事ができたので、次に先ほど登場したadminページにアクセスしたいと思う。 adminページアクセスする。 ログインが必要なのでユーザを下記のコマンドから作成する。 python manage.py createsuperuser # 実行すると下記の入力画面が登場する。 Username:名前を入力 Email address:@と.comがあれば架空で良い Password: Password(again): ユーザを作成したら開発サーバを起動してhttp://127.0.0.1:8000/admin/ にアクセスするとログイン画面となるのでログインする。 するとそこから作成したモデルを閲覧したりデータを追加したりできる。 ChoiceモデルからはQuestionを選択して使用することしか出来ないのが確認できる。 右側にある + ボタンを押すと Questionのページに飛びそこから新しい質問を追加することはできる。 Views.pyからデータベースの値を取得する 先ほどDjango shellで使用したPythonコードを使って、 views.py にデータを取得していく。 # 最後はHttpResponseを返す必要があるのでimportする。 from django.http import HttpResponse # データベースを操作するためにモデルを読み込んでおく。 from .models import Question def index(request): # データベースから最新5件を取得する。 # こんな感じのデータになる。<QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]> latest_question_list = Question.objects.order_by('-pub_date')[:5] # "test3, hello, what's up?"区切った文字列にしてHttpResponseに渡す。 output = ', '.join([q.question_text for q in latest_question_list]) return HttpResponse(output) # Leave the rest of the views (detail, results, vote) unchanged Viewとページデザインを切り離す Viewではデータの取得や操作を専門的に行ってもらい、そのデータをテンプレート(htmlにPythonの変数を入れられる。)に渡してページをレンダリングしてもらうようにする。 pollsディレクトリの中に、templatesディレクトリを作成する。システムでそのディレクトリを認識する。作成した templatesディレクトリにpollsフォルダを作成する。なので polls/templates/polls みたいなディレクトリが完成する。その中にテンプレート index.html を作成する。なぜ templates/polls とするのかというとDjangoは名前がマッチした最初のテンプレートを使用するので、もし異なるアプリケーションの中に同じ名前のテンプレートがあるとそちらを読み込む。それを回避するために名前空間(所属する領域)を与えている。 views.py をテンプレートにデータを渡せるように書き換える。 from django.http import HttpResponse from django.template import loader from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] # テンプレートを読み込む template = loader.get_template('polls/index.html') # 辞書型に最新5件のデータを格納する。 context = { 'latest_question_list': latest_question_list, } # 辞書型のデータをテンプレートに渡してページを作成する。その結果をHttpResponseに返す。 return HttpResponse(template.render(context, request)) view.pyをさらに短くする。 from django.template import loader from django.http import HttpResponse を使わない書き方 より簡素にする事が出来る。 その場合、 render() 関数は第一引数に requestオブジェクト , 第二引数に テンプレート名 , 第三引数に 辞書型(テンプレートに渡したいデータ) を記述する。 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) 404エラーを出力する。 from django.http import Http404 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) # 質問の詳細ページのビュー def detail(request, question_id): try: # アクセスのあったURLでpkの値が変わる。/polls/1/なら1になる。 # データベースでエラーになるとHttp404を出力する。 question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("Question does not exist") return render(request, 'polls/detail.html', {'question': question}) 上記のdetail()を短くする。 django.shortcuts にはこうしたコードを省略する関数が多くあるので調べると面白いかもしれない。 from django.shortcuts import get_object_or_404, render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) index.html index() に対するテンプレートにはこのように記述する。 ビューで latest_question_list オブジェクトが辞書型に格納されて渡されているので、それを受け取ってテンプレート内でオブジェクトに格納された値を属性アクセス . して取得している。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} index.htmlのハードコード(直接記述している)を削除する このように直接URLを書き込むと変更に弱いコードになってしまうので、 polls.urlsモジュールのpath() 関数でname引数を定義したのでそれをURLに使用する。 {%url%} を使う。 <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> 変更後 <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> そしてテンプレートを入れるディレクトリを作成した際のように名前空間を追加する。システムが別々のアプリ内で同じname引数を含んでいても区別が付けられるように template/polls/detail.html にアクセスしたい場合は polls:detail と記述する。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} そして、URLconf(urls.py)に名前空間を追加 app_name = 'polls' from django.urls import path from . import views # ここを新たに追加した。 app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] こうするとモジュールに指定されたURLの定義を検索出来る。例えば polls/specifics/12 のようにURLを変更した場合 urls.py に書かれたパスを変更する事でテンプレート側に変更を加える必要がない。 path('specifics/<int:question_id>/', views.detail, name='detail'), detail.html detail() に対するテンプレートはこのように記述する。 <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul> フォームを使って質問に対して回答を送信する。 detail.html に <form> を追加してサーバにデータを送信して質問に対して、投票出来るようにする。 下記のように detail.html を変更する。 <!-- 質問の内容 --> <h1>{{ question.question_text }}</h1> <!-- もしデータベースから質問が取得出来ない場合エラーが表示される。 --> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> <!-- セキュリティのため --> {% csrf_token %} <!-- 質問に対する選択肢を並べる --> {% for choice in question.choice_set.all %} <!-- forloop.counterはforタグのループが何度実行されたかを表す値です。 --> <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> {% endfor %} <input type="submit" value="Vote"> </form> views.pyにvote()関数を追加する コードの流れとしては ユーザがdetailページの質問に対する選択肢を選択する。 Voteボタンをクリックする。 データがサーバに送信される。 選択された選択肢の投票数をインクリメントする。 results.htmlにリダイレクトする。Postデータが成功した後は基本的に HttpResponse ではなく HttpResponseRedirect を返す必要がある。 # 追加した。HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render # 追加した。reverse from django.urls import reverse # Choiceを追加した。 from .models import Choice, Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) # 質問に対して選択して投票する。 def vote(request, question_id): # まず質問があるかどうか確認する。 question = get_object_or_404(Question, pk=question_id) try: # ユーザが選択した値からpk値を取得して、それを元にモデルから選択肢のオブジェクトを取得する。 # なければYou didn't...choiceと表示される。 selected_choice = question.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: # 選択肢オブジェクトから何回投票されたか表示するvotesオブジェクトをインクリメントする。 selected_choice.votes += 1 # データベースに保存する。 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. # データの保存に成功したら、results.htmlにリダイレクトする。 return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) reverse()関数とは この関数を使うと、vote関数中でのURLのハードコードを防ぐ事が出来る。 引数としては polls:results リダイレクト先のビュー名とそのビューに与えるURLパターン question.id を渡せる。 reverse('polls:results', args=(question.id,)) # 返り値 '/polls/3/results/' Post通信が成功した際のresults関数を作成する。 views.pyにresults関数を追加する。 # 先ほどまで書いてきたviews.pyにresults関数を追加する。 def results(request, question_id): # 指定したpkキーにデータがあれば返す、なければエラーを返す。 question = get_object_or_404(Question, pk=question_id) # 質問オブジェクトを引数で貰ってページを作成する。 return render(request, 'polls/results.html', {'question': question}) results.htmlを作成する。 <!-- 質問を表示する。 --> <h1>{{ question.question_text }}</h1> <!--質問の選択肢とそれに対する投票数を取得する。--> <ul> {% for choice in question.choice_set.all %} <!--choice.votes|pluralizeは投票数が2以上の場合vote s とsを追加してくれる。--> <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a> Built-in template tags and filters | Django documentation | Django 汎用ビューを使って今まで書いたコードをさらに短くする。 views.py に書かれた index(), detail(), results() 関数は3つとも似たような機能でURLを介して渡されたパラメータに従ってデータベースからデータを取り出しページを作成する。これらの一連の動作はよくある事なのでDjangoでは汎用ビューというショートカットを用意してより簡素に機能を実装出来るようにしている。 汎用ビューを適用するにはいくつかこれまでに書いたコードを修正する必要がある。 URLconfを変換する。 古い不要なビューを削除する。 新しいビューにDjango汎用ビューを設定する。 URLconfの修正 変更前 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] 変更後 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('<int:pk>/', views.DetailView.as_view(), name='detail'), path('<int:pk>/results/', views.ResultsView.as_view(), name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] views.IndexView.as_view(), views.DetailView.as_view(), views.ResultsView.as_view() と as_view() と書くようになった。 そして、 question_id が pk に変更された。ここは同じでもいいような気もする。結局同じ数値が返り値として入るから。 ※後述する DetailView には pk キーを渡す必要があるので同じではダメなようだ。 viewsの修正 index(), detail(), results() 関数を削除しクラスベースに書き換える。 indexでは ListView を継承している。 detail, resultsでは DetailView を継承している。 ListView 「オブジェクトのリストを表示する。」 メソッドのフローチャート継承したメソッドが下記の順番で自動で実行される。 1.setup() 2.dispatch() 3.http_method_not_allowed() 4.get_template_names() 5.get_queryset() 6.get_context_object_name() 7.get_context_data() 8.get() 9.render_to_response() このようにメソッドが実行されるので継承したクラスに get_quesryset() メソッドを追加して内容を上書きする事が出来る。 template_name ListView ではデフォルトの場合 <app name>/<model name>_list.html を自動で生成して使用する。 その場合、テンプレート名は polls/question_list.html になる。 しかし元々作成してある polls/index.html を使用したい場合は template_name に 'polls/detail.html' を代入する事でDjangoがそちらを使用するように認識してくれる。 DetailView 「あるタイプのオブジェクト詳細ページを表示する。」 なので ListViewの詳細ページをDetailViewで表示するみたいな使われ方をする。 template_name そしてデフォルトでは DetailView は <app name>/<model name>_detail.html という名前のテンプレートを自動生成して使用する。 その場合、テンプレート名は polls/question_detail.html になるが、今回は自動生成されたものではなく元々作成してある polls/detail.html を使いたいので template_name を指定して元々のテンプレートを使用する。方法はListViewの時と同じで template_name に polls/detail.html を代入する。 model このクラス変数はビューが使用するモデルを指定している。 model = Question の場合は裏側で Question.objects.all() を行ってくれる。なので queryset = Question.objects.all() としても良い。 from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic from .models import Choice, Question class IndexView(generic.ListView): # デフォルトのビューを使用せず、元々作成してあったものを使用する。 template_name = 'polls/index.html' # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。 context_object_name = 'latest_question_list' def get_queryset(self): """最新の5件を取得する。""" return Question.objects.order_by('-pub_date')[:5] class DetailView(generic.DetailView): # 自分がどのモデルに対して動作するかを伝えている。 # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。 # pkの部分はurls.pyで先に指定してある。 model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): ... # 前回のまま変更しない。 これでサーバを起動して特にエラーもなく質問のリストページ(index.html)、詳細ページ(投票するページdetail.html)、投票後の今ままでの投票数を表示するページ(results.html)が表示されていれば汎用ビューでのアプリ構築ができたと思う。 最後に 過去にRuby on railsのフレームワークの中身がどう動作しているのかイメージ出来ないのが苦痛(フレームワークは面倒な中身を気にしなくてもアプリが作れるように設計してあるので仕方ないかもしれない。)で挫折しているので今回Djangoのチュートリアルまだ途中ですが挫折せずにアプリ作成まで出来てよかったです。普段Jsonしか触らなかったので少しですがデータベースを作成して操作する経験が出来たのでこれを気にSQL構文をもう少し勉強しようと思う。多対多、多対一の関係とかも自分で作成出来るまでになります。自分の作成したアプリのER図をかける書けるようになりたい。 参照 SECRET_KEYを誤ってGitHubにプッシュしたときの対処法(Django編) - Qiita Pythonの相対インポートで上位ディレクトリ・サブディレクトリを指定 | note.nkmk.me ワイルドカードインポート(import *)は推奨されない 「カラム名」と「フィールド名」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 3. データベースマイグレーション | densan-labs.net モデル(データベース)の作成 プログラミングでよく見かける"コンテキスト(context)って何? - Qiita Python Django チュートリアル(3) - Qiita DJangoのお勉強(1) - Qiita ドキュメント Djangoの汎用ビュー入門(ListView) FOREIGN KEY制約(外部キー制約を設定する) 外部キー制約とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 Django ForeignKeyで1対多のモデルを構築 記事に関するコメント等は ?:Twitter ?:Youtube ?:Instagram ???:Github ?:Stackoverflow でも受け付けています。どこかにはいます。
- 投稿日:2021-06-15T20:42:38+09:00
Django公式チュートリアル(5~7)で分からない所、徹底的に調べた。
最初に 前回のチュートリアルで作成した投票アプリに対してテストコードを書く所から始める。内容はチュートリアル5〜7をカバーする予定になる。 Djangoでテストコードを書く チュートリアルが用意してくれたバグに対してテストコードを書いていく。 まずはバグを確認する。 Qustion.was_published_recently() のメソッドはQuestionが昨日以降に作成された場合に True を返すが 未来の日付になっている場合にもTrueを返す。これがバグになる。自分は前編の記事で登場した models.py に書かれた was_published_recently() はバグに対応済みなので下記のコードに入れ替えてバグを作り出す必要がある。 これだと pub_date が未来の場合も True を返す。 def was_published_recently(self): return self.pub_date >= timezone.now() - datetime.timedelta(days=1) バグの確認 コードに故意にバグを生み出した所でバグを確認したいと思う。 python manage.py shell データベース APIを叩いていく。 import datetime from django.utils import timezone from polls.models import Question # 投稿日を今から30日後に設定した Questionオブジェクトを作成する。 # この状態ではQuestionクラスからインスタンスを生成しただけでデータベースに保存はされていない。 future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) # 結果 True テストを作成する。 pollsアプリのディレクトリに tests.py というファイルがあると思うのでそこにテストコードを書いていく。 import datetime from django.test import TestCase from django.utils import timezone from .models import Question class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) テストを実行する。 python manage.py test polls # 実行結果 Creating test database for alias 'default'... System check identified no issues (0 silenced). F ====================================================================== FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question self.assertIs(future_question.was_published_recently(), False) AssertionError: True is not False ---------------------------------------------------------------------- Ran 1 test in 0.001s FAILED (failures=1) Destroying test database for alias 'default'... テストは失敗したと出力されると思う。 バグを修正する models.py の記述された関数 was_published_recently() を元に戻してバグがない状態にしたいと思う。 def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now もう一度実行してみる。 python manage.py test polls # 実行結果 Creating test database for alias 'default'... System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.001s OK Destroying test database for alias 'default'... 今度はテストはOKと出力され、テストに通った。 複数のテストを実行する 先ほど作成した QuestionModelTests クラスに別のテストも追加してみましょう。 import datetime from django.test import TestCase from django.utils import timezone from .models import Question class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) # 新しくテストを追加していく。 def test_was_published_recently_with_old_question(self): """ was_published_recently()はpub_dateが1日より過去の場合 Falseを返す """ # 現在時刻より一日1秒前の質問のインスタンスを作成する。 time = timezone.now() - timedelta(days=1, seconds=1) old_question = Question(pub_date=time) # 返り値がFalseならテストに通る。 self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが1日以内ならTrueを返す """ # 一日以内の質問インスタンスを作成する。 time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True) これで過去、現在、未来に対してのテストが揃った。これで期待通りに動作する事を保証できるようになった。 Djangoのviewをテストする。 ビューレベルでのユーザ動作をシュミレートする事ができるClientを用意しているので、 tests.py や shellで使用する事ができる。 最初はshellから使用してみる。 python manage.py shell 下記のコードを一行ずつshellで実行する。 from django.test.utils import setup_test_environment # テンプレートのレンダラーをインストールする # response.context等の属性を調査できるようになる。 setup_test_environment() from django.test import Client # クライアントインスタンスを作成してページアクセスしたように操作する。 client = Client() response = client.get('/') # 実行結果 Not Found: / response.status_code # 実行結果 404 from django.urls import reverse response = client.get(reverse('polls:index')) response.status_code # 実行結果 200 # ページのhtmlが返ってくる。 response.content # 実行結果 b'\n <ul>\n \n <li><a href="/polls/3/">test3</a></li>\n \n <li><a href="/polls/2/">hello</a></li>\n \n <li><a href="/polls/1/">what's up?</a></li>\n \n </ul>\n\n' response.context['latest_question_list'] # 実行結果 <QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]> 現在の投票一覧は最新5件を取得しているため、未来の投稿日の質問も表示している。これを views.py の get_queryset() に変更を加えていく。 from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic # 新しく追加した from django.utils import timezone from .models import Choice, Question class IndexView(generic.ListView): # デフォルトのビューを使用せず、元々作成してあったものを使用する。 template_name = 'polls/index.html' # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。 context_object_name = 'latest_question_list' # 変更する箇所 def get_queryset(self): """ 最新の5件を取得する。ただし投稿日が現在時刻より前にある投稿のみ表示。 filter(pub_date__lte=timezone.now()) = if Question.pub_date <= timezone.now(): return Question.pub_date """ return Question.objects.filter( pub_date__lte=timezone.now() ).order_by('-pub_date')[:5] class DetailView(generic.DetailView): # 自分がどのモデルに対して動作するかを伝えている。 # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。 # pkの部分はurls.pyで先に指定してある。 model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): ... # 前回のまま変更しない。 Djangoでは querySetの filter() を使用する際に変数を比較したい場合下記のような記述を取る事が出来る。 下記は Product.weight の値が2以下の場合 Trueになり返却されるオブジェクトになる。 # weight <= 2 products = Product.objects.filter(weight__lte=2) 他にも比較したり出来る、下記のサイトで説明されている。 Django逆引きチートシート(QuerySet編) - Qiita viewのテストを追加する import datetime from django.test import TestCase from django.utils import timezone from .models import Question # 新しく追加した。 from django.urls import reverse class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpubdateが現在時刻より未来に設定された 場合はFalseを返さないといけない。 """ time = timezone.now() + detetime.timedelta(days=30) future_question = Question(pub_date=time) # ここで返却値がFalse出ない場合はテストに通らない事を設定している。 self.assertIs(future_question.was_published_recently(), False) # 新しくテストを追加していく。 def test_was_published_recently_with_old_question(self): """ was_published_recently()はpub_dateが1日より過去の場合 Falseを返す """ # 現在時刻より一日1秒前の質問のインスタンスを作成する。 time = timezone.now() - timedelta(days=1, seconds=1) old_question = Question(pub_date=time) # 返り値がFalseならテストに通る。 self.assertIs(old_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが1日以内ならTrueを返す """ # 一日以内の質問インスタンスを作成する。 time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True) # 新しく追加した def create_question(question_text, days): """ 引数から質問を作成する。過去に投稿された質問を作りたいなら-1~nの値を第二引数に取る、 まだ公開されてない質問を作成したいなら+1~nの値を第二引数に取る。 現在から10日後の投稿日の質問を作成したいなら 例: create_question('今日は何食べる?' , 10) 15日前の質問を作成したいなら create_question('今日は何食べる?' , -15) """ time = timezone.now() + datetime.timedelta(days=days) return Question.objects.create(question_text=question_text, pub_date=time) class QestionIndexViewTests(TestCase): def test_no_question(self): """ 質問がデータベースにない際に適切なメッセージを 表示出来てるか確認する。 """ response = self.client.get(reverse('polls:index')) # テスト合格条件 # ステータスコードが200である事 self.assertEqual(response.status_code, 200) # コンテンツに No polls are availableが含まれる事 self.assertContains(response, "No polls are available.") # データベースが空である事 self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_past_question(self): """ 過去の投稿日の質問一覧が表示されるか確認する。 """ # 投稿日が30日前の質問を作成する。ダミーデータなので実際のデータベースにデータ作成されることはない。 # そしてメソッドが終了すればダミーデータは破棄される。 # なので新しくテストする際は質問は空の状態から始まる。 question = create_question(question_text="過去の質問", days=-30) response = self.client.get(reverse('polls:index')) # テスト合格条件 # 先ほど作成した質問が表示されているか表示する。 self.assertQuerysetEqual(response.context['latest_quesiton_list'], [question],) def test_future_question(self): """ 投稿日が未来の質問が表示されていないか確認する。 """ create_question(question_text="未来の質問", days=30) response = self.client.get(reverse('polls:index')) # テスト合格条件 # コンテンツに No polls are awailableが含まれる事 self.assertContains(response, "No polls are available.") # 最新の質問5件が質問が空な事 self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_future_question_and_past_question(self): """ 過去・未来の質問の両方ある時に過去の質問だけ表示される。 """ # 片方だけ変数に入れるのはテストの合格条件を判別する際に過去質問が表示されているのを確認するため question = create_question(question_text="Past question.", days=-30) create_question(question_text="Future question.", days=30) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question], ) def test_two_past_questions(self): """ 過去の質問2つが表示されているか確認する。 """ question1 = create_question(question_text="Past question 1.", days=-30) question2 = create_question(question_text="Past question 2.", days=-5) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question2, question1], ) システムに問題がなければテストに全て合格する。作成された質問はデータベースに保存される事なく各テストが実行されて終わるたびに破棄される。 DetailViewのテスト 上記のテストは上手く動作して未来の質問はindexに表示されないが、 detail.html への正しいURLを知っていたり推測したユーザは、まだページに到達する事が出来る。そのため同じように未来の投稿日の場合はページを表示しないように polls/views.py コードを書き換える必要がある。 class DetailView(generic.DetailView): # テンプレートで変数にアクセスする際はquestionになる。 model = Question template_name = 'polls/detail.html' # 新しく追加した def get_queryset(self): """ まだ公開されていない質問は除外する。 """ return Question.objects.filter(pub_date__lte=timezone.now()) そして新たに追加した機能が動作するか確認するテストを書く。 tests.py に下記のコードを追加する。 class QuestionDataViewTests(TestCase): def test_future_question(self): """ detail.htmlの未来の日付のページにアクセスする場合は404を表示する、 """ # 現在から5日後の質問を作成する future_question = create_question(question_text = '未来の質問', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) # 合格条件 # ページにアクセスした際のステータスコードが404 self.assertEqual(response.status_code, 404) def test_past_question(self): """ 過去の質問の場合はページを表示する。 """ past_question = create_question(question_text='過去の質問', days=-5) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) # ページに過去の質問が含まれている。 self.assertContains(response, past_question.question_text) detailビューのテストも書いてきましたが、同様にresultsビューが必要になるが似たようなコードになるのでチュートリアルでは紹介されていない。 別の問題として現在の状態ではChoice(質問に対する選択肢)を持たない質問が公開されている。それを views.py で処理する事が出来るので機能を追加して、ChoicesがないQuestionを作成し、それが公開されないことをテスト、同じようにChoiceがあるQuestionを作成し、それが公開されることをテストをする。 get_queryset()に選択肢がない質問を表示しないようにfilterを追加する。 class IndexView(generic.ListView): template_name = 'polls/index.html' # テンプレート側でQuestion.objects.order_by('-pub_date')[:5]を呼び出す際の名前を設定している。 context_object_name = 'latest_question_list' def get_queryset(self): # filter内の条件は現在より過去の質問かつ選択肢がある場合に質問オブジェクトを返すようになっている。 return Question.objects.filter(pub_date__lte=timezone.now(), choice__isnull=False).distinct().order_by('-pub_date')[:5] class DetailView(generic.DetailView): # テンプレートで変数にアクセスする際はquestionになる。 model = Question template_name = 'polls/detail.html' def get_queryset(self): print(Question.objects) return Question.objects.filter(pub_date__lte=timezone.now(), choice__isnull=False).distinct() choice__isnull=False で逆参照を行い 各質問にぶら下がる選択肢を確認する。 選択肢がある場合はおそらく内部でこんな感じに取得できると考えている。 Djangoのシェルに移動して直接データベースAPIを操作して選択肢があり参照関係になっている質問を確認する事ができる。 # 登録された選択肢を全て取り出して、それぞれがどこの質問に結びつけらているか表示している。 # 図でいう1, 1, 2を取り出しているのでそれに結びついたQuestionオブジェクトが表示されている。 [obj.question for obj in Choice.objects.all()] # 実行結果 [<Question: what's up?>, <Question: what's up?>, <Question: hello>] そして参照関係にない質問は obj.questtion しても空なので false になりその質問には選択肢がないと判断する事ができる。 distinct()で重複する結果を表示しないようにする。 親テーブルと子テーブルをJoinして作成された新しいテーブルになる。 そして同じフィールドに別の値を入れる事が出来ないので選択肢に対してどの質問が参照されているのかという表示方法になる。 そのため、1つの質問で複数の選択肢を参照している質問は参照する選択肢の数だけ表示されることになる。 http://127.0.0.1:8000/polls/ アクセスすると質問が重複して表示される。 この重複項目をなくすために distict() を使用する。 そうすると重複項目がなくなり、選択肢がない質問だけを表示する事ができる。 新しく追加した機能のテストコードを書いていく。 まず tests.py の create_question() で選択肢を含む質問を作成できるようにする。 choice_texts に値がある場合は、それを元に選択肢を作成する。複数作成することもできる。 def create_question(question_text, days, choice_texts=[]): """ 質問を `question_text` と投稿された日から作成する。現在より過去の時間で投稿したい場合は days= -days、 未来の時間で投稿したい場合は対してはdays= +daysとする。 """ time = timezone.now() + datetime.timedelta(days=days) q = Question.objects.create(question_text=question_text, pub_date=time) # 選択肢がある場合とない場合で変数に格納した際返ってくるモデルが変わるから注意が必要 # 選択肢があるとChoiceオブジェクトが変える。ないとQuestionオブジェクトになる。 if choice_texts: for choice_text in choice_texts: return q.choice_set.create(choice_text=choice_text, votes=0) else: return q これを使って、先ほど追加した indexページ、detailページで選択肢がない質問が表示されていないか確認するテストコードを書いていく、そして前回作成したテストも選択肢がない質問の場合ページが表示されなくなっているので、作成する質問に選択肢を付けてあげないとテストが通らなくなっている。 その修正も行う。このように一部変更を加えたために今まで通ってたテストを含めて、全体を修正しなくてはならないコードはとても修正が大変なので良いコードとは言えないかもしれない。もしもっと良いテストコードの書き方があったら教えて下さい。 tests.py これがテストの全体コードになる。 import datetime from django.http import response from django.test import TestCase from django.urls import reverse from django.utils import timezone from .models import Choice, Question # テストコードの書き方はTestCaseを継承する事 # メソッド名をtestから始める事でDjango側で実行してくれるようになる。 class QuestionModelTests(TestCase): def test_was_published_recently_with_future_question(self): """ was_published_recently()はpub_dateが未来の場合Falseを返す。 """ time = timezone.now() + datetime.timedelta(days=30) future_question = Question(pub_date=time) self.assertIs(future_question.was_published_recently(), False) def test_was_published_recently_with_recent_question(self): """ was_published_recently()はpub_dateが昨日までに投稿されたものなら Trueを返す。 """ time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59) recent_question = Question(pub_date=time) self.assertIs(recent_question.was_published_recently(), True) def create_question(question_text, days, choice_texts=[]): """ 質問を `question_text` と投稿された日から作成する。現在より過去の時間で投稿したい場合は days= -days、 未来の時間で投稿したい場合は対してはdays= +daysとする。 """ time = timezone.now() + datetime.timedelta(days=days) q = Question.objects.create(question_text=question_text, pub_date=time) # 選択肢がある場合とない場合で変数に格納した際返ってくるモデルが変わるから注意が必要 # 選択肢があるとChoiceオブジェクトが変える。ないとQuestionオブジェクトになる。 if choice_texts: for choice_text in choice_texts: return q.choice_set.create(choice_text=choice_text, votes=0) else: return q class QuestionIndexViewTests(TestCase): def test_no_questions(self): # reverse('polls:index')でpollsのindexページURLを返している。それを利用してアクセスしている。 response = self.client.get(reverse('polls:index')) self.assertEqual(response.status_code, 200) self.assertContains(response, "No polls are available") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_past_question(self): question = create_question(question_text="Past question.", days=-30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question.question], ) def test_future_question(self): create_question(question_text="Future question.", days=30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertContains(response, "No polls are available") self.assertQuerysetEqual(response.context['latest_question_list'], []) def test_future_question_and_past_question(self): # 片方だけ変数に入れるのはテストの合格条件を判別する際に過去質問が表示されているのを確認するため question = create_question(question_text="Past question.", days=-30, choice_texts=['game set']) create_question(question_text="Future question.", days=30, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question.question], ) def test_two_past_question(self): question1 = create_question(question_text="Past question 1.", days=-30, choice_texts=['game set']) question2 = create_question(question_text="Past qustion 2.", days=-5, choice_texts=['game set']) response = self.client.get(reverse('polls:index')) self.assertQuerysetEqual( response.context['latest_question_list'], [question2.question, question1.question], ) def test_choice_question(self): """ Indexページで 選択肢のある質問を表示する。 """ choice_question = create_question(question_text='Indexページでの選択肢のある質問', days=-1, choice_texts=['game set']) url = reverse('polls:index') response = self.client.get(url) self.assertContains(response, choice_question.question) def test_no_choice_question(self): """ Indexページで 選択肢がない質問は表示しない。 """ no_choice_question = create_question(question_text='Indexページでの選択肢のない質問', days=-1) url = reverse('polls:index') response = self.client.get(url) self.assertNotContains(response, no_choice_question) class QuestionDataViewTests(TestCase): def test_future_question(self): """ detail.htmlの未来の日付のページにアクセスする場合は404を表示する、 """ # 現在から5日後の質問を作成する future_question = create_question(question_text = '未来の質問', days=5) url = reverse('polls:detail', args=(future_question.id,)) response = self.client.get(url) # 合格条件 # ページにアクセスした際のステータスコードが404 self.assertEqual(response.status_code, 404) def test_past_question(self): """ Detailページ 過去の質問の場合はページを表示する。 """ past_question = create_question(question_text='過去の質問', days=-5, choice_texts=['geme set']) url = reverse('polls:detail', args=(past_question.id,)) response = self.client.get(url) # ページに過去の質問が含まれている。 self.assertContains(response, past_question.question) def test_choice_question(self): """ Detailページ 選択肢のある質問を表示する。 """ choice_question = create_question(question_text='detailページでの選択肢がある質問', days=-1, choice_texts=['game set']) url = reverse('polls:detail', args=(choice_question.id,)) response = self.client.get(url) self.assertContains(response, choice_question.choice_text) def test_no_choice_question(self): """ 選択肢がない質問は表示しない。 """ no_choice_question = create_question(question_text='detailページでの選択肢のない質問', days=-1) url = reverse('polls:detail', args=(no_choice_question.id,)) response = self.client.get(url) self.assertEqual(response.status_code, 404) まずは今まで動作していたテストが選択肢がない質問だったので、選択肢 ['game set'] を追加して再び動作するように変更する。 その際に choice_set.create で選択肢を追加した場合、返り値が Questionオブジェクトではなく Choiceオブジェクトになるので質問を取り出す際は 返り値.question とする必要がある。 問題なければ、テストが13個実行され OK と表示される。 スタイルシート・静的ファイルを追加する。 スタイルシートを追加 pollsディレクトリにstaticディレクトリを作成する。そうするとDjangoはそこから静的ファイルを探してくれる。 polls/static/polls と templateディレクトリを作成した時みたいになる。 先ほど追加したディレクトリに style.css を追加する。 polls/static/polls/style.css のようになる。 style.css li a { color: green; } polls/templates/polls/index.html の上部に下記のコードを追加する。 {% load static %} <link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}"> 画像を追加する polls/static/polls/images/ とディレクトリを作成する。その中に 好きな画像をおく。 スタイルシートで背景画像として読み込む body { background: white url("images/background.gif") no-repeat; } li a { color: green; } adminのフォームをカスタマイズする 編集フォームでのフィールドの並び順を替える 質問の詳細ページでのフィールドの並び順を変更する。 polls/admin.py from django.contrib import admin from .models import Question class QuestionAdmin(admin.ModelAdmin): # この順番で表示されるようになる。 fields = ['pub_date', 'question_text'] # 第二引数で作成したclassを渡す admin.site.register(Question, QuestionAdmin) admin.site.register(Choice) 変更前 変更後 pub_dateとquestion_textの位置が入れ替わってる。 フィールドを分割する。 polls/admin.py from django.contrib import admin from .models import Question class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date']}), ] admin.site.register(Question, QuestionAdmin) ChoiceオブジェクトをQuestionフォームから追加・編集する。 現在Choiceフォームから質問に選択肢を追加・編集可能ですが、これだとページを移動したりと効率が悪いので Questionフォームから追加・編集できるようにする。 polls/admin.py from django.contrib import admin from .models import Choice, Question class ChoiceInline(admin.StackedInline): model = Choice extra = 3 class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] inlines = [ChoiceInline] admin.site.register(Question, QuestionAdmin) コードを追加するとQuestionフォームに3つ(extraで数の調整ができる)の choice_text, votes を設定できる項目が追加される。 今のままだと多くの画面スペースを必要とするのでこれを小さくする。 class ChoiceInline の引数を TabularInline に変更する。 class ChoiceInline(admin.TabularInline): #... これでコンパクトになったと思う。 pollsの質問一覧ページをカスタマイズする。 チェンジリストページと呼ばれるページで(http://127.0.0.1:8000/admin/polls/question/)質問の一覧が表示されている。 現在は オブジェクトの名前(どんな質問が格納されているのがわかる)だけが表示されていますが、各フィールドの値を表示してより多くの情報をここで確認できるようにする。 polls/admin.py class QuestionAdmin(admin.ModelAdmin): # ... list_display = ('question_text', 'pub_date', 'was_published_recently') 各カラムのヘッダーをクリックすると並び替えを行えるが、 was_published_recently だけは並び替えをサポート出来ていないので、 @ デコレータを使用して並び替えの対応させていく。 デコレータなのでクラスメソッドの直前に追加する。 polls/models.py from django.contrib import admin class Question(models.Model): # ... # ここを新しく追加した。 @admin.display( boolean=True, ordering='pub_date', description='Published recently?', ) def was_published_recently(self): now = timezone.now() return now - datetime.timedelta(days=1) <= self.pub_date <= now 質問を日付でフィルター掛けれるようにする。 pub_date の日付を元に質問を絞れるようにする。フィルタは対象のフィールドの種類によって変化する。 pub_date は DateTimeField なので、Django はこのフィールドにふさわしいフィルタオプションが、「すべての期間 ("Any date")」「今日 ("Today")」「今週 ("Past 7 days")」「今月 ("This month")」 を用意してくれる。 polls/admin.py from django.contrib import admin from .models import Choice, Question class ChoiceInline(admin.StackedInline): model = Choice extra = 3 class QuestionAdmin(admin.ModelAdmin): fieldsets = [ (None, {'fields': ['question_text']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ] inlines = [ChoiceInline] # 新しく追加した。 list_filter = ['pub_date'] admin.site.register(Question, QuestionAdmin) 質問の検索機能を追加する。 先ほどのコードにさらに変数を追加する。 question_text フィールドをユーザが入力した文字列を元に Likeクエリで検索するのでデータベースに割と負荷がかかるみたいで常識の範囲で使用しましょうとチュートリアルに記述されている。 # ... list_filter = ['pub_date'] # 新しく追加した search_fields = ['question_text'] 管理サイトの見た目をカスタマイズする。 管理サイトの上部に Django administration と書かれているのでこれを Polls administration と変更してみたいと思う。 manage.py が置かれているディレクトリに templates ディレクトリを作成する。その中に adminフォルダを作成する。 templates/admin みたいな構成になる。 その中にデフォルトのDjango adminのテンプレートをコピーして貼り付ける。 場所は 下記のコマンドから確認できる。anacondaの環境の場合は仮想環境内で実行する必要がある。 python -c "import django; print(django.__path__)" そして開いたファイルを下記のように編集する。 変更前 {% extends "admin/base.html" %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} <h1 id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Django administration') }}</a></h1> {% endblock %} {% block nav-global %}{% endblock %} 変更後 {% extends "admin/base.html" %} {% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} {% block branding %} <h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1> {% endblock %} {% block nav-global %}{% endblock %} 次に mysite/settings.py を開いて TEMPLATES 設定オプションの中にある DIRS オプションを下記のように変更する。 TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', # ここを新しく追加した。 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] これでデフォルトのテンプレートをオーバライドすることが出来た。 これでチュートリアル5~7の内容は終了した。 最後に チュートリアル5でテストコードを初めて書く経験が出来てよかったです。途中チュートリアルから外れた事をしようとした際に逆参照でモデルからデータを取得する方法がわからなくてかなり時間が掛かりました。SQLデータベースの理解がまだ乏しいのでもう少しデータベースに慣れてからDjangoでアウトプットとして、Webアプリを作成したいと思います。 参照 DjangoのModelからデータを取り出す方法をまとめとく - やる気がストロングZERO LEFT JOIN / INNER JOIN を実行すると同じ内容のレコードが複数含まれる - SQLの構文 ドキュメント 記事に関するコメント等は ?:Twitter ?:Youtube ?:Instagram ???:Github ?:Stackoverflow でも受け付けています。どこかにはいます。
- 投稿日:2021-06-15T20:19:54+09:00
Django公式チュートリアル(1~4)で分からない所、徹底的に調べた。
最初に 本格的なWebアプリケーションを作成したいのでPythonのフレームワークDjango(読み方:ジャンゴらしい。ディーどこ行った。)についてチュートリアルをこなしながら学んで行こうと思う。実際に作成するアプリは質問に対して回答して投票を表示するアプリになる。 この記事は公式チュートリアルの1〜4までに気になった事躓いた事をまとめていく。全部で1〜7まであるが4までにアプリは完成する。 5からはテストコード等を書いていくのでボリュームが多くなるため、前編として今回の記事を投稿する。後編も必ず書こうと思う。 今回のチュートリアルで作成したもの 質問一覧のページがあって、そこから質問に対して投票を行う。その後今まで投票された数を表示するページリダイレクトされる。 VSCodeのPython用の拡張機能をインストールする。 コードを書くにあたって構文エラーは事前に無くしたいので、拡張機能をインストールする。マイクロソフトがPython用に提供しているものがあるのでそちらをインストールする。 自分はanaconda環境での使用をしているので下記の通知が出てきた。 terminal.integrated.inheritEnv を false にした方が良いらしい。 We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings. VSCodeの code > preference > settings に terminal.integrated.inheritEnv と入力して出てきたチェックを外す。 【Mac/Python】VSCodeターミナル動作が通常ターミナルと違う時 | ゆうきのせかい それだけだと Import "django.contrib" could not be resolved from source という警告がでたままなので、赤枠の箇所をクリックしてDjangoをインストールした環境を選択すると警告文が消えます。 インストール 好きなフォルダーを作成して、そこに開発していく。Pythonの環境構築は自分は下記のように行っている。 https://techblog-pink.vercel.app/posts/cc111706c3167 anacondaで仮想環境を作成して、Djangoをインストールしていく。 pip install Django 開発を始めたいフォルダに移動して下記を実行する。 django-admin startproject mysite mysiteというディレクトリが生成される。 mysite/ manage.py mysite/ __init__.py settings.py urls.py asgi.py wsgi.py フォルダはこのような構成になっている。 mysite/mysite となっているのが不思議だ。外側の mysite はDjangoのシステム的には何でも良いらしい。 urls.py はプロジェクトのURLを宣言する。目次のような機能を提供する。 サーバを起動する。 実行すると http://127.0.0.1:8000/ でサイトにアクセス出来るようになる。 最初はデータベース等の設定をしていないので、ターミナルに警告が表示されるが他に問題がなければアクセス出来る。 ctr + c でサーバを終了することが出来る。 python manage.py runserver GitHubで管理する これから開発していくので、Githubにコードをあげて進捗を管理したい。しかし、このままだと settings.py に書かれた SECRET_KEY も一緒にアップロードしてしまうので別ファイル local_settings.py に SECRET_KEY を書く。ちなみに私は気付かずに一度GitHubに、そのままアップしてしまった。そのため新たに SECRET_KEY を作成する手間が掛かる。 local_settings.py SECRET_KEY = 'settings.pyにあったシークレットキーまたは、新たに作成したもの' この変数を settings.py で読み込んでいく。下記のコードを追加する。 try: # 同じ階層のlocal_settingsファイルからSECRET_KEYをkeyとして読み込む。 # 参考記事等では .local_settingsとなっているが、local_settingsはファイルなので必要ない # 将来的に複雑にファイルを分ける必要が出てフォルダにする場合は .が必要となる。 # ダメだった .local_settingsが正しかった。Django環境だとパッケージとして見なされるのか... from .local_settings import SECRET_KEY as key except ImportError: pass # シークレットキーの部分を読み込んだ変数に置き換える。 SECRET_KEY = key 実際に test.py と test2.py を同じ階層に作成してどのように読み込まれるか調べてみた。 test.py hello = "hello" test2.py # .testとするとImportError: attempted relative import with no known parent package # と表示される。 # asを付けない場合は helloで読み込まれる。 from test import hello as A print(A) settings.py と同じ環境に出来ていると思ったが、全然違った。 local_settings.py は パッケージとして認識されるが、 test.py はパッケージとして認識されないので . を使用するとエラーになる。そもそも python test2.py と直接実行しているので settings.py とは違う実行状況になる。 詳しくはここで回答を頂いている。 SECRET_KEYの作成 自分は知らずにシークレットキーをGitHubにあげてしまったので新たに作り直す必要があるのでシークレットキーを生成してくれるプログラムを実行する。これを直接ターミナルで実行するとシークレットキーが生成されるのでそれを local_settings.py に貼り付ける。 get_random_secret_key.py from django.core.management.utils import get_random_secret_key secret_key = get_random_secret_key() text = 'SECRET_KEY = \'{0}\''.format(secret_key) print(text) これでようやくチュートリアルに専念してコードを書き進める事が出来る。 プロジェクトとアプリ Djangoではプロジェクトの中にアプリが含まれる。なので特定のDjangoで作成されたWebサイト全体をプロジェクトと呼び、その中に含まれる小規模な投票アプリ、ログシステムをアプリと呼ぶ。 アプリを作成する。 manage.py と同じ階層に移動して python manage.py startapp polls を実行すると polls というフォルダが生成される。 polls/ __init__.py admin.py apps.py migrations/ __init__.py models.py tests.py views.py これでプロジェクトにpollsというアプリが作成された事になる。 Viewを作成する。 URLからパスにアクセスがあって、その際に実行する関数がViewになるここでHTMLファイルを返したりと処理を決めることが出来る。 views.py に記述する。 def detail(request, question_id): return HttpResponse("You're looking at question %s." % question_id) def results(request, question_id): response = "You're looking at the results of question %s." return HttpResponse(response % question_id) def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) viewメソッドの第一引数には必ずHttpRequestクラスを受け取る。 引数 request には今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってくる。そして戻り値としては HttpResponseクラスを返す必要がある。 実際にHttpResponseとHttpRequestの中身がどんな感じになっているのか気になる人は下記のページで確認できます。 ざっくりですが、 views.py があって実行されるとクラスの中身はこんな感じに格納されているみたいです。 def sample(request): print(request) response = HttpResponse('') print(response) return response # 実行結果 # <WSGIRequest: GET '/hello/'> # <HttpResponse status_code=200, "text/html; charset=utf-8"> Djangoはリクエストを受け取ってレスポンスを返しているだけです【詳しく解説】 views.py だけではURLと紐づいていないので URLconfを作成する。 URLconf urls.py を作成する URLconfを作成するには urls.py というファイルを views.py と同じ階層に作成する。mysiteフォルダ内には urls.py がすでにあるので pollsフォルダ内に作成する。中身はこんな感じになる。 include()を使ってアプリのURLを結び付ける。 polls/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] <int:question_id> が views.py 関数の引数として渡される。この <> を使用すると、URLの一部がキャプチャされ渡される。文字列 :quesiton_id> は一致するパターンを定義し、 <int: の部分はURLパスに当てはまる値の型を指定している。なので <str:, <slug: などもある。 そして、これをmysiteフォルダ内の urls.py に結びつけてあげる必要がある。一応こっちが最初に読み込まれるのでここに後から追加したアプリのURLを include() を使って追加していくイメージになる。 mysite/urls.py from django.contrib import admin from django.urls import include, path urlpatterns = [ path('polls/', include('polls.urls')), path('admin/', admin.site.urls), ] これでpollsアプリのURLを結び付けることができた。サーバを起動して http://localhost:8000/polls/ にアクセスするとViewが返されるようになる。 path()の引数 4つの引数を受け取ることが出来る。そのうち route と view の2つは必須で残り kwargs と name は省略出来る。 route:URLパターンを含む文字列が入る。リクエストを処理する際に urlpatterns を順番にみて最初にマッチしたものを取り出す。このパターンはGET、POSTのパラメータに影響は受けないあくまでURLパスだけを見る。 view:URLがマッチしたら、そこに付随するView関数を返す仕組みになっている。 kwargs:任意のキーワード引数を辞書としてView関数に渡せる。 name:URLに名前を付ける事で reverse() を使って呼び出せるようになり、htmlのテンプレートでformに指定するURLを変更する際に動的にURLが変更されるようになる。 polls/urls.py がこんな感じだとして from django.urls import path from . import views import re urlpatterns = [ path('', views.index, name='index'), path('<int:number>', views.subView, name='suburl'), ] 試しに下記の views.py を実行してみる。 from django.http import HttpResponse from django.urls import reverse def index(request): urlName = reverse('index') print(urlName) return HttpResponse("Hello, world. You're at the polls index.{0}".format(urlName)) # 実行結果 # /polls/ こうすると例えば pollllllls/urls.py とURLを変更してもコードを変更する必要がないので、変更に強いコードになります。 ※pathのnameについて python、djangoのurls.pyで設定するnameってなんやねん?? - Qiita DjangoのURL ユーザがDjangoで作られたサイトにアクセスした際にどのような処理が走るのか。 ROOT_URLCONFに設定されているURLを確認する。(HttpRequestオブジェクトにurlconfという属性が設定されている場合はその値をROOT_URLCONFとする。) urlpatternsという名前の変数を探す。この変数値は django.urls.path() または django.urls.re_path() インスタンスのsequenceでなければならない。 urlpatternsから順番に要求されたURLパターンを探す。 マッチしたらViewを返す。 マッチしなかったらエラーハンドリングビューを返す。 URLconfのサンプル pathの左側がマッチするURL(route), 右側がマッチしたら呼び出される関数(view) from django.urls import path from . import views urlpatterns = [ # /articles/2003/にアクセスした場合 # Views.special_case_2003(request)を呼び出す。最後の/もしっかりないとマッチしない。 # 引数としてrequestが関数に渡る。 path('articles/2003/', views.special_case_2003), path('articles/<int:year>/', views.year_archive), path('articles/<int:year>/<int:month>/', views.month_archive), path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail), ] /articles/2005/03/ というアクセスがあった場合上記のパターンの中からviews.month_archive(request, year=2005, month=3) という views.py (上記で記したviews.pyとは別で例としてあげてるurls.pyに対応するviews.pyがあったらという話で見てもらいたい。)に書かれたviewメソッドに引数を渡して呼び出す事になる。引数 request にはHttpRequestクラスが入り 今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってる。 /articles/2003/03/building-a-django-site/ なら最後のパターンにマッチして、このように views.article_detail(request, year=2003, month=3, slug="building-a-django-site") 関数を呼び出す。 タイムゾーンの設定 デフォルトではUTCと世界標準時間になっているので、ここで日本時間に変更しておきたいと思う。 settings.py の TIME_ZONE = 'UTC' を TIME_ZONE = 'Asia/Tokyo' に変更する。 データベースの作成 ここでデータベース用語についてまとめて置く。 カラム:縦列の事を指し、別名では列と呼ばれる。 カラム名:列全体に付けられた名前 フィールド:データが入っている場所。 フィールド名:そのデータが入っているカラム名を指す。 項目名:フィールド名を指す。 テーブル:Excelでいうシートのようなもの。 レコード:データそのものを指す言葉になる。もう一つは横列の事を指し、行と呼ばれる。そして行をロウと呼ぶこともある。 これからデータベースを設定していく、チュートリアルの段階なのでまずは複雑な設定の必要がないSQLiteを使用していく。 python manage.py migrate 実行すると settings.py に書かれた INSTALLED_APPS の設定を参照して mysite/settings.py ファイルのデータベース設定に従って必要な全てのデータベーステーブルを作成する。 migrate(マイグレート)するとは データベースを削除してから作り直すと、DBに保存されている情報が全て削除されてしまう。こういった事態を回避する方法として、データベースマイグレーションを行う方法が生まれた。マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うことが出来る。 モデルの作成 models.py models.py はデータを保存したり、取り出したりするときの設定を記録するファイルになる。データベースの取扱説明書と書かれることが多い。 models.py からデータベースが作成される流れ models.pyファイルでモデル(データベースの型)を作成する。 migrationファイルを作成する。 migrate(最終的にはおそらくSQL文に変換されて、データベースに命令を出してデータベースを作成する。)する。ここで上記のファイルを元にデータベースが作成される。 つまりモデルはデータベースのレイアウトとそれに付随するメタデータになる。 簡単な例を示す。(チュートリアルとは関係ないモデル) # modelsモジュールを読み込むこれはデータベースを作成するのに必要な機能が格納されている。 from django.db import models # データベース作成機能を継承してモデルを書き込んでいく。 class BookModel(models.Model): # booknameという文字を入力することが出来るフィールドを作成する命令をだす。 # bookname = models.CharField(max_length = 50)とすると50文字までと制限をかけれる。 # CharFieldには必須の引数がありmax_lengthを設定しないといけない。 bookname = models.CharField(max_length = 50) # CharFieldとほぼ同じで文字列を扱うがTextFieldの方がデータの読み出し等にコストがかかるらしい。 summary = models.TextField() # 整数値を入れるフィールドを作成する。 rating = models.IntegerField() ForeignKey(外部制約キー) これを使ってこの後2つのモデルを双方向に参照できるようにするのだが、その前にデータベースにおいて外部制約キーまたは外部キーとも呼ばれるがどのような役割を果たしているのかみていこうと思う。 外部キーとは関係データベースにおいてデータの整合性を保つための制約(参照整合性制約) 外部キーに設定されている列(子テーブルのカラム)には、参照先となるテーブルの列内(親テーブルのカラム内)に存在している値しか設定できない。 そのため、外部キーに設定されている子テーブルの列内に親テーブルの列内に存在しない値を追加しようとするとエラーになる。 なので新しく値を追加したい場合は一度、親テーブルで追加する必要がある。 このように制約を結ぶ事でデータの整合性を保つ事ができる。 Djangoでは foreign keyが設定されている方が子テーブルになる。 今度は少し複雑なモデルを見ていく。 図のようなQuestionとChoiceという2つのモデルを作成する。 ChoiceにQuestionが ForeignKey を使って紐ずけられている。 on_delete=models.CASCADE は紐づけられたモデルが削除される際にどのような動作をするかを決める事が出来る。削除された後そのモデルだった部分をNullで埋めたり、そもそも削除できないようにしたりと出来る。 CASCADE は紐づけられた側のモデルで関連するオブジェクトも削除するという動きになる。なので Questionが削除されたら、Questionと関連のあるChoice側のオブジェクトも削除するような動作を取る。 詳しくはここの記事が分かりやすい。 Django2.0から必須になったon_deleteの使い方 - DjangoBrothers from django.db import models class Question(models.Model): # Question textというフィードを作成してそこに入る文字列は200文字までと制限している。 question_text = models.CharField(max_length=200) # 基本的には変数名がフィールド名として使用されるが、引数で文字列を渡す事でフィールド名設定する事が出来る。 pub_date = models.DateTimeField('date published') class Choice(models.Model): # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) # Votesフィールドは整数値を受け付ける。最初は0が入る。 votes = models.IntegerField(default=0) モデル間の双方向やりとりについて 【Django】1対多の関係( related_name, _set.all() )について - Qiita モデルを作成したのでデータベースにマイグレート(モデルを元にデータベースのレイアウトを作成するデータを追加するわけではない。)していく migrationファイルを作成する前に、新たに作成したアプリpollsを伝える必要がある。 settings.py の INSTALLED_APPS の配列に 'polls.apps.PollsConfig', を追加する。 次にmigrationファイルを作成する。 新たにファイルを作成する必要はなくターミナルでコマンド実行する。 python manage.py makemigrations polls 実行すると models.py を元にmigrationファイルが生成される。 python manage.py migrate 実行するとmigrationファイルを元にデータベースが作成される。 試しにコマンドラインからDjango shellを通してデータベースを変更したりしてみる。 シェルに入るには下記のコマンドを実行する。 python manage.py shell するとPythonコード >>> を書ける状態になるのでここからデータベースにアクセスするコードを書く。 # 作成したモデルを読み込む from polls.models import Choice, Question # 格納されたデータを確認する。まだ追加していないので、空になっている。 Question.objects.all() from django.utils import timezone # モデルにデータを追加する。 q = Question(question_text="What's new?", pub_date=timezone.now()) # データベースに保存する。 q.save() q.id q.question_text q.pub_date # データの上書き q.question_text = "What's up?" q.save() # データが格納されているのが確認できる。 Question.objects.all() # 実行結果:<QuerySet [<Question: Question object (1)>]> このままではadminページでオブジェクト名が Question object (1) と表示され分かりにくいので 特殊メソッド __str__() をモデルに追加する。 あと追加で was_published_recently(self) メソッドを書きました。 データが最近追加されたかどうかを判定するメソッドで True or False で返します。 class Question(models.Model): # クラス変数を定義する。データベースフィールドを表現している。 # Charフィールドは文字のフィールド question_text = models.CharField(max_length=200) # 日時のフィールド pub_date = models.DateTimeField('date published') def __str__(self): # インスタンスを生成して、printした際にここが実行される。 # シェルで表示されるオブジェクトに質問名が使われるだけでなく # adminでオブジェクトを表現する際にも使用されるので追加する必要がある。 return self.question_text def was_published_recently(self): now = timezone.now() # now - datetime.timedelta(days=1)は今の時間から一日引いた日付を出す。 # 2021-05-19 23:29:56.216634こんな感じの値になる。 # pub_dateが現在時刻より過去で現在時刻から一日以内の場合はTrueを返すメソッド return now - datetime.timedelta(days=1) <= self.pub_date <= now class Choice(models.Model): # これはChoiceがQuestionに関連付けられている事を伝えている。 # データベースの多対一、多対多、一対一のようなデータベースリレーションシップに対応する。 # Question ← → Choiseと双方向のやりとりが可能となる。 question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) votes = models.IntegerField(default=0) def __str__(self): return self.choice_text すると下記のように表示されるので、 何のデータが入っているのか分かり易くなった。 Djangoのshell内でも下記のようにオブジェクトの中身が分かり易くなった。 from polls.models import Choice, Question Question.objects.all() # 実行結果:<QuerySet [<Question: What's up?>]> # 続けて色々な関数を試してデータベースから # データを取得してみる。 # filterをかけてデータを取得する。 # idはデータベースにデータを追加した際に1から順番に自動で割り振られる。 Question.objects.filter(id=1) # 実行結果:<QuerySet [<Question: What's up?>]> # Questionオブジェクトのquestion_textフィールドで"What"から # 始まるデータを取得する。 Question.objects.filter(question_text__startswith="What") # 実行結果:<QuerySet [<Question: What's up?>]> from django.utils import timezone # 今年作成されたデータを取得する。 current_year = timezone.now().year Question.objects.get(pub_date__year=current_year) # id 2のデータを取得する。 # filterで指定しなくてもgetでも取得できる。 Question.objects.get(id=2) # 実行結果はない場合はエラーになります。 # プライマリーキーと呼ばれるものでいまいちidとの違いが分からない。 # 取得するデータはidの場合と同じになる。 q = Question.objects.get(pk=1) q.was_published_recently() # 実行結果:True # ChoiseはQuestionと関連付けられてるのでQuestionからも # データにアクセスする事ができる。 # Choice側にはまだデータを入れてないので結果は何も表示されない。 # Choicecは質問に対する回答の選択肢をデータとして持つ。 # qには今What's upという質問が入っているので、それに対しての # 選択肢を作成した。 q.choice_set.all() q.choice_set.create(choice_text="Not much", votes=0) q.choice_set.create(choice_text="The sky", votes=0) c = q.choice_set.create(choice_text='Just hacking again', votes=0) # 選択肢が関連づけられている質問を返す。 c.question # 実行結果:<Question: What's up?> q.choice_set.all() # 選択肢が何個あるか数える。 q.choice_set.count() Choice.objects.filter(question__pub_date__year=current_year) # 実行結果:<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]> # just hackingの選択肢だけ削除する。 c = q.choice_set.filter(choice_text__startswith="Just hacking") c.delete() pkとidの違い pkは primary key の略で、データベースでは 主キー と呼ばれている。主キーはテーブルで一意の値を取る。 どのレコードを主キーにするかはフィールド名を定義する時に primary_key=True を付ければ設定できる。 codeというフィールドに入る値を主キーとする。 code = models.CharField(max_length=10, primary_key=True) djangoの場合はModel(=テーブル)には必ず1つの主キー用のフィールドが必要になる。ユーザが定義しない場合はidという名前のAutoFiekd(int型の連番1~n)が作成される。 そのため、pkキーを定義しない場合はpkはidのショートカットになる。 pkキーを上記の code のように定義した場合はidは作成されない。 SQLに直接アクセスする。(おまけ) sqllite3を使用してデータベースを作成している場合はmysiteフォルダ内にデータベースのファイルが生成されていると思うので、下記のコマンドからSQLで操作するシェルに入る事ができる。 sqlite3 db.sqlite3 # シェル内での操作 # 作成されたテーブル一覧を確認できる。 >>> .table auth_group django_admin_log auth_group_permissions django_content_type auth_permission django_migrations auth_user django_session # 先ほどモデルから作成されたテーブル auth_user_groups polls_choice auth_user_user_permissions polls_question # 構造を確認できるみたいだけど、見てもよく分からなかった。 >>> .schema # シェルから抜ける。 >>> .quit モデルからデータベースを操作する事ができたので、次に先ほど登場したadminページにアクセスしたいと思う。 adminページアクセスする。 ログインが必要なのでユーザを下記のコマンドから作成する。 python manage.py createsuperuser # 実行すると下記の入力画面が登場する。 Username:名前を入力 Email address:@と.comがあれば架空で良い Password: Password(again): ユーザを作成したら開発サーバを起動してhttp://127.0.0.1:8000/admin/ にアクセスするとログイン画面となるのでログインする。 するとそこから作成したモデルを閲覧したりデータを追加したりできる。 ChoiceモデルからはQuestionを選択して使用することしか出来ないのが確認できる。 右側にある + ボタンを押すと Questionのページに飛びそこから新しい質問を追加することはできる。 Views.pyからデータベースの値を取得する 先ほどDjango shellで使用したPythonコードを使って、 views.py にデータを取得していく。 # 最後はHttpResponseを返す必要があるのでimportする。 from django.http import HttpResponse # データベースを操作するためにモデルを読み込んでおく。 from .models import Question def index(request): # データベースから最新5件を取得する。 # こんな感じのデータになる。<QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]> latest_question_list = Question.objects.order_by('-pub_date')[:5] # "test3, hello, what's up?"区切った文字列にしてHttpResponseに渡す。 output = ', '.join([q.question_text for q in latest_question_list]) return HttpResponse(output) # Leave the rest of the views (detail, results, vote) unchanged Viewとページデザインを切り離す Viewではデータの取得や操作を専門的に行ってもらい、そのデータをテンプレート(htmlにPythonの変数を入れられる。)に渡してページをレンダリングしてもらうようにする。 pollsディレクトリの中に、templatesディレクトリを作成する。システムでそのディレクトリを認識する。作成した templatesディレクトリにpollsフォルダを作成する。なので polls/templates/polls みたいなディレクトリが完成する。その中にテンプレート index.html を作成する。なぜ templates/polls とするのかというとDjangoは名前がマッチした最初のテンプレートを使用するので、もし異なるアプリケーションの中に同じ名前のテンプレートがあるとそちらを読み込む。それを回避するために名前空間(所属する領域)を与えている。 views.py をテンプレートにデータを渡せるように書き換える。 from django.http import HttpResponse from django.template import loader from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] # テンプレートを読み込む template = loader.get_template('polls/index.html') # 辞書型に最新5件のデータを格納する。 context = { 'latest_question_list': latest_question_list, } # 辞書型のデータをテンプレートに渡してページを作成する。その結果をHttpResponseに返す。 return HttpResponse(template.render(context, request)) view.pyをさらに短くする。 from django.template import loader from django.http import HttpResponse を使わない書き方 より簡素にする事が出来る。 その場合、 render() 関数は第一引数に requestオブジェクト , 第二引数に テンプレート名 , 第三引数に 辞書型(テンプレートに渡したいデータ) を記述する。 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) 404エラーを出力する。 from django.http import Http404 from django.shortcuts import render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) # 質問の詳細ページのビュー def detail(request, question_id): try: # アクセスのあったURLでpkの値が変わる。/polls/1/なら1になる。 # データベースでエラーになるとHttp404を出力する。 question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("Question does not exist") return render(request, 'polls/detail.html', {'question': question}) 上記のdetail()を短くする。 django.shortcuts にはこうしたコードを省略する関数が多くあるので調べると面白いかもしれない。 from django.shortcuts import get_object_or_404, render from .models import Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) index.html index() に対するテンプレートにはこのように記述する。 ビューで latest_question_list オブジェクトが辞書型に格納されて渡されているので、それを受け取ってテンプレート内でオブジェクトに格納された値を属性アクセス . して取得している。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} index.htmlのハードコード(直接記述している)を削除する このように直接URLを書き込むと変更に弱いコードになってしまうので、 polls.urlsモジュールのpath() 関数でname引数を定義したのでそれをURLに使用する。 {%url%} を使う。 <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li> 変更後 <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li> そしてテンプレートを入れるディレクトリを作成した際のように名前空間を追加する。システムが別々のアプリ内で同じname引数を含んでいても区別が付けられるように template/polls/detail.html にアクセスしたい場合は polls:detail と記述する。 <!-- 受け取った変数にデータがあるか確認する。 --> {% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %} そして、URLconf(urls.py)に名前空間を追加 app_name = 'polls' from django.urls import path from . import views # ここを新たに追加した。 app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] こうするとモジュールに指定されたURLの定義を検索出来る。例えば polls/specifics/12 のようにURLを変更した場合 urls.py に書かれたパスを変更する事でテンプレート側に変更を加える必要がない。 path('specifics/<int:question_id>/', views.detail, name='detail'), detail.html detail() に対するテンプレートはこのように記述する。 <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }}</li> {% endfor %} </ul> フォームを使って質問に対して回答を送信する。 detail.html に <form> を追加してサーバにデータを送信して質問に対して、投票出来るようにする。 下記のように detail.html を変更する。 <!-- 質問の内容 --> <h1>{{ question.question_text }}</h1> <!-- もしデータベースから質問が取得出来ない場合エラーが表示される。 --> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} <form action="{% url 'polls:vote' question.id %}" method="post"> <!-- セキュリティのため --> {% csrf_token %} <!-- 質問に対する選択肢を並べる --> {% for choice in question.choice_set.all %} <!-- forloop.counterはforタグのループが何度実行されたかを表す値です。 --> <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> {% endfor %} <input type="submit" value="Vote"> </form> views.pyにvote()関数を追加する コードの流れとしては ユーザがdetailページの質問に対する選択肢を選択する。 Voteボタンをクリックする。 データがサーバに送信される。 選択された選択肢の投票数をインクリメントする。 results.htmlにリダイレクトする。Postデータが成功した後は基本的に HttpResponse ではなく HttpResponseRedirect を返す必要がある。 # 追加した。HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render # 追加した。reverse from django.urls import reverse # Choiceを追加した。 from .models import Choice, Question def index(request): latest_question_list = Question.objects.order_by('-pub_date')[:5] context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context)。 # モデルにアクセスするobjects.get()とHttp404が一緒になっている。 def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/detail.html', {'question': question}) # 質問に対して選択して投票する。 def vote(request, question_id): # まず質問があるかどうか確認する。 question = get_object_or_404(Question, pk=question_id) try: # ユーザが選択した値からpk値を取得して、それを元にモデルから選択肢のオブジェクトを取得する。 # なければYou didn't...choiceと表示される。 selected_choice = question.choice_set.get(pk=request.POST['choice']) except (KeyError, Choice.DoesNotExist): # Redisplay the question voting form. return render(request, 'polls/detail.html', { 'question': question, 'error_message': "You didn't select a choice.", }) else: # 選択肢オブジェクトから何回投票されたか表示するvotesオブジェクトをインクリメントする。 selected_choice.votes += 1 # データベースに保存する。 selected_choice.save() # Always return an HttpResponseRedirect after successfully dealing # with POST data. This prevents data from being posted twice if a # user hits the Back button. # データの保存に成功したら、results.htmlにリダイレクトする。 return HttpResponseRedirect(reverse('polls:results', args=(question.id,))) reverse()関数とは この関数を使うと、vote関数中でのURLのハードコードを防ぐ事が出来る。 引数としては polls:results リダイレクト先のビュー名とそのビューに与えるURLパターン question.id を渡せる。 reverse('polls:results', args=(question.id,)) # 返り値 '/polls/3/results/' Post通信が成功した際のresults関数を作成する。 views.pyにresults関数を追加する。 # 先ほどまで書いてきたviews.pyにresults関数を追加する。 def results(request, question_id): # 指定したpkキーにデータがあれば返す、なければエラーを返す。 question = get_object_or_404(Question, pk=question_id) # 質問オブジェクトを引数で貰ってページを作成する。 return render(request, 'polls/results.html', {'question': question}) results.htmlを作成する。 <!-- 質問を表示する。 --> <h1>{{ question.question_text }}</h1> <!--質問の選択肢とそれに対する投票数を取得する。--> <ul> {% for choice in question.choice_set.all %} <!--choice.votes|pluralizeは投票数が2以上の場合vote s とsを追加してくれる。--> <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a> Built-in template tags and filters | Django documentation | Django 汎用ビューを使って今まで書いたコードをさらに短くする。 views.py に書かれた index(), detail(), results() 関数は3つとも似たような機能でURLを介して渡されたパラメータに従ってデータベースからデータを取り出しページを作成する。これらの一連の動作はよくある事なのでDjangoでは汎用ビューというショートカットを用意してより簡素に機能を実装出来るようにしている。 汎用ビューを適用するにはいくつかこれまでに書いたコードを修正する必要がある。 URLconfを変換する。 古い不要なビューを削除する。 新しいビューにDjango汎用ビューを設定する。 URLconfの修正 変更前 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.index, name='index'), path('<int:question_id>/', views.detail, name='detail'), path('<int:question_id>/results/', views.results, name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] 変更後 from django.urls import path from . import views app_name = 'polls' urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('<int:pk>/', views.DetailView.as_view(), name='detail'), path('<int:pk>/results/', views.ResultsView.as_view(), name='results'), path('<int:question_id>/vote/', views.vote, name='vote'), ] views.IndexView.as_view(), views.DetailView.as_view(), views.ResultsView.as_view() と as_view() と書くようになった。 そして、 question_id が pk に変更された。ここは同じでもいいような気もする。結局同じ数値が返り値として入るから。 ※後述する DetailView には pk キーを渡す必要があるので同じではダメなようだ。 viewsの修正 index(), detail(), results() 関数を削除しクラスベースに書き換える。 indexでは ListView を継承している。 detail, resultsでは DetailView を継承している。 ListView 「オブジェクトのリストを表示する。」 メソッドのフローチャート継承したメソッドが下記の順番で自動で実行される。 1.setup() 2.dispatch() 3.http_method_not_allowed() 4.get_template_names() 5.get_queryset() 6.get_context_object_name() 7.get_context_data() 8.get() 9.render_to_response() このようにメソッドが実行されるので継承したクラスに get_quesryset() メソッドを追加して内容を上書きする事が出来る。 template_name ListView ではデフォルトの場合 <app name>/<model name>_list.html を自動で生成して使用する。 その場合、テンプレート名は polls/question_list.html になる。 しかし元々作成してある polls/index.html を使用したい場合は template_name に 'polls/detail.html' を代入する事でDjangoがそちらを使用するように認識してくれる。 DetailView 「あるタイプのオブジェクト詳細ページを表示する。」 なので ListViewの詳細ページをDetailViewで表示するみたいな使われ方をする。 template_name そしてデフォルトでは DetailView は <app name>/<model name>_detail.html という名前のテンプレートを自動生成して使用する。 その場合、テンプレート名は polls/question_detail.html になるが、今回は自動生成されたものではなく元々作成してある polls/detail.html を使いたいので template_name を指定して元々のテンプレートを使用する。方法はListViewの時と同じで template_name に polls/detail.html を代入する。 model このクラス変数はビューが使用するモデルを指定している。 model = Question の場合は裏側で Question.objects.all() を行ってくれる。なので queryset = Question.objects.all() としても良い。 from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic from .models import Choice, Question class IndexView(generic.ListView): # デフォルトのビューを使用せず、元々作成してあったものを使用する。 template_name = 'polls/index.html' # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。 context_object_name = 'latest_question_list' def get_queryset(self): """最新の5件を取得する。""" return Question.objects.order_by('-pub_date')[:5] class DetailView(generic.DetailView): # 自分がどのモデルに対して動作するかを伝えている。 # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。 # pkの部分はurls.pyで先に指定してある。 model = Question template_name = 'polls/detail.html' class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' def vote(request, question_id): ... # 前回のまま変更しない。 これでサーバを起動して特にエラーもなく質問のリストページ(index.html)、詳細ページ(投票するページdetail.html)、投票後の今ままでの投票数を表示するページ(results.html)が表示されていれば汎用ビューでのアプリ構築ができたと思う。 最後に 過去にRuby on railsのフレームワークの中身がどう動作しているのかイメージ出来ないのが苦痛(フレームワークは面倒な中身を気にしなくてもアプリが作れるように設計してあるので仕方ないかもしれない。)で挫折しているので今回Djangoのチュートリアルまだ途中ですが挫折せずにアプリ作成まで出来てよかったです。普段Jsonしか触らなかったので少しですがデータベースを作成して操作する経験が出来たのでこれを気にSQL構文をもう少し勉強しようと思う。多対多、多対一の関係とかも自分で作成出来るまでになります。自分の作成したアプリのER図をかける書けるようになりたい。 参照 SECRET_KEYを誤ってGitHubにプッシュしたときの対処法(Django編) - Qiita Pythonの相対インポートで上位ディレクトリ・サブディレクトリを指定 | note.nkmk.me ワイルドカードインポート(import *)は推奨されない 「カラム名」と「フィールド名」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 3. データベースマイグレーション | densan-labs.net モデル(データベース)の作成 プログラミングでよく見かける"コンテキスト(context)って何? - Qiita Python Django チュートリアル(3) - Qiita DJangoのお勉強(1) - Qiita ドキュメント Djangoの汎用ビュー入門(ListView) FOREIGN KEY制約(外部キー制約を設定する) 外部キー制約とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典 Django ForeignKeyで1対多のモデルを構築 記事に関するコメント等は ?:Twitter ?:Youtube ?:Instagram ???:Github ?:Stackoverflow でも受け付けています。どこかにはいます。
- 投稿日:2021-06-15T19:46:24+09:00
【LeetCode】就活に向けたコーディングテスト対策 #06
はじめに こんばんは. M1就活生がLeetCodeから,easy問題を中心にPythonを用いて解いていきます. ↓では,解いた問題のまとめを随時更新しています. まとめ記事 問題 今回解いたのは,難易度easyから 問題20のValid Parentheses です. 問題としては,括弧文字からなる入力文字列sが与えられたとき,開いた括弧が同じ種類の括弧かつ,正しい順序で閉じられているかどうか判定するというもの. 入力例と出力例は以下の通りです. Example 1: Input: s = "()" Output: true Example 2: Input: s = "()[]{}" Output: true Example 3: Input: s = "(]" Output: false Example 4: Input: s = "([)]" Output: false Example 5: Input: s = "{[]}" Output: true 書いたコード とりあえず,思いついたままに書いてみました. class Solution: def isValid(self, s: str) -> bool: left_parenthesis = ["(", "{", "["] right_parenthesis = [")", "}", "]"] parenthesis_map = {"(": ")", "{": "}", "[": "]"} stack = [] for parenthesis in s: if parenthesis in left_parenthesis: stack.append(parenthesis_map[parenthesis]) elif parenthesis in right_parenthesis: if parenthesis != stack.pop(): return False return True 入力文字列sから,1文字ずつ見ていき,開き括弧であれば,それに対応する閉じ括弧をstackの末尾に追加,閉じ括弧であれば,stackの末尾から取り出します.閉じ括弧の場合に,取り出したもの(開き括弧に対応する閉じ括弧)が,異なればFalseを返すことで,有効な括弧の判定を実装しました.最後まで走査できれば,有効な括弧ということでTrueを返します. 今回書いたコードでは,用意したリストが,辞書の内容と重複しています.辞書だけを利用してうまく書き直せないか調べたところ,keys()とvalues()というメソッドが有効そうであったのでこちらで書き直しました. class Solution: def isValid(self, s: str) -> bool: parenthesis_map = {"(": ")", "{": "}", "[": "]"} stack = [] for parenthesis in s: if parenthesis in parenthesis_map.keys(): stack.append(parenthesis_map[parenthesis]) elif parenthesis in parenthesis_map.values(): if parenthesis != stack.pop(): return False return True おわりに 簡単な問題でも,普段使わないものを使うことができるので,様々な知見が増えて楽しいです. 変数名や,コミットメッセージなどの決定に少し時間を掛けてしまったので,ルールを決めて効率化を図りたいものです.余談ですが,自分はオンラインゲームなどで始める名前を決めるのに時間が掛かるタイプです.皆さんはいかがでしょうか? 今回書いたコードはGitHubにもあげておきます. 前回 次回
- 投稿日:2021-06-15T19:06:35+09:00
初学者がコンペでデータ分析の流れを身に付ける
はじめに Qiita初投稿の(@danish55)です。現在、カーディラーの販売促進部署に勤めております。 現在、自己研鑽の為にデータ分析を学んでおります。 学習のアウトプットとして本記事を投稿させて頂きます。 未熟者ゆえ至らぬ点が多々有るかと存じますが、暖かくご指摘頂けましたら幸いです。 本記事の概要 データ分析の流れを身に付ける為に、一般的な流れに沿ってデータ分析を行います。 実務でデータ分析を行っている方にインタビューしながら記事にしました。 教材としてSIGNATEのコンペティションの1つである、「自動車の走行距離予測」に取り組んでみます。 言語はPython、環境はGoogle Colaboratoryを使用しました。 データ分析プロセスの確認 一般的なデータ分析では、以下の6つのステージの順番で進めていきます。 1.問題の定義 2.データの取得と理解 3.データの前処理 4.パターンの分析・特定 5.モデルの作成 6.結果の提出 注意点として必ずこのプロセス通りに進むという訳では有りません。 前のステージに戻ることや、次のステージをスキップすることも有ります。 1.問題の定義 明らかにしたい問いや課題を定義するステージです。問題の定義が明確だと作業の道筋が立てやすく、無駄な作業が減り効率的になります。実務で問題を定義する際には、その業界や会社の理解、そして業務フローの理解が重要となります。その為、実際に業務を行っている人に対して今の現状や課題をヒアリングします。 今回のコンペの問題の定義は自動車の属性データからガソリン1ガロン(≒3.78L)当たりの走行距離を予測することです。蓄積された過去の入力データから連続値を予測するものなので「回帰」を行えば良いことが分かります。 2.データの取得と理解 データの取得及び性質を理解することでどのようにデータを変換・補完・修正していくかの方針を立てるステージです。実務では、非構造化データと呼ばれるデータ分析しにくいデータも取り扱います。非構造化データを収集する際には、そのデータを構造化データに変換する方法も考えつつ収集します。 今回のコンペの訓練およびテストデータは、SIGNATEのコンペティションページから取得できます。 データの理解についは、今回は3つを行います。 1つ目はデータの始まりと終わり5行を目視で確認します。この意味はこのデータに何がどのように含まれているのかを、ざっくりと確認することです。 2つ目は欠損値とデータ型を確認します。欠損値がある場合やデータ型に異常が有る場合は、統計的処理が不可能になることが有ります。 3つ目に要約統計量を確認します。データがどのような分散をしているのか、重複があるかなどを簡単に確認します。 データ中身を目視で確認 それではデータを読み込んで、中身を確認をしたいと思います。 #データを取り込みと確認 import pandas as pd df_train = pd.read_table("./drive/MyDrive/train.tsv") print(df_train.head()) print(df_train.tail()) 出力結果から以下、10種類のデータが入っているようです。(ヘッダ名称を和訳してます) ①インデックス(整数) ②ガソリン1ガロンあたりの走行距離(小数) ③シリンダー(整数) ④排気量(小数) ⑤馬力(小数) ⑥重量(小数) ⑦加速度(小数) ⑧年式(整数) ⑨起源(整数)←車の原産国です。(1=アメリカ, 2=ヨーロッパ, 3=日本) ⑩車名(文字列) 「起源」のようなカテゴリ変数を説明変数として使用する場合は、ダミー変数化する必要が有りそうです。 id mpg cylinders displacement horsepower weight acceleration \ 0 0 29.0 4 135.0 84.00 2525.0 16.0 1 3 31.9 4 89.0 71.00 1925.0 14.0 2 9 19.0 6 156.0 108.0 2930.0 15.5 3 11 28.0 4 90.0 75.00 2125.0 14.5 4 13 37.7 4 89.0 62.00 2050.0 17.3 model year origin car name 0 82 1 dodge aries se 1 79 2 vw rabbit custom 2 76 3 toyota mark ii 3 74 1 dodge colt 4 81 3 toyota tercel id mpg cylinders displacement horsepower weight acceleration \ 194 384 40.8 4 85.0 65.00 2110.0 19.2 195 385 20.2 8 302.0 139.0 3570.0 12.8 196 387 16.0 8 304.0 150.0 3433.0 12.0 197 395 43.4 4 90.0 48.00 2335.0 23.7 198 396 26.0 4 98.0 90.00 2265.0 15.5 model year origin car name 194 80 3 datsun 210 195 78 1 mercury monarch ghia 196 70 1 amc rebel sst 197 80 2 vw dasher (diesel) 198 73 2 fiat 124 sport coupe 欠損値とデータ型の確認 次に欠損値とデータ型を確認します。 #特徴量の情報を出力 print(df_train.info) 出力結果から、今回は欠損値は無いようです。 しかし、先ほど小数として出力されているように見えた「馬力」が文字列として出力されているようです。数値として扱う為にデータ型を変更する必要が有りそうです。 <class 'pandas.core.frame.DataFrame'> RangeIndex: 199 entries, 0 to 198 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 199 non-null int64 1 mpg 199 non-null float64 2 cylinders 199 non-null int64 3 displacement 199 non-null float64 4 horsepower 199 non-null object 5 weight 199 non-null float64 6 acceleration 199 non-null float64 7 model year 199 non-null int64 8 origin 199 non-null int64 9 car name 199 non-null object dtypes: float64(4), int64(4), object(2) 要約統計量の確認 最後に要約統計量からデータの雰囲気を掴みます。 # 基本統計量を出力 print(df_train.describe(include='all')) 特徴量の単位がバラバラです。尺度を揃える為に標準化が必要そうです。 また「車名」はユニークなものが多いです。もし車名を特徴量として使う場合は、加工が必要そうです。 id mpg cylinders displacement horsepower \ count 199.000000 199.000000 199.000000 199.000000 199 unique NaN NaN NaN NaN 71 top NaN NaN NaN NaN 100.0 freq NaN NaN NaN NaN 12 mean 200.170854 24.307035 5.296482 183.311558 NaN std 113.432759 7.797665 1.644562 98.400457 NaN min 0.000000 9.000000 3.000000 71.000000 NaN 25% 98.500000 18.000000 4.000000 98.000000 NaN 50% 202.000000 24.000000 4.000000 140.000000 NaN 75% 294.500000 30.500000 6.000000 250.000000 NaN max 396.000000 44.600000 8.000000 454.000000 NaN weight acceleration model year origin car name count 199.000000 199.000000 199.000000 199.000000 199 unique NaN NaN NaN NaN 167 top NaN NaN NaN NaN chevrolet impala freq NaN NaN NaN NaN 4 mean 2883.839196 15.647739 76.165829 1.582915 NaN std 819.766870 2.701885 3.802928 0.798932 NaN min 1613.000000 8.500000 70.000000 1.000000 NaN 25% 2217.500000 14.000000 73.000000 1.000000 NaN 50% 2702.000000 15.500000 76.000000 1.000000 NaN 75% 3426.500000 17.150000 80.000000 2.000000 NaN max 5140.000000 23.700000 82.000000 3.000000 NaN 3.データの前処理 データを分析しやすい様に加工するステージです。実務のデータ分析でも、この部分が全体の作業量の80%以上を占めると言われるくらい重要な部分になります。この前処理を適切に行うことによって有用なデータ分析が行えるかが決まります。 今回は、3つの前処理を行いたいと思います。 1つ目は「馬力」のデータ型を数値型に変更する処理を行います。理由は先に述べましたようにこのままでは数字としての処理が行えない為です。 2つ目はカテゴリー型である「起源」のダミー変数化です。理由は、数値に大小関係の存在しない項目をそのまま説明変数に使うと精度が下がってしまいまうからです。 3つ目は連続値データの標準化です。理由は「重量」「排気量」など桁数が異なるものを説明変数としてそのまま使うと、桁数の違いなどで係数の違いがそのまま影響度を表すとは限らなくなります。 データ型の変更と欠損値の補完 さっそく「馬力」のデータ型を数値に変更する処理を行います。「馬力」データの中身を確認するとデータの内、4件が「?」と入力されていました。恐らく欠損が有るものの値として「?」を入力したものと思われます。そのため、まず「馬力」の平均を取り、その後「?」の4件に平均を代入するという処理を行います。 #「馬力」のデータ型変換と欠損値へ平均値を代入 import pandas as pd horsepower_mean = df_train[~df_train["horsepower"].str.contains('\?')]['horsepower'].astype(float).mean() print("馬力の平均", horsepower_mean) df_train['horsepower'] = df_train['horsepower'].replace(['\?'],horsepower_mean,regex=True).astype(float) print(df_train.info()) 正しくデータ型の変更が行えました。 馬力の平均 101.2974358974359 <class 'pandas.core.frame.DataFrame'> RangeIndex: 199 entries, 0 to 198 Data columns (total 10 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 199 non-null int64 1 mpg 199 non-null float64 2 cylinders 199 non-null int64 3 displacement 199 non-null float64 4 horsepower 199 non-null float64 5 weight 199 non-null float64 6 acceleration 199 non-null float64 7 model year 199 non-null int64 8 origin 199 non-null int64 9 car name 199 non-null object dtypes: float64(5), int64(4), object(1) ダミー変数化 続いてカテゴリー型である「起源」をダミー変数化します。この際に、注意しなければならないのは多重共線性という問題です。 回帰分析にダミー変数をそのまま使用すると、変数同士の相関が高くなり、多重共線性が起こり、テストデータに対する予測精度が低下してしまいます。 そこでダミー変数化する際には、列の1つを削除します。 #ダミー変数化 import pandas as pd df_train = pd.get_dummies(df_train, columns=["origin"],drop_first = True) print(df_train.head()) ダミー変数化できました。削除したorigin_1(アメリカ)は、origin_2(ドイツ)、origin_3(日本)が共に0として表されます。 id mpg cylinders displacement horsepower weight acceleration \ 0 0 29.0 4 135.0 84.0 2525.0 16.0 1 3 31.9 4 89.0 71.0 1925.0 14.0 2 9 19.0 6 156.0 108.0 2930.0 15.5 3 11 28.0 4 90.0 75.0 2125.0 14.5 4 13 37.7 4 89.0 62.0 2050.0 17.3 model year car name origin_2 origin_3 0 82 dodge aries se 0 0 1 79 vw rabbit custom 1 0 2 76 toyota mark ii 0 1 3 74 dodge colt 0 0 4 81 toyota tercel 0 1 標準化 最後にデータの標準化を行います。標準化とは、データを分散1、平均0の値に加工することです。 # 標準化 # StandardScalerのインポート from sklearn.preprocessing import StandardScaler scaler = StandardScaler() df_train[[ 'displacement', 'horsepower', 'weight', 'acceleration','model year', ]] = scaler.fit_transform(df_train[[ 'displacement', 'horsepower', 'weight', 'acceleration','model year', ]]) print(df_train.head()) 標準化できました。 id mpg cylinders displacement horsepower weight acceleration \ 0 0 29.0 4 -0.492207 -0.492627 -0.438837 0.130705 1 3 31.9 4 -0.960864 -0.862864 -1.172599 -0.611386 2 9 19.0 6 -0.278255 0.190887 0.056452 -0.054818 3 11 28.0 4 -0.950675 -0.748945 -0.928011 -0.425863 4 13 37.7 4 -0.960864 -1.119182 -1.019732 0.613064 model year car name origin_2 origin_3 0 1.537995 dodge aries se 0 0 1 0.747140 vw rabbit custom 1 0 2 -0.043716 toyota mark ii 0 1 3 -0.570953 dodge colt 0 0 4 1.274377 toyota tercel 0 1 4.パターンの分析・特定 データを視覚化してパターンを探したり、特徴量や目的変数の相関性を見るステージです。パターンといっても多種多様で早期に特定するには、経験を積むのが一番とのことです。 今回は相関関係を中心に見ていきます。 まずは相関係数を出力して数値として見ます。 その後散布図を出力して視覚として見ます。 相関係数 まず目的変数である「mpg(ガソリン1ガロンあたりの走行距離)」とその他の特徴量(説明変数)との相関係数を見ます。 相関係数は、-1〜1までの値の数値を取ります。相関係数の絶対値が1に近いほど相関が強く、0に近いほど相関が弱いという関係があります。 目安としては、以下の表のようになります。 相関係数(絶対値) 評価 0~0.3未満 ほぼ無関係 0.3~0.5未満 非常に弱い相関 0.5~0.7未満 相関がある 0.9以上 非常に強い相関 それでは相関関係を見ていきます。 #相関係数を出力 print(df_train.corr()) 出力結果から、一見すると目的変数である「mpg」との相関関係は「シリンダー」「排気量」「馬力」「重さ」に負の相関、「年式」に正の相関が有りそうです。ただし、この相関をこのまま鵜呑みにしてはいけません。 id mpg cylinders displacement horsepower \ id 1.000000 -0.052688 0.103419 0.098416 0.082364 mpg -0.052688 1.000000 -0.770160 -0.804635 -0.778664 cylinders 0.103419 -0.770160 1.000000 0.950600 0.846099 displacement 0.098416 -0.804635 0.950600 1.000000 0.889019 horsepower 0.082364 -0.778664 0.846099 0.889019 1.000000 weight 0.070563 -0.820660 0.893256 0.933038 0.865194 acceleration -0.087649 0.379721 -0.479561 -0.523955 -0.652973 model year -0.093272 0.568471 -0.303462 -0.329817 -0.377874 origin_2 -0.044349 0.297894 -0.360582 -0.387486 -0.295186 origin_3 -0.009584 0.388215 -0.320742 -0.390168 -0.275663 weight acceleration model year origin_2 origin_3 id 0.070563 -0.087649 -0.093272 -0.044349 -0.009584 mpg -0.820660 0.379721 0.568471 0.297894 0.388215 cylinders 0.893256 -0.479561 -0.303462 -0.360582 -0.320742 displacement 0.933038 -0.523955 -0.329817 -0.387486 -0.390168 horsepower 0.865194 -0.652973 -0.377874 -0.295186 -0.275663 weight 1.000000 -0.401757 -0.265562 -0.327802 -0.375637 acceleration -0.401757 1.000000 0.194854 0.160272 0.110562 model year -0.265562 0.194854 1.000000 -0.027979 0.138603 origin_2 -0.327802 0.160272 -0.027979 1.000000 -0.239856 origin_3 -0.375637 0.110562 0.138603 -0.239856 1.000000 気をつけなければならないのが特徴量同士の相関係数になります。なぜならば、重回帰分析を行う場合、特徴量同士で相関関係が強いものを説明変数として使用してしまうと「多重共線性」という現象が起き、テストデータに対する予測精度が低下してしまうからです。 今回で言うと「シリンダー」と「排気量」に非常に強い相関が見れます。調べてみると「排気量=シリンダー内径面積×ピストン行程×シリンダー数」で求められる為、相関が有るのは当然のようです。何を説明変数として採用するかはその分野の知識も必要です。 散布図 次に散布図として可視化して確かめてみます。この意味は数値として相関係数が低く、相関が無さそうな特徴量も何かしらの法則性に基づいてデータが分布している可能性が有るからです。そのため相関分析をする際には散布図を描画し、見落としを防ぐ必要があります。 #散布図を作成 import seaborn as sns sns.pairplot(df_train, x_vars=[ 'cylinders', 'displacement', 'horsepower', 'weight', 'acceleration', 'model year', 'origin_2','origin_3'], y_vars=["mpg"]) ダミー変数化した項目については、0と1で表される為このような分布になります。その他の項目については特殊な分布の形は無さそうです。 5.モデルの作成 アルゴリズム選定とモデル実装・評価のステージです。モデル作成に使用できる機械学習のアルゴリズムは現在、60個以上存在します。その為、問題の種類と解決策の要件を理解した上で、適切なアルゴリズムを選択する必要があります。 今回は「線形重回帰分析」を選択します。回帰モデルには、他にも一般的に使われている「ロジスティック回帰」など有ります。私が今回「線形重回帰分析」を選んだ理由は、現在統計学を学んでおり、その中で学んだことのある線形重回帰を使ってみたいと思ったからです それでは「mpg」を目的変数、説明変数を「排気量」「重さ」「馬力」「年式」「起源」として学習用データで重回帰モデルを作成します。(「シリンダー」は、先の理由から外しました。「重さ」や「馬力」についても特徴量同士の相関が高かったですが、それぞれ特徴量の性質が異なると判断して今回は目的変数として使用します。) また今回はデータ量が少ないのでデータの評価方法として「KFold法」を選択します。データを5分割にして4個分を学習データ、1個をバリテーションデータにして決定係数を算出します。それを5回繰り返して、平均を取ったものを今回の決定係数として採用します。 #モデルの実装と評価 import numpy as np from sklearn.linear_model import LinearRegression from sklearn.model_selection import KFold X = df_train[["displacement","weight","horsepower","model year",'origin_2','origin_3']] y = df_train[["mpg"]] acc_results = [] kf = KFold(n_splits=5,shuffle=True,random_state=20) linear_regression = LinearRegression() for train_index, val_index in kf.split(X): X_train, X_val = X.iloc[train_index], X.iloc[val_index] y_train, y_val = y.iloc[train_index], y.iloc[val_index] linear_regression.fit(X_train, y_train) acc = linear_regression.score(X_val, y_val) acc_results.append(acc) print("傾き:", linear_regression.coef_) print("切片:", linear_regression.intercept_) print("決定係数:", np.mean(acc_results)) 結果は以下のようになりました。決定係数は1に近いほど、回帰式が実際のデータに当てはまっていることを表しており、今回の値はまずまずの値と言えます。 傾き: [[ 1.44732828 -5.13878094 -1.33239738 3.13828398 3.14843987 3.22627336]] 切片: [23.10479828] 決定係数: 0.8137744478423075 視覚化 作成したモデルの有用性を第三者に説明する必要が有るケースも有ります。 その際には視覚化して分り易くする必要が有ります。 今回は作成した重回帰モデルの説明変数の重要度を視覚化します。 # 特徴量重要度の視覚化 import matplotlib.pyplot as pyplot import japanize_matplotlib importance = linear_regression.coef_.ravel() labels = ["排気量", "重さ", "馬力", "年式", "ドイツ車", "日本車"] pyplot.bar([x for x in range(len(importance))], importance, tick_label=labels) pyplot.show() これでどの説明変数がどのくらい結果に寄与しているかが視覚として分かりました。 6.結果の提出 成果物の適用ステージです。実務では成果物をリリースしたら終了ではなく、その後定期的な保守管理を行うようです。また、「最終のレポート」や「プロジェクトのレビュー」などもするようです。 今回はSIGNATE作ったモデルを使用して「mpg」を予測したものをcsv形式で提出して終了とさせていただきます。 # テストデータでの予測と結果提出 import pandas as pd from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler df_test = pd.read_table("./drive/MyDrive/test.tsv", sep='\t') df_test['horsepower'] = df_test['horsepower'].replace(['\?'],horsepower_mean,regex=True).astype(float) df_test = pd.get_dummies(df_test, columns=["origin"],drop_first = True) df_test[[ 'displacement', 'horsepower', 'weight', 'acceleration','model year']] = scaler.fit_transform(df_test[[ 'displacement', 'horsepower', 'weight', 'acceleration','model year']]) X_test = df_test[["displacement","weight","horsepower","model year",'origin_2','origin_3']] y_pred = linear_regression.predict(X_test) df_test['mpg'] = y_pred df_test[['id', 'mpg']].to_csv('./submit.csv', header=False,index=False) 結果は誤差が3.7478877でした。この誤差は「RMSE」という評価関数で表され0に近い程予測の精度が高いことを示します。リーダーボードを見たらもっと誤差の低い人が多くいましたのでまだまだ改善の余地が有るということだと思います。 ちなみに「多重共線性」を危惧して説明変数から外した「シリンダー」を入れて再度モデルを作って見た結果、誤差が3.6852274と少しだけ改善されました。また、そのモデルに更に相関係数が低かった為、説明変数として入れなかった「加速度」を追加したところ誤差が6.0582930と大幅に悪化しました。 作ったモデルに対してどの特徴量を当てはめたら一番良い結果になるのかは、実際に色々試してみないと分からないということだと思います。 結果に対する考察 ここからは結果を提出した後に気づいたことを書きます。 自身の記事を読み直してふと違和感を感じました。 「排気量」は目的変数に対して負の相関が有りました。しかし、回帰係数の符号はプラスでした。これでは、排気量が高いほど、走行距離は伸びることになり、相関関係とは真逆となります。 そこで、調べてみると今回のケースのような場合は「多重共線性」の可能性が有るとのことでした。 「排気量」と相関の有ったが説明変数として使った「重さ」「馬力」が悪さをしているようでした。 そこで、目的変数と一番相関の高かった「重さ」を残し、「排気量」「馬力」を消して再度モデル化してみました。結果は以下のようになりました。 傾き: [[-5.00727484 3.2225105 2.67519918 2.70510764]] 切片: [23.28833663] 決定係数: 0.8134900711409323 コンペの誤差は3.7589497となり、誤差、決定係数ともに最初のモデルの結果とほぼ変わりませんでした。今回の件から言えるのは、何でもかんでも相関係数の高いものを説明変数に入れて重回帰分析をしても精度は上がらないということです。データ分析には、実装する為のプログラミング能力とは別に統計的な知識が不可欠だと感じました。 今回出来なかったこと 今回出来なかったこととして「車名」の列に入っていた文字列から「TOYOTA」といったメーカーの名前の入った列を新規で作ります。そしてその後にダミー変数化してメーカーを説明変数として使用して予測すればもっと精度が良いものになるかと感じました。 また、線形重回帰のみではなく複数のモデルとの比較によってどのモデルが最も良い精度を出すのかを行うともっと面白いかと思います。 おわりに ここまで見ていただき、ありがとうございました。 「データ分析は、誰にも『本当のゴール』が分からない」という言葉を聞いたことが有ります。私は学習を通じて、データ分析の本質とは「完璧な正解方法が無いからこそ、試行錯誤によって「よりよい正解」を目指すこと」だと感じました。試行錯誤の多いデータ分析だからこそ基本的な流れを身に付けることはとても重要だと感じました。
- 投稿日:2021-06-15T18:35:04+09:00
Pythonでクレカ発行してみた(Marqetaチュートリアル)
データモデルの理解 Card Products こちらを読む感じだと、Card Productはクレカ商品を指します(なんとかカードってCMでやってるような感じのもの)。 物理カードやバーチャルのカードについて、動作や機能を定義することができます。Card Productで指定することが可能な属性は、カードがATMやオンラインで使用できるかどうか、また、現在有効であるかどうかを設定できます。物理カードの場合、Card Productは、カードの券面デザインを指定することができます。オプションとして、認証制御やベロシティ制御をCard Productに関連付けることで、関連するカードの使用場所や使用方法を制限することができます。 Cards カードです。Card Productによって生成、管理できます。 作ってみる チュートリアルではCurlで叩いてますが、Python SDKを使ってみます。 カードホルダであるユーザを作成(ダッシュボード) ダッシュボードに記載があるHello Worldなcurlを実行してください。返却されたtokenをこの後使うのでメモしておいてください。 SDKの準備 私はpipでインストールしました。 pip install marqeta 基本的な使い方は以下の通りです。 from marqeta import Client base_url = "ダッシュボードから取得" application_token = "ダッシュボードから取得" access_token = "ダッシュボードから取得" timeout = 60 # seconds client = Client(base_url, application_token, access_token, timeout) Card Productトークンの取得 サンドボックスではすでにいくつかCard Productが作られています。その中から"Reloadable Card"という名前のCard Productのトークンを取得します。 card_products = client.card_products.list() # 25個しか取得できないという制限がある模様。25を超える場合、streamメソッド等を利用すること。 card_product_token = [x for x in card_products if x.name == 'Reloadable Card'][0].token カードの作成 カードユーザ向けにカードを発行します。 先ほど作ったユーザに向けにカードを作成します。 data = { "user_token": "a4e3decd-8540-4940-aebc-9b64b2e93e24", "card_product_token": card_product_token} client.cards.create(data) カードの利用 商店のIDであるMID(merchant ID)を指定して、$10を擬似的に送ってみます。 シミュレータ実行はSDKでは用意されていないため、こちらからcurlを生成し呼び出します。 https://www.marqeta.com/docs/developer-guides/core-api-quick-start 作ったカードのトークンは、Pythonコード内でcreated_card.tokenで取得できます。 ダッシュボードを確認するとtransactionが作成されていました。 Authorization Control、Velocity Controlを作成するとトランザクションが流れそうです。一旦カードが作れて使えそうなことがわかったのでこの記事は、ここまでとします。
- 投稿日:2021-06-15T18:26:25+09:00
Django Generic Base お前まじ誰? (没)
何のための記事? generic baseに含まれるクラス関数について、ソースコード読んで勉強していくよ! Djangoのクラスベースビューが色んな記事読んでも理解できなくて、分からないが分からないなら! ソースコード読んで分からないをしろう?ゴリゴリ脳筋プレーでソースコード読みながら、 参考になりそうなサイトをピックアップしていきます。アホほど参考サイトが出てきます。全部読めば完璧 この記事が役に立つ方 この記事はソースコードを丸々読んでるだけあって、大分雑です!。 丁寧に書きたいのは山々ですが、丁寧に書くほど複雑で読みにくいものになると認識しています。 なので、ソースコード読んででも根本を知りたい方にはおすすめです。 最低限、TemplateViewやCreateView触ったことがないと分かりづらいかもです。 今回しようするコードはソースコードを参照しています。 Mixin class Mixin class とは? wikiから引用したものですが、以下に書いてあるとおり単体で動作することを考慮していないクラスです。 詳しく知りたい方は、こちらのサイトを参照(ミックスインってなに?) mixin とはオブジェクト指向プログラミング言語において、サブクラスによって継承されることにより機能を提供し、単体で動作することを意図しないクラスである 参照元:wikipedia Generic Baseに含まれるMixin classは以下の2個です。 ContextMixin TemplateMixin 一つずつ解説していきます。 ContextMixin 当たり前ながら、これ単体をみても何やってるのかさっぱりですね。知識が足りない。。。 contextとは誰?ってところから 参考サイト:contextの正体をわかりやすく解説 contextの簡易的な見解 辞書型 templateにレンダリングするデータを保管可能(templateに表示したい変数) get_context_data(self, **kwargs) ContextMixin では、get_context_data(**kwargs)で受け取ったキーワード引数をコンテキストとして保存する。 再記するが、コンテキスト(context)はテンプレートに渡される、変数名が含まれた辞書である。 これを使うことで、templateに表示したい値を追加できる。 class ContextMixin: """ get_context_data()で受け取ったキーワード引数をテンプレート・コンテキストとして渡す """ extra_context = None def get_context_data(self, **kwargs): kwargs.setdefault('view', self) # key:'view'が無い時、value:selfを追加 if self.extra_context is not None: kwargs.update(self.extra_context) # extra_contextがある時、kwargsに上書き return kwargs # get_context_dataをオーバーレイ参考コード class IndexView(TemplateView): template_name = 'index.html' def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) ctx['items'] = ... ctx[xxx] = ... return ctx 例として、TemplateViewを使った時のget_context_data()オーバライドしたものである。 template側で{{ items }} をすることで、値をレンダリングできる。 get_context_dataをオーバーライドの参考コード元 オーバライドについて わかりやすい記事が見当たらなかったので検索で一番上のものです。 ContextMixin で注目しておくポイント get_context_data関数にオーバーライドすることで、テンプレートコンテキストに直接書き込めるため 他の関数等を使わずともテンプレートにレンダリングする値を追加できる。 TemplateMixin 今回は先の内容も踏まえると簡単である。 先に見たいのは、get_template_names関数である。 get_template_names(self) クラスベースビューを使ってれば一度は見たことあるであろう。template_name変数がある。 ここでは、render_to_response関数が呼び出された時に、template_nameが空(None)だとエラーを返す処理である。 render_to_response(self, context, **response_kwargs) 'response_class'を使用して、与えられたコンテキストでレンダリングされた テンプレートを持つレスポンスを返してます。 簡単に言うと、テンプレートとそれをレンダリングするのに必要な値をセットにして返す処理です。 class TemplateResponseMixin: """テンプレートをレンダリングするために使用""" template_name = None template_engine = None response_class = TemplateResponse content_type = None def render_to_response(self, context, **response_kwargs): """ 'response_class'を使用して、与えられたコンテキストでレンダリングされた テンプレートを持つレスポンスを返します。 レスポンスクラスのコンストラクタには、response_kwargs を渡します。 """ response_kwargs.setdefault('content_type', self.content_type) return self.response_class( request=self.request, template=self.get_template_names(), #get_template_names()呼び出してる。 context=context, using=self.template_engine, **response_kwargs ) def get_template_names(self): """ リクエストに使用されるテンプレート名のリストを返します。 render_to_response()がオーバーライドされている場合は呼び出されません。 """ if self.template_name is None: raise ImproperlyConfigured( #errorコード "TemplateResponseMixin requires either a definition of " "'template_name' or an implementation of 'get_template_names()'" ) else: return [self.template_name] ContextMixin で注目しておくポイント template_nameだけ知ってれば良い!もう、みんな友達だと思うけど この子を使って、表示したいtemplateの名前を与える!そこに限る。 template_name = "index.html" こんな感じ☺️ Main class Mixinクラスを説明し終わったので、Generic Baseで主なクラスの説明をしていこうと思います。 Generic Baseで定義されているのは以下の3クラスです。 View TemplateView RedirectView こっからDjangoのクラスベースビューで大事になってくる要素がいくつか含まれてくるので、 そこは理解できるまで、いろんな記事を読み漁ったり実際にソースコードを見てみるなりした方が良いです。 それでは、一つずつ解説していきます。 View class こいつ抜きにして、Djangoのクラスベースビューは機能しない!ってくらい大事です。 簡単に大切さを説明すると、どのDjangoのクラスベースビューのクラスを辿ってもViewくんが継承されています! ※ 今説明した通り、jangoのクラスベースビューのクラスです。MixinクラスはViewくんを継承していません。 ここでViewくんやクラスベースビューの構造がよく分からない方はこの記事がおすすめです。 意図的にシンプルにした、すべてのビューの親クラスです。 Dispatch-by-methodと簡単なサニティチェックのみを実装しています。(参照元:ソースコード) サニティチェックとは、計算結果が正しいかどうかを素早く評価するための基本的なテスト(参照元:wikipedia) それでは中身を見ていきます。元のソースコード まず、View class がもつメソッドは以下の7個です。 __init__ as_view setup dispatch http_method_not_allowed options _allowed_methods 備考: http_method_names = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'] それじゃ、一つずつ見ていきます。 def __init__ 有名なコンストラクタ関数です。知らない方はこちらのサイトを参照 中身では、インスタンスの際にキーワード因数を与えられた場合 そのデータを検証してインスタンスに保存するだけです。 組み込み関数 - setattr関数についての参考資料 def __init__(self, **kwargs): """ コンストラクタ。 役に立つ追加のキーワード引数やその他のものを含むことができます。 """ #キーワード引数を調べて, その値をインスタンスに保存するか, エラーを発生させます. for key, value in kwargs.items(): setattr(self, key, value) as_view(cls, **initkwargs) as_viewの特徴はクラスメソッドであるところですね。 中身のコードを一つずつ見ていきます。 最初のところでは、二つのエラーコードを設定してあります。 一つは、http_method_namesで定義された値をkeyで受け取った時と、classのインスタンス変数以外を受け取った時 詳しくは話さないが、と言うか分からないがわかる人いたら教えて欲しい 次のところでview関数を定義されている。 この中では、先ほどあげた関数のsetup関数とdispatch関数が呼び出されている。 最後にデコレータについて書かれています。 @classonlymethod def as_view(cls, **initkwargs): """リクエストやレスポンスの主な処理が定義されている。""" for key in initkwargs: if key in cls.http_method_names: raise TypeError("You tried to pass in the %s method name as a " "keyword argument to %s(). Don't do that." % (key, cls.__name__)) if not hasattr(cls, key): raise TypeError("%s() received an invalid keyword %r. as_view " "only accepts arguments that are already " "attributes of the class." % (cls.__name__, key)) def view(request, *args, **kwargs): self = cls(**initkwargs) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.setup(request, *args, **kwargs) if not hasattr(self, 'request'): raise AttributeError( "%s instance has no 'request' attribute. Did you override " "setup() and forget to call super()?" % cls.__name__ ) return self.dispatch(request, *args, **kwargs) view.view_class = cls view.view_initkwargs = initkwargs # クラスから、Docstringとクラス名を呼び出す update_wrapper(view, cls, updated=()) # デコレーターで設定可能な属性を呼び出す。 update_wrapper(view, cls.dispatch, assigned=()) return view setup(self, request, *args, **kwargs) ここは至って簡単です。 request, args, kwargs をインスタンス変数として登録しています。 def setup(self, request, *args, **kwargs): """すべてのビューメソッドで共有される属性を初期化します。""" self.request = request self.args = args self.kwargs = kwargs dispatch(self, request, *args, **kwargs) ここもそれほど難しい事は行っていません。 httpメソッドに対応するメソッドを探して呼び出しています。(GET,POST) もし、http_method_namesに未定義なものかhttpメソッドに対応するメソッドが見つからない場合 http_method_not_allowed()を呼び出します。 def dispatch(self, request, *args, **kwargs): if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs) http_method_not_allowed(self, request, *args, **kwargs) _allowed_methods(self) loggerを使ってwarning文の追加と _allowed_methods(self)を呼び出してますね ここら辺は正直、dispatchでhttp_method_namesに未定義なものか httpメソッドに対応するメソッドが見つからない場合のエラーコードを定義しているところと捉えています。 なので、ここで終わりです。 def http_method_not_allowed(self, request, *args, **kwargs): logger.warning( 'Method Not Allowed (%s): %s', request.method, request.path, extra={'status_code': 405, 'request': request} ) return HttpResponseNotAllowed(self._allowed_methods()) def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] options(self, request, *args, **kwargs) HTTPの要求への応答を処理します。Allowビューで許可されているHTTPメソッド名のリストを含むヘッダーを含む応答を返します。 def options(self, request, *args, **kwargs): """Handle responding to requests for the OPTIONS HTTP verb.""" response = HttpResponse() response['Allow'] = ', '.join(self._allowed_methods()) response['Content-Length'] = '0' return response 没 編集予定未定 ここまで記事書いてなんなんですが、理解できて無いところが複数出てきて 役に立たない情報をまとめても仕方ない、、、となってしまっています。 ここまで読んで続きが読みたいとかあれば、LGTMよろしくお願いします。 先に、記事書きながら勉強を進めていましたが 勉強を先にして理解してから書きます。
- 投稿日:2021-06-15T18:24:29+09:00
DynamoDBのscanでPaginatorを使用し、filterExpressionとprojectionExpressionを使う方法(bot3編)
はじめに boto3を利用したdynamoDBのscanでPaginatorを使用した場合に、filterExpressionを使用した際に、詰まったので、記事に残しておきます。 filterExpression FilterExpressionとは、条件を満たした項目だけが返されるような条件を指定します。他のすべての項目は破棄されます。 projectionExpression projectionExpressionとは、スキャン結果に必要な属性を指定します。 発見したバグ issueとしても上がっているのですが、boto3.dynamodb.conditions を利用した方法でfilterExpressionを使用すると、うまく使えない問題が発生します。 下記がその時のサンプル sample.py import boto3 from boto3.dynamodb.conditions import Key, Attr page_iterator = paginator.paginate( TableName=TABLE_NAME, IndexName=INDEX_NAME, FilterExpression=Key('X').eq(x) & Key('Y').lte(y) ) PaginatorでFilterExpressionを利用したい場合の書き方 簡単に書くと、ExpressionAttributeValuesを利用して、文字列で渡してあげるやり方にするとFilterExpressionを使用することができます。 sample2.py def scan_dynamo(): dynamoDBName = 'testdb' dateStr = 'xxx-xx-xx' filterExpression = 'time_stamp <= :searchDate' projectionExpression = 'id' try: cnt = 0 for page in dynamoPaginator.paginate( TableName=dynamoDBName, PaginationConfig=paginationConfig, ProjectionExpression=projectionExpression, FilterExpression=filterExpression, ExpressionAttributeValues={ ":searchDate": { "S": dateStr }, } ): print('Scan件数:{}'.format(cnt)) except Exception as e: print('DynamoDBのScanに失敗しました。TableName={0}, Error={1}'.format(dynamoDBName, e)) 参考
- 投稿日:2021-06-15T18:17:23+09:00
Visual Studio Code 用 Jupyter Extension(2021 年 6 月版)がリリースされました
皆さんごきげんよう。本記事は、2021 年 6 月 14 日(米国時間)ポストされた Jupyter in Visual Studio Code – June 2021 Release の速報的なアレというか意訳のようななにかとなります。内容については下記が原文で、正となります。 Visual Studio Code (以下 VS Code) 用 Jupyter Extension(2021 年 6 月版)がリリースされました。 VS Code で Python をご利用いただいている方は、Visual Studio Marketplace から Python Extension を直接ダウンロードするか、あるいは VS Code の Extension Galary から直接インストールいただけます。 すでに Python の Extension インストールしていただいいている場合は、VS Code を再起動することで最新アップデートを取得いただけます。 Note : VS Code の Extension Marketplace の呼び出し方 ※ Visual Studio Code 上からは、View: Extensions コマンドを入力するか、Ctrl + Shift + X キーを同時押しで、Extension Marketplace が呼び出せます。 VS Code の Python サポートについての詳細 こちらのドキュメント(英語)をご覧くださいませ。 今回のリリースで超がんばったポイント セキュリティ対策を強化しました ネイティブ Notebook レイアウトの設定を追加 Data Viewer と Variable Explorer (変数エクスプローラ) を改善しました GitHub の変更ログから改善されたポイントの全リストを確認いただけます。 ワークスペースの信頼 VS Code はセキュリティに真剣に取り組んでおります。 この「ワークスペースの信頼」機能では、信頼するプロジェクトのフォルダとコンテンツ、および制限つきモードにしておくプロジェクト フォルダを決定いただけます。 Notebook に関してどのように動作するのでしょうかね? VS Code でフォルダを開く際に、フォルダ作成者や内容を信頼するかどうかを聞いてきます。信頼大事。 フォルダを信頼対象と設定した場合、すべての Notebook とその output はレンダリングされ、コード実行が可能です。 一方、Notebook の格納元フォルダが信頼対象とされていない場合は、制限付きモードでの動作になります。 制限付きモードから、信頼済みに動作を変更したい場合は、左下の歯車アイコンをクリックして [Manage Workspace Trust] を選択してくださいね。 注意: Notebook には、Output に有害なコードが埋め込まれている可能性があり、セルを実行しなくても、実行されてしまう可能性があることを理解しておくことが重要です。VS Code は、利用者が対象の Notebook を明示的に信頼するまで、Output を抑制します。ダウンロードしてきた Notebook や、外部ソースからもちこまれたような Notebook については信頼するかどうかの判断は、特に慎重に検討することを推奨します。 ワークスペースの信頼についての詳細は以下を参照してください。 データ ビューアにおけるフィルタリング機能の向上(ワイルドカード使えるよ!) データ ビューアでは、"*"(ワイルドカード文字)サポートが追加されました。これにより、string (文字列) 値のフィルター機能が向上しました。やったね。 以下の例は、"male" と "female" の両方のエントリが含まれている列に対して、フィルタリングを行っています。 ワイルドカードを使わないで、"male" という値を入力し、検索すると、完全に一致する値のみが返されます。 文字列の最後が "male" で終わるすべての値を検索するには、フィルタボックスで頭にワイルドカードをつけて、"* male"と入力します。 このように、ワイルドカード文字 "*" は、任意の数の文字にマッチし、文字列内の任意の場所で使用できます。 Valiable Explorer でのソート処理 ご要望の多かった、Notebook 内の変数 (Value) のソートが可能になりました。Variable Explorer の "Name" または "Type" のヘッダをクリックすると、ソートされます。ヘッダーの矢印は、今どの列でソートされているかを示しています。また、その方向は、変数がどのヘッダーでソートされているか、また、ソートがアルファベット順かアルファベット順の逆のどちらで行われているかを示しています。 Data Viewer と、Variable Explorer に対して、これらの控えめに言って最高なアップデートを実現することができたのは、なんと Microsoft Jupyter Extension のソフトウェア エンジニアリング インターンの Vandy Liu さんのご尽力のおかげです。Vandyさん、このような素晴らしい機能拡張を実現してくれてありがとうございます…! uikou 注 : これはマジで神 New Look for Native Notebook! 今すぐ Native Notebook をお試しいただくには、VS Code Insiders と Jupyter Extension をダウンロードしてください。Python Notebook を使いたいなぁという方には、Python Extension の利用を激しくお勧めします。 Notebook のツールバーが左上に表示されます。ここに、Notebook 関連の [お気に入り] についての操作や、機能などがすべて表示されるようになります。 Kernel Picker 環境の切り替えが簡単にできるよう、右上に表示されるようになりました。 セル インジケータが太くなりました。特に長いセルや出力を扱う際に、Notebook 内でどこにいるかを確認するのに、このガター インジケータがより役立つでしょう。 カスタマイズ可能な Native Notebook 上記のハイライトでは、Notebook のために、すぐに使えるものを紹介してきましたが、もちろん Notebook をあなた好みにカスタマイズすることもできます。 いつでも俺(私 / 僕)色に染め上げて開発をしたい…そんなあなたにぴったりの Notebook を作りあげるために、いくつかの設定をご用意しました。Notebook のレイアウトの設定を確認するには、ツールバーの端っこにある [More actions] アイコンをクリックし、[Customize Notebook Layout] を選択しますと、Notebook に関連するレイアウト設定のページが表示されますので、ぼくのかんがえるさいきょうの Notebook のレイアウトを作り上げることができます。やったね。 ★ Notebook のレイアウト設定の全リスト 項目名 内容 Notebook.insertToolbarLocation 新しいセルを挿入するボタン(+Code、+Markdown)について以下のいずれになるかを制御します。(セルの間に表示する / ツール バーに表示する / セル、ツールバー双方に表示する / 非表示にする) Notebook.consolidatedRunButton 2 つの新しいアクションがあります。("Execute Above Cells" と "Execute Cell and Below") これらは既定で cell toolbar に表示されますが、この設定を有効にすると、実行ボタンのとなりの新しいコンテキスト メニューに移動します。 Notebook.cellFocusIndicator Jupyterと同様に、セルの側面にカラー バーを表示して、セルのフォーカス状態を示すオプションを追加します。 Notebook.cellToolbarVisibility セルのフォーカス時、あるいはホバーされている状態でセル ツールバーを表示するかどうかを決定します。既定値では、セルのフォーカス時のみ、セル ツールバーを表示するようになっています。 Notebook.compactView この値が有効だと、セルはよりコンパクトなスタイルで描画され、空きスペースが少なくなります。既定で有効です。 Notebook.consolidatedOutputButton セルの Output をクリアするアクションと、別の Output レンダラーまたは mimetype を選択するボタンが、セル出力の横にある1つの […] メニューに統合されました。この設定で新しいメニューを無効にすることができます。 Notebook.dragAndDropEnabled セルのドラッグ & ドロップを無効にします。既定では、Alt + Up / Alt + Down を使用してセルの再配置が可能です。 Notebook.globalToolbar Notebook エディタの上部にツール バーを追加します。 Notebook.showCellStatusBar この設定には新しいオプション visibleAfterExecute があります。セルが実行されるまでスペースを節約するためにセルのステータスバーを非表示にします。なお、セルが実行されるたびにステータス バーが表示されるので、このオプションをセットしても、今まで通り実行の詳細を確認できます。 Notebook.showFoldingControls Markdown ヘッダに表示される折りたたみシェブロンを常に表示するか、マウス オーバー時にのみ表示するかを設定します。 Notebook.editorOptionsCustomizations Notebook のセル エディタ設定をカスタマイズできるようにします。 その他の変更と機能強化 そのほか、VS Codeで Notebook を使用する際の利便性を向上させるために、ユーザーの皆様から要望のあったプチ機能強化や問題の修正を行いました。その中でもぜひ注目していただきたい変更点は以下の通りです。 セルの言語ピッカーに表示される言語を、カーネルがサポートする言語に制限する (#5580) Variable Explorer の除外リストに、ABCMeta と type を追加 (#5865) Variable View の fit & finish を VS Code に合わせて調整 (#5955) 削除された Python 環境に属しているカーネルをカーネル ピッカーから隠す (#6164) 是非、Visual Studio Code用の Python Extension と Jupyter Extension をダウンロードして、こうした改善点についてお試しくださいませ。 また、何か問題が発生した場合や、こうやって見たらいいんじゃないかな、といったご提案については、VS Code 用 Jupyter Extension GitHub リポジトリ の、issues からぜひお寄せ下さい。皆様のご提案など、フィードバックおまちしております。 ご意見はこちらから! 以上、Jeffrey さんからでした。 それではみなさんごきげんよう。
- 投稿日:2021-06-15T18:10:39+09:00
【LeetCode】就活に向けたコーディングテスト対策 #05
はじめに こんばんは. M1就活生がLeetCodeから,easy問題を中心にPythonを用いて解いていきます. ↓では,解いた問題のまとめを随時更新しています. まとめ記事 問題 今回解いたのは,難易度easyから 問題14のLongest Common Prefix です. 問題としては,入力として与えられる文字列の配列の中から,最長の共通の接頭辞を持つ文字列を見つけるというもの. 入力例と出力例は以下の通りです. Example 1: Input: strs = ["flower","flow","flight"] Output: "fl" Example 2: Input: strs = ["dog","racecar","car"] Output: "" Explanation: There is no common prefix among the input strings. なお,この問題は文字列のペア間ではなく,配列中の全ての文字列が対象とのことです. 例えば,入力配列が ["a", "a", "b"] なら "" を返します("a" ではない). 書いたコード とりあえず,思いついたままに書いてみました. class Solution: def longestCommonPrefix(self, strs: List[str]) -> str: prefix = "" flag = True for i in range(len(strs)): prefix_candidate = strs[0][i] for str_element in strs[1:]: if str_element[i] != prefix_candidate: flag = False break if flag: prefix += prefix_candidate return prefix 入力配列strsの先頭文字列の要素を順に接頭辞候補とします.接頭辞候補を,残りの文字列の同じ位置の要素と同じかどうか走査していくことで,接頭辞を見つけます. しかし,このままではリストの範囲外を参照してしまうのと,接頭辞が異なる判定となった場合も,文字列の最後の要素まで走査してしまいます.そのため,入力配列strs中に含まれる文字列の最低文字数を繰り返しの範囲とし,flagがFalseであれば,走査を終了する判定を追加しました. class Solution: def longestCommonPrefix(self, strs: List[str]) -> str: prefix = "" flag = True for i in range(len(min(strs))): prefix_candidate = strs[0][i] for str_element in strs[1:]: if str_element[i] != prefix_candidate: flag = False break if flag: prefix += prefix_candidate else: break return prefix 内側の繰り返しでも,明示的に繰り返し回数を指定してあげたほうがリーダブルかつ効率的かも...? おわりに LeetCodeでは,Testcaseがいくつか用意されているようですが,今回のようにたまたまうまく動いた場合もあるみたいです(上側のコード).例外的なケースにも対応できるコーディングを意識していきたいと思いました. 今回書いたコードはGitHubにもあげておきます. 前回 次回
- 投稿日:2021-06-15T17:46:17+09:00
Raspberry PIでPython WEB#3(DB編)
概要 今回はMySQLDBを利用して簡単なCRUDコードを作成したいと思います。 MySQLライブラリはSQLAlchemyというものを使用します。 ※MySQLサーバーのバージョンが「5.5.54」ですが、下記の事前作業には最新MySQLをインストールするコマンドをいれました。 事前作業 ①次のコマンドでSQLAlchemyをインストールします。 # Python環境駆逐 sudo apt-get install python2.7-dev python3-dev # MySQLサーバインストール sudo apt-get install mariadb-server-10.0 sudo apt-get install python-mysqldb # DBライブラリ pip3 install sqlalchemy # 次のエラーが発生し、インストールします。 # ModuleNotFoundError: No module named 'MySQLdb' pip3 install mysqlclient ②データベース及びテーブル作成 ※テーブル作成についてはboard.pyからも作成ができます。 # データベース作成 CREATE DATABASE testDB; # テーブル作成SQL CREATE TABLE `boards` ( `id` INTEGER NOT NULL AUTO_INCREMENT, `title` varchar(100) DEFAULT NULL, `body` varchar(200) DEFAULT NULL, `writer` varchar(50) DEFAULT NULL, `email` varchar(100) DEFAULT NULL, `password` varchar(50) NOT NULL, PRIMARY KEY(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; サンプルソース作成 ①以下3つのファイルを作成します。 # file name : setting.py # pwd : /home/pi/work/setting.py from sqlalchemy import * from sqlalchemy.orm import * from sqlalchemy.ext.declarative import declarative_base # mysqlのDBの設定 DATABASE = 'mysql://%s:%s@%s/%s?charset=utf8' % ( "flaskweb", # username "flaskweb123", # password "192.168.1.25", # host "testDB", # database name ) ENGINE = create_engine( DATABASE, encoding = "utf-8", echo=True # Trueだと実行のたびにSQLが出力される ) # Sessionの作成 session = scoped_session( # ORM実行時の設定。自動コミットするか、自動反映するなど。 sessionmaker( autocommit = False, autoflush = False, bind = ENGINE ) ) Base = declarative_base() Base.query = session.query_property() # file name : board.py # pwd : /home/pi/work/board.py import sys from sqlalchemy.ext.declarative import declarative_base from sqlalchemy import Column, Integer, String, Float, DateTime from setting import Base from setting import ENGINE class Board(Base): __tablename__ = 'boards' id = Column('id', Integer, primary_key = True) title = Column('title', String(100)) body = Column('body', String(200)) writer = Column('writer', String(50)) email = Column('email', String(100)) password = Column('password', String(50), nullable=False ) # NOT NULLの指定方法 def main(args): Base.metadata.create_all(bind=ENGINE) if __name__ == "__main__": main(sys.argv) # file name : main.py # pwd : /home/pi/work/main.py from setting import session from board import * board = Board() board.title = 'title1' board.body = 'test body1' board.writer = 'tester' board.email = 'test@localhost' board.password = '1111' session.add(board) session.commit() # query関連の条件指定は参考サイトを参照してください。 boards = session.query(Board).all() for board in boards: print(board.title) ~ テーブル作成 次のコマンドでテーブル作成ができます。 ※テーブル作成後に該当DDLも表示ができました。 pi@raspberrypi:~/work $ python3 board.py 2021-06-15 09:04:06,377 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'sql_mode' 2021-06-15 09:04:06,378 INFO sqlalchemy.engine.Engine [raw sql] () 2021-06-15 09:04:06,380 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'lower_case_table_names' 2021-06-15 09:04:06,380 INFO sqlalchemy.engine.Engine [generated in 0.00040s] () 2021-06-15 09:04:06,383 INFO sqlalchemy.engine.Engine SELECT DATABASE() 2021-06-15 09:04:06,384 INFO sqlalchemy.engine.Engine [raw sql] () 2021-06-15 09:04:06,386 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-06-15 09:04:06,388 INFO sqlalchemy.engine.Engine SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = %s 2021-06-15 09:04:06,388 INFO sqlalchemy.engine.Engine [generated in 0.00043s] ('testDB', 'boards') 2021-06-15 09:04:06,392 INFO sqlalchemy.engine.Engine CREATE TABLE boards ( id INTEGER NOT NULL AUTO_INCREMENT, title VARCHAR(100), body VARCHAR(200), writer VARCHAR(50), email VARCHAR(100), password VARCHAR(50) NOT NULL, PRIMARY KEY (id) ) 2021-06-15 09:04:06,392 INFO sqlalchemy.engine.Engine [no key 0.00038s] () 2021-06-15 09:04:06,406 INFO sqlalchemy.engine.Engine COMMIT データ登録 次のコマンドで1つのレコードが登録できます。 ※レコードの登録と最後に登録されたレコードのタイトルのみ表示しています。 pi@raspberrypi:~/work $ python3 main.py 2021-06-15 09:07:08,710 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'sql_mode' 2021-06-15 09:07:08,710 INFO sqlalchemy.engine.Engine [raw sql] () 2021-06-15 09:07:08,713 INFO sqlalchemy.engine.Engine SHOW VARIABLES LIKE 'lower_case_table_names' 2021-06-15 09:07:08,713 INFO sqlalchemy.engine.Engine [generated in 0.00038s] () 2021-06-15 09:07:08,716 INFO sqlalchemy.engine.Engine SELECT DATABASE() 2021-06-15 09:07:08,717 INFO sqlalchemy.engine.Engine [raw sql] () 2021-06-15 09:07:08,719 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-06-15 09:07:08,723 INFO sqlalchemy.engine.Engine INSERT INTO boards (title, body, writer, email, password) VALUES (%s, %s, %s, %s, %s) 2021-06-15 09:07:08,723 INFO sqlalchemy.engine.Engine [generated in 0.00045s] ('title1', 'test body1', 'tester', 'test@localhost', '1111') 2021-06-15 09:07:08,725 INFO sqlalchemy.engine.Engine COMMIT 2021-06-15 09:07:08,733 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-06-15 09:07:08,740 INFO sqlalchemy.engine.Engine SELECT boards.id AS boards_id, boards.title AS boards_title, boards.body AS boards_body, boards.writer AS boards_writer, boards.email AS boards_email, boards.password AS boards_password FROM boards 2021-06-15 09:07:08,740 INFO sqlalchemy.engine.Engine [generated in 0.00045s] () title1 DBを確認 次のコマンドで登録されたレコードを確認します。 pi@raspberrypi:~/work $ mysql -uflaskweb -pflaskweb123 testDB Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 62 Server version: 5.5.54-0+deb8u1 (Raspbian) Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> select * from boards; +----+--------+------------+--------+----------------+----------+ | id | title | body | writer | email | password | +----+--------+------------+--------+----------------+----------+ | 1 | title1 | test body1 | tester | test@localhost | 1111 | +----+--------+------------+--------+----------------+----------+ 1 row in set (0.00 sec) mysql> 参考 ①【PythonのORM】SQLAlchemyで基本的なSQLクエリまとめ https://qiita.com/tomo0/items/a762b1bc0f192a55eae8 ・DB接続及びテーブルのモデル作成、データの登録及び条件指定方法など参照 ②SQLAlchemy で Update するには? https://qiita.com/nskydiving/items/745f985a42da89a16934 ・全データ削除、一括登録、更新内容を参照 終わりに 午前中まではPyMySQLを利用して調べていましたが、 SQLAlchemyの方が以下のメリットがありましたので、このライブラリに決めました。次回はWEBに連動してCRUDの掲示板を作成してみたいと思います。 ■メリット ・SQLに該当するロジックを書くこと。 ・MYSQL、SQL、PostgreSQLの特殊なSQL文を気にしなくてもよいこと。 ・DBMSが変更されてもSQL内容の変更は不要。(接続文字列関連のロジックは修正が必要) ■デメリット ・SQLをコードで書いてあるため、デバッグするのがちょっと不便かもです。
- 投稿日:2021-06-15T17:06:36+09:00
Jupyter Notebookで思いもよらない罠にハマった話
Pythonで否定が!じゃないことを忘れていて書いた俺も悪いんだけどさ。さすがに、こんなんなると思わないじゃないですか。 問題: Jupyter Notebookでの実行結果を予想せよ a = !True if a: print(a) 答え: ['/bin/bash: 行 1: True: コマンドが見つかりません'] (ノ`´)ノミ┻┻
- 投稿日:2021-06-15T16:57:59+09:00
各言語のDockerコンテナでIPアドレスを解決したい場合の代替コマンド
みなさん、Dockerは使ってますか?Dockerをドカドカ使っていると、 「社内のHTTP APIを呼びたいのに、なぜかホスト名をIPアドレスに解決できない」 「本番環境のではなく開発環境のHTTP APIを呼んでしまっている(っぽい)」 なんて、ことが稀によく発生します。 一方で、定石に従ってDockerイメージを作っていると、 「Dockerイメージには最小限のバイナリしか入れていないので hostもnslookup も使えない。だからホスト名がどう解決されているかも分からない。」 なんてことも起きがち。 そこで、最小限のコンテナに docker exec した時にホスト名解決を(雑に)確認できるコマンドを紹介します。 getent glibc に含まれるコマンドです。 $ getent hosts qiita.com 2406:da14:add:902:68c3:7e8d:a9c:949e qiita.com 2406:da14:add:900:8bc:d6f3:591c:eb91 qiita.com 2406:da14:add:901:3a2b:2b27:a25f:7360 qiita.com Ruby $ ruby -rresolv -e 'puts Resolv.getaddress ARGV[0]' qiita.com 54.92.41.140 Python $ python3 -c 'import sys, socket; print(socket.gethostbyname(sys.argv[1]))' qiita.com 54.250.97.111h
- 投稿日:2021-06-15T16:57:59+09:00
Dockerコンテナ内でIPアドレスを解決したい場合の代替コマンド
みなさん、Dockerは使ってますか?Dockerをドカドカ使っていると、 「社内のHTTP APIを呼びたいのに、なぜかホスト名をIPアドレスに解決できない」 「本番環境のではなく開発環境のHTTP APIを呼んでしまっている(っぽい)」 なんて、ことが稀によく発生します。 一方で、定石に従ってDockerイメージを作っていると、 「Dockerイメージには最小限のバイナリしか入れていないので hostもnslookup も使えない。だからホスト名がどう解決されているかも分からない。」 なんてことも起きがち。 そこで、最小限のコンテナに docker exec した時にホスト名解決を(雑に)確認できるコマンドを紹介します。 getent glibc に含まれるコマンドです。 $ getent hosts qiita.com 2406:da14:add:902:68c3:7e8d:a9c:949e qiita.com 2406:da14:add:900:8bc:d6f3:591c:eb91 qiita.com 2406:da14:add:901:3a2b:2b27:a25f:7360 qiita.com Ruby $ ruby -rresolv -e 'puts Resolv.getaddress ARGV[0]' qiita.com 54.92.41.140 Python $ python3 -c 'import sys, socket; print(socket.gethostbyname(sys.argv[1]))' qiita.com 54.250.97.111h
- 投稿日:2021-06-15T16:15:14+09:00
Slackで自分の投稿にリアクションされたら通知する
はじめに Slackで自分が投稿したメッセージに対してリアクションされた場合に通知されて欲しいなと思い調べてみましたが見つかりません…。 未読やメンションは通知され、かつバッヂで残るので見逃しは基本ありませんが、リアクションは自発的に確認する必要があるので確認が遅れたり忘れたりすることがあります。 返信が必ず欲しい内容ではメンションしてもらうよう促せば良いですがリアクションでの返事の手軽さは中々手放せません。 なのでリアクションが通知されるようにSlackアプリを作りました。 完成したアプリ通知はこんな感じです。 漠然と以下をイメージしながら進めました。 自分にだけ表示 自分のリアクションは無視 特定のメッセージのみ通知 環境 Python3.8 Azure App Service Slackアプリ作成 以下URLにアクセスしてアプリ作成開始。 https://api.slack.com/apps アプリ名とワークスペースを選択して作成。 アプリ作成後はアプリのスコープを設定します。 設定するのは以下です。 channels:history channels:read chat:write groups:history reactions:read users:read botユーザを作成します。 ワークスペースにアプリをインストールします。 次にイベントAPIの設定をしていきますが、先にChallengeパラメータを返す処理を用意する必要があります。 Slackの設定はまた後で行います。 Pythonアプリの作成 WebフレームワークはFlaskを利用します。 Slackとの接続やAPI処理はslack-bolt、slack-sdkを使用します。 必要なパッケージは requirements.txtに追加します。 slack-boltの指定でslack-sdkも一緒にインストールされます。 requirements.txt Flask>=1.0 slack-bolt BOTのトークン等はApp Serviceのアプリケーション設定で設定して処理の中で取得するようにします。 os.environで取得している箇所です。 app.py import logging import os from slack_bolt import App from slack_bolt.adapter.flask import SlackRequestHandler from slack_sdk import WebClient logging.basicConfig(level=logging.INFO) client = WebClient(os.environ["SLACK_BOT_TOKEN"]) app = App() @app.event("reaction_added") def reaction_add(event, say): emoji = event["reaction"] user = event["user"] item_user = event["item_user"] channel = event["item"]["channel"] ts = event["item"]["ts"] if user == item_user: return # タイムスタンプでメッセージを特定 conversations_history = client.conversations_history( channel=channel, oldest=ts, latest=ts, inclusive=1 ) messages = conversations_history.data["messages"] # メッセージが取得出来ない場合、スレッドからメッセージを特定 if not messages: group_history = client.conversations_replies(channel=channel, ts=ts) messages = group_history.data["messages"] reactions = messages[0]["reactions"] target_reaction = os.environ["SLACK_REACTION_KEY"] start_postmessage = False reactions_text = "" for reaction in reactions: reactions_text += ":{}:{} ".format(reaction["name"], reaction["count"]) if reaction["name"] == target_reaction: start_postmessage = True if not start_postmessage: return userslist = client.users_list() members = userslist["members"] for member in members: if member["id"] == user: reaction_user = member["real_name"] break response = client.chat_postEphemeral( channel=channel, user=item_user, text=f"{reaction_user} さんが以下のメッセージにリアクション :{emoji}: を追加しました\n\n" + messages[0]["text"] + f"\n{reactions_text}", ) from flask import Flask, request flask_app = Flask(__name__) handler = SlackRequestHandler(app) @flask_app.route("/slack/events", methods=["POST"]) def slack_events(): return handler.handle(request) if __name__ == "__main__": flask_app.run(host="0.0.0.0", port=3000) Azure App Serviceの作成 WebサーバはAzureのApp Serviceを使用します。 ローカルGitリポジトリでデプロイするため、コードを選択します。 ローカルGitリポジトリを選択します。 資格情報を登録します。 Gitクローン作製時の認証に使用します。 アプリケーション設定 環境変数を定義します。 SLACK_REACTION_KEYはリアクション通知対象のフラグを立てる絵文字名を入れます。 今回はstarを入れてます。 SLACK_BOT_TOKENとSLACK_SIGNING_SECRETはSlackのアプリ画面から取得します。 SLACK_SIGNING_SECRET SLACK_BOT_TOKEN 次にカスタムスタートアップコマンドを設定します。 デフォルトはappというWSGIサーバが指定されています。 Pythonソースコード上はflask_appとしているのでこちらで起動するように設定します。 カスタムスタートアップコマンド gunicorn --bind=0.0.0.0 --timeout 600 app:flask_app App Serviceの設定が完了したらPythonアプリをデプロイします。 MSドキュメントの手順に従ってデプロイ設定をします。 Azure App Service へのローカル Git デプロイ Slackアプリ設定 Event Subscriptions画面でChallengeパラメータを返すURLを入力し、Verifiedになれば成功です。 今回のRequest URLは以下の赤枠 + /slack/eventsです。 続いてリアクションされた時のイベントを連携するための設定をします。 設定追加後は忘れずに保存してください。 Slackのチャンネルにアプリ追加 アプリを利用したいチャンネルの詳細画面を開きアプリを追加します。 これで冒頭のようにアプリ設定したチャンネルにおいてリアクションが通知されるようになります。 今回の例ですと自身の投稿にのリアクションがついているものは他の人が追加でリアクションした場合に通知されます。 投稿のリアクションが即座に確認したい場合に利用してみてください。 参考にさせていただいたサイトや記事 slackapi/bolt-python(サンプルプログラムもあります) Slack Bolt for Python Python で Slack API や Webhook を扱うなら公式 SDK(slack-sdk/slack-bolt)を使おう Slack Python SDK でチャンネルにメッセージを投稿しよう Azure App Service 向けの Linux Python アプリを構成する
- 投稿日:2021-06-15T15:59:22+09:00
OpenCV4(python)の動画取り込みと関数,引数の備忘録
はじめに pythonでopencvを扱う際に動画ファイルの取り扱いと引数の役割を忘れそうになるのでメモ 今回は非常にシンプルな動画出力を記述。 実行環境 OS: Windous10 python 3.7.2 opencv-python 4.2 動画の抽出サンプルコード import cv2 capture = cv2.VideoCapture("動画ファイルパス") while True: retval, image = capture.read() cv2.imshow("表示ウインド名称", image) if waitKey(10) == 27: break cv2.destroyAllWindows() capture.released() retvalは真偽値を返す(imageのデータが存在しなくなったときにFalseを返します) imageは画像データのndarray配列を返します。1ループ目では1フレーム目の配列というようにループ番号に対応したフレームの配列を返します。 各種引数について cv2.VideoCapture() 動画を指定して取り込みます。第1引数はファイルパスで第2引数はビデオ処理メカニズムの指定でデフォルトはcv2.CAP_ANYです。 cv2.VideoCapture.read() 引数なし。動画データの1フレームを読み込みます。戻り値はタプルで画像配列と画像配列の有無を表す真偽値を返します。 cv2.imshow() 与えられた画像配列を指定したウィンドで表示する。第1引数には表示するウィンドの名前,第2引数には表示する画像の配列を指定。 waitKey() ここで特筆する内容ではないが引数で待ち時間を設定できる。(単位は(msec)である。) ==27の記述はASCIIコードで指定されており27はEscキーに当たる。つまりEscキーを押下すると分岐に入りループを抜ける。 cv2.destroyAllWindows() 引数なし。すべてのウィンドを閉じます。 参照 OpenCV公式 https://docs.opencv.org/4.5.2/d8/dfe/classcv_1_1VideoCapture.html
- 投稿日:2021-06-15T15:21:26+09:00
フィボナッチ数列を求めるワンライナー各言語まとめ
お遊び記事です.こちらの記事の姉妹版です.いやその,某ラノベ読んでたらとりあえず21番目までのフィボナッチ数列を求める魔術式ワンライナーが書きたくなりましてね,はい.そっち方面(?)の知り合いのネタ出しのためにそそのかされたともいう. 制約 フィボナッチ数を求める方法はいろいろありすぎるので,次の制約を設けます. 一般項は使わない. ライブラリやモジュールの読み込みは行わない. こうすると,自ずと関数型プログラミングに近くなるかもしれません.ますます魔(略 各言語での記述例 0番目から21番目の値を求める最も短いと思われる記述のみを載せています.より短い表現&他言語版歓迎. CommonLisp/Scheme (do((a 0 b)(b 1(+ a b))(l '()`(,@l ,a)))((=(length l)22)l)) Python2 [x[0]for x in[reduce(lambda x,y:(x[0]+x[1],x[0]),[(0,1)]*(n+1))for n in range(22)]] Python3 [(lambda g:g(g))(lambda g:lambda n,a,b:a if n==0 else g(g)(n-1,b,a+b))(n,0,1)for n in range(22)] Ruby a=[0,1];20.times{a<<a[-2]+a[-1]};a JavaScript [...Array(22).keys()].map(n=>[[0,1]].concat([...Array(n)]).reduce((x,y)=>[x[0]+x[1],x[0]])[0]) J言語 (,+/@(_2&{.))^:20(0 1) Haskell let f@(_:g)=0:1:zipWith(+)f g in take 22 f PostScript 0 == 0 1 20 {0 1 2 index -1 1 {pop exch 1 index add} for == pop pop} for Smalltalk Array streamContents: [ :s | | n1 n2 | s nextPut: (n1 := 0); nextPut: (n2 := 1). 20 timesRepeat: [ s nextPut: (n2 := (n1 + (n1 := n2))) ] ] Perl do{@_=(0,1);$_[$_+2]=$_[$_]+$_[$_+1]for 0..19;@_} Clojure (take 22((fn f[](lazy-cat[0 1](map +(f)(rest(f))))))) 備考 記事に関する補足 reduceつおい.次々と繰り返し構文に置き換えられてしまった…. J言語の例はねっとからのぱくりです.独自には記述できなかった…. Perlの記述例については,デバッガモード起動(perl -de0)のxコマンドで表示することを想定しています. 更新履歴 2021-06-15:Rubyの記述例を変更(Twitter,コメントより) 2021-06-15:SchemeとCommon Lispを統合(Twitterより) 2021-06-15:Haskell,PostScript,Smalltalk,Perl,Clojureの記述例を追加(Twitter,コメントより) 2021-06-15:初版公開(Scheme,Common Lisp,Python2,Python3,Ruby,JavaScript,J言語)
- 投稿日:2021-06-15T14:33:56+09:00
Fargateを安く動かしたい! + 突然終了を検知する方法
はじめに こんにちは,ある企業の長期インターンシップに参加している大学4年生です. 今回インターンにて利用しているAWSのサービスの1つ「Fargate」について,安く運用したいと思ったこととその方法について書きたいと思います. そもそもFargateとは 公式ドキュメント: https://aws.amazon.com/jp/fargate/ コンテナ向けサーバーレスコンピューティングの一種であり,Dockerのイメージファイルを作るだけで簡単にデプロイできる便利なサービスです. 今回の課題 料金 ざっくり料金表(データ転送料+為替レートにより前後します) vcpu メモリ(GB) 料金(円/月) 0.25 0.5 1,236円 0.5 1.0 2,473円 1.0 2.0 4,947円 2.0 4.0 9,894円 (2021年6月12日現在の為替レートで月に732時間動作させた際の表です) 現在のインターン業務ではこのタスクをかなりの数同時稼動等をしているのでまともに動かすと, 100,000円 ~ /月を余裕に超えてしまうためこのコストそのものが課題になっています. 課題解決策 FargateSpot AWS Fargate Spotの発表 – Fargateとスポットインスタンスの統合 AWS Fargate キャパシティープロバイダー 大雑把に説明すると,価格が7割引になるFargateです. 実際に利用してみる まずFargateを利用するクラスターを作り,作成したクラスターを選択します. 右上にクラスターの更新というボタンがあるので押します. クラスターの更新を押すとキャパシティープロパイダー戦略という項目があるので追加し,FARGATE_SPOTを選択します. そして実行したいタスクをクラスター内にて利用することでFargateSpotを利用することが出来ます. 唯一の弱点 FargateSpotは連続稼働を保証しておらず,ある日突然タスクが終了することがあります... (今まで一度も終了したこと見たことないけど) Fargate Spotが空きキャパシティを確保できるかぎり、ユーザーは指定したタスクを起動することができます。 弱点をカバーする方法 タスク終了前にsignalが送られるのでタスク内で再起動処理を行う! スポットの中断により Fargate Spot キャパシティーを使用するタスクが停止すると、 タスクが停止する前に 2 分間の警告が送信されます。 警告は、タスク状態変更イベントとして Amazon EventBridge に送信され、 実行中のタスクに SIGTERM シグナルが送信されます。 実際にタスク終了を検知するソースコードを下記に記載します. ソースコード import signal import sys import time import random import pprint # 何かしらの処理を行い最終的に出力する変数 # 配列・辞書等... response_data = [] def signal_receive_process(sig, frame): """SIGTERMシグナルを受け取った際に行う処理を書く""" print("SIGTERM!!") # 正常終了時の処理を行う task_finish_process() # タスクを終わらせる sys.exit(0) def task_finish_process(): """タスクが終了する際に行う処理を書く""" # 今回は単に表示させるだけ pprint.pprint(response_data[0:20]) def main(): """何かしらの処理(web・machine_running etc...)を書く""" # SIGTERMを検知する signal.signal(signal.SIGTERM, signal_receive_process) for idx in range(100): print(idx) # 何かしらの処理を行う append_data = idx * random.random() # データを格納する response_data.append(append_data) idx += 1 time.sleep(1) # 正常終了時の処理を行う task_finish_process() if __name__ == '__main__': main() 終わりに 今回,Fargateを安く利用する方法について主に書くことができました! 今後も様々なことに書いていきたいと思います!
- 投稿日:2021-06-15T12:48:31+09:00
天文データ解析入門 その14 (astrodendroの使い方: scimes編)
本記事では、以前の記事 (天文データ解析入門 その6 (astrodendroの使い方: 基本編)) に関連して、scimes の使い方について紹介します。 scimes の概要 簡単に言うと、astrodendro の Dendrogram.compute で生成した Dendrogram オブジェクトを入力すると、それに含まれる構造を k-means法 でクラスタリングしてくれるモジュールです。詳細はドキュメント を参照してください。 インストール方法 ドキュメントには pip install scimes とありますが、うまくいかないことが多いです。 githubのページ に行って、「↓Code」から「DOWNLOAD ZIP」を選んでください。解凍した後、 cd SCIMES-master python setup.py install を実行します。 astropy_helpers がないと言われたら、 pip install astropy_helpers を実行してください。 使用方法 必要なものを import し、fitsを読み込みます。fits は、上の SCIMES-master に入っている orion_12CO.fits を用います。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS from astropy import units as u from astrodendro import Dendrogram from astrodendro import ppv_catalog from scimes import SpectralCloudstering hdu = fits.open("SCIMES-master/orion_12CO.fits")[0] # 3D w = WCS(hdu.header) 普通に astrodendrogram を実行します。 dendro_min_value = 0.3 dendro_min_delta = 0.6 dendro_min_npix = 4 d = Dendrogram.compute(hdu.data, min_value=dendro_min_value, min_delta=dendro_min_delta, min_npix=dendro_min_npix, verbose=False) metadata を定義します。そしてカタログを作ります。 metadata = {} metadata['data_unit'] = u.Jy # u.K はなぜか対応していないので、とりあえず Jy にしておく。 cat = ppv_catalog(d, metadata) 以下でクラスタリングを実行します。 dclust = SpectralCloudstering(d, cat, hdu.header) デフォルトでは、"luminosity"(flux) と "volume" が使われるようです。どちらか片方にしたい場合などは criteria で指定します。 dclust = SpectralCloudstering(d, cat, hdu.header, criteria=["luminosity"]) dclust = SpectralCloudstering(d, cat, hdu.header, criteria=["volume"]) また、デフォルトでは独立した leaf (つまり、trunk 兼 leaf) はクラスタとして認識しません。認識させたい場合は、SpectralCloudstering 実行時に save_isol_leaves=True を加えてください。 さらに、k-means のパラメータも調節可能です。例えば user_k で個数を、user_iter でイテレーション回数を指定できます (大抵思うようにいきませんが)。 以下を実行することで結果の樹形図が表示されます。 dclust.showdendro() 同定したクラスタの番号と、ランダムに割り当てた色を定義します。 clusts = dclust.clusters colors = dclust.colors print(clusts) print(colors) #[11, 37, 43, ..., 428, 475, 755] #['#1BA58D', '#5F7FE6', '#BDB864', ..., '#EAED72', '#77E1E4', '#B37499'] aplpy で plot します。 hdu_integ = fits.open("./SCIMES-master/orion_12CO_mom0.fits")[0] f = aplpy.FITSFigure(hdu_integ, figsize=(8, 6), convention='wells', slices=[0]) f.show_colorscale(cmap='gray_r', vmin=1, vmax=80, stretch = 'log') count = 0 for c in clusts: mask = d[c].get_mask() mask_hdu = fits.PrimaryHDU(mask.astype('short'), hdu.header) mask_coll = np.amax(mask_hdu.data, axis = 0) mask_coll_hdu = fits.PrimaryHDU(mask_coll.astype('short'), hdu.header) f.show_contour(mask_coll_hdu, colors=colors[count], linewidths=2, convention='wells', levels = [0], slices=[0]) count += 1 f.add_colorbar() clusts_structure = [d[c] for c in clusts] と定義してしまえば、天文データ解析入門 その6 (astrodendroの使い方: 基本編) や 天文データ解析入門 その13 (astrodendroの使い方: 中級編) の方法で色々解析ができます。 以上です。 リンク 目次
- 投稿日:2021-06-15T12:18:50+09:00
M1 MacのPythonでgrpcioを入れる方法
M1 Macにgrpcioが入らなかった問題。 解決方法 Macに最初から入っているPythonを使用しつつ、環境変数を設定すると解決しました。 最初、pyenvのpython-buildを使って入れたPythonでgrpcioをインストールしようとしていたのですが、clangのエラーが発生してしまいインストールできませんでした。 /usr/bin/python3にシステムで最初から入っているPython 3.8.2が入っているので、こちらを使うとうまく行きました。 また、GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1とGRPC_PYTHON_BUILD_SYSTEM_ZLIB=1の環境変数を設定しました。 GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=1 GRPC_PYTHON_BUILD_SYSTEM_ZLIB=1 /usr/bin/python3 -m pip install grpcio Homebrewでopensslを入れているため、以下の環境変数を設定しています export LDFLAGS="-L/opt/homebrew/opt/openssl/lib" export CPPFLAGS="-I/opt/homebrew/opt/openssl/include"
- 投稿日:2021-06-15T10:02:38+09:00
天文データ解析入門 その13 (astrodendroの使い方: 中級編)
本記事では、以前の記事 (天文データ解析入門 その6 (astrodendroの使い方: 基本編)) よりも一歩進んだ使い方について記述します。 今回も、例として国立天文台の FUGIN プロジェクトで得られた野辺山45m電波望遠鏡の CO 輝線のアーカイブデータを用います。データは http://jvo.nao.ac.jp/portal/nobeyama/ から FGN_03100+0000_2x2_12CO_v1.00_cube.fits FGN_03100+0000_2x2_13CO_v1.00_cube.fits の2つをダウンロードします (重いです)。 12CO と 13CO のデータの grid はすでに揃っています。揃っていないデータで輝線強度比等を扱う場合は、あらかじめ天文データ解析入門 その7 (fitsのregrid) を参照して grid を揃えましょう。 まずは例によって必要なものを import し、fitsを読み込みます。 from astropy.io import fits import numpy as np from matplotlib import pyplot as plt import aplpy from astropy.wcs import WCS import os import shutil import copy hdu_12CO = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_12CO_v1.00_cube.fits")[0] # 3D hdu_13CO = fits.open("~/your/fits/dir/FGN_03100+0000_2x2_13CO_v1.00_cube.fits")[0] # 3D w = WCS(hdu_12CO.header) 今回、簡単のため 2D fits で行います。 以下のような関数を定義します。 def v2ch(v, w): # v(km/s)をchに変える x_tempo, y_tempo, v_tempo = w.wcs_pix2world(0, 0, 0, 0) x_ch, y_ch, v_ch = w.wcs_world2pix(x_tempo, y_tempo, v*1000.0, 0) v_ch = int(round(float(v_ch), 0)) return v_ch def del_header_key(header, keys): # headerのkeyを消す import copy h = copy.deepcopy(header) for k in keys: try: del h[k] except: pass return h def make_new_hdu_integ(hdu, v_start_wcs, v_end_wcs, w): # 積分強度のhduを作る data = hdu.data header = hdu.header start_ch, end_ch = v2ch(v_start_wcs, w), v2ch(v_end_wcs, w) new_data = np.nansum(data[start_ch:end_ch+1], axis=0)*header["CDELT3"]/1000.0 header = del_header_key(header, ["CRVAL3", "CRPIX3", "CRVAL3", "CDELT3", "CUNIT3", "CTYPE3", "CROTA3", "NAXIS3"]) header["NAXIS"] = 2 new_hdu = fits.PrimaryHDU(new_data, header) return new_hdu 以下のように 2D fits を作成します。 integ_hdu_12CO = make_new_hdu_integ(hdu_12CO, 75.0, 125.0, w) integ_hdu_13CO = make_new_hdu_integ(hdu_13CO, 75.0, 125.0, w) 都合上、edge の部分を NaN にします。 integ_hdu_12CO.data[0, :] = np.nan integ_hdu_12CO.data[-1, :] = np.nan integ_hdu_12CO.data[:, 0] = np.nan integ_hdu_12CO.data[:, -1] = np.nan 前回と同様、以下を実行します。 Dendrogram と、統計量を出すためのものなどを import します。 from astrodendro import Dendrogram from astrodendro.analysis import PPStatistic import datetime 以下のように、3つのパラメータを設定します。詳しくはドキュメントを参照してください。 dendro_min_value = 5 dendro_min_delta = 25 dendro_min_npix = 100 ここまでは前回と同じです。 leaf 分岐時に任意の条件を加える (is_independent) Dendrogram.compute の is_independent に任意の関数を渡すことによって、leaf 分岐時 (分割時) に新たな条件を課すことができます。 例として、新しい構造の peak 値がノイズレベルの 5 倍以下の場合 leaf として分岐させたくない場合を考えます。 (データのノイズレベルが均等ではない時などに便利です。) まずはノイズレベルの map を作ります。 # rms の fits がある場合 (後ろに今回使用した ch 数と ch 幅をかけている) rms_map = fits.getdata("~/your/fits/dir/rms.fits")*np.sqrt(77)*hdu_12CO.header["CDELT3"]/1000.0 # 数字として既知の場合 (3.0 K km/s など) rms_map = np.ones_like(integ_hdu_12CO.data)*3.0 # cube から自分で作る場合 rms_map = np.nanstd(hdu_12CO.data[0:20], axis=0)*np.sqrt(77)*hdu_12CO.header["CDELT3"]/1000.0 # ↑ 20 ch目まで emission free かつ baseline が引けている場合 # 77 は今回積分した時の使用した ch 数 def custom_independent(structure, index=None, value=None): # peak が 5 rms 以上だと True peak_index, peak_value = structure.get_peak() rms = rms_map[peak_index[0], peak_index[1]] return peak_value > rms*5.0 以下でDendrogramを実行します。結果は全て d_12CO に格納されます。 (一応時間を計測しています。) t1 = datetime.datetime.now() print("[dendro] started......", t1) d_12CO = Dendrogram.compute(integ_hdu_12CO.data, min_value=dendro_min_value, min_delta=dendro_min_delta, min_npix=dendro_min_npix, verbose=False, is_independent=custom_independent) t2 = datetime.datetime.now() print("[dendro] finished.....", t2) print("[dendro] total time... ", (t2 - t1).total_seconds(),"sec") 前回同様、pandas のDataFrame をゴリ押しで作ります。 w_integ = WCS(integ_hdu_12CO.header) x_peak, y_peak, x_cen, y_cen, val_min, val_max = [], [], [], [], [], [] index_list, ancestor = [], [] n_vox, area_ellipse, area_exact, minor_sigma, major_sigma, position_angle = [], [], [], [], [], [] radius, total_flux = [], [] for s in d_12CO.leaves: stat = PPStatistic(s) index_list.append(s.idx) ancestor.append(str(s.ancestor.idx)) y_ch, x_ch = s.get_peak()[0] x_peak_, y_peak_ = w_integ.wcs_pix2world(x_ch, y_ch, 0) x_peak.append(round(float(x_peak_), 6)) y_peak.append(round(float(y_peak_), 6)) x_cen_ch, y_cen_ch = stat.x_cen.value, stat.y_cen.value x_cen_, y_cen_ = w_integ.wcs_pix2world(x_cen_ch, y_cen_ch, 0) x_cen.append(round(float(x_cen_), 6)) y_cen.append(round(float(y_cen_), 6)) val_min.append(round(s.vmin, 6)) val_max.append(round(s.vmax, 6)) ind = s.indices() n_vox.append(len(ind[0])) area_ellipse.append(round(stat.area_ellipse.value, 6)) area_exact.append(round(stat.area_exact.value, 0)) minor_sigma.append(round(stat.minor_sigma.value, 6)) major_sigma.append(round(stat.major_sigma.value, 6)) position_angle.append(round(stat.position_angle.value, 6)) radius.append(round(stat.radius.value, 6)) total_flux.append(round(stat.stat.mom0(), 6)) results_leaves_12CO = pd.DataFrame({ 'id':index_list, # ID 'ancestor':ancestor, # (最も)先祖のID 'x_peak':x_peak, # peak の位置のx座標 'y_peak':y_peak, # peak の位置のy座標 'x_cen':x_cen, # 重心位置のx座標 'y_cen':y_cen, # 重心位置のy座標 'val_min':val_min, # 構造の持つ最小値 'val_max':val_max, # 構造の持つ最大値 'n_vox':n_vox, # 構造が使っている pixel 数 'area_ellipse':area_ellipse, # 構造の重みつき 1 sigma の楕円面積 (pixel^2) 'area_exact':area_exact, # 構造が実際に使っている面積 (pixel^2) 'minor_sigma':minor_sigma, # 楕円の短軸 (pixel) 'major_sigma':major_sigma, # 楕円の長軸 (pixel) 'position_angle':position_angle, # 楕円の position angle (degree) 'radius':radius, # 楕円の長軸短軸の相乗平均 (pixel) 'total_flux':total_flux # 構造の持つ値の総和 }) leaf と trunk のコントアを描く 構造の輪郭のコントアを引きたい場合は、 mask = np.zeros(integ_hdu_12CO.data.shape, dtype=bool) for s in d_12CO.leaves: mask = mask | s.get_mask() mask_hdu_12CO_leaves = fits.PrimaryHDU(mask.astype('short'), integ_hdu_12CO.header) for s in d_12CO.trunk: mask = mask | s.get_mask() mask_hdu_12CO_trunk = fits.PrimaryHDU(mask.astype('short'), integ_hdu_12CO.header) fig = plt.figure(figsize=(8, 8)) f = aplpy.FITSFigure(integ_hdu_12CO, slices=[0], convention='wells', figure=fig) f.show_colorscale(vmin=1, vmax=800, stretch='log', cmap="Greys", aspect="equal") f.show_contour(mask_hdu_12CO_leaves, colors='b', linewidths=0.5) f.show_contour(mask_hdu_12CO_trunk, colors='r', linewidths=0.5) plt.show() 構造ごとの輝線強度比を計算する def calc_total_flux_2D(s, data): ind = s.indices() data_new = np.zeros_like(data) for i in range(len(ind[0])): data_new[ind[0][i]][ind[1][i]] = data[ind[0][i]][ind[1][i]] return np.nansum(data_new) 以下を実行します。 leaf_flux_13CO_list = [calc_total_flux_2D(s, integ_hdu_13CO.data) for s in d_12CO.leaves] # leaf ごとに同じ場所の 13CO の強度を取得して積算 results_leaves_12CO["total_flux_13CO"] = leaf_flux_13CO_list # df に追加 試しに 12CO の flux と 13CO の flux の相関を見てみます。 plt.scatter(results_leaves_12CO["total_flux"], results_leaves_12CO["total_flux_13CO"]) plt.xscale("log") plt.yscale("log") plt.show() 横軸と縦軸は単に pixel の合計値なので、柱密度に変換する場合などは pixel 幅などをかけてやる必要があります。 構造の fits を保存する 構造をそれぞれ fits にして保存したい時があるかと思います。 以下のような、構造の周り 1 pixel までの範囲の fits を作る関数を用意します。 def make_best_hdu_2D(structure, data, header): header_new = copy.deepcopy(header) ind = structure.indices() data_new = np.zeros_like(data) for i in range(len(ind[0])): data_new[ind[0][i]][ind[1][i]] = data[ind[0][i]][ind[1][i]] max_axis1, max_axis2 = np.max(ind[1]), np.max(ind[0]) min_axis1, min_axis2 = np.min(ind[1]), np.min(ind[0]) data_new = data_new[min_axis2-1:max_axis2+2, min_axis1-1:max_axis1+2] header_new['CRPIX1'] = header_new['CRPIX1'] - (min_axis1 - 1) header_new['CRPIX2'] = header_new['CRPIX2'] - (min_axis2 - 1) header_new['NAXIS1'] = max_axis1 - min_axis1 + 3 header_new['NAXIS2'] = max_axis2 - min_axis2 + 3 hdu_new = fits.PrimaryHDU(data_new, header_new) return hdu_new まず保存先を作ります。 dir_name = "12CO_leaves" if os.path.exists(dir_name): shutil.rmtree(dir_name) # すでにあったら削除 os.mkdir(dir_name) else: os.mkdir(dir_name) for s in d_12CO.leaves: i = s.idx # 構造の index を取得 make_best_hdu_2D(s, integ_hdu_12CO.data, integ_hdu_12CO.header).writeto(os.path.join(dir_name, "d%s.fits"%str(i).zfill(8))) もちろん、integ_hdu_12CO.data を integ_hdu_13CO.data にすれば 13CO の fits が作れます。 fits ができればあとは何でも好きなことができます。 今回は例として 2D で行いましたが、何箇所か書き換えるだけで 3D (cube) にも適用できます。 以上です。 リンク 目次