با زیاد شدن سرورها، نگه‌داشتن نام و آدرس و وضعیت در متغیرهای پراکنده دشوار می‌شود. در این شب هر سرور را یک رکورد در نظر می‌گیریم و چند رکورد را در فهرست قرار می‌دهیم. سپس تصمیم‌گیری دربارهٔ سلامت سرویس را به تابع می‌سپاریم تا بتوانیم همان منطق را برای چند ورودی و بعداً در API به کار ببریم.

فهرست برای چند عضو، دیکشنری برای یک رکورد#

فهرست یا list دنباله‌ای مرتب از مقدارهاست. می‌توانید عضوها را با شمارهٔ موقعیت بخوانید و با append() عضو تازه‌ای به انتها اضافه کنید. شماره‌گذاری از صفر شروع می‌شود؛ بنابراین عضو نخست services[0] است. تابع len() تعداد عضوها را می‌دهد؛ این تعداد با آخرین شمارهٔ موقعیت یکسان نیست. فهرستی با سه عضو، موقعیت‌های صفر تا دو دارد.

services = ["keycloak", "nexus", "grafana"]
print(services[0])
print(len(services))
services.append("graylog")

دیکشنری یا dict مقدارها را با کلید نگه می‌دارد. کلیدی مثل hostname معنای داده را روشن می‌کند و خواننده لازم نیست بداند نام میزبان در چندمین جای یک فهرست قرار دارد. در این دوره کلیدهای رشته‌ای استفاده می‌کنیم. کلید را دقیق بنویسید؛ hostname و host_name دو نام متفاوت‌اند و خواندن کلید ناموجود با کروشه، KeyError ایجاد می‌کند.

server = {
    "hostname": "pve01",
    "address": "192.168.10.11",
    "role": "hypervisor",
    "online": True,
}
print(server["hostname"])

اکنون می‌توانیم فهرستی از دیکشنری‌ها داشته باشیم: لایهٔ بیرونی چند سرور را نگه می‌دارد و هر دیکشنری ویژگی‌های یک سرور را. عبارت servers[0] نخستین دیکشنری و عبارت servers[0]["hostname"] نام میزبان همان رکورد را می‌دهد. این ساختار بعداً به‌صورت طبیعی به آرایه‌ای از شیءها در JSON تبدیل می‌شود؛ JSON قالب متنی تبادل داده است که در شب چهارم می‌خوانیم.

پوشهٔ ~/python-backend/night-03 را بسازید و نمونهٔ زیر را در inventory.py قرار دهید. در اینجا فهرست اولیه دو عضو دارد و عضو سوم با append() اضافه می‌شود. سپس حلقه، هر دیکشنری را به نام محلی server در اختیار بدنه قرار می‌دهد. تغییر این نام به معنی ساختن کپی مستقل از رکورد نیست؛ فعلاً فقط مقادیر را می‌خوانیم.

servers = [
    {"hostname": "pve01", "address": "192.168.10.11",
     "role": "hypervisor", "online": True},
    {"hostname": "nexus01", "address": "192.168.10.20",
     "role": "registry", "online": False},
]
servers.append({
    "hostname": "graylog01", "address": "192.168.10.30",
    "role": "logging", "online": True,
})

for server in servers:
    if server["online"]:
        status = "UP"
    else:
        status = "DOWN"
    print(f'{server["hostname"]} | {server["address"]} | '
          f'{server["role"]} | {status}')

print(f"Total servers: {len(servers)}")
python3 inventory.py

خروجی مورد انتظار سه ردیف با وضعیت‌های UP، DOWN و UP و تعداد نهایی سه است. علامت‌های نقل‌قول بیرونی و داخلی در رشته‌های قالب‌دار را با دقت ببینید. استفاده از نقل‌قول تکی برای بیرون و دوتایی برای کلید دیکشنری، خوانایی مثال را بهتر کرده است. برای شروع، همین شیوه را یکدست نگه دارید تا هنگام افزودن فیلد تازه رشته را ناخواسته نبندید.

تابع برای بیان یک کار مشخص#

تابع مجموعه‌ای از دستورها با نام مشخص است. با def تعریفش می‌کنیم و هنگام فراخوانی، مقدارهای ورودی را به آن می‌دهیم. نام ورودی در تعریف، پارامتر و مقدار واقعی هنگام فراخوانی، آرگومان است. در def greet(name)، نام name پارامتر است؛ در greet("MJ")، رشتهٔ "MJ" آرگومان است. جداکردن این دو به فهم پیام‌های خطا و مستندات کمک می‌کند.

return نتیجه را به فراخواننده می‌دهد و اجرای همان تابع را تمام می‌کند. برنامه پس از محل فراخوانی ادامه پیدا می‌کند؛ کل برنامه متوقف نمی‌شود. در مقابل، print() فقط چیزی را نمایش می‌دهد. تابعی که تنها چاپ می‌کند و return ندارد، به‌طور ضمنی None برمی‌گرداند؛ این مقدار نشان‌دهندهٔ نبود نتیجهٔ مشخص است و با متن چاپ‌شده یکی نیست.

