شب ۵: ماژول، محیط مجازی و مدل داده با dataclass
فهرست نوشته
برنامهٔ فهرست سرورها اکنون چند مسئولیت دارد: ساخت داده، تصمیمگیری و نمایش. در این شب کد قابل استفاده را از نقطهٔ اجرای برنامه جدا میکنیم، بستههای پروژه را در محیط مجازی نگه میداریم و ساختار سرور را با نشانههای نوع و dataclass روشن میکنیم. در پایان میتوانید منطق سلامت را از فایل دیگری فراخوانی کنید، بدون اینکه دریافت ورودی یا چاپ ناخواسته آغاز شود.
هر فایل لازم نیست همهٔ کارها را انجام دهد#
در مثال ما، ماژول یک فایل پایتون است که تعریفهایی مانند تابع را نگه میدارد. دستور from inventory_tools import build_server نام تابع را از ماژول وارد میکند. پایتون هنگام نخستین واردکردن، کد سطح بالای ماژول را هم اجرا میکند؛ بنابراین بهتر است فایل ابزار فقط تعریف داشته باشد و گفتوگو با کاربر را به فایل اصلی بسپاریم. در غیر این صورت، یک import ساده ممکن است منتظر ورودی بماند.
پوشهٔ ~/python-backend/night-05 را بسازید. فایل inventory_tools.py را با دو تابع زیر پر کنید. تابع نخست رکورد میسازد و تابع دوم نمایش متنی آن را تولید میکند. هیچکدام خودشان print() یا input() ندارند. این جداسازی به ما اجازه میدهد بعدها همان رکورد را بهجای چاپ در ترمینال، در فایل یا پاسخ شبکه استفاده کنیم.
def build_server(hostname, address, role):
return {"hostname": hostname, "address": address, "role": role}
def format_server(server):
return f'{server["hostname"]} | {server["address"]} | {server["role"]}'
در همان پوشه فایل main.py را بسازید. نام ماژول در import پسوند .py ندارد. از نامهایی مثل json.py یا requests.py برای فایل تمرین استفاده نکنید، چون ممکن است هنگام import بهجای کتابخانهٔ موردنظر، فایل محلی خودتان پیدا شود. پیام خطای import را با توجه به نام فایل و محل اجرای فرمان بخوانید.
from inventory_tools import build_server, format_server
servers = [
build_server("pve01", "192.168.10.11", "hypervisor"),
build_server("nexus01", "192.168.10.20", "registry"),
]
for server in servers:
print(format_server(server))
محیط مجازی چه چیزی را جدا میکند؟#
محیط مجازی یا virtual environment محل مستقلی برای نصب بستههای یک پروژه فراهم میکند. اگر دو پروژه نسخههای متفاوت یک کتابخانه را بخواهند، لازم نیست وابستگی آنها را در یک محیط سراسری مخلوط کنید. این محیط ماشین مجازی یا کانتینر نیست و فایلها و شبکهٔ سیستم را جدا نمیکند. کاربردش در این مرحله انتخاب مفسر و محل بستههای پروژه است.
cd ~/python-backend/night-05
python3 -m venv .venv
source .venv/bin/activate
command -v python
python --version
python -m pip --version
python main.py
مسیر python باید به .venv/bin/python همین پروژه برسد و اجرای برنامه دو ردیف سرور را چاپ کند. اگر ساخت محیط در Ubuntu بهعلت نبود پشتیبانی venv شکست خورد، بستهٔ python3-venv را با مدیر بستهٔ سیستم نصب و دوباره تلاش کنید. نصب بستهٔ سیستم به دسترسی مدیریتی نیاز دارد؛ نصب وابستگیهای پروژه داخل محیط مجازی چنین نیازی ندارد.
sudo apt install python3-venv
پس از فعالسازی، از python -m pip استفاده کنید تا همان مفسر انتخابشده، pip خود را اجرا کند. فرمان مستقل pip ممکن است به نصب دیگری تعلق داشته باشد. برای خروج از محیط، deactivate را بزنید. در ترمینال تازه باید محیط را دوباره فعال کنید؛ فعالسازی یک ترمینال به همهٔ پنجرهها منتقل نمیشود.
python -m pip list
python -m pip freeze > requirements.txt
فرمان دوم نسخهٔ بستههای نصبشده را ثبت میکند. علامت > فایل قبلی را جایگزین میکند، پس این کار را در محیط اختصاصی و تمیز پروژه انجام دهید. برای نصب دوباره از python -m pip install -r requirements.txt استفاده میشود. این فهرست به بازسازی محیط کمک میکند، اما بهتنهایی قفل کامل همهٔ تفاوتهای سیستمعامل و فایلهای توزیع بسته نیست.
نوعها قرارداد را خواناتر میکنند#
نشانهٔ نوع یا type hint توضیح میدهد هر ورودی و نتیجه چه نوعی انتظار دارد. در def build_url(address: str, port: int) -> str دو ورودی بهترتیب رشته و عدد صحیحاند و نتیجه رشته است. ویرایشگر و ابزار بررسی ایستا میتوانند از این اطلاعات استفاده کنند. خود پایتون صرفاً بهخاطر این نشانهها مقدار نامناسب را هنگام اجرا رد نمیکند.
این تفاوت را جدی بگیرید: annotation یا نشانهٔ نوع، اعتبارسنجی ورودی نیست. تابعی با port: int همچنان ممکن است رشته دریافت کند و بسته به بدنه تا مدتی هم اجرا شود. برای دادهٔ کاربر، شرط، تبدیل یا کتابخانهٔ اعتبارسنجی لازم است. در شب هشتم Pydantic را میبینیم که واقعاً هنگام اجرای درخواست داده را بررسی میکند.
در typed_inventory.py ساختار زیر را بنویسید. کلاس قالب ساختن شیءهایی با ویژگیهای مشخص است. فعلاً لازم نیست همهٔ جزئیات شیءگرایی را بدانید؛ @dataclass از تعریف فیلدها، روش سازندهٔ __init__() و نمایش و مقایسهٔ معمول را تولید میکند تا لازم نباشد این کد تکراری را خودمان بنویسیم.
from dataclasses import asdict, dataclass
@dataclass
class Server:
hostname: str
address: str
role: str
online: bool = True
def format_server(server: Server) -> str:
status = "UP" if server.online else "DOWN"
return f"{server.hostname} | {server.address} | {server.role} | {status}"
servers: list[Server] = [
Server("pve01", "192.168.10.11", "hypervisor"),
Server("nexus01", "192.168.10.20", "registry", False),
]
for server in servers:
print(format_server(server))
print(asdict(servers[0]))
ویژگی را با server.hostname میخوانیم؛ این با کلید دیکشنری فرق دارد. online مقدار پیشفرض True دارد و میتوان آن را هنگام ساخت حذف کرد. عبارت شرطی یکخطی، UP را در حالت درست و DOWN را در حالت دیگر برمیگزیند. asdict() هم نمونهٔ dataclass را به دیکشنری تبدیل میکند؛ هنوز JSON تولید نشده و برای تبدیل به متن باید از json استفاده شود.
تمرین و بررسی مرز مسئولیتها#
در health.py تابع دستهبندی سلامت شب سوم را قرار دهید و در app.py آن را import کنید. فهرستی با سه سرویس بسازید و گزارش بگیرید. با فرمان python -c 'from health import classify_service; print(classify_service(200, 80))' باید فقط HEALTHY چاپ شود. اگر هنگام import پیامهای اضافی یا درخواست ورودی دیدید، هنوز کد اجرایی نامناسبی در ماژول ابزار مانده است.
سپس در service_models.py یک dataclass به نام Service با فیلدهای name، port، status_code و response_ms بسازید. تابع classify_service(service: Service) -> str همان قرارداد قبلی را با دسترسی نقطهای اجرا کند. نمونههای grafana با زمان 80، nexus با 450 و graylog با کد 503 بسازید؛ انتظارها بهترتیب سالم، کند و قطع هستند. مرز دقیق 1000 با کد 200 باید DEGRADED باشد.
یک نمونه را با asdict() چاپ کنید و تفاوت نمایش شیء با دیکشنری را ببینید. برای خودآزمایی توضیح دهید محیط مجازی چه چیزی را جدا میکند، import چه تعریفی را وارد میکند و چرا dataclass با وجود نشانههای نوع، جای اعتبارسنجی درخواست را نمیگیرد. پاسخ این سه پرسش برای ورود به بخش HTTP ضروری است.
برای بررسی محیط، مسیر گزارششدهٔ pip را هم با مسیر مفسر مقایسه کنید. اگر بسته نصب شده ولی import پیدا نمیشود، معمولاً یکی از نخستین احتمالها تفاوت مفسر نصب و اجراست. فرمان را از همان پوشه و همان ترمینال فعالشده تکرار کنید. پاککردن محیط یا نصب سراسری اولین راهحل نیست؛ ابتدا مشخص کنید کدام Python واقعاً فایل را اجرا میکند.