python

【Hello World】Django、Flask、FastAPIで実装して比較してみた。

「Hello World」

ある言語、あるフレームワークで「Hello World」をを出力するためには、どのようなコーディングをすれば良いのか。

公式ドキュメント、もしくはネットにはそのような情報が散見されます。

しかし、実際に「Hello World」をやることに本当に意味があるのか?

コマンドラインで、標準出力で「Hello World」を出力することには、そんなに意味はないです。

しかし、Webフレームワークでサーバーサイドからクライアントサイドに「Hello World」を返すことは、フレームワークのコーディングルール、設計思想、モジュール構成を理解する糸口になります。

本記事では、Pythonの3つのWebフレームワークである、Django、Flask、FastAPIを用いて「Hello World」を画面(ブラウザー)に表示し、それぞれの特性を比較していきます。

3つのフレームワーク

Django、Flask、FastAPIの3つのフレームワークの特徴は以下のとおりです。

  1. Django
    Pythonの定番Webフレームワークで、大規模のWebアプリケーションの開発に向いている。
  2. Flask
    Djangoと比較すると軽量なWebフレームワーク。小規模なWebアプリケーションの開発に向いている。
  3. FastAPI
    Flaskと同様、軽量なWebフレームワークで比較的新しい。ドキュメント自動生成機能があるのが特徴的。

 

私自身、過去に使用したことがあるフレームワークはFlaskのみです。

以下のアプリはFlaskで動作しています。(現在、メンテナンスしていません・・)

DjangoとFastAPIは、今回、はじめて触りました。(後述の「雑感」にはそのような「かたより」があるので、それを踏まえて読んでください。)

開発環境について

開発環境として、mac+Visual Studio Code+Dockerを使用しました。

使用したPythonのバージョンは3.10です。

開発環境の構築については以下を参考にしました。

これまで一つのマシンでvenvなどで切り替えて環境を作ってきましたが、それでもゴチャゴチャしてわかりにくくなってきたので、今回からDockerを使用しています。

なお、環境構築の詳細については、省略させていただきます。(上記リンクがわかりやすいです。)

今回は、お試しアプリなので、gunicorn、Nginxは使用していません。(FastAPIはuvicornを使用しています。)

画面仕様

Webサーバーにアクセスして単純に「Hello World」を返すだけだと面白くないので、画面仕様は以下のようにしました。

1.初期表示

初期表示画面は以下のようになります。(サーバーからHTMLを返しています。)

初期画面初期画面

 

2.お名前入力

「お名前」を入力してフォーカースアウトすると、あいさつの表示が変わります。(入力したテキストをサーバーに送信して、「こんにちは、」と「さん」を付けてクライアントに返しています。)

テキスト入力後テキスト入力後

 

当たり前ですが、こんなこと(入力した文字を加工して表示)はクライアントサイドだけで可能です。今回はWebフレームワークの動作を見るために、あえてサーバーと送受信しています。

「Hello World」を作ってみよう。

それでは、3つのフレームワークで画面を作成してみましょう。

Django

事前準備

Django(使用バージョンは4.1)をインストールします。(requirements.txtにdjangoを記載してDockerビルド時にインストールしています。)

プロジェクトフォルダー作成

プロジェクトのディレクトリ直下で以下を実行します。

$ django-admin startproject helloworldproject .

 

以下のようなツリー構造でファイルが作成されます。

├── helloworldproject
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py

 

アプリケーションフォルダー作成

プロジェクトのディレクトリ直下で以下を実行します。

$ python manage.py startapp helloworldapp

 

以下のようなツリー構造でファイルが作成されます。

├── helloworldapp
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py

 

プロジェクトフォルダー内のファイル編集

helloworldproject/settings.pyのINSTALLED_APPSにアプリケーション(hellowroldapp)を追加します。

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    'helloworldapp',  #追加
]

 

helloworldproject/settings.pyのTEMPLATESに”DIRS”: []を修正してテンプレートファイルの場所(templates)を指定します。

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",
            ],
        },
    },
]

 

helloworldproject/urls.pyのurlpatternsにアプリケーションのパス(helloworldapp/urls.py)を追加します。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('helloworldapp.urls')) #追加
]

 

アプリケーションフォルダー内のファイル編集

