ارزشیابی پایانی
این جلسه، جلسه آزمون است و مطلب تازهای ندارد. این فصل برای مرور نوشته شده است: خلاصه فصل ۳۰ تا ۳۹، راهنمای دفاع از پروژه پایانی، یک نمونه آزمون کامل، و در پایان نقشه راهی برای ادامه مسیر بعد از این دوره.
۱. چه چیزهایی ارزشیابی میشود
| فصل | موضوع | وزن تقریبی |
|---|---|---|
| ۳۰ | SQL: جدول، شرط، تجمیع، JOIN | زیاد |
| ۳۱ | sqlite3، پارامتر، تراکنش | زیاد |
| ۳۲ | ORM، SQLAlchemy، رابطهها | زیاد |
| ۳۳ | الگوهای طراحی | متوسط |
| ۳۴ و ۳۵ | tkinter، چیدمان، رویداد، Treeview | زیاد |
| ۳۶ | PySide6، Designer، PyInstaller | متوسط |
| ۳۷ | نخ، قفل، صف، asyncio | متوسط |
| ۳۸ | Flask: مسیر، قالب، فرم، نشست، API | زیاد |
| ۳۹ | معماری پروژه، Git و GitHub | زیاد |
۲. مرور پایگاه داده و ORM
| سؤال | پاسخ کوتاه |
|---|---|
| کلید اصلی و کلید خارجی چه فرقی دارند | اولی هر سطر را یکتا میکند، دومی به سطری در جدول دیگر اشاره میکند |
WHERE و HAVING | اولی روی سطرها پیش از گروهبندی، دومی روی گروهها بعد از آن |
سه نوع JOIN | INNER فقط مشترکها، LEFT همه چپ، RIGHT همه راست |
چرا پارامتر ? | تنها دفاع در برابر تزریق SQL؛ مقدار هرگز به کد تبدیل نمیشود |
| تراکنش یعنی چه | چند دستور که یا همه انجام میشوند یا هیچکدام |
PRAGMA foreign_keys = ON | در هر اتصال لازم است؛ بدون آن ON DELETE هیچ کاری نمیکند |
| ORM چیست | جدول به کلاس، سطر به شیء، ستون به صفت |
flush و commit | اولی شناسه میسازد، دومی تراکنش را میبندد |
| مسئله N+1 | یک پرسوجو برای هر سطر؛ درمانش selectinload یا joinedload |
۳. مرور رابط گرافیکی
| موضوع | tkinter | Qt (PySide6) |
|---|---|---|
| شروع | Tk() و mainloop() | QApplication و app.exec() |
| پاسخ به کلیک | command=func | button.clicked.connect(func) |
| چیدمان | pack و grid | QVBoxLayout و خانوادهاش |
| جدول | ttk.Treeview | QTableWidget |
| پنجره دوم | Toplevel + grab_set + wait_window | QDialog.exec() |
| بستن پنجره | protocol('WM_DELETE_WINDOW', f) | closeEvent |
| کار زمانبندیشده | after() | QTimer |
سه قانونی که در هر دو یکساناند: ویجت را با مختصات پیکسلی نچینید؛ هرگز از نخ دیگری به ویجت دست نزنید؛ و ویجت را انبار داده خود نکنید.
۴. مرور همروندی
| برنامه وقتش را صرف چه میکند | ابزار |
|---|---|
| انتظار، با تعداد کم کار | threading یا ThreadPoolExecutor |
| انتظار، با صدها یا هزاران کار | asyncio |
| محاسبه | ProcessPoolExecutor |
- شرایط رقابتی:
counter += 1سه قدم است. دو نخ که روی یک متغیر مینویسند، به قفل نیاز دارند. - صف به قفل نیاز ندارد، چون خودش امن است. داده را از راه صف رد و بدل کنید.
- GIL: هر لحظه یک نخ بایتکد اجرا میکند. برای محاسبه، نخ بیفایده است.
- asyncio: هرگز تابع مسدودکننده در کوروتین صدا نزنید.
asyncio.sleep، نهtime.sleep. - استثنای داخل یک
Futureتا وقتیresult()را نخوانید دیده نمیشود.
۵. مرور وب
| موضوع | نکته کلیدی |
|---|---|
@app.route | آدرس به تابع نما وصل میشود |
url_for | هرگز لینک را دستی ننویسید |
| ۴۰۴ در برابر ۵۰۰ | «وجود ندارد» در برابر «من اشتباه کردم» |
| گریز خودکار Jinja | محافظ XSS؛ safe روی داده کاربر ممنوع |
| POST / Redirect / GET | تا F5 فرم را دوباره نفرستد |
| نشست | کوکی امضاشده ولی خواندنی |
| رمز عبور | فقط درهم ذخیره میشود، هرگز خام |
| کد وضعیت API | ۲۰۱ برای ساخت، ۴۰۰ برای خطای مشتری، ۲۰۴ برای حذف |
debug=True | هرگز روی سرور واقعی |
۶. دفاع از پروژه پایانی
در این جلسه پروژه پایانیتان را ارائه و از آن دفاع میکنید. سؤالهایی که تقریباً حتماً پرسیده میشوند:
- برنامهتان را در سه جمله توصیف کنید. چه مسئلهای را حل میکند؟
- یک کلیک را از دکمه تا پایگاه داده دنبال کنید و همه توابعی را که اجرا میشوند نام ببرید.
- یک قانون کسبوکار پروژهتان را نشان دهید و بگویید دقیقاً کجا نوشته شده است. اگر در دو جا نوشته شده، چرا؟
- اگر بخواهیم همین برنامه را فردا به شکل یک سایت دربیاوریم، چه بخشی از کد را باید دوباره بنویسید؟
- کدام عمل برنامه شما باید حتماً در یک تراکنش باشد و چرا؟
- یک ورودی اشتباه به من نشان دهید و بگویید برنامه با آن چه میکند.
- در
.gitignoreچه گذاشتهاید و هر خطش جلوی چه اشتباهی را میگیرد؟ - تاریخچه کامیتهایتان را نشان دهید. پیامها گویا هستند؟
برنامهای که ده امکان دارد و هنگام کلیک اول میترکد، از برنامهای با چهار امکان که همهشان بینقص کار میکنند ضعیفتر است. اول کار کردن، بعد کامل بودن. پیش از ارائه، برنامه را روی یک پوشه خالی و تازه یک بار کامل اجرا کنید.
۷. چکلیست آمادگی
| باید بتوانم | |
|---|---|
| ۱ | جدولی با کلید خارجی بسازم و سه نوع JOIN را بنویسم |
| ۲ | از پایتون به SQLite وصل شوم و چهار عمل اصلی را با پارامتر بنویسم |
| ۳ | یک عمل دومرحلهای را در یک تراکنش امن کنم |
| ۴ | دو مدل SQLAlchemy با رابطه یکبهچند تعریف کنم |
| ۵ | یک زنجیره if/elif را با الگوی استراتژی بازنویسی کنم |
| ۶ | فرمی با اعتبارسنجی کامل در tkinter بسازم |
| ۷ | یک Treeview را از داده واقعی پر کنم و مرتبش کنم |
| ۸ | دیالوگ مودالی بنویسم که مقدار برگرداند |
| ۹ | یک پنجره PySide6 با سیگنال و اسلات بسازم |
| ۱۰ | برنامهام را به یک فایل اجرایی تبدیل کنم و بدانم فایل داده کجا نوشته میشود |
| ۱۱ | کار طولانی را به نخ ببرم و از راه صف با پنجره حرف بزنم |
| ۱۲ | چند کوروتین را با gather همزمان اجرا کنم |
| ۱۳ | سایتی با قالب پایه، فرم و هدایت بعد از POST بنویسم |
| ۱۴ | ورود کاربر با رمز درهمشده و دکوراتور محافظ بسازم |
| ۱۵ | یک REST API با کدهای وضعیت درست بنویسم |
| ۱۶ | پروژهای را با core/ و gui/ لایهبندی کنم |
| ۱۷ | مخزنی بسازم، شاخه بزنم، تعارض را حل کنم و روی GitHub منتشر کنم |
۸. اشتباههای پرتکرار
| اشتباه | درستش |
|---|---|
| ساختن SQL با f-string | پارامتر ? یا پارامتر ORM |
فراموش کردن commit | هیچ خطایی نمیگیرید، فقط داده غیبش میزند |
خواندن fetchone() بدون بررسی None | if row is None: ... |
| دو دستور با دو commit جدا | یک تراکنش |
| خواندن داده از داخل ویجت | داده واقعی را در پایتون نگه دارید |
command=func() با پرانتز | command=func |
لامبدا در حلقه بدون x=x | اتصال دیرهنگام؛ مقدار را منجمد کنید |
قاطی کردن pack و grid | برنامه بیصدا قفل میشود |
گم شدن مرجع PhotoImage | روی self نگهش دارید |
time.sleep در تابع پاسخ یا کوروتین | after()، نخ، یا asyncio.sleep |
| دست زدن به ویجت از نخ کارگر | از راه صف پیام بفرستید |
نخواندن future.result() | خطا برای همیشه پنهان میماند |
قورت دادن CancelledError | بگیرید، تمیزکاری کنید، دوباره پرتابش کنید |
| رندر کردن صفحه بعد از POST موفق | هدایت کنید |
| صفحه ۵۰۰ برای ورودی غلط کاربر | ۴۰۴ یا ۴۰۰ |
| ذخیره رمز عبور خام | فقط درهم |
کامیت کردن .venv و .db و توکن | .gitignore پیش از اولین کامیت |
نوشتن فایل داده کنار __file__ در برنامه منجمد | کنار sys.executable |
۹. نمونه آزمون تشریحی
بخش اول: مفهومی، هر سؤال یک نمره
- تفاوت
INNER JOINوLEFT JOINرا با یک مثال از جدول کتاب و امانت توضیح دهید. - چرا
WHEREنمیتواند روی نتیجهCOUNTشرط بگذارد؟ چه چیزی میتواند؟ - یک عملیات در برنامهتان نام ببرید که باید داخل یک تراکنش انجام شود و توضیح دهید اگر تراکنش به درستی مدیریت نشود، چه مشکلی ممکن است برای دادهها ایجاد شود.
- سه سود ORM و دو موردی که SQL خام بهتر است را بنویسید.
- الگوی استراتژی چه مسئلهای را حل میکند و کی نباید به وظیفه آن برد؟
- چرا در tkinter نباید
packوgridرا در یک ظرف قاطی کرد؟ علامت این اشتباه چیست؟ - «ویجت انبار داده شما نیست» یعنی چه؟ یک باگ واقعی از این جنس شرح دهید.
- سیگنال و اسلات در Qt با
commandدر tkinter چه فرقی دارد؟ یک مزیت Qt بنویسید. - چرا PyInstaller نمیتواند روی لینوکس فایل اجرایی ویندوز بسازد؟
- تفاوت
resource_pathوdata_pathرا توضیح دهید و بگویید هر کدام کجا به کار میآیند. - شرایط رقابتی را با مثال
counter += 1توضیح دهید. چرا باگی است که ممکن است ماهها پیدا نشود؟ - چرا
queue.Queueبه قفل نیاز ندارد ولی یک لیست ساده دارد؟ - GIL چیست و چه تصمیمی را در طراحی برنامه شما عوض میکند؟
- چرا
time.sleepدر یک کوروتین یک باگ است و نه فقط یک ناکارآمدی؟ - قاعده POST / Redirect / GET چیست و جلوی چه اتفاقی را میگیرد؟
- چرا فیلتر
safeروی داده کاربر یک حفره امنیتی است؟ - چرا نباید رمز عبور را ذخیره کرد؟ به جایش چه ذخیره میشود و چرا امنتر است؟
- تفاوت ۴۰۰ و ۴۰۴ و ۵۰۰ را با یک مثال از یک API بنویسید.
- چرا در پروژه پایانی،
core/هیچ چیزی از PySide6 وارد نمیکند؟ - رمزی را کامیت و پوش کردهاید. چرا پاک کردن آن خط کافی نیست؟
بخش دوم: خروجی یا اشکال کد را بنویسید
۲۱. این کد چه چاپ میکند و چرا؟
import threading
counter = 0
def work():
global counter
for _ in range(100_000):
counter += 1
threads = [threading.Thread(target=work) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter)
۲۲. اشکال این تابع کجاست؟
def borrow(conn, book_id, member_id):
conn.execute('INSERT INTO borrows (book_id, member_id) VALUES (?, ?)',
(book_id, member_id))
conn.commit()
conn.execute('UPDATE books SET available = 0 WHERE id = ?', (book_id,))
conn.commit()
۲۳. چرا هر چهار دکمه یک کار میکنند؟
for name in ('add', 'sub', 'mul', 'div'):
button = tk.Button(root, text=name,
command=lambda: run(name))
button.pack()
۲۴. این تابع نما چه کد وضعیتی برمیگرداند وقتی مشتری بدنه JSON نفرستد؟ چه باید برگرداند؟
@app.route('/api/books', methods=['POST'])
def create_book():
payload = request.get_json()
title = payload['title']
...
۲۵. چرا این کوروتین برنامه را کند میکند؟
async def fetch_all(urls):
results = []
for url in urls:
results.append(await fetch(url))
return results
۱۰. نمونه آزمون عملی
سؤال یک: پایگاه داده (۵ نمره)
یک پایگاه داده clinic.db با سه جدول بسازید: بیمار،
پزشک و نوبت. کلیدهای خارجی را با ON DELETE درست
تعریف کنید. سپس با SQLAlchemy مدلهایشان را بنویسید و پرسوجویی
بسازید که برای هر پزشک، تعداد نوبتهای امروزش را بدهد.
سؤال دو: تراکنش (۳ نمره)
تابع book_appointment(patient, doctor, when) را
بنویسید که نوبت را ثبت کند و فقط در صورتی موفق شود که پزشک در آن
ساعت آزاد باشد. بررسی و ثبت باید اتمی باشند.
توضیح دهید اگر دو کاربر همزمان دکمه را بزنند چه میشود.
سؤال سه: رابط گرافیکی (۵ نمره)
پنجرهای با دو زبانه بسازید: زبانه بیماران با جدول، جستوجوی زنده و سه دکمه افزودن و ویرایش و حذف؛ و زبانه نوبتها که نوبتهای گذشته را قرمز نشان دهد. فرم افزودن باید یک دیالوگ مودال باشد که داده اعتبارسنجیشده برمیگرداند. هیچ خط SQL نباید در فایل رابط کاربری باشد.
سؤال چهار: همروندی (۳ نمره)
به همان برنامه، دکمه «پشتیبانگیری» اضافه کنید که کار را در یک نخ انجام دهد، پیشرفت را از راه یک صف به پنجره بدهد، و پنجره در تمام مدت پاسخگو بماند. دکمه باید در حین کار غیرفعال شود.
سؤال پنج: وب (۴ نمره)
یک REST API برای همان نوبتها بنویسید با چهار آدرس: فهرست، ساخت،
گرفتن یکی و حذف. کدهای وضعیت باید درست باشند و بدنه غیر JSON باید
۴۰۰ بگیرد نه ۵۰۰. بعد یک مشتری requests با
timeout برایش بنویسید.
۱۱. پاسخ کوتاه سؤالهای مفهومی
این بخش را فقط بعد از اینکه سؤالهای بالا را جواب دادید بخوانید. خواندن جواب پیش از تلاش، هیچ چیزی یاد نمیدهد.
| سؤال | پاسخ کوتاه |
|---|---|
| ۲۱ | عددی کمتر از ۵۰۰۰۰۰ و هر بار متفاوت. counter += 1 اتمی نیست و افزایشها گم میشوند. |
| ۲۲ | دو commit جدا. اگر دومی شکست بخورد، کتاب هم امانت داده شده و هم موجود میماند. باید یک تراکنش باشد، و بررسی «هنوز آزاد است؟» جزو خود UPDATE. |
| ۲۳ | اتصال دیرهنگام: همه لامبداها به یک متغیر name نگاه میکنند و مقدار نهایی div است. درستش lambda n=name: run(n). |
| ۲۴ | الان ۵۰۰ میدهد، چون payload برابر None است. باید ۴۰۰ بدهد؛ با get_json(silent=True) و بررسی. |
| ۲۵ | حلقه با await ترتیبی است. باید asyncio.gather به کار برود تا همه با هم اجرا شوند. |
| ۲ | چون WHERE پیش از گروهبندی اجرا میشود و هنوز گروهی وجود ندارد. HAVING این کار را میکند. |
| ۶ | چون مدیر چیدمان دوم با اولی بر سر اندازه ظرف بیپایان چانه میزند. علامتش این است که پنجره بدون هیچ خطایی قفل میشود. |
| ۱۲ | چون put و get قفل را خودشان میگیرند؛ یک لیست ساده هیچ محافظتی ندارد. |
| ۲۰ | چون رمز در تاریخچه مخزن باقی میماند. باید باطلش کنید و رمز تازه بگیرید. |
۱۲. نقشه راه ادامه مسیر
این دوره تمام شد، ولی یادگیری برنامهنویسی با دوره تمام نمیشود. چیزهایی که منطقاً بعد از اینجا میآیند:
| مسیر | قدم بعدی |
|---|---|
| کیفیت کد | آزمون خودکار با pytest، که موضوع دوره جداگانهای است |
| وب | Django یا FastAPI، بعد از Flask این دوره |
| داده | pandas، numpy و matplotlib |
| پایگاه داده | PostgreSQL، مهاجرت ساختار با Alembic، ایندکسگذاری |
| استقرار | Docker، سرور لینوکسی، انتشار خودکار |
| دسکتاپ | Qt پیشرفته: مدل و نما، نخهای Qt، ترجمه |
| مبانی | ساختمان داده و الگوریتم، که در هر زبانی به کارتان میآید |
بهترین راه ادامه دادن، ساختن است. برنامهای بنویسید که خودتان واقعاً به آن نیاز دارید، هر چقدر هم کوچک. یک پروژه تمامشده که روزی ازش استفاده میکنید، از ده دوره تماشاشده بیشتر یاد میدهد. کدتان را روی GitHub بگذارید، کد دیگران را بخوانید، و از نوشتن کد بد در قدم اول نترسید: هر برنامهنویسی از همانجا شروع کرده است.
پوشه S40-evaluation فقط یک README.md
دارد و هیچ فایل تمرینی ندارد، چون این جلسه، جلسه آزمون است. برای
تمرین، به پوشههای S30 تا S39 برگردید.