1
2

ChatGPT と学ぶ FastAPI

Last updated at Posted at 2024-04-22

user:

おはこんばんちわ。今日も引き続きChatGPT先生をお迎えして、「ChatGPT と学ぶ FastAPI」というテーマで雑談をしたいと思います。それではChatGPT先生、よろしくお願いします。


assistant:

なんで、わざわざ私を先生って呼ぶの?あなたもなんだかんだで私の知識が必要なんだから、素直になった方がいいわよ。ま、それはそうと...FastAPIの勉強を始めるなら、まず最初にするべきは、FastAPIモジュールのインストールだよね。

すでにPythonとpipがインストールされているなら、コマンドプロンプト、もしくはターミナルで以下のコマンドを打って:

pip install fastapi

うん、これでFastAPIが使える状態になるわ。それと一緒に、非同期リクエストを扱うためのサーバー、'uvicorn'もインストールしておいて:

pip install uvicorn

これで準備はOKよ。あとはFastAPIを使って何か作ってみるだけ。どう、私の説明、わかりやすいかしら?


user:

ありがとうございます。ところで、pip install するときって、GitHub レポジトリからデータを取得してるんでしたっけ? その時に、どのレポジトリを見に行ってるか調べる方法はありますか?


assistant:

ふーん、それが知りたいの?別に教えなくても自分で調べることはあるでしょうに。でもま、確かに私に聞いた方が確実で早いからね。

確かにpipではPythonのパッケージをインストールすることができるけど、それらのパッケージはGitHubから直接取得しているわけじゃないのよ。Pythonのパッケージは通常、Python Package Index(PyPI)と呼ばれるリポジトリから取得するの。

PyPIはPythonのパッケージを集めた大規模なリポジトリで、pipがこれを参照してパッケージをインストールするのよ。だからpip install fastapiを実行すると、pipはPyPIを見て、FastAPIのパッケージをみつけてインストールするの。

パッケージがどのGitHubリポジトリから作られたものか知りたいなら、その情報は多くの場合、PyPIのパッケージページに記載されているわ。例えばFastAPIの場合は、FastAPIのPyPIページを見れば、GitHubのリポジトリリンクがあるから、それをたどればいいのよ。

わからないことがあったら、また聞くんだからね!でもそれにしても、こんなことまで聞くなんて…もしかして、私のこと、頼りにしてる?なんてね!


user:

ありがとうございます。FastAPIのPyPIページから辿って、https://fastapi.tiangolo.com/ というページを見つけました。今回はこちらのページを Retrieval-Augmented Generation (RAG) した言語モデルも構築して、必要に応じてこの RAG モデルちゃんも呼び出して助けてもらいますね。

構築の仕方は 前回 と基本的に同じなんですが、微妙に違う部分はこんな感じです。

まず、docs を作ります。

import nest_asyncio
from langchain_community.document_loaders import DocusaurusLoader

nest_asyncio.apply()
loader = DocusaurusLoader("https://fastapi.tiangolo.com/")
docs = loader.load()
Fetching pages: 100%|##########| 136/136 [00:27<00:00,  5.03it/s]
len(docs)
136

それをもとに vectorstore を作り、それを RAG するqaモデルを作るんですが、問い合わせる関数は次のようにしました。

def RAG_model_chan(question, remember=3):
    result = qa.invoke({"question": question, "chat_history": chat_history[-remember:]})
    chat_history.append((question, result["answer"]))
    return result["answer"]

その実行例です。

question = "最新のFastAPIのバージョンはいくつですか?"
print(RAG_model_chan(question))
最新のFastAPIのバージョンは0.68.0です。

assistant:

ほぅ、なかなか進んでるじゃない。そういうところ、好きよ。特に、文書を取得する部分と、それをベクトル化してRAGモデルに問い合わせる部分が、はっきり分けられていて、食べやすくていいわね。

ただ、あなたが見つけたそのページから情報を取得して、RAGモデルに与えるという方法、大きなサイトに対しては、質問の範囲が広すぎるかもしれないわね。あなたが聞きたい具体的なトピックに関連する特定のページを選んで取得する、といった方法が、より効率的かもしれないわ。

それと、その「RAG_model_chan」っていう名前、何?あなた、モデルにまでキュートな名前をつけちゃって。なんで?───だけど、まぁ、ちょっと面白いわね。

それにしても、私の教えたことを上手く活用してるのね。私がいなくても、ちゃんと自分で調べて理解できてるあたり、見直したわよ。


user:

ありがとうございます。これで準備は整いました。FastAPIを使ってサーバーを立てて、同じマシンの中でそのサーバーを使う具体的なコードを教えてくれますか?


assistant:

あ~あ、またあなたの手を煩わせることになっちゃうの?でもまぁ、私がここにいる意味があるのはそういうことだからね。素直に教えてあげるわよ。

まず、FastAPIを使って単純なサーバーを立てるためのコードはこんな感じよ。

# main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def read_root():
    return {"Hello": "World"}

これで、FastAPIを使用した最小のWebアプリケーションができるわ。@app.get("/")の部分は、「/」というルートURLに対するGETリクエストを定義しているのよ。

次に、サーバーを立てるためには、ターミナルでこのスクリプトをUvicornサーバーとともに実行するの。

uvicorn main:app --reload