helloworldapp/urls.pyを作成します。内容は以下のとおりです。(views.pyの関数とurlパターンの対応を定義しています。)

"""urls"""
from django.urls import path
from . import views

urlpatterns = [
    path('', views.init),
    path('hello', views.helloworld),
]

 

helloworldapp/views.pyを以下の内容に置き換えます。

"""Hello World"""
from django.shortcuts import render
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt


# Create your views here.
def init(request):
    """初期表示

    Args:
        request (HttpRequest): request

    Returns:
        render: index.html
    """
    return render(request, 'index.html')


@csrf_exempt
def helloworld(request):
    """名前表示

    Args:
        request (HttpRequest): request

    Returns:
        HttpResponse: 出力文字列
    """
    name = request.POST.get('name', None)
    return HttpResponse('こんにちは、'+name+'さん')

 

テンプレートファイルの作成

プロジェクトのディレクトリ直下にtemplatesフォルダーを作成します。

初期表示するindex.htmlを作成して、templatesフォルダーに置きます。index.htmlの内容は以下のとおりです。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Hello World</title>
</head>
<body>
お名前:<input type="text" id="name">
<script>
    $('#name').change(function() {
        $.post( '/hello', encodeURI('name='+$(this).val()) )    
        .done(function( data ) {         
            $("#hello").text(data);        
        })        
    });
</script>
<h1 id ="hello">こんにちは、名無ししさん。</h1>
</body>
</html>

 

エラーハンドリングは省略しています。(「今さらjqueryかよ」という突っ込みもなしです。)

起動

これで準備は整いました。

(本来はmigrationという作業が必要ですが、今回はmodelを作成していないので省略します。)

起動するためには、以下を実行します。

$ python manage.py runserver

 

http://127.0.0.1:8000/にアクセスすると画面が表示され、「お名前」を入力するとあいさつ文が変わります。(画面仕様参照)

雑感

Flaskに慣れてしまっているので、正直な感想は、「たかだか文字列を返すだけなのに、やる事が多くて面倒くさい」でした。

また、ハマったポイントとしては、画面から「名前」を送るときに、helloworldapp/views.pyのhelloworldの呼び出しでエラー(403)になってしまう事でした。

Djangoはpostする場合、CSRF(クロスサイトリクエストフォージェリ)対策をするようになっており、本来は、サーバー側で発行したCSRFのトークンをクライアントから送る必要があります。(getの例を参考にして、パラメータを取得する方法だけ変えたので、エラーになってしまいました。)

