آموزش برنامهنویسی
آموزش پایتون از صفر؛ یک برنامهٔ گزارش هزینه بساز
درس پروژهای پایتون با متغیر، نوع داده، شرط، حلقه، دیکشنری، تابع، CSV، خطا و تست. فایل کامل را اجرا کن و مرحلهبهمرحله تغییر بده.

تصویر اختصاصی مجلهٔ ساروی؛ ساختهشده با هوش مصنوعی
بهجای حفظ فهرستی از دستورهای پایتون، یک برنامه میسازیم: هزینهها را از CSV بخواند، برای هر دسته جمع بزند و اگر ردیفی خراب بود، شمارهٔ همان ردیف را بگوید. پروژه کوچک است، اما تقریباً همهٔ مفاهیم پایه در آن حضور دارند. در پایان میتوانی همین ابزار را برای دادهٔ خودت تغییر بدهی و دربارهٔ هر بخش کد توضیح بدهی.
این پروژه را با راهنمایی ساروی یاد بگیر۱. چه چیزی باید از قبل آماده باشد؟
به Python 3.10 یا جدیدتر، یک ویرایشگر متن و یک پوشهٔ خالی نیاز داری. این پروژه فقط از کتابخانهٔ استاندارد استفاده میکند؛ لازم نیست بستهای نصب کنی. در ترمینال، python3 --version را اجرا کن. در ویندوز ممکن است فرمان py یا python نام درست نصب تو باشد.
دو فایل expenses.py و expenses.csv را از همین صفحه بگیر و در یک پوشه بگذار. ابتدا برنامهٔ کامل را اجرا میکنیم تا مقصد روشن باشد، سپس آن را به قطعههای کوچک میشکنیم. اگر فرمان پیدا نشد، نصب پایتون یا مسیر آن را بررسی کن؛ تغییر کد برنامه این خطا را حل نمیکند.
python3 --version
python3 expenses.py expenses.csv
# books: 180,000 toman
# food: 200,000 toman
# transport: 45,000 toman
# TOTAL: 425,000 toman۲. متغیر، نام یک مقدار است
amount = 45000 یک عدد صحیح را به نام amount وصل میکند. category = "food" یک رشته است. علامت مساوی اینجا مقایسه نیست؛ مقداردهی است. برای مقایسه از == استفاده میکنیم.
در کد این پروژه مبلغها عدد صحیح به توماناند. با این قرارداد، جمع کردن دقیق است و اعشار نداریم. اگر بعداً ارز با واحد کوچکتر یا نرخ اعشاری وارد کردی، باید قرارداد داده و نوع محاسبه را عوض کنی؛ استفادهٔ بیفکر از float برای پول میتواند خطای گرد کردن بسازد.
نام خوب، معنی داده را روشن میکند. amount_toman از x بهتر است چون واحد هم معلوم است. لازم نیست همهٔ نامها طولانی باشند؛ index یا row در محدودهٔ یک حلقه معنی مشخصی دارند.
amount = 45000
category = "transport"
print(type(amount).__name__) # int
print(type(category).__name__) # str
print(amount == 45000) # True۳. رشتهٔ «45000» هنوز عدد نیست
دادهای که از CSV میآید متن است. int("45000") آن را به عدد صحیح تبدیل میکند. int("forty") خطا میدهد، چون تفسیر عددی مشخصی ندارد. حدس زدن مبلغ از متن خراب، رفتار مناسبی برای این ابزار نیست.
فضای اضافی ابتدا و انتهای مقدار را با strip حذف میکنیم. داخل دادهٔ نمونه از جداکنندهٔ هزارگان استفاده نشده است. اگر فایل واقعی تو عددهای 45,000 دارد، CSV باید آن سلول را درست نقلقول کند و برنامه نیز قرارداد پاکسازی مشخص داشته باشد.
باید فرق خطای ورودی با خطای منطق برنامه را بشناسی. اینجا ValueError در تبدیل عدد، یک ورودی نامعتبر را نشان میدهد. آن را با شمارهٔ ردیف گزارش میکنیم تا اصلاح فایل آسان باشد.
۴. فهرست برای چند مقدار، دیکشنری برای نام و مقدار
فهرست میتواند چند مبلغ را به ترتیب نگه دارد: [45000, 120000, 180000]. دیکشنری، کلید را به مقدار وصل میکند: {"food": 200000}. در گزارش ما، نام دسته کلید است و جمع مبلغ آن دسته مقدار است.
این انتخاب به سؤال مسئله وصل است. نمیخواهیم فقط همهٔ ردیفها را نگه داریم؛ میخواهیم «جمع food چقدر است؟» را جواب بدهیم. دیکشنری این ارتباط را مستقیم نشان میدهد.
اگر به کلیدی که وجود ندارد با totals[category] دسترسی بگیری، KeyError میگیری. get(category, 0) میگوید اگر دسته هنوز دیده نشده، جمع قبلی را صفر بگیر. صفر اینجا مقدار شروع جمع است، نه تبدیل یک دادهٔ گمشده به مبلغ صفر.
۵. حلقه: هر ردیف یک بار بررسی میشود
for روی ردیفها حرکت میکند. enumerate علاوه بر ردیف، شمارنده میدهد. چون خط اول فایل عنوان ستونهاست، شمارنده را از ۲ شروع کردهایم. اگر ردیف سوم خراب باشد، پیام خطا هم به خط سوم فایل اشاره میکند.
در هر دور، دسته و مبلغ را میخوانیم، اعتبارسنجی میکنیم و جمع را بهروزرسانی میکنیم. این ترتیب جلوی وارد شدن دادهٔ خراب به گزارش را میگیرد. برای یادگیری، قبل از نوشتن تابع کامل همین جمع ساده را اجرا کن.
rows = [("transport", 45000), ("food", 120000), ("food", 80000)]
totals = {}
for category, amount in rows:
totals[category] = totals.get(category, 0) + amount
print(totals)
# {'transport': 45000, 'food': 200000}۶. شرط: قرارداد داده را اجرا کن
در این پروژه دسته باید خالی نباشد و مبلغ باید عدد صحیح نامنفی باشد. شرط if amount < 0 ورودی خارج از قرارداد را میگیرد. این تصمیم به معنی آن نیست که در همهٔ سیستمهای مالی مبلغ منفی غلط است؛ فقط در ابزار هزینهٔ سادهٔ ما، مرجوعی و اصلاح حساب تعریف نشدهاند.
None in row یک حالت CSV با ستون اضافه را میگیرد. اگر کسی داخل مبلغ کاما بگذارد و آن را نقلقول نکند، ممکن است ردیف به ستونهای بیشتر شکسته شود. گزارش خطا بهتر از جمع زدن یک تکهٔ اشتباه مبلغ است.
در بازنویسی با AI، همین قرارداد را در پیام نگه دار. «خطا را حذف کن» ممکن است به نادیده گرفتن ردیفهای خراب منجر شود. درخواست دقیقتر این است: «ردیف خراب باید با شمارهٔ خط گزارش شود و گزارش ناقص تولید نشود».
۷. تابع، یک کار مشخص را جدا میکند
summarize یک جریان متن میگیرد و دیکشنری جمعها را برمیگرداند. مسئول چاپ یا انتخاب فایل نیست. این جداسازی باعث میشود برای آزمون، بهجای ساخت فایل واقعی از StringIO استفاده کنیم.
return خروجی تابع را به فراخواننده میدهد. print فقط چیزی را نمایش میدهد. اگر تابعی فقط چاپ کند، استفادهٔ دوباره از نتیجه در بخش دیگر برنامه سختتر میشود. در این پروژه، summarize محاسبه میکند و main نتیجه را چاپ میکند.
متغیر totals داخل تابع ساخته میشود؛ هر بار فراخوانی، یک جمع تازه داریم. از ورودی پیشفرض mutable مثل def f(items=[]) برای نگهداری دادهٔ موقت استفاده نکن، چون همان شیء پیشفرض بین فراخوانیها میماند.
۸. خواندن CSV و بستن فایل
csv.DictReader هر ردیف را به دیکشنری بر اساس عنوان ستونها تبدیل میکند. with فایل را باز میکند و بعد از خروج از بلوک آن را میبندد، حتی اگر خطایی اتفاق بیفتد. encoding="utf-8-sig" هم UTF-8 معمولی و هم فایل دارای BOM را میخواند.
عنوانها را دقیقاً بررسی میکنیم تا جابهجایی یا نام اشتباه ستونها پنهان نماند. newline="" برای رفتار مناسب خوانندهٔ CSV استفاده شده است. نام و واحد ستون بخشی از قرارداد ابزار است؛ با تغییر آنها، تابع نیز باید تغییر کند.
with args.file.open(encoding="utf-8-sig", newline="") as stream:
totals = summarize(stream)منبع این بخش: Python: کتابخانهٔ csv ↗
۹. مدیریت خطا را محدود نگه دار
دو خطای مورد انتظار داریم: فایل باز نمیشود یا محتوایش قرارداد را نقض میکند. main آنها را با پیام روشن و کد خروج ۱ گزارش میکند. همهٔ Exceptionها را بیصدا قورت نمیدهیم؛ خطای برنامهنویسی باید دیده و اصلاح شود.
اگر در ترمینال FileNotFoundError میبینی، مسیر فایل را بررسی کن. اگر ValueError با شمارهٔ خط داری، همان ردیف را باز کن. اگر TypeError داری، نوع داده و عملیاتی را که روی آن انجام شده ببین. نام خطا نقطهٔ شروع بررسی است، نه چیزی که فقط باید مخفی شود.
۱۰. با تست نشان بده کدام رفتار را انتظار داری
برای دادهٔ food با ۱۲۰٬۰۰۰ و ۸۰٬۰۰۰ تومان، خروجی باید {"food": 200000} باشد. فایل فقط با عنوان ستون، گزارش خالی میدهد. مبلغ منفی، متن غیرعددی، دستهٔ خالی و ستون اضافه باید رد شوند.
اینها آزمون قراردادند. اگر فردا کد را سریعتر کردی، همان آزمونها کمک میکنند مطمئن شوی معنی برنامه عوض نشده است. در فایل کامل، self_test همین حالتها را اجرا میکند.
python3 expenses.py --self-test
# Expenses: all checks passed۱۱. خطا را به سؤال خوب برای ساروی تبدیل کن
کد کامل هزارخطی را با «کار نمیکند» نفرست. نام تابع، ورودی کوچک، خروجی فعلی، خروجی مورد انتظار و متن خطا را کنار هم بده. این اطلاعات اجازه میدهد مشکل بازسازی شود.
مثال: «برای ردیف food,80000 خروجی درست است، ولی ردیف food,80,000 به سه ستون تبدیل میشود. میخواهم ردیف نامعتبر رد شود؛ فایل نباید بیصدا اصلاح شود.» این پیام جلوی تغییر ناخواستهٔ قرارداد را میگیرد.
اگر هنوز مفهوم را یاد میگیری، بگو «فقط یک سرنخ بده». وقتی اصلاح کردی، از ساروی بخواه یک ورودی پیدا کند که کدت را خراب میکند. حل Two Sum یک نمونهٔ کامل از همین روش بررسی است.
۱۲. سه تمرین برای تبدیل نمونه به پروژهٔ خودت
تمرین اول: دستهای با بیشترین هزینه را نمایش بده. برای فایل خالی تصمیم بگیر چه پیامی مناسب است. تمرین دوم: خروجی گزارش را در CSV تازه بنویس، بدون تغییر فایل ورودی. تمرین سوم: یک ستون تاریخ اضافه کن و فقط یک ماه مشخص را گزارش بده.
قبل از هر تغییر، دو ورودی و خروجی مورد انتظار بنویس. سپس پیادهسازی کن. برای تمرین ماه، حتماً تاریخ نامعتبر و مرز پایان ماه را در نظر بگیر. از AI بخواه طرح تغییر را توضیح دهد و فقط بعد از روشن شدن قرارداد کد بنویسد.
وقتی پروژه را برای رزومه معرفی میکنی، همین رفتارهای واقعی را بنویس: خواندن CSV، اعتبارسنجی ردیف، گروهبندی و تست. راهنمای رزومه نشان میدهد چطور تجربه را بدون عددسازی بیان کنی.
این کار را خودت انجام بده.
- ابزار را با CSV نمونه اجرا و جمع ۴۲۵هزار را کنترل کن.
- ورودی خراب و مبلغ منفی را امتحان کن.
- یک گزارش تازه با تست مستقل اضافه کن.
برای بررسی بیشتر
Python: آموزش رسمیPython: شرط، حلقه و تابعدر انجام این کار به مشکل خوردی؟
از پشتیبانی کمک بگیر