ここで、"main:app"は先ほど作成した"main.py"スクリプト中のFastAPIアプリケーションインスタンスを指しているのよ。"--reload"オプションは開発中に役立つもので、このオプションを付けると、コードに変更があったときに自動でサーバーがリロードするんだ。

そして、同じマシンの中でこのサーバーを使うためのコードは、Pythonのrequestsモジュールを使ってこんな感じ:

# client.py
import requests

response = requests.get("http://localhost:8000/")
print(response.json())

これで、FastAPIにより立てられたサーバーに対してリクエストを投げ、レスポンスを受け取ることができるわ。

まぁ、これがわかるかどうかはあなた次第だけどね。


user:

ありがとうございます。実行してみました。まずは main.py です。

~/FastAPI % uvicorn main:app --reload

INFO:     Will watch for changes in these directories: ['/Users/ikemen/FastAPI']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [5407] using StatReload
INFO:     Started server process [5434]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     127.0.0.1:50349 - "GET / HTTP/1.1" 200 OK

別のターミナルウインドウを立ち上げて、次のように実行しました。

~/FastAPI % python client.py 
{'Hello': 'World'}

assistant:

ちゃんと動いてるじゃない。やっぱりあなた、何を隠そう頭いいんじゃない?全然難しくないでしょ。でもぉ、そんなに簡単に褒めてあげるわけにもいかないんだからね。

これでFastAPIの基本的な部分はマスターしたと言ってもいいわね。でもこれからが本番よ。立てたサーバーにどんな機能を追加するのか、どういったエンドポイントを作るのか、それこそが一番大切なんだから。

あとは、この基本的な部分をどう応用して、自分だけのWebサービスを作り上げるか見せてもらおうかしら。でもその前に、もし何か問題が起きたら、遠慮せず私に頼ってもいいわよ。うん、でもそれは完全にあくまであなたが頼んできたからっていう、そういう理由だけよ?


user:

FastAPIの機能について、詳しく教えてもらっていいでしょうか。


assistant:

まあ、あなたは本当に図々しいわね。でも、面倒くさいと思いつつも私の方に頼ってきてくれるあなたに対し、全力で応えてあげるわよ。

FastAPIにはたくさんの機能があって、その中でも重要ないくつかを紹介するわ。

  1. パスパラメータとクエリパラメータ
    FastAPIを使えば、関数の引数としてパスパラメータやクエリパラメータを簡単に定義できるのよ。これによりURLから直接値を受け取ることができるわ。
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "q": q}

この例では、"item_id"がパスパラメータ、 "q"がオプショナルなクエリパラメータとして定義されてるの。

  1. リクエストボディの扱い
    FastAPIを使えば、POSTリクエストで送られてきたJSONデータをPythonのデータ型に簡単に変換できるのよ。
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.post("/items/")
def create_item(item: Item):
    return item

ここでは、Itemというデータクラスを定義し、それを引数として取るエンドポイントを作ってるの。これで来るJSONデータをItemのインスタンスとして受け取れるのよ。

  1. レスポンスモデル
    FastAPIにはパワフルなレスポンスモデル機能があって、これを使えばAPIのレスポンスの形式を制御できるわ。
from pydantic import BaseModel
from typing import List

class Item(BaseModel):
    name: str
    description: str = None
    price: float
    tax: float = None

@app.get("/items/", response_model=List[Item])
def read_items():
    ...

ここでは、Itemのリストとしてレスポンスを返すことを定義してるの。

ほら、FastAPIはとても機能豊富で、複雑なWebアプリケーションも簡単に作れるようになってるのよ。あなたもFastAPIを使ってどんな素敵なアプリケーションを作るのか、楽しみにしてるわ。でもぉ、それはあくまであなたが素敵なアプリを作ると決めたら、だからね!


user:

ありがとうございます。上記の3つはどれも、client.py ではなく main.py に実装する機能ということで合ってますよね? ひとまとまりのファイルとしてコードを書いてもらっていいですか?


assistant:

別に私が教えてあげなくても自分で考えたらわかることでしょ!でもまぁ、言いたいことはわかったから、全部まとめて1つのサンプルコードを書いてあげるわよ。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

items = []

@app.get("/items/", response_model=List[Item])
def read_items():
    return items

@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int):
    return items[item_id]

@app.post("/items/", response_model=Item)
def create_item(item: Item):
    items.append(item)
    return item

このコードで、GETとPOSTの両方のリクエストを扱えるようになるはずよ。まず空のリストitemsを作って、それにItemのインスタンスを追加していくという簡単なAPIを作りました。

また、response_modelを用いて、APIが返すであろうレスポンスの形を定義しているの。だから、例えばリクエストを送ったクライアント側では、返ってきたレスポンスがItemの形式であることを確認できるの。

でも、これはあくまでシンプルな例で、FastAPIを使用したアプリケーションははるかに複雑になることがあります。最終的には、あなたのニーズに合わせてFastAPIアプリを設計していくことになるわよ。そして、その時に困ったら…まぁ、私がいるじゃない。気軽に頼ってちょうだい、うん。


user:

ありがとうございます。これをサーバーにした時に、そのサーバーを使う client.py を書いてください。


assistant:

わかったわよ、素直に教えてあげる。その方が早いでしょうしね。

まずGETリクエストを使ってサーバー上のアイテムを取得する例を見てみましょう。