در health_functions.py تابع زیر را بنویسید. ورودی و خروجی آن روشن است و به ترمینال وابسته نیست. به همین دلیل بعداً می‌توان یک مسیر API، یک برنامهٔ خط فرمان یا یک آزمون خودکار را به همین تابع وصل کرد. دریافت داده و نحوهٔ نمایش، مسئولیت کد بیرون تابع می‌ماند.

def classify_service(status_code, response_ms):
    if status_code != 200:
        return "DOWN"
    if response_ms >= 1000:
        return "DEGRADED"
    if response_ms >= 300:
        return "SLOW"
    return "HEALTHY"


services = [
    {"name": "keycloak", "status_code": 200, "response_ms": 85},
    {"name": "nexus", "status_code": 200, "response_ms": 430},
    {"name": "graylog", "status_code": 503, "response_ms": 40},
]
for service in services:
    health = classify_service(service["status_code"], service["response_ms"])
    print(f'{service["name"]}: {health}')

پس از اجرای فایل، به‌ترتیب HEALTHY، SLOW و DOWN انتظار می‌رود. اینجا چند if مستقل مشکلی ایجاد نمی‌کنند، چون هر شاخهٔ مطابق بلافاصله با return از تابع خارج می‌شود. اگر فقط مقدار متغیر را عوض می‌کردیم و ادامه می‌دادیم، باید احتمال اجرای شاخه‌های بعدی را هم در نظر می‌گرفتیم. قاعدهٔ خروج زودهنگام را با ترتیب شرط‌ها کنار هم بخوانید.

تمرین ساخت داده و استفادهٔ دوباره از منطق#

در service_registry.py ابتدا فهرست خالی services بسازید. تعداد سرویس‌ها را بگیرید و در هر دور نام، درگاه و کد پاسخ را دریافت کنید. برای همان دور یک دیکشنری تازه با کلیدهای name، port و status_code بسازید و به فهرست اضافه کنید. ساختن دیکشنری داخل حلقه مهم است؛ قرار نیست همهٔ عضوها به یک رکورد مشترک اشاره کنند.

پس از پایان ورودی‌ها، حلقهٔ جداگانه‌ای برای نمایش بنویسید. کد 200 را UP و بقیه را DOWN چاپ کنید و تعداد نهایی را با len() نشان دهید. با یک سرویس سالم، دو سرویس با وضعیت متفاوت و فهرست خالی امتحان کنید. جدایی دریافت و گزارش اجازه می‌دهد بعداً بدون تغییر قالب داده، منبع ورودی را از ترمینال به فایل یا درخواست شبکه عوض کنیم.

تمرین دوم backup_health.py است. تابع classify_backup(age_hours) باید سن حداکثر 24 ساعت را FRESH، بیش از 24 تا خود 48 را STALE و بیشتر از آن را EXPIRED برگرداند. نام پشتیبان و سن آن را بیرون تابع بگیرید، نتیجه را ذخیره و سپس چاپ کنید. در این قرارداد، سن منفی مجاز نیست؛ فعلاً ورودی غیرمنفی بدهید و در شب بعد کنترلش کنید.

مقدارهای 8، 36 و 72 باید سه دستهٔ متفاوت بدهند. دو مرز 24 و 48 را هم بررسی کنید؛ به‌ترتیب تازه و کهنه هستند. این تمرین نشان می‌دهد «همیشه آستانهٔ بزرگ‌تر اول» یک قانون عمومی نیست. در این مسئله می‌توان از پایین به بالا بررسی کرد، چون شرط‌ها از جنس «کوچک‌تر یا مساوی» هستند. مهم این است که نخستین شاخهٔ مطابق دقیقاً دستهٔ مطلوب باشد.

برای بازبینی، به تابع نگاه کنید و بپرسید آیا بدون input() می‌توان آن را صدا زد و نتیجه گرفت. سپس توضیح دهید چرا چاپ نتیجه داخل تابع، استفاده از آن در پاسخ API را دشوارتر می‌کند. اگر می‌توانید بدون اجرای فایل، نتیجهٔ هر ورودی مرزی را دنبال کنید، ساختار داده و جریان تابع را درست فهمیده‌اید.

هنگام بازبینی تمرین، یک فیلد تازه مثل توضیح سرویس اضافه کنید و ببینید کدام بخش‌ها واقعاً نیاز به تغییر دارند. اگر تابع سلامت فقط کد پاسخ و زمان را می‌خواهد، افزودن توضیح نباید نتیجهٔ آن را عوض کند. این آزمایش کوچک نشان می‌دهد انتخاب ورودی‌های محدود و روشن، استفادهٔ دوباره از منطق را ساده‌تر می‌کند و تغییر نمایش را از تغییر تصمیم جدا نگه می‌دارد.

منابع#