- Python 3.11.4 / Django 4.2.4
変数や関数などpythonの場合型を明記しなくてもいいですが、あえて型を明記して書くようにしてみてください。どんな型でもいいようにAnyで対応すると予期せぬ動作を事前に気づくことができずバグが発生する可能性があるので基本的には型を明示することをお勧めします。
# 基本的な型ヒント
def greet(name: str) -> str:
return "Hello, " + name
# リストの型ヒント
from typing import List
def total(numbers: List[int]) -> int:
return sum(numbers)
# ディクショナリの型ヒント
from typing import Tuple
def dimensions(rectangle: Tuple[int, int]) -> str:
return f"Width: {rectangle[0]}, Height: {rectangle[1]}"
# オプションの型ヒント
from typing import Optional
def find_person(people: List[str], name: str) -> Optional[str]:
return name if name in people else None
# ↓のように表記できる!
# str | Noneは、変数がstrまたはNoneのいずれかであることを示す
from typing import List
def find_person(people: List[str], name: str) -> str | None:
return name if name in people else None
# Anyの型ヒント
from typing import Any
def print_data(data: Any) -> None:
print(data)
目次
チュートリアル③
- オーバービュー ビューとは、Djangoのアプリケーションにおいて特定の機能を提供するウェブページの「型 (type)」であり、各々のテンプレートを持っている。例えばブログアプリケーションなら、以下のようなビューがある。
- Blog ホームページ – 最新エントリーをいくつか表示
- エントリー詳細ページ – 1エントリーへのパーマリンク (permalink) ページ
- 年ごとのアーカイブページ – 指定された年のエントリーの月を全て表示
- 月ごとのアーカイブページ – 指定された月のエントリーの日を全て表示
- 日ごとのアーカイブページ – 指定された日の全てのエントリーを表示
- コメント投稿 – エントリーに対するコメントの投稿を受付
- 質問 “インデックス” ページ — 最新の質問をいくつか表示
- 質問 “詳細” ページ — 結果を表示せず、質問テキストと投票フォームを表示
- 質問 “結果” ページ — 特定の質問の結果を表示
- 投票ページ — 特定の質問の選択を投票として受付
Djangoでは、ウェブページとコンテンツはビューによって提供される。各ビューは単純に Python 関数 (クラスベースビューの場合はメソッド) として実装されている。Django はビューを、リクエストされたURL (正確には、URLのドメイン以降の部分) から決定する。
URLからビューを得るために、Djangoは「URLconf」と呼ばれているものを使う。URLconf はURLパターンをビューにマッピングする。
- もっとビューを書いてみる polls/viers.pyにビューを追加する。これから追加するビューでは引数をとる。
from django.http import HttpRequest, HttpResponse def detail(request: HttpRequest, question_id: str) -> HttpResponse: return HttpResponse("You're looking at question %s." % question_id) def results(request: HttpRequest, question_id: str) -> HttpResponse: response = "You're looking at the results of question %s." return HttpResponse(response % question_id) def vote(request: HttpRequest, question_id: str) -> HttpResponse: return HttpResponse("You're voting on question %s." % question_id)
- f文字列 文字列メソッドformat()では、順番に引数を指定したり名前を決めてキーワード引数で指定したりして置換フィールド{}に値を挿入できる。
a **=** 123 b = 'abc' print('{} and {}'.format(a, b)) # 123 and abc print('{first} and {second}'.format(first=a, second=b)) # 123 and abc
f文字列(f-strings)は文字列リテラルの前にfまたはFを置く(f’…’, F’…’)。文字列中の置換フィールドに変数をそのまま指定できる。print(f'{a} and {b}') # 123 and abc print(F'{a} and {b}') # 123 and abc
from django.urls import path from . import views urlpatterns = [ # ex: /polls/ path("", views.index, name="index"), # ex: /polls/5/ path("<int:question_id>/", views.detail, name="detail"), # ex: /polls/5/results/ path("<int:question_id>/results/", views.results, name="results"), # ex: /polls/5/vote/ path("<int:question_id>/vote/", views.vote, name="vote"), ]
ブラウザで http://127.0.0.1:8000/polls/34/ を見ると、detail()メソッドが実行され、URLで提供したIDが表示される。”/polls/34/results/” と “/polls/34/vote/” も試してみると、結果と投票ページのプレースホルダーがそれぞれ表示される。 - f文字列 文字列メソッドformat()では、順番に引数を指定したり名前を決めてキーワード引数で指定したりして置換フィールド{}に値を挿入できる。
- 誰かがwebサイトの「/polls/34」をリクエストする。
- DjangoはROOT_URLCONFに指定されているPythonモジュールmysite.urlsをロードする。
- そのモジュール内のurlpatternsという変数を探し順番にパターンを検査する。
- polls/にマッチして箇所を見つけた後、一致した文字列(polls/)を取り除き、残りの文字列(34/)を「polls.urls」のURLconfに渡す。
- これが <int:question_id>/ に一致する。
- 次のようなdetailが呼び出される。
detail(request=<HttpRequest object>, question_id=34)
- 実際に動作するビューを書く 各ビューには2つの役割がある。それ以外の処理はユーザー次第。
- リクエストされたページのコンテンツを含むHttpResponseオブジェクトを返すこと
- Http404のような例外の送出
# polls/views.py from django.http import HttpResponse, HttpRequest from typing import List from .models import Question def index(request: HttpRequest) -> HttpResponse: latest_question_list: List[Question] = Question.objects.order_by("-pub_date")[:5] output: str = ", ".join([q.question_text for q in latest_question_list]) return HttpResponse(output) # Leave the rest of the views (detail, results, vote) unchanged
このコードでは、ビューの中でページのデザインがハードコードされている。ページの見栄えをを変更するたびにPythonコードを編集する必要がある。Djangoのテンプレートシステムを使って、ビューから使用できるテンプレートを作成し、Pythonからデザインを分離する。 まず、pollsディレクトリの中にtemplatesディレクトリを作成する。Djangoはそこからテンプレートを探す。 今作成したtemplatesディレクトリ内でpollsというディレクトリを作成し、さらにその中にindex.htmlというファイルを作成し、テンプレートはその中に書く。{% if latest_question_list %} <ul> {% for question in latest_question_list %} <li><a href="/polls/{{ [question.id](<http://question.id/>) }}/">{{ question.question_text }}</a> </li> {% endfor %} </ul> {% else %} <p>No polls are available.</p> {% endif %}
このテンプレートを使用するためにpolls/views.pyのindexビューを更新する。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") context = { "latest_question_list": latest_question_list, } return HttpResponse(template.render(context, request))
ブラウザでhttp://127.0.0.1:8000/polls/を開くと、箇条書きのリストが表示される。 - ショートカット:render() テンプレートをロードしてコンテキストに値を入れ、テンプレートをレンダリングした結果をHttpResponseオブジェクトで返す、というイディオムは非常によく使われる。Djangoはこのためのショートカットを提供している。これを使ってindex()ビューを書き換えてみる。
# polls/views.py 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)
全部のビューをこのように書き換えてしまえば、loaderやHttpResponseをimportする必要はなくなる。(detail, results, voteを引き続きスタブメソッドにするなら、HttpResponseはそのままにしておいたほうがいい) render()関数は第一引数としてrequestオブジェクトを、第二引数としてテンプレート名を、第三引数(任意)として辞書を受け取る。 - 404エラーの送出 質問詳細ビューに取り組む!このページは指定された投票の質問文を表示する。polls/views.pyに以下のコードを書く。
from django.http import Http404 from django.shortcuts import render from .models import Question # ... def detail(request, question_id): try: question = Question.objects.get(pk=question_id) except Question.DoesNotExist: raise Http404("Question does not exist") return render(request, "polls/detail.html", {"question": question})
このビューは、リクエストされたIDを持つ質問が存在しない時にHttp404を送出する。 polls/detail.htmlテンプレートにはいったん下のを書いておく。{{ question }}
- ショートカット:get_object_or_404( get()を実行し、オブジェクトが存在しない場合にはHttp404を送出するのはよく使われるイディオムで、Djangoはこのためのショートカットを提供している。ショートカットを使ってdetail()ビューを書き換えてみる。
# polls/views.py from django.shortcuts import get_object_or_404, render from .models import Question # ... def detail(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, "polls/detail.html", {"question": question})
get_object_or_404()関数は、Djangoモデルを第一引数に、任意の数のキーワード引数を取り、モデルのマネージャのget()関数に渡す。オブジェクトが存在しない場合はHttp404を発生させる。- get_list_or_404()という関数もあり、リストがからの場合はHttp404を送出する。この関数はget_object_or_404()と同じように動くが、get()ではなく、filter()を使う。
- テンプレートシステムを使う 投票アプリのdetail()ビューに戻る! コンテキスト変数をquestionとすると、polls/detail.htmlテンプレートは次のようになる。
<!--polls/templates/polls/detail.html--> <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <!--関連するchoiceに対してループを開始--> <li>{{ choice.choice_text }}</li> {% endfor %} <!--ループ終了--> </ul>
テンプレートシステムは、変数の属性にアクセスするためにドット検索の構文を使用する。{{ question.question_text }}を例にする↓- questionオブジェクトに辞書検索を行う。 questionが辞書で、question_textという名前のキーが存在すれば、そのキーに対応する値が取り出される。例:
context = { 'question': {'question_text': 'What is your name?'} }
この場合、テンプレートの{{ question.question_text }}は、What is your name?を表示する。 - それに失敗したら今度は属性として検索を行う。 questionがquestion_textという名前の属性を持っているかどうかを確認する。
class Question: def __init__(self): self.question_text = 'What is your favorite color?' context = { 'question': Question() }
この場合、テンプレートの{{ question.question_text }}は、What is your favorite color?を表示する。 - このケースの場合成功するが、失敗したらリストインデックスでの検索を行う。 questionがリストのようなオブジェクトであるかを確認し、question_textが整数値である場合、それをインデックスとして使ってアクセスを試みる。
context = { 'question': ['What is your name?', 'How old are you?'] }
この場合、テンプレートの{{ question.0 }}はWhat is your name?を表示するが、question_textは整数ではないため、この{{ question.question_text }}の形式ではリストインデックス検索は適用されない。この検索方法は{{ question.1 }}のような形式で使用される。(質問の答えが整数だから?)
- questionオブジェクトに辞書検索を行う。 questionが辞書で、question_textという名前のキーが存在すれば、そのキーに対応する値が取り出される。例:
- テンプレート内のハードコードされたURLを削除 polls/index.htmlテンプレートで質問へのリンクを書いたとき、リンクの一部が次のようにハードコードされていた。
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
このままでは、プロジェクトにテンプレートが多数ある場合、URLの変更が困難になってしまう。 polls.urlsモジュールのpath()関数でnama引数を定義したので、テンプレートタグの{%url%}を使用して、URL 設定で定義されている特定のURLパスへの依存をなくすことができる。<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
これが機能するのは、polls.ulrsモジュールに指定されたURLの定義を検索するから。detailのURL名は以下の箇所で定義されている。... # the 'name' value as called by the {% url %} template tagpath("<int:question_id>/", views.detail, name="detail"), ...
投票の詳細ビューのURLを何か他のものに変更したい場合、たとえばpolls/specifics/12/のように変更したいとき、対象となるテンプレートを変更する代わりに、polls/urls.pyを変更する。... # added the word 'specifics'path("specifics/<int:question_id>/", views.detail, name="detail"), ...
- URL名の名前空間 このチュートリアルプロジェクトが持つアプリはpollsアプリ1つだけだが、実際のDjangoプロジェクトでは、5個、10個、あるいはそれ以上のアプリがあるかもしれない。Djangoはどうやってこれらの間の URL 名を区別する? 例えば、pollsアプリはdetailビューを含むが、同じプロジェクトにブログのためのアプリがあり、そのアプリも同名のビューを含むかもしれない。{% url %}テンプレートタグを使ったとき、 Django はどのアプリのビューに対してurlを作成すればいいのか?これをDjangoにどう知らせればいいのか? 答えは、 URLconf に名前空間を追加する! polls/urls.pyファイル内でapp_nameを追加し、アプリケーションの名前空間を設定する。
# polls/urls.py 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"), ]
そして、polls/index.htmlテンプレートをの次の部分を<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
下のように変更し、名前空間つきの詳細ビューを指すようにする。<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
チュートリアル④
- 簡単なフォームを書く チュートリアル③で作成した投票詳細テンプレート(polls/detail.html)を更新して、htmlの<form>要素を入れる。
<!--polls/templates/polls/detail.html--> <form action="{%url 'polls:vote' question.id %}" method="post"> {% csrf_token %} <fieldset> <legend><h1>{{ question.question_text }}</h1></legend> {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} {% for choice in question.choice_set.all %} <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}"> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br> {% endfor %} </fieldset> <input type="submit" value="Vote"> </form>
- このテンプレートは、各質問の選択肢のラジオボタンを表示するもの。各ラジオボタンのvalueは、関連する質問の選択肢のIDで、各ラジオボタンのnameはchoice。つまり、投票者がラジオボタンの1つを選択しフォームを送信すると、POSTデータchoice=#(#は選んだ選択肢のID)が送信される。
- フォームのactionを{% url ‘polls:vote’ question.id %}に設定し、さらにmethond=”post”を設定する。(method=getではなく!)
- forloop.counterは、forタグのループが何度実行されたかを表す値。
# polls/urls.py path("<int:question_id>/vote/", views.vote, name="vote"),
この時、vote()関数のダミー実装も作成したので、今度は本物を実装する。以下をpolls/views.pyに追加する。from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render from django.urls import reverse from .models import Choice, Question # ... def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) try: 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: 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. return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))
- request.POSTは辞書のようなオブジェクトで、キーを指定すると送信したデータにアクセスできる。この場合、request.POST[’choice’]は、指定された選択肢のIDを文字列として返す。request.POSTの値は常に文字列。
- POSTデータにchoiceがなければ、request.POST[’choice’]はKeyErrorを送出する。上のコードでは、KeyErrorをチェックし、choiceがない場合にはエラーメッセージ付きの質問フォームを再表示する。
- choiceのカウントをインクリメント(変数の値を1増やす)した後、このコードは通常のHttpResponseではなくHttpResponseRedirectを返す。HttpResponseRedirectはひとつの引数(リダイレクト先のURL)をとる。
- この例では、HttpResponseRedirectコンストラクタの中でreverse()関数を使用している。この関数を使うと、ビュー関数中でのURLのハードコードを防げる。関数には制御を渡したいビューの名前と、そのビューに与えるURLパターンの位置引数を与える。 この例では、チュートリアル③で設定したURLconfを使っているので、reverse()を呼ぶと次のような文字列が返ってくる。
"/polls/3/results/"
リダイレクト先のURLは’result’ビューを呼び出す。
# polls/views.py from django.shortcuts import get_object_or_404, render def results(request, question_id): question = get_object_or_404(Question, pk=question_id) return render(request, "polls/results.html", {"question": question})
チュートリアル③のdetail()とほぼ同じ。 polls/results.htmlテンプレートを作成する。<!--polls/template/polls/results.html--> <h1>{{ question.question_text }}</h1> <ul> {% for choice in question.choice_set.all %} <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li> {% endfor %} </ul> <a href="{% url 'polls:detail' question.id %}">Vote again?</a>
ブラウザで/polls/1を表示して投票してみる!票を入れるたびに結果のページが更新されていることがわかるはず。選択肢を選ばずにフォームを送信すると、エラーメッセージが表示されるはず。 - 汎用ビューを使う detail()ビューとresukt()ビューは簡単で冗長。投票の一覧を表示するindex()ビューも同様。 これらのビューはURLを介して渡されたパラメータに従ってデータベースからデータを取り出し、テンプレートをロードして、連たリングしたテンプレートを返す。これは極めてよくあることなので、Djangoでは汎用ビュー(generic view / よくあるパターンを抽象化して、 Python コードすら書かずにアプリケーションを書き上げられる状態にしたもの)というショートカットを提供している。 これまで作成してきたpollアプリを汎用ビューシステムに変換して、コードをばっさり捨てられるようにする! 変換のステップ
- URLconfを変換する。
- 古い不要なビューを削除する。
- 新しいビューにDjangoの汎用ビューを設定する。
- URLconfの修正 まず、URLconfのpolls/urls.pyを開き次のように変更する。
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"), ]
2つ目と3つ目のパス文字列に一致するパターンの名前が<question_id>から<pk>に変更されたことに注意。 - viewsの修正 次に、古いindex, detail, resultsのビューを削除し、代わりにDjangoの汎用ビューを使用する。これを行うには、polls/views.pyファイルを開き、次のように変更する。
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" context_object_name = "latest_question_list" def get_queryset(self): """Return the last five published questions.""" return Question.objects.order_by("-pub_date")[:5] class DetailView(generic.DetailView): model = Question template_name = "polls/detail.html" class ResultsView(generic.DetailView): model = Question template_name = "polls/results.html" def vote(request, question_id): ... # same as above, no changes needed.
ここでは、ListViewとDetailViewを使用している。これらのビューはそれぞれ、「オブジェクトのリストを表示する」「あるタイプのオブジェクトの詳細ページを表示する」という二つの概念を抽象化している。 DetailView汎用ビューには、pkという名前でURLからプライマリーキーをキャプチャして渡すことになっているので、汎用ビュー向けにquestion_idをpkに変更している。 デフォルトでは、DetailView汎用ビューには<app name>/<model name>_detail.html という名前のテンプレートを使う。この場合、テンプレートの名前は polls/question_detail.html 。 同様に、ListView汎用ビューは<app name>/<model>_list.htmlというデフォルトのテンプレートを使うので、template_nameを使ってListViewに既存のpolls/index.htmlテンプレートを使用するように伝える。 Djangoのチュートリアルで、特定の情報をテンプレートに渡す時に、変数名として question や latest_question_list が使われていた。例えば、DetailViewを使うと、自動的に question という名前の変数がテンプレートに渡される。これは、背後で動いているDjangoモデルが「Question」なので、その名前を基に変数名が決められるから。 一方、ListViewを使う場合、自動で渡される変数名は question_list になる。しかし、これを「latest_question_list」という名前に変えたいなら、context_object_name 属性を設定すれば良い。 別の方法として、テンプレート自体を編集して変数名を変更することも可能だが、単にDjangoに所望の変数名を指定するほうが手っ取り早い。 サーバーを実行して新しく汎用ビューベースにした投票アプリケーションを使ってみる!
コメント