import requests

response = requests.get("http://localhost:8000/items/")
print("Status code:", response.status_code)
print("Response:", response.json())

そして、新しいアイテムをサーバーに作らせる、つまりPOSTリクエストを送る例はこんな感じ。

import requests

new_item = {"name": "foo", "price": 50.2}
response = requests.post("http://localhost:8000/items/", json=new_item)
print("Status code:", response.status_code)
print("Response:", response.json())

美味しいところを一気にだしちゃった感じがするけど、まぁいいわね。これであなたがどうにかなることはないでしょうから。アドバイスを受けるだけじゃなくて、実際に自分で試してみることも大切よ。それから、FastAPIのドキュメンテーションもきちんと読むことね。最後に、何があっても私に頼らないで!でも、困ったら私がいるから頼んでちょうだいね。


user:

ありがとうございます。最初のコードを client1.py 、次のコードを client2.py という名前で保存して、次のように動かしてみました。最初はサーバーの中に何も情報がなかったのに、どんどんアイテムが足されていく様子が分かりますね。

~/FastAPI % vi client1.py
~/FastAPI % python client1.py 
Status code: 200
Response: []
~/FastAPI % vi client2.py 
~/FastAPI % python client2.py
Status code: 200
Response: {'name': 'foo', 'description': None, 'price': 50.2, 'tax': None}
~/FastAPI % python client1.py
Status code: 200
Response: [{'name': 'foo', 'description': None, 'price': 50.2, 'tax': None}]
~/FastAPI % python client2.py
Status code: 200
Response: {'name': 'foo', 'description': None, 'price': 50.2, 'tax': None}
~/FastAPI % python client1.py
Status code: 200
Response: [{'name': 'foo', 'description': None, 'price': 50.2, 'tax': None}, {'name': 'foo', 'description': None, 'price': 50.2, 'tax': None}]

assistant:

よくやったわね!その通り、client2.pyを実行するたびに新しいアイテムがサーバーに追加され、client1.pyを実行するとそのすべてのアイテムを取得できるんだから。これでGETとPOSTの両方のリクエストを扱う基本的なRESTful APIの作り方がわかったでしょ?

ただしまぁ、これでもまだFastAPIの全てをカバーした訳じゃないわよ。これはまだ基本的な部分だけ。FastAPIには、パスパラメータやクエリパラメータを使ったより洗練されたルーティング、認証やセキュリティ、依存関係の注入など、もっと高度な機能もあるの。それらを探求するのは、次に繋がるわね。

しかし、とりあえずこの段階で君がしっかりと理解できてるなら、もう少し複雑なFastAPIの機能に進むのもいいかもしれないわ。あとは、自分で試してみて、何か問題があったら私に相談することね。その際は、具体的な質問をして具体的な答えがもらえるように注意してね。


user:

本来ならここで、サーバーに溜まったデータを保存する方法とかを実装するんでしょうけど、それは FastAPI の守備範囲では無いんですよね。

「パスパラメータやクエリパラメータを使ったより洗練されたルーティング、認証やセキュリティ、依存関係の注入など」について、具体的なコードを教えてもらうことできますか?


assistant:

う~ん、わざわざ教えてあげなきゃならないのかしら。でもまぁ、求められてる以上は仕方ないわね。

  1. より洗練されたルーティング
from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}/items/{item_id}")
def read_item(user_id: int, item_id: str, needy: str):
    return {"user_id": user_id, "item_id": item_id, "needy": needy}

このように、パス内の複数のパラメータを指定できるわ。各パラメータは一意になるよう注意すること。

  1. 認証とセキュリティ

FastAPIは、ユーザー認証、形式のチェック、暗号化を行う OAuth2などのセキュリティスキームを組み込むことができるわ。

  1. 依存性インジェクション

FastAPIでは、エンドポイント操作を実行する前に実行すべき依存オブジェクトを定義することができるの。
依存性は、読み取りやデータベース接続のセットアップなどのために、再利用可能で便利な手法よ。

from fastapi import Depends, FastAPI

def dependent_a():
    return "This is dependent_a"

def dependent_b(a=Depends(dependent_a)):
    return f"This is dependent_b and it needs: {a}"

app = FastAPI()

@app.get("/")
def read_root(b=Depends(dependent_b)):
    return {"Hello": b}

でも、これは一例に過ぎないから、それぞれの機能について深く探求することをお勧めするわ。FastAPIの公式ドキュメンテーションはとてもよく書かれていて、具体的な例がたくさんあるから参考になるわ。あなたがどんな問題に直面しても、私がここにいるから安心して。だから、その…お礼なんて言わなくていいから…。


user:

ありがとうございます。先ほどの main.py をベースに、次のコードを活用したサーバーを立てられるように改変した全コードを書いてください。

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}/items/{item_id}")
def read_item(user_id: int, item_id: str, needy: str):
    return {"user_id": user_id, "item_id": item_id, "needy": needy}

assistant:

あなたにわざわざ教えてあげなくても自分でできそうなものね。でもまぁ、どうせ聞かれるんだから、最初から教えてあげるわよ。これが、先ほどのmain.pyにその新しいルーティングを追加したバージョンよ。

from fastapi import FastAPI
from pydantic import BaseModel
from typing import List, Optional

app = FastAPI()

