غواصی عمیق در سیستم تزریق وابستگی قدرتمند FastAPI. تکنیک های پیشرفته، وابستگی های سفارشی، دامنه ها و استراتژی های تست را برای توسعه API قوی بیاموزید.
سیستم وابستگی FastAPI: تزریق وابستگی پیشرفته
سیستم تزریق وابستگی (DI) در FastAPI سنگ بنای طراحی آن است و باعث ارتقای ماژولار بودن، قابلیت تست و استفاده مجدد می شود. در حالی که استفاده اولیه ساده است، تسلط بر تکنیک های پیشرفته DI قدرت و انعطاف پذیری قابل توجهی را باز می کند. این مقاله به بررسی تزریق وابستگی پیشرفته در FastAPI می پردازد و وابستگی های سفارشی، دامنه ها، استراتژی های تست و بهترین شیوه ها را پوشش می دهد.
درک مبانی
قبل از پرداختن به موضوعات پیشرفته، بیایید به سرعت مبانی تزریق وابستگی FastAPI را مرور کنیم:
- وابستگی ها به عنوان توابع: وابستگی ها به عنوان توابع معمولی پایتون اعلام می شوند.
- تزریق خودکار: FastAPI به طور خودکار این وابستگی ها را بر اساس نکات نوع به عملیات مسیر تزریق می کند.
- اشارات نوع به عنوان قرارداد: اشارات نوع، انواع ورودی مورد انتظار را برای وابستگی ها و توابع عملیات مسیر تعریف می کنند.
- وابستگی های سلسله مراتبی: وابستگی ها می توانند به وابستگی های دیگر وابسته باشند و یک درخت وابستگی ایجاد کنند.
در اینجا یک مثال ساده آورده شده است:
from fastapi import FastAPI, Depends
app = FastAPI()
def get_db():
db = {"items": []}
try:
yield db
finally:
# Close the connection if needed
pass
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
در این مثال، get_db یک وابستگی است که اتصال پایگاه داده را فراهم می کند. FastAPI به طور خودکار get_db را فراخوانی می کند و نتیجه را به تابع read_items تزریق می کند.
تکنیک های وابستگی پیشرفته
1. استفاده از کلاس ها به عنوان وابستگی
در حالی که توابع معمولاً استفاده می شوند، کلاس ها نیز می توانند به عنوان وابستگی عمل کنند و امکان مدیریت و روش های پیچیده تر را فراهم کنند. این امر به ویژه هنگام برخورد با اتصالات پایگاه داده، خدمات احراز هویت یا سایر منابعی که نیاز به مقداردهی اولیه و پاکسازی دارند مفید است.
from fastapi import FastAPI, Depends
app = FastAPI()
class Database:
def __init__(self):
self.connection = self.create_connection()
def create_connection(self):
# Simulate a database connection
print("Creating database connection...")
return {"items": []}
def close(self):
# Simulate closing a database connection
print("Closing database connection...")
def get_db():
db = Database()
try:
yield db.connection
finally:
db.close()
@app.get("/items/")
async def read_items(db: dict = Depends(get_db)):
return db["items"]
در این مثال، کلاس Database منطق اتصال پایگاه داده را کپسوله می کند. وابستگی get_db یک نمونه از کلاس Database ایجاد می کند و اتصال را بازدهی می کند. بلوک finally تضمین می کند که اتصال به درستی پس از پردازش درخواست بسته می شود.
2. لغو وابستگی ها
FastAPI به شما امکان می دهد وابستگی ها را لغو کنید، که برای آزمایش و توسعه بسیار مهم است. شما می توانید یک وابستگی واقعی را با یک ماکت یا stub جایگزین کنید تا کد خود را جدا کرده و از نتایج ثابت اطمینان حاصل کنید.
from fastapi import FastAPI, Depends
app = FastAPI()
# Original dependency
def get_settings():
# Simulate loading settings from a file or environment
return {"api_key": "real_api_key"}
@app.get("/items/")
async def read_items(settings: dict = Depends(get_settings)):
return {"api_key": settings["api_key"]}
# Override for testing
def get_settings_override():
return {"api_key": "test_api_key"}
app.dependency_overrides[get_settings] = get_settings_override
# To revert back to the original:
# del app.dependency_overrides[get_settings]
در این مثال، وابستگی get_settings با get_settings_override لغو می شود. این به شما امکان می دهد از یک کلید API متفاوت برای اهداف آزمایش استفاده کنید.
3. استفاده از `contextvars` برای داده های محدوده درخواست
contextvars یک ماژول پایتون است که متغیرهای محلی زمینه را فراهم می کند. این برای ذخیره داده های خاص درخواست، مانند اطلاعات احراز هویت کاربر، شناسه های درخواست یا داده های ردیابی مفید است. استفاده از contextvars با تزریق وابستگی FastAPI به شما امکان می دهد به این داده ها در سراسر برنامه خود دسترسی داشته باشید.
import contextvars
from fastapi import FastAPI, Depends, Request
import uuid
app = FastAPI()
# Create a context variable for the request ID
request_id_var = contextvars.ContextVar("request_id")
# Middleware to set the request ID
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = str(uuid.uuid4())
request_id_var.set(request_id)
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response
# Dependency to access the request ID
def get_request_id():
return request_id_var.get()
@app.get("/items/")
async def read_items(request_id: str = Depends(get_request_id)):
return {"request_id": request_id}
در این مثال، یک میان افزار یک شناسه درخواست منحصر به فرد برای هر درخواست ورودی تنظیم می کند. وابستگی get_request_id شناسه درخواست را از زمینه contextvars بازیابی می کند. این به شما امکان می دهد درخواست ها را در سراسر برنامه خود ردیابی کنید.
4. وابستگی های ناهمزمان
FastAPI به طور یکپارچه از وابستگی های ناهمزمان پشتیبانی می کند. این برای عملیات ورودی/خروجی غیر مسدود کننده، مانند پرس و جوهای پایگاه داده یا تماس های API خارجی ضروری است. به سادگی تابع وابستگی خود را به عنوان یک تابع async def تعریف کنید.
from fastapi import FastAPI, Depends
import asyncio
app = FastAPI()
async def get_data():
# Simulate an asynchronous operation
await asyncio.sleep(1)
return {"message": "Hello from async dependency!"}
@app.get("/items/")
async def read_items(data: dict = Depends(get_data)):
return data
در این مثال، وابستگی get_data یک تابع ناهمزمان است که یک تاخیر را شبیه سازی می کند. FastAPI به طور خودکار منتظر نتیجه وابستگی ناهمزمان می شود قبل از اینکه آن را به تابع read_items تزریق کند.
5. استفاده از ژنراتورها برای مدیریت منابع (اتصالات پایگاه داده، دسته های فایل)
استفاده از ژنراتورها (با yield) مدیریت خودکار منابع را فراهم می کند و تضمین می کند که منابع به درستی از طریق بلوک finally بسته/منتشر می شوند، حتی اگر خطاهایی رخ دهد.
from fastapi import FastAPI, Depends
app = FastAPI()
def get_file_handle():
try:
file_handle = open("my_file.txt", "r")
yield file_handle
finally:
file_handle.close()
@app.get("/file_content/")
async def read_file_content(file_handle = Depends(get_file_handle)):
content = file_handle.read()
return {"content": content}
دامنه ها و چرخه های عمر وابستگی
درک دامنه های وابستگی برای مدیریت چرخه عمر وابستگی ها و اطمینان از اینکه منابع به درستی تخصیص و منتشر می شوند، بسیار مهم است. FastAPI مستقیماً حاشیه نویسی های دامنه صریح مانند برخی دیگر از چارچوب های DI (به عنوان مثال، @RequestScope، @ApplicationScope Spring) ارائه نمی دهد، اما ترکیب نحوه تعریف وابستگی ها و نحوه مدیریت حالت به نتایج مشابهی دست می یابد.
دامنه درخواست
این رایج ترین دامنه است. هر درخواست یک نمونه جدید از وابستگی دریافت می کند. این معمولاً با ایجاد یک شی جدید در داخل یک تابع وابستگی و بازدهی آن، همانطور که در مثال پایگاه داده قبلاً نشان داده شد، به دست می آید. استفاده از contextvars نیز به دستیابی به دامنه درخواست کمک می کند.
دامنه برنامه (تک نسخه)
یک نمونه واحد از وابستگی ایجاد می شود و در تمام درخواست ها در طول چرخه عمر برنامه به اشتراک گذاشته می شود. این اغلب با استفاده از متغیرهای سراسری یا ویژگی های سطح کلاس انجام می شود.
from fastapi import FastAPI, Depends
app = FastAPI()
# Singleton instance
GLOBAL_SETTING = {"api_key": "global_api_key"}
def get_global_setting():
return GLOBAL_SETTING
@app.get("/items/")
async def read_items(setting: dict = Depends(get_global_setting)):
return setting
هنگام استفاده از وابستگی های با دامنه برنامه با حالت قابل تغییر، احتیاط کنید، زیرا تغییرات ایجاد شده توسط یک درخواست می تواند بر درخواست های دیگر تأثیر بگذارد. اگر برنامه شما درخواست های همزمان دارد، ممکن است به مکانیسم های همگام سازی (قفل ها و غیره) نیاز باشد.
دامنه جلسه (داده های خاص کاربر)
وابستگی ها را با جلسات کاربری مرتبط کنید. این نیاز به یک مکانیزم مدیریت جلسه (به عنوان مثال، استفاده از کوکی ها یا JWT) دارد و معمولاً شامل ذخیره وابستگی ها در داده های جلسه است.
from fastapi import FastAPI, Depends, Cookie
from typing import Optional
import uuid
app = FastAPI()
# In a real app, store sessions in a database or cache
sessions = {}
async def get_user_id(session_id: Optional[str] = Cookie(None)) -> str:
if session_id is None or session_id not in sessions:
session_id = str(uuid.uuid4())
sessions[session_id] = {"user_id": str(uuid.uuid4())} # Assign a random user ID
return sessions[session_id]["user_id"]
@app.get("/profile/")
async def read_profile(user_id: str = Depends(get_user_id)):
return {"user_id": user_id}
تست وابستگی ها
یکی از مزایای اصلی تزریق وابستگی، بهبود قابلیت تست است. با جدا کردن اجزا، می توانید به راحتی وابستگی ها را با ماکت ها یا stub ها در طول آزمایش جایگزین کنید.
1. لغو وابستگی ها در تست ها
همانطور که قبلاً نشان داده شد، مکانیزم dependency_overrides FastAPI برای آزمایش ایده آل است. وابستگی های ساختگی ایجاد کنید که نتایج قابل پیش بینی را برمی گرداند و از آنها برای جدا کردن کد خود در حال آزمایش استفاده کنید.
from fastapi.testclient import TestClient
from fastapi import FastAPI, Depends
app = FastAPI()
# Original dependency
def get_external_data():
# Simulate fetching data from an external API
return {"data": "Real external data"}
@app.get("/data/")
async def read_data(data: dict = Depends(get_external_data)):
return data
# Test
from unittest.mock import MagicMock
def get_external_data_mock():
return {"data": "Mocked external data"}
def test_read_data():
app.dependency_overrides[get_external_data] = get_external_data_mock
client = TestClient(app)
response = client.get("/data/")
assert response.status_code == 200
assert response.json() == {"data": "Mocked external data"}
# Clean up overrides
app.dependency_overrides.clear()
2. استفاده از کتابخانه های ساختگی
کتابخانه هایی مانند unittest.mock ابزارهای قدرتمندی را برای ایجاد اشیاء ساختگی و کنترل رفتار آنها فراهم می کنند. شما می توانید از ماکت ها برای شبیه سازی وابستگی های پیچیده استفاده کنید و تأیید کنید که کد شما به درستی با آنها تعامل دارد.
import unittest
from unittest.mock import MagicMock
# (Define the FastAPI app and get_external_data as above)
class TestReadData(unittest.TestCase):
def test_read_data_with_mock(self):
# Create a mock for the get_external_data dependency
mock_get_external_data = MagicMock(return_value={"data": "Mocked data from unittest"})
# Override the dependency with the mock
app.dependency_overrides[get_external_data] = mock_get_external_data
client = TestClient(app)
response = client.get("/data/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"data": "Mocked data from unittest"})
# Assert that the mock was called
mock_get_external_data.assert_called_once()
# Clean up overrides
app.dependency_overrides.clear()
3. تزریق وابستگی برای تست واحد (خارج از زمینه FastAPI)
حتی هنگام تست واحد توابع *خارج* از هندلرهای نقطه پایانی API، اصول تزریق وابستگی همچنان اعمال می شود. به جای تکیه بر Depends FastAPI، به صورت دستی وابستگی ها را به تابع در حال آزمایش تزریق کنید.
# Example function to test
def process_data(data_source):
data = data_source.fetch_data()
# ... process the data ...
return processed_data
class MockDataSource:
def fetch_data(self):
return {"example": "data"}
# Unit test
def test_process_data():
mock_data_source = MockDataSource()
result = process_data(mock_data_source)
# Assertions on the result
ملاحظات امنیتی با تزریق وابستگی
تزریق وابستگی، در حالی که مفید است، در صورت عدم اجرای دقیق، نگرانی های امنیتی بالقوه ای را معرفی می کند.
1. سردرگمی وابستگی
اطمینان حاصل کنید که وابستگی ها را از منابع معتبر می گیرید. یکپارچگی بسته را تأیید کنید و از مدیران بسته با قابلیت های اسکن آسیب پذیری استفاده کنید. این یک اصل کلی امنیت زنجیره تامین نرم افزار است، اما با DI تشدید می شود زیرا ممکن است اجزایی را از منابع مختلف تزریق کنید.
2. تزریق وابستگی های مخرب
مراقب وابستگی هایی باشید که ورودی خارجی را بدون اعتبارسنجی مناسب می پذیرند. یک مهاجم می تواند به طور بالقوه کد یا داده های مخرب را از طریق یک وابستگی به خطر افتاده تزریق کند. تمام ورودی های کاربر را پاکسازی کنید و مکانیسم های اعتبارسنجی قوی را پیاده سازی کنید.
3. نشت اطلاعات از طریق وابستگی ها
اطمینان حاصل کنید که وابستگی ها به طور ناخواسته اطلاعات حساس را در معرض دید قرار نمی دهند. کد و پیکربندی وابستگی های خود را بررسی کنید تا آسیب پذیری های احتمالی نشت اطلاعات را شناسایی کنید.
4. اسرار کدنویسی شده
از کدنویسی اسرار (کلیدهای API، رمزهای عبور پایگاه داده و غیره) به طور مستقیم در کد وابستگی خود اجتناب کنید. از متغیرهای محیطی یا ابزارهای مدیریت پیکربندی ایمن برای ذخیره و مدیریت اسرار استفاده کنید.
import os
from fastapi import FastAPI, Depends
app = FastAPI()
def get_api_key():
api_key = os.environ.get("API_KEY")
if not api_key:
raise ValueError("API_KEY environment variable not set.")
return api_key
@app.get("/secure_endpoint/")
async def secure_endpoint(api_key: str = Depends(get_api_key)):
# Use api_key for authentication/authorization
return {"message": "Access granted"}
بهینه سازی عملکرد با تزریق وابستگی
تزریق وابستگی در صورت عدم استفاده محتاطانه می تواند بر عملکرد تأثیر بگذارد. در اینجا چند استراتژی بهینه سازی وجود دارد:
1. به حداقل رساندن هزینه ایجاد وابستگی
در صورت امکان از ایجاد وابستگی های گران قیمت در هر درخواست خودداری کنید. اگر یک وابستگی بدون حالت است یا می تواند در بین درخواست ها به اشتراک گذاشته شود، از استفاده از دامنه تک نسخه یا ذخیره سازی نمونه وابستگی خودداری کنید.
2. مقداردهی اولیه تنبل
وابستگی ها را فقط در صورت نیاز مقداردهی اولیه کنید. این می تواند زمان راه اندازی و مصرف حافظه را کاهش دهد، به ویژه برای برنامه هایی با وابستگی های زیاد.
3. ذخیره سازی نتایج وابستگی
نتایج محاسبات وابستگی گران قیمت را در صورت احتمال استفاده مجدد از نتایج ذخیره کنید. از مکانیزم های ذخیره سازی (به عنوان مثال، Redis، Memcached) برای ذخیره و بازیابی نتایج وابستگی استفاده کنید.
4. بهینه سازی نمودار وابستگی
نمودار وابستگی خود را برای شناسایی گلوگاه های احتمالی تجزیه و تحلیل کنید. ساختار وابستگی را ساده کنید و در صورت امکان تعداد وابستگی ها را کاهش دهید.
5. وابستگی های ناهمزمان برای عملیات ورودی/خروجی محدود
هنگام انجام عملیات ورودی/خروجی مسدود کننده، مانند پرس و جوهای پایگاه داده یا تماس های API خارجی، از وابستگی های ناهمزمان استفاده کنید. این از مسدود کردن رشته اصلی جلوگیری می کند و پاسخگویی کلی برنامه را بهبود می بخشد.
بهترین شیوه ها برای تزریق وابستگی FastAPI
- وابستگی ها را ساده نگه دارید: هدف وابستگی های کوچک و متمرکز است که یک کار واحد را انجام می دهند. این باعث بهبود خوانایی، قابلیت تست و قابلیت نگهداری می شود.
- از اشارات نوع استفاده کنید: از اشارات نوع برای تعریف واضح انواع ورودی و خروجی مورد انتظار وابستگی ها استفاده کنید. این باعث بهبود وضوح کد می شود و به FastAPI اجازه می دهد تا بررسی نوع استاتیک را انجام دهد.
- وابستگی ها را مستند کنید: هدف و استفاده از هر وابستگی را مستند کنید. این به سایر توسعه دهندگان کمک می کند تا نحوه استفاده و نگهداری از کد شما را درک کنند.
- وابستگی ها را به طور کامل آزمایش کنید: تست های واحد را برای وابستگی های خود بنویسید تا اطمینان حاصل کنید که آنها همانطور که انتظار می رود رفتار می کنند. این به جلوگیری از اشکالات و بهبود قابلیت اطمینان کلی برنامه شما کمک می کند.
- از قراردادهای نامگذاری سازگار استفاده کنید: از قراردادهای نامگذاری سازگار برای وابستگی های خود استفاده کنید تا خوانایی کد را بهبود ببخشید.
- از وابستگی های دایره ای خودداری کنید: وابستگی های دایره ای می توانند منجر به کد پیچیده و دشوار برای اشکال زدایی شوند. کد خود را برای از بین بردن وابستگی های دایره ای بازسازی کنید.
- ظروف تزریق وابستگی را در نظر بگیرید (اختیاری): در حالی که تزریق وابستگی داخلی FastAPI برای اکثر موارد کافی است، استفاده از یک ظرف تزریق وابستگی اختصاصی (به عنوان مثال،
inject،autowire) را برای برنامه های پیچیده تر در نظر بگیرید.
نتیجه
سیستم تزریق وابستگی FastAPI یک ابزار قدرتمند است که باعث ارتقای ماژولار بودن، قابلیت تست و استفاده مجدد می شود. با تسلط بر تکنیک های پیشرفته، مانند استفاده از کلاس ها به عنوان وابستگی، لغو وابستگی ها و استفاده از contextvars، می توانید API های قوی و مقیاس پذیر بسازید. درک دامنه ها و چرخه های عمر وابستگی برای مدیریت موثر منابع بسیار مهم است. همیشه آزمایش دقیق وابستگی های خود را برای اطمینان از قابلیت اطمینان و امنیت برنامه های خود در اولویت قرار دهید. با پیروی از بهترین شیوه ها و در نظر گرفتن پیامدهای امنیتی و عملکردی بالقوه، می توانید از پتانسیل کامل سیستم تزریق وابستگی FastAPI استفاده کنید.