در این فصل چه یاد میگیری#
هیچ مفهومِ تازهای در این فصل نیست. کاری که میکنی این است: همهٔ چیزهایی را که در ده فصلِ گذشته یاد گرفتی کنارِ هم میگذاری و یک ابزارِ واقعی میسازی — چیزی که یک متنِ فارسی را میگیرد و دربارهٔ آن گزارش میدهد: چند کلمه دارد، چند کلمهٔ یکتا، طولِ میانگینِ کلمهها، و پرتکرارترینها کداماند. ابزارت را تکهتکه و با تابع میسازی، چون همین ابزار پایهٔ کاری است که در ترمِ ۲ روی دادهٔ متنیِ واقعی انجام میدهی.

آخر این فصل میتوانی:
- یک متنِ خام را برای پردازش تمیز کنی
- کلمهها را بشماری و پرتکرارترینها را پیدا کنی
- یک ابزار را به چند تابعِ کوچکِ قابلِآزمون تقسیم کنی
- بگویی چرا نتیجهٔ ابزارت به تصمیمهایی که موقعِ تمیزکردن گرفتی وابسته است
قبل از شروع#
از فصل ۳: split()، strip()، replace() و اینکه رشته تغییرناپذیر است.
از فصل ۴: dict و get() با مقدارِ پیشفرض، و tuple.
از فصل ۶ و ۷: حلقه، الگوی انباشتگر، تابع با return و رد کردنِ حالتِ خاص در خطِ اول.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. قدمِ اول: تمیزکردنِ متن#
متنِ خام برای شمردن آماده نیست. «سرنخ.» و «سرنخ» و «سرنخ؟» سه کلمهٔ متفاوت بهحساب میآیند مگر اینکه نشانههای نگارشی را برداری. و در فارسی یک مشکلِ اضافه هم هست: حرفِ «ی» و «ک» دو شکلِ یونیکدیِ متفاوت دارند — فارسی و عربی — که برای چشمِ تو یکساناند و برای پایتون نه.
PUNCTUATION = "،.؟!:؛()«»\"'-—…"
def clean_text(text):
"""متن را برای شمارش آماده میکند: «ی» و «ک» عربی را یکدست و نشانهها را به فاصله تبدیل میکند."""
text = text.replace("ي", "ی").replace("ك", "ک")
for mark in PUNCTUATION:
text = text.replace(mark, " ")
return text.strip()
def word_list(text):
"""فهرستِ کلمههای متنِ تمیزشده."""
return clean_text(text).split()
print(word_list("سلامِ دنیا! این یک آزمایش است."))
['سلامِ', 'دنیا', 'این', 'یک', 'آزمایش', 'است']
سه نکته در همین چند خط:
PUNCTUATIONرا با حرفِ بزرگ نوشتیم. این یک قرارداد پایتونی است: یعنی «این یک مقدارِ ثابت است، عوضش نکن».- نشانهها را با فاصله جایگزین کردیم نه با رشتهٔ خالی. اگر خالی میگذاشتیم، «سلام» و «دنیا» به یک کلمهٔ چسبیده تبدیل میشد.
split()بدونِ آرگومان، فاصلههای پشتِسرِهم را هم یکی حساب میکند. برای همین آن دو فاصلهٔ بینِ «سلامِ» و «دنیا» کلمهٔ خالی نساخت.
✅ چک کن: خروجی باید دقیقاً شش کلمه باشد. اگر رشتهٔ خالی (
'') در فهرست دیدی، یعنی بهجایsplit()ازsplit(" ")استفاده کردهای — و آن دو یکی نیستند.
⚠️ مواظب باش: «سلامِ» با کسرهٔ آخر ماند و از «سلام» جدا شمرده میشود. اعرابِ فارسی، نیمفاصله و شکلهای مختلفِ نوشتنِ یک کلمه هر کدام یک تصمیم میخواهند. اینجا عمداً ساده نگهش داشتیم؛ ابزارِ حرفهایِ این کار را در ترمِ ۲ فصلِ ۹ میبینی.
۲. قدمِ دوم: شمردن#
def count_words(words):
"""دیکشنریِ «کلمه ← تعداد تکرار»."""
counts = {}
for word in words:
counts[word] = counts.get(word, 0) + 1
return counts
print(count_words(["داده", "سرنخ", "داده"]))
{'داده': 2, 'سرنخ': 1}
قلبِ این تابع یک خط است: counts[word] = counts.get(word, 0) + 1. آن get(word, 0) دقیقاً همان چیزی است که در فصلِ ۴ دیدی — «اگر این کلمه را قبلاً ندیدهای، صفر حساب کن» — و بدونِ آن، اولین باری که کلمهای میآید KeyError میگرفتی.
۳. قدمِ سوم: پرتکرارترینها#
def top_words(counts, n=5):
"""n کلمهٔ پرتکرار، بهصورتِ فهرستی از (کلمه، تعداد)."""
pairs = []
for word in counts:
pairs.append((counts[word], word))
pairs.sort(reverse=True)
top = []
for count, word in pairs[:n]:
top.append((word, count))
return top
print(top_words({"داده": 2, "سرنخ": 5, "مدل": 1}, n=2))
[('سرنخ', 5), ('داده', 2)]
اینجا یک ترفندِ خوب هست. برای مرتبکردن بر اساسِ تعداد، اول هر جفت را بهصورتِ (تعداد، کلمه) ساختیم — با تعداد اول. چون tupleها از روی عنصرِ اولشان مقایسه میشوند، sort خودبهخود بر اساسِ تعداد مرتب میکند. بعد در حلقهٔ دوم جایشان را برمیگردانیم تا خروجی خوانا باشد.
pairs.sort(reverse=True) فهرست را جای خودش مرتب میکند (مثلِ append، چون فهرست تغییرپذیر است) و reverse=True یعنی از بزرگ به کوچک.
قبل از ادامه، جواب بده: اگر دو کلمه تعدادِ یکسانی داشته باشند، کدام اول میآید؟ جواب: وقتی عنصرِ اولِ دو tuple برابر باشد، مقایسه سراغِ عنصرِ دوم میرود — یعنی خودِ کلمه. و چون reverse=True روی کلِ مقایسه اثر میگذارد، آن کلمهها هم برعکسِ ترتیبِ الفبا میآیند. در خروجیِ بخشِ بعد همین را میبینی: چهار کلمهٔ دوتکراری بهترتیبِ «یک، فقط، سوال، داده» آمدهاند، که دقیقاً وارونهٔ الفباست. نکتهٔ مهم این نیست که کدام اول است؛ این است که هر بار همان ترتیب درمیآید — و همین چیزی است که نتیجه را تکرارپذیر میکند.
۴. قدمِ چهارم: سرِهمکردنِ گزارش#
def report(text):
"""گزارشِ کاملِ یک متن. برای متنِ خالی None برمیگرداند."""
words = word_list(text)
if not words:
return None
counts = count_words(words)
total_length = 0
for word in words:
total_length = total_length + len(word)
return {
"total_words": len(words),
"unique_words": len(counts),
"average_length": total_length / len(words),
"top": top_words(counts),
}
SAMPLE = """
داده بدون سوال فقط عدد است. سوال خوب داده را به سرنخ تبدیل میکند.
هر سرنخ باید وارسی شود؛ سرنخ وارسینشده فقط یک حدس است، نه یک نتیجه.
"""
result = report(SAMPLE)
for key in result:
print(f"{key} = {result[key]}")
total_words = 28
unique_words = 21
average_length = 3.6785714285714284
top = [('سرنخ', 3), ('یک', 2), ('فقط', 2), ('سوال', 2), ('داده', 2)]
هر تابع یک کارِ کوچک میکند و report فقط آنها را بهترتیب صدا میزند. این تقسیم تصادفی نیست: هر تکه را جداگانه میشود امتحان کرد، و وقتی نتیجه غلط درآمد میدانی کدام تکه را نگاه کنی.
و حالتِ خاص، همانطور که در فصلِ ۷ یاد گرفتی، در خطِ اول رد شد:
print(report(" "))
print(report(""))
None
None
✅ چک کن: عددِ ۲۸ را با دست بشمار — کلمههای متنِ نمونه را یکییکی. اگر عددت فرق کرد، احتمالاً «میکند» را دو کلمه شمردهای یا «؛» را نادیده گرفتهای. هر اختلافی که پیدا کنی، در واقع یک تصمیمِ تمیزکردن است که ابزارت گرفته و تو نمیدانستی.
📏 اندازه بگیر:
average_lengthعددِ3.6785714285714284را داد. این عدد دقیق است ولی گمراهکننده، به دو دلیل. اول اینکه چهارده رقمِ اعشار برای «طولِ میانگینِ کلمه» بیمعناست؛ دقتی را نشان میدهد که وجود ندارد. دوم و مهمتر:lenتعدادِ نویسه را میشمارد، و «میکند» بهخاطرِ نیمفاصله یک نویسهٔ نامرئی بیشتر دارد. پس این عدد به تصمیمهای بخشِ ۱ وابسته است، نه فقط به متن. همیشه بپرس این عدد از کجا آمد — این اولین تمرینِ واقعیِ نخِ دوره است.
۵. خرابش کن، تا یاد بگیری#
سه تغییر بده و ببین چه میشود. هر کدام یک درسِ واقعی دارند:
PUNCTUATIONرا خالی کن (PUNCTUATION = ""). حالاtotal_wordsچقدر عوض شد وunique_wordsچقدر؟ کدامشان بیشتر آسیب دید و چرا؟- در
clean_textنشانهها را با""عوض کن بهجای" ". یک کلمهٔ جدید و عجیب در فهرستِ پرتکرارها پیدا میکنی؟ - در
top_wordsمقدارِreverse=Trueرا بردار. خروجی چه میشود، و چرا این باگ در یک نگاه دیده نمیشود؟
🔧 اگر کار نکرد:
TypeError: '<' not supported between instances of 'str' and 'int'یعنی موقعِ ساختنِpairsجای کلمه و تعداد را عوض کردهای.tupleوقتی عنصرِ اولش همنوع نباشد نمیتواند مقایسه شود. ترتیب باید(تعداد، کلمه)باشد.
🤖 از دستیارت بپرس: «این تابعِ
clean_textبرای متنِ فارسی چه مواردی را جا انداخته؟ فقط فهرست کن، کد ننویس.» جوابش احتمالاً نیمفاصله، اعراب و ارقامِ فارسی را میآورد. هر کدام را خودت با یک نمونهٔ کوچک امتحان کن و ببین واقعاً مشکلساز است یا نه — بعضیهایشان برای متنِ تو اصلاً اهمیتی ندارند، و تشخیصِ همین تفاوت کارِ توست نه کارِ دستیار.
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
ترمِ یک تمام شد. حالا پایتون مینویسی، خطا را میخوانی، و میدانی نوتبوک کجا دروغ میگوید. ولی همهٔ دادههایی را که تا اینجا دیدی خودت با دست تایپ کردی — چهار محصول، هفت دما، یک پاراگراف.
ترمِ دو با دادهٔ واقعی شروع میشود: فایلهایی با هزاران ردیف، پُر از خانههای خالی و مقدارهای خراب و نوعهای اشتباه. اولین فصلش میپرسد چرا برای این کار به ابزارِ تازهای بهنامِ numpy نیاز داری، و چرا حلقهای که همین حالا بلدی — هرچند درست کار میکند — برای هزاران ردیف انتخابِ اشتباهی است.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.