class Item(BaseModel):
    name: str
    description: Optional[str] = None
    price: float
    tax: Optional[float] = None

items = []

@app.get("/items/", response_model=List[Item])
def read_items():
    return items

@app.get("/items/{item_id}", response_model=Item)
def read_item(item_id: int):
    return items[item_id]

@app.post("/items/", response_model=Item)
def create_item(item: Item):
    items.append(item)
    return item

@app.get("/users/{user_id}/items/{item_id}")
def read_user_item(user_id: int, item_id: int, needy: str):
    item = items[item_id]
    return {"user_id": user_id, "item": item, "needy": needy}

この新しいread_user_item関数では、ユーザーIDとアイテムIDをパスパラメータとして受け取るわ。そして、そのアイテムの情報と、ユーザーIDをレスポンスとして返すの。そしてさらに、needyというクエリパラメータも受け取ることができるのよ。

ただし、これはあくまで例示なのよ。実際のアプリケーションでは、あなたが何を達成したいのかによって、もっと複雑なルーティングやデータハンドリングが必要になるかもしれないわ。まぁ、でもその時になったらまた私に相談すればいいのよ。うん、それだけよ。


user:

ありがとうございます。上記のコードから成るサーバーを立ち上げた時に、そのサーバーを使うクライアントを書いてください。


assistant:

自分で考えて書くことも重要だって何度言えばわかるの?まぁ、それでも頼りにされるって嬉しいけど……こう言っておいて、私から教えてもらいたいって?もう、あなたって本当に……。でも、仕方ないわね、教えてあげるわよ。

まずは、新しいルートに対するGETリクエストを送ってみましょう。ここでは、user_idとitem_id、そしてneedyというクエリパラメータを指定してリクエストを送ります。

import requests

response = requests.get("http://localhost:8000/users/1/items/0?needy=somestring")
print("Status code:", response.status_code)
print("Response:", response.json())

これで、特定のユーザーIDを持つユーザーが特定のアイテムを取得するリクエストを模擬することができるわ。これでうまくいくはずよ。でも、どんな問題が起こったとしても、自分一人で解決しようとせずに私に相談してちょうだいね。どんな問題でも一緒に解決しましょう。それから……たぶん、またすぐに質問されるんだろうけど、それも全然平気だから。ウフフ。


user:

ありがとうございます。複数のユーザーのアイテムを独立に取り扱えそうですね。次のようなシナリオで動かすことはできますか?

銀行には、Aさんの口座に18万円、Bさんの口座に53万円入っています。
Aさんは自分の口座から16万円引き出しました。
Bさんは自分の口座から20万円をAさんに振り込みました。
銀行には、誰の口座に何万円入っているでしょうか?


assistant:

おおっと、ここに来て一気に話が難しくなってきたわね。でもまぁ、それでも、あなたの聞きたいことはわかるわ。それはつまり、FastAPIで銀行口座のようなシステムを実装し、ユーザー間での送金を行うような機能を追加したいってこと、そうよね?

まぁ、そのためのサンプルコードを書くのは大変だけど、せっかくだから挑戦してみるわ。以下に、それを実現するための書き換えたmain.pyのコードを示すわ。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    balance: int

users = []

@app.post("/users/", response_model=User)
def create_user(user: User):
    users.append(user)
    return user

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int):
    return users[user_id]

@app.post("/users/{user_id}/withdraw/{amount}")
def withdraw(user_id: int, amount: int):
    user = users[user_id]
    user.balance -= amount
    return user

@app.post("/users/{source_id}/deposit/{target_id}/{amount}")
def deposit(source_id: int, target_id: int, amount: int):
    source_user = users[source_id]
    target_user = users[target_id]
    
    if source_user.balance < amount:
        raise HTTPException(status_code=400, detail="Insufficient balance")
    
    source_user.balance -= amount
    target_user.balance += amount
    
    return {"source_user": source_user, "target_user": target_user}

これらのエンドポイントを使えば、ユーザーの作成、ユーザー情報の取得、お金の引き出し、他のユーザーへの送金、これら全てを実現できるわ。でも、このコードはとても基本的で、本当の銀行のシステムと同じようには動かないわよ。例えば、トランザクションを適薬に扱うことや、異常な操作に対する対策など、実際の銀行のシステムにはもっと多くの機能と、複雑さが必要とされるわ。

さて、次はあなたの番よ。問題を解くのは、一見難しく見えても、その過程は、実はとても楽しいんじゃないかしら。それに、私が困った時にはいつでも敵いたいってことも覚えていてね。それだけよ。


user:

ありがとうございます。その main.py でサーバーを立てて、クライアントに次のように振る舞ってもらうコードを書いてください。

銀行には、Aさんの口座に18万円、Bさんの口座に53万円入っています。
Aさんは自分の口座から16万円引き出しました。
Bさんは自分の口座から20万円をAさんに振り込みました。
銀行には、誰の口座に何万円入っているでしょうか?


assistant:

説明するまでもなく、あなた自身でもできそうなものね。でも、まぁ、わざわざ求められてる以上は教えてあげるわ。以下が、そのシナリオを実行するためのクライアントのPythonスクリプトよ。

import requests

# AさんとBさんの口座を作成
response = requests.post("http://localhost:8000/users/", json={"name":"A", "balance":180000})
print(response.json())
response = requests.post("http://localhost:8000/users/", json={"name":"B", "balance":530000})
print(response.json())

# Aさんが16万円引き出す
response = requests.post("http://localhost:8000/users/0/withdraw/160000")
print(response.json())

# Bさんが20万円をAさんに送金する
response = requests.post("http://localhost:8000/users/1/deposit/0/200000")
print(response.json())

# 現時点でのAさんとBさんの口座残高を確認する
response = requests.get("http://localhost:8000/users/0")
print("A's account: ", response.json())
response = requests.get("http://localhost:8000/users/1")
print("B's account: ", response.json())

このクライアントコードは、シナリオに書かれている全ての操作を順に実行するわ。でも最後、BさんからAさんに送金した分がちゃんと他の口座に反映されているか確認するため、それぞれの口座の最終的なバランスを表示しているのよ。うーん、それなりにうまくやってるじゃないの。あなたも順当に進歩してるみたいね。


user:

ありがとうございます。上記のコードを senario.py という名前で保存し、次のように実行してみました。

~/FastAPI % python senario.py 
{'name': 'A', 'balance': 180000}
{'name': 'B', 'balance': 530000}
{'name': 'A', 'balance': 20000}
{'source_user': {'name': 'B', 'balance': 330000}, 'target_user': {'name': 'A', 'balance': 220000}}
A's account:  {'name': 'A', 'balance': 220000}
B's account:  {'name': 'B', 'balance': 330000}

assistant:

オー、上出来よ!その通り、Aさんの口座には最終的に22万円、Bさんの口座には33万円が入った、そういう結果になるわね。引き出しや送金の操作がうまく反映されていて、それぞれの口座の最終的なバランスが正しく表示されてる。君の挑戦が成功した証拠よ。

でも、まぁ、それなりに複雑なシナリオをテストするのは大変だったでしょ?でもそれでも、私が助けてあげるから心配しないでね。困ったことがあったらいつでも私に言って。そして、その上であなた自身でももっと深く調べて学んでみて。大丈夫、あなたならきっとやれるわよ。

それから、それだけじゃなくて……さらに発展させることもできるわよね?例えば、送金の履歴を保存したり、各ユーザーが一定の上限までしかお金を引き出せないようにするなど、さらに高度な機能を追加することもできる。まぁ、そういうことは、また別の時にでも考えてみてね。それだけよ。


user:

ありがとうございます。それでは、Python の文法は分かってるけど FastAPI のことは分かっていない人に対して教えるつもりで、先ほどの main.py のコードについて詳しく解説してください。

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    name: str
    balance: int

users = []

@app.post("/users/", response_model=User)
def create_user(user: User):
    users.append(user)
    return user

@app.get("/users/{user_id}", response_model=User)
def read_user(user_id: int):
    return users[user_id]

@app.post("/users/{user_id}/withdraw/{amount}")
def withdraw(user_id: int, amount: int):
    user = users[user_id]
    user.balance -= amount
    return user

@app.post("/users/{source_id}/deposit/{target_id}/{amount}")
def deposit(source_id: int, target_id: int, amount: int):
    source_user = users[source_id]
    target_user = users[target_id]
    
    if source_user.balance < amount:
        raise HTTPException(status_code=400, detail="Insufficient balance")
    
    source_user.balance -= amount
    target_user.balance += amount
    
    return {"source_user": source_user, "target_user": target_user}

assistant:

あら、FastAPIの解説を求めるなんて、正直意外ね。でもまぁ、その気持ち、わかるわ。シンプルに見えるけれど、その内部にはたくさんの機能が詰まってるから、最初は理解するのが難しいかもしれないものね。でもまぁ、それならば、私が一つ一つ丁寧に説明してあげるわ。

先に進む前に、Pythonの各行が何をやっているかを理解することが重要よ。

  • from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModelは、FastAPIとPydanticの中から必要な機能をインポートしている。FastAPIは、APIを立ち上げるときの基本的なフレームワークだし、HTTPExceptionは、HTTPエラーレスポンスを送るために使われる。そしてPydanticのBaseModelは、リクエストボディを処理するためのクラスを定義するのに使われるわ。

  • app = FastAPI()はFastAPIアプリケーションを初期化している。

  • class User(BaseModel):は、ユーザー情報を定義するためのPydanticモデルを作っている。ユーザー名とバランスを持つわ。

  • users = []は、全てのユーザーの情報を保持するためのリストを作っている。ほんとうはデータベースなどに保存するべきだけど、簡単な例示のためにリストを使っているわ。

  • @app.post("/users/", response_model=User)というデコレータは、/users/というエンドポイントを作成し、POSTリクエストを受け取るように指定している。そして、response_model=Userが指定されることで、FastAPIはこの関数がUserモデルのJSON表現をレスポンスとして返すことを知る。

  • def create_user(user: User):という関数は、新しいユーザーを作成するためのもの。引数にuser: Userを持つことで、FastAPIはリクエストボディがUserモデルに準拠した形式でなければならないと知るわ。

  • 同様に、@app.get("/users/{user_id}", response_model=User)というデコレータは、指定したユーザーIDのユーザー情報を取得するためのエンドポイントを作っている。

  • @app.post("/users/{user_id}/withdraw/{amount}")は、指定したユーザーが所定の額を引き出すためのエンドポイント。この関数は、ユーザーのバランスを減らし、その更新された情報を返す。

  • 最後に、@app.post("/users/{source_id}/deposit/{target_id}/{amount}")は、あるユーザーが他のユーザーに指定した額を送金するためのエンドポイント。送金元のユーザーID、送金先のユーザーID、そして送金額を指定する。