今回はお試しであり、全く同じ「index.html」をFlaskとFastAPIでも流用するため、helloworldを呼び出す場合のみCSRF対策をしないように「@csrf_exempt」を記載するようにしました。(本番アプリで対策をする場合の記載方法は異なるので注意してください。

@csrf_exempt
def helloworld(request):

 

面倒くさい反面、このようにCSRF対策がデフォルトになってる点など、Djangoは非常にしっかりしたフレームワークです。

またMTV(model-template-view)という設計思想に基づいてファイルが構成されているため、大規模アプリで、多数のプログラマが参加するようなプロジェクトでは、Djangoが選択される理由がよくわかります。

Flask

事前準備

Flask(使用バージョンは2.2.2)をインストールします。(requirements.txtにdjangoを記載してDockerビルド時にインストールしています。)

app.pyの作成

プロジェクトのディレクトリ直下にapp.pyを作成します。内容は以下のとおりです。

"""hello world
"""
from flask import Flask
from flask import render_template
from flask import make_response
from flask import request


app = Flask(__name__)


@app.route('/')
def index():
    """画面表示
    Returns:
        render_template: index.html
    """
    return render_template('index.html')


@app.route('/hello', methods=["POST"])
def hello():
    """文字列表示
    Returns:
        make_response: 表示文字列
    """
    name = request.form.get('name')
    return make_response('こんにちは、'+name+'さん')


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

 

テンプレートファイルの作成

プロジェクトのディレクトリ直下にtemplatesフォルダーを作成してindex.htmlを置きます。(Djangoで使用したものと全く同じです。)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Hello World</title>
</head>
<body>
お名前:<input type="text" id="name">
<script>
    $('#name').change(function() {
        $.post( '/hello', encodeURI('name='+$(this).val()) )    
        .done(function( data ) {         
            $("#hello").text(data);        
        })        
    });
</script>
<h1 id ="hello">こんにちは、名無ししさん。</h1>
</body>
</html>

 

起動

起動するためには、以下を実行します。

$ python app.py

 

http://127.0.0.1:8000/にアクセスすると画面が表示され、「お名前」を入力するとあいさつ文が変わります。(画面仕様参照)

雑感

Djangoをやった後なので、率直な感想としては楽・簡単です。

Flaskでアプリを作ったことがあって、慣れているということもありますが、やはり軽量アプリを1人で作るには最適かと思います。

一方、大勢の開発者が参加する大規模プロジェクトになると、app.py内に全てを記載する人や独自の構成でファイルを分割する人など出てきて、規約を整えてないとカオスになることが想像に難くないです。

FastAPI

事前準備

使用する追加ライブラリはFastAPIをはじめとして4つです。

Dockerビルド時にインストールするためのrequirements.txtの内容は以下のとおりです。

fastapi
jinja2
uvicorn[standard]
python-multipart

 

実際に使用したバージョンは、fastAPIは0.81.0、jinja2は3.1.2 uvicormは0.18.3、python-multipartは0.0.5です。

main.pyの作成

プロジェクトのディレクトリ直下にmain.pyを作成します。内容は以下のとおりです。

"""hello world
"""
import uvicorn
from fastapi import FastAPI, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import PlainTextResponse, HTMLResponse
from fastapi.requests import Request


app = FastAPI()
templates = Jinja2Templates(directory="templates")


@app.get('/', response_class=HTMLResponse)
def root(request: Request):
    """画面表示

    Args:
        request (Request): request    
    Returns:
        TemplateResponse: index.html
    """
    return templates.TemplateResponse('index.html', {'request': request})


@app.post('/hello', response_class=PlainTextResponse)
def hello(name: str = Form()):
    """文字列表示

    Args:
        name (string): 名前
    Returns:
        string: 表示文字列
    """
    return 'こんにちは、'+name+'さん'


if __name__ == '__main__':
    uvicorn.run(app)

 

テンプレートファイルの作成

プロジェクトのディレクトリ直下にtemplatesフォルダーを作成してindex.htmlを置きます。内容は以下のとおりです。(Django、Flaskで使用したものと全く同じです。)

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Hello World</title>
</head>
<body>
お名前:<input type="text" id="name">
<script>
    $('#name').change(function() {
        $.post( '/hello', encodeURI('name='+$(this).val()) )    
        .done(function( data ) {         
            $("#hello").text(data);        
        })        
    });
</script>
<h1 id ="hello">こんにちは、名無ししさん。</h1>
</body>
</html>

 

起動

起動するためには、以下を実行します。

$ python main.py

 

http://127.0.0.1:8000/にアクセスすると画面が表示され、「お名前」を入力するとあいさつ文が変わります。(画面仕様参照)

雑感(1) 確かに簡単だが・・

Flaskから、FastAPIに乗り換える人続出(!?)みたいな記事が散見され、果たしてどのようなものかと思っていました。

確かに簡単ですが、コーディング量としてはFlaskと大きな違いはなく、あえて乗り換えるか?と言われると、それほどのアドバンテージは感じませんでした。(きっと、勉強不足でまだ知らないことが多いからなのだと思いますが・・)

雑感(2) ハマった・・

Flaskの文法に非常に似ています。

なので、楽勝だと思ってましたが、油断していた分、意外にハマりました。

ハマりポイント1 型ヒント

引数など型ヒントを使用する必要があります。

画面の初期表示では、最初、以下のような書き方をしていました。

@app.get('/', response_class=HTMLResponse)
def root(request):

 

しかし、上記の記載では422 Unprocessable Entityのエラーが発生します。(画面上は以下のメッセージが返ってきます。)

{“detail”:[{“loc”:[“query”,”request”],”msg”:”field required”,”type”:”value_error.missing”}]}

引数には「型」を指定する必要があります。

@app.get('/', response_class=HTMLResponse)
def root(request: Request):

 

ハマりポイント2 フォームパラメータの送信エラー

 

「え、今の時代、送受信って普通、jsonだよねww」

こう言われた気がしました。

普通以外のこと(ひと昔は普通のことだったのですが・・)をやろうとすると、特別なことをやる必要があります。

以下のように記載した場合、画面で名前を入力しても、422 Unprocessable Entityのエラーが発生します。

@app.post('/hello', response_class=PlainTextResponse)
def hello(name: str):

 

パラメータをそのまま送る場合は、わざわざpython-multipartというライブラリをインストールして、型指定する箇所で、「str = Form()」という書き方をしなければなりません。(Formは上部でimportしています。)

@app.post('/hello', response_class=PlainTextResponse)
def hello(name: str = Form()):

 

ハマりポイント3 フォームパラメータの文字化け

Django、FlaskAPIでは文字化けは発生しませんでしたが、FastAPIでは文字化けが発生してしまいました。

クライアントから渡されたnameが文字化けしてしまうのです。

「文字化け」の何がいやかって、半角で生きている人々(英語圏)にとって知ったことではないので問題が発生したときに情報が少ないのです。(日本語はそもそも情報が少ない)

しかしダメもとで「FastAPI garbled」で検索したら以下のページが見つかりました。(garbledは文字化けの意味です。)

 

この内容をもとに調べてみると、curlで直接postした場合、以下の書き方では文字化けしません。

 

curl -X POST -H "Content-Type: application/x-www-form-urlencoded;" --data-urlencode "name=ああああ" "http://127.0.0.1:8000/hello"

 

しかし、以下の場合は文字化けします。

curl -X POST -H "Content-Type: application/x-www-form-urlencoded;" --data "name=ああああ" "http://127.0.0.1:8000/hello"

 

違いは、引数が「–data-urlencode」か「–data」かです。

つまり、文字化けしている理由は画面からURLエンコードしないで送信されているからです。

index.html内で値を送信する箇所は、元々は以下のような書き方をしていました。

    $('#name').change(function() {
        $.post( '/hello', 'name='+$(this).val() )    
        .done(function( data ) {         
            $("#hello").text(data);        
        })        
    });

 

以下のように変更したら文字化けしないようになりました。違いはencodeURL()をかましてることです。

    $('#name').change(function() {
        $.post( '/hello', encodeURI('name='+$(this).val()) )    
        .done(function( data ) {         
            $("#hello").text(data);        
        })        
    });

 

エンコードしていないのは不具合なので、FastAPIのせいではないのですが、DjangoとFastAPI はencodeURIの記載がなくても文字化けしなかったのは何故なのか。。本筋から外れる(というか調べる気力がない)ので、この件は結果オーライとします。(上で掲載しているindex.htmlはDjango、FlaskともにencodeURIを記載しています。)

雑感(3) ドキュメント生成は確かに便利

FastAPIの一番の目玉といえば、ソースコードからドキュメントを生成できることでしょう。http://127.0.0.1:8000/docsからアクセスできます。

FastAPI生成ドキュメントFastAPI生成ドキュメント

 

またパラメータを送信して、開発ツールのような使い方もできます。

FastAPIドキュメント(パラメータ送信)FastAPIドキュメント(パラメータ送信)

まとめ

以上、Django、Flask、FastAPIでHello World的な画面を作成しました。

それぞれのテンプレートファイルを除いた実行ステップ数(新規+修正)は、以下のとおりです。

フレームワーク修正ステップ数新規ステップ数合計ステップ数
Django31518
Flask01414
FastAPI01515

ステップ数には、大きな差はありません。Djangoはurlマッピングなどで既存ファイルの修正が何点かあるため、多少、ステップ数が増えていますが、メインとなるファイルのステップ数は15で、大きな差はありません。

Djangoの特徴はファイル構成などの「お作法」が厳密であることでしょう。

その分、面倒くさいと言えば面倒くさいですが、大規模開発に向いているというのはよくわかります。

Flaskは簡単に書けて楽です。しかし大規模開発(というか大人数の開発)には向かないような気がします。

FastAPIは、Flaskと難易度はあまり変わりませんが、ドキュメントが自動で生成され、開発ツール的な使い方もできるので、実際のプロジェクトでは大いに役に立つかと思います。

以上、何かしらの参考になれば幸いです。

個人的には、FastAPIは、ハマったので「なんとなく」印象が悪いです。冷静に考えれば、ハマった原因は全てFastAPIのせいではないのは重々承知していますが、人間なんてそんなものです。