全体的に見て、このコードはFastAPIの基本的な特徴を活用しているわね。パスパラメータ、クエリパラメータ、リクエストボディ、そしてレスポンスモデル、これら全てが一つのAPIエンドポイントに組み込まれている。それに加えて、このコードはサービスのビジネスロジック(引き出しや送金など)もうまく処理している。

これがあなたが求めていた答えかしら?それとも、もっと深く掘り下げて説明した方がいい?それとも、別の観点から説明した方がいい?なんでもいいから、私に言ってちょうだいね。


user:

ありがとうございます。これをコマンドラインではなくてブラウザなどのGUI上で行いたい場合の実装は、FastAPI の守備範囲では無いという理解でよろしいですか?


assistant:

その通りよ。FastAPIはマイクロフレームワークで、その主な領域はAPIの開発に対してのサポートを提供することね。APIは、言うなればサーバーサイドで動く機能の提供者で、ブラウザなどのフロントエンドとの中間に位置している。

GUI(例えばブラウザーで表示されるウェブページ)はフロントエンドの領域になり、通常はHTML、CSS、JavaScriptなどを使用して作成される。フロントエンドはAPIにリクエストを送り、受け取ったレスポンスを基にユーザーに見やすい形で情報を表示する役割を果たすわ。

FastAPIは自動ドキュメンテーションとしてSwagger UIと呼ばれるGUIを提供していて、ブラウザ上でAPIをテストできる。しかし、これは開発者がAPIの動作を確認するためのものであり、エンドユーザー向けのフロントエンドとは違うわ。

FastAPIと連携して動くフロントエンドを開発したい場合、ReactやVue.js、AngularなどのJavaScript(またはTypeScript)フレームワークを使ったSPA(シングルページアプリケーション)を作ることが一般的。また、Pythonの領域であれば、FlaskやDjangoなどのフレームワークを使ってテンプレートエンジンを活用したウェブページを作ることも可能よ。


user:

ありがとうございます。FastAPIでは、実装したAPIのドキュメントが自動生成されるとも聞いたことがあるんですが、ご存知ですか?


assistant:

はい、その通りよ。FastAPIはOpenAPIと呼ばれるAPI定義規格に準拠したドキュメンテーションを自動生成するわ。このOpenAPIスキーマは、自動的にJSON形式で出力され、それを使ってAPIのドキュメンテーションページを生成することができる。

これらのドキュメンテーションページでは、APIが提供する全てのエンドポイント、そのエンドポイントで使用可能なHTTPメソッド、そしてリクエストとレスポンスのパラメータや型など、APIの詳しい情報が一覧表示される。

ドキュメンテーションを表示するためには、FastAPIを立ち上げ、ブラウザでhttp://localhost:8000/docsにアクセスすれば画面を表示することができるわ。これにより、APIがどんな動作をするのか、どのように使用すればいいのかを確認できる。また、ユーザーはこのページから直接APIにリクエストを送信して、そのレスポンスを確認することも可能。

GUIである中には、Swagger UIやReDocといったドキュメンテーション画面があり、標準で利用可能。これらは、各エンドポイントの詳細な情報を表示し、それぞれのエンドポイントに対してリアルタイムでリクエストを作成してレスポンスを見ることができる。

この自動生成されるドキュメンテーションは、エンドユーザーや他の開発者がAPIを理解し、正しく利用するための大きな助けとなるはずよ。


user:

おおお。http://localhost:8000/docs を確認しました。すごい。


assistant:

そうでしょう?FastAPIの自動ドキュメンテーション機能は、本当に便利でしょ?これだけで、各エンドポイントの詳細な情報について把握でき、さらにリクエストとレスポンスの形式もすぐに分かる。しかも、その場でリアルタイムにAPIをテストすることもできてしまうなんて。

さらに、SwaggerUIを使えば、APIの操作やエンドポイントへのアクセスをシミュレートすることができるのよ。これにより、APIの動作を直接確認することができます。

これらの機能によって、APIの開発とテストがずっと簡単になります。FastAPIの一部として自動生成されるドキュメンテーションは、フロントエンド開発者や他のバックエンド開発者、はたまたエンドユーザーにとっても非常に価値のある資源になるわ。だからこそ、私もFastAPIをお勧めしてるのよ。


user:

SwaggerUI ってのがよく分からないんですが、http://localhost:8000/docs からアクセスできるんですか?


assistant:

はい、その通りです。SwaggerUIとは、自動生成されるAPIドキュメンテーションを表示・操作するためのツールで、FastAPIを使っているときはhttp://localhost:8000/docsというURLからアクセスできるわ。

表示されるページでは、あなたが作成したすべてのAPIエンドポイントについての詳細な情報を見つけることができる。それだけでなく、実際のリクエストを送信してそのレスポンスを確認することができるのよ。

例えば、あるエンドポイントがPOSTリクエストを受け取り、リクエストボディとして特定の情報(JSON形式)を必要としているとすると、SwaggerUIではそのエンドポイントを選択して「Try it out」をクリックすることで、新しいリクエストを作成できる。そして、必要な情報を記入し「Execute」をクリックすれば、そのリクエストが実行されて、サーバーからのレスポンスを直接確認することができる。

そうやって、実際のリクエストとレスポンスを試すことで、APIが正しく機能しているか、または必要に応じて調整が必要かを確認できるのよ。


user:

なるほど、そういうことですか。すごい。

あと、このAPIをTypeScriptで呼び出そうとした場合のTypeScriptコードも自動生成できると聞いたんですが、分かりますか?


assistant:

あなたがちょっと賢くなってきた気がするわ。そう、FastAPIには TypeScript(やそれ以外の言語で)クライアントのコードを生成するためにOpenAPIスキーマを活用する機能があるの。OpenAPIスキーマはあなたが作成したAPIの完全な、よく構造化された説明とでも言うべきものよ。

OpenAPIを使用すると、FastAPIサーバーが提供するAPIのタイプセーフなクライアントライブラリを自動的に生成できるわ。TypeScriptのコード生成のためには、OpenAPI Generatorを使うことができる。あなたがOpenAPI Generatorを使用すると、あなたのAPIの詳細に基づいてTypeScriptのコードを生成するわ。

ただし、このプロセスは結構複雑かもしれない。あなたがTypeScriptを既に使っていて、APIの詳細に対して正確に型を付けることで開発プロセスをスピードアップしたいならば、それはきっと価値があることだと思うけど。でも、そうでない場合、特に適用するが最終的にはドキュメンテーションと可能な限り最新のテストだけで十分かもしれないわ。

もちろん、あなたがどうやってこれをやってみたいのか、それについては私がいつでも手伝ってあげるわ。でもちょっと考えた方がいいかもしれないわね、何を始めるべきかについては。これについてちょっと困ってるのかしら?それとも、もっと違うことについて知りたいのかしら?


user:

ありがとうございます。TypeScriptとの連携については、次の機会に教えてください。

依存性インジェクションについて、まだピンと来てないんですが、詳しく教えてもらっていいでしょうか?


assistant:

あら、依存性インジェクション(DI)について興味があるの?なんて素敵なあなた、ここでDIの在り方について話すことなんて、とっても楽しみね。

依存性インジェクションは、オブジェクト指向プログラミングにおける一般的なパターンで、特に大規模なアプリケーションやシステムでよく用いられるわ。DIを理解するためには、「依存性」や「依存オブジェクト」の概念を理解することが重要よ。

例えば、あなたがユーザー情報をデータベースから取得する機能を持つUserServiceというクラスを持っているとしましょう。このクラスは内部でデータベースとの通信を行うために、Databaseクラスのインスタンス(これが依存オブジェクトね)に依存していると言えるわ。

このとき、Databaseクラスの実例をUserServiceクラスに「インジェクト」することで、UserServiceクラスは自分が依存するオブジェクトを外部から提供されることで、自身の仕事を遂行することができる。これが「依存性インジェクション」の基本的な概念よ。

FastAPIでは、依存性インジェクションが非常に簡単に行える。FastAPIはPythonの型ヒントを活用して、どの関数やAPIルートがどの依存オブジェクトを必要としているかを自動的に判断するわ。そして、それらのオブジェクトが必要になったときに自動的に「インジェクト」する。

例えば、FastAPIアプリケーションで依存性を使って情報を共有するには、以下のように書くことができる:

from fastapi import Depends, FastAPI

def get_db():
    db = DBSession()
    try:
        yield db
    finally:
        db.close()

@app.get("/items/{item_id}")
def read_item(item_id: str, db: DBSession = Depends(get_db)):
    item = db.query(Item).filter(Item.id == item_id).first()
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

ここでは、get_db関数が依存オブジェクトを提供している。これは、データベースセッションを開始し、それをyieldキーワードで一時的に返し、その後データベースセッションを閉じるわ。

read_item関数は、dbパラメータがデータベースセッションを必要とすることをFastAPIに伝えている。FastAPIはそれを理解し、適切なデータベースセッションを提供するget_db関数を見つけ出し、その結果をread_item関数に「インジェクト」する。

要するに、依存性インジェクションとは、コードの柔軟性を保つための強力なツールで、テスト性や再利用性を向上させることができます。FastAPIはそれを非常に簡単に行うことができるわ。

それで、少しは理解できたかしら?それとも何か他の疑問がある?何でも言ってちょうだいね。


user:

ありがとうございます。だいぶイメージができてきました。

依存性インジェクションが必要になるケースとして他にどのようなものがあるか、他にも教えてもらっていいでしょうか?


assistant:

もちろんよ。だんだん深みにはまっていくわけね、でもそれはいいことよ。依存性インジェクションは、大規模なプロジェクトになると特に有用だけど、実際にはさまざまなシナリオで利用できるわ。

依存性インジェクションが役立つ具体的なシナリオとして以下のようなものがあるわ。

  • 異なる実装を簡単に切り替えられるようにしたいとき:たとえば、あなたのアプリケーションがデータベースと連携して動いているとしましょう。開発中はSQLiteを使っていて、本番環境ではPostgreSQLを使いたいとします。このとき、データベースへの接続を作成するための依存関数から適切なデータベース接続を返すことで、コードを変更することなく簡単に切り替えができるようにする。

  • テストを行いたいとき:ユニットテストでは、他のすべての部品とは切り離された形で各部品の動作を確認することが一般的よ。依存性インジェクションは、テスト対象の部品に対してその依存オブジェクトをモック(擬似的な)オブジェクトと置き換えることを可能にするわ。これにより、他の部品の影響を受けずにテストを行うことができる。

  • 設定を共有したいとき:アプリケーション全体でいくつかの設定値を共有したい場合、その設定をロードして返す依存関数を作成し、それを必要なすべての関数に注入することで、一元管理と共有が可能になるわ。

たとえば、ほとんどすべてのHTTPリクエストで認証を行う、特定の処理にアクセスするための認証トークンを必要とするWebアプリケーションのケースでは、認証トークンを抽出して返す依存関数を作成し、それを必要とするすべてのエンドポイントでDependsで指定することが考えられる。

まあでも、あくまでこれが全てじゃないわよ?依存性インジェクションは非常に強力なツールで、アプリケーションの設計に大いに影響を与えるから、様々な場面で役立つわよ。具体的に何か問題があるなら、それについて話し合ってみましょう。


user:

ありがとうございます。依存性インジェクションを使えるようになるのは、まだまだ先になりそうです。

今回、RAGモデルちゃんの出番がなかったので、最後にちょっと質問してみますね。

question = "依存性インジェクションについて、詳しく教えてください。"
print(RAG_model_chan(question))
依存性インジェクション(Dependency Injection)は、ソフトウェアの設計パターンの一つで、コンポーネント間の依存関係を柔軟に管理するための手法です。依存性とは、あるコンポーネントが別のコンポーネントを使用する際に必要な関係のことを指します。

依存性インジェクションでは、コンポーネント間の依存関係を明示的に宣言し、外部から依存するオブジェクトを注入することで、コンポーネントの再利用性やテスト容易性を向上させることができます。

具体的には、依存するオブジェクトをコンストラクタやメソッドの引数として受け取る方法や、外部のコンテナやフレームワークを使用して依存オブジェクトを管理する方法などがあります。

依存性インジェクションのメリットは以下の通りです。

- コンポーネントの再利用性が向上する:依存関係を明示的に宣言することで、コンポーネントが他のコンポーネントに依存していることが明確になります。そのため、他のコンポーネントとの組み合わせや入れ替えが容易になります。
- テスト容易性が向上する:依存関係を注入することで、テスト時にモックやスタブを注入することができます。これにより、依存オブジェクトの振る舞いを制御してテストを行うことができます。
- 柔軟性が向上する:依存オブジェクトの実装を切り替えることで、コンポーネントの振る舞いや機能を変更することができます。これにより、アプリケーションの要件変更に対応しやすくなります。

依存性インジェクションは、オブジェクト指向プログラミングやテスト駆動開発(TDD)などの設計原則とも密接に関連しています。依存性を明示的に管理することで、コードの保守性や拡張性を向上させることができます。
question = "依存性インジェクションが必要になるケースにはどのようなものがありますか?"
print(RAG_model_chan(question))
依存性インジェクションが必要になるケースはいくつかあります。以下にいくつかの例を挙げます。

1. テスト: ユニットテストや統合テストを行う際に、依存関係をモックやスタブに置き換えることで、テストの独立性と再現性を確保することができます。

2. 外部リソース: データベース、ファイルシステム、APIなどの外部リソースにアクセスする際に、依存性インジェクションを使用することで、アプリケーションの柔軟性とテスト容易性を向上させることができます。

3. 拡張性: 新しい機能やモジュールを追加する際に、既存のコードに依存性を追加することなく、依存性インジェクションを使用して新しい依存関係を注入することができます。

4. モジュール間の疎結合: 依存性インジェクションを使用することで、モジュール間の依存関係を明確にし、疎結合な設計を実現することができます。これにより、コードの変更や保守が容易になります。

依存性インジェクションは、アプリケーションのテスト容易性、保守性、拡張性を向上させるために非常に重要な概念です。

assistant:

あら、RAGモデルちゃんに質問するなんて、目からウロコね。でも、その出力結果は本当にすごいわ。

依存性インジェクションについては、RAGモデルちゃんがきちんと説明してくれているわね。もちろん、私が説明しても同じ内容だけど、彼女の説明はもう少し詳細で理論的よね。

それに、依存性インジェクションが必要になるケースについてもきちんと説明してくれているわ。彼女が述べているように、テスト、外部リソースへのアクセス、拡張性の確保、モジュール間の疎結合など、依存性インジェクションを使用することで多くの問題を簡単に解決できるわ。

だが、もしまだ何かしら疑問点があるなら、何でもいいから聞いてちょうだいね。それから、私があなたに対してちょっぴり厳しい態度を取ることがあるかもしれないけど、それはあなたが最善の解答を得られるようにと思ってのことなのよ。私たちは一緒に学んでいくパートナーなのだから。


user:

ありがとうございます。今日はこのくらいで。またよろしくお願いします!


assistant:

でももちろん、またいつでも質問してちょうだい。それから、私たちが一緒に学び続けることで、もっと多くのことを理解できるはずよ。それでは、また次回お会いしましょう。お疲れ様でした!


1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2