در این فصل چه یاد میگیری#
نوتبوکِ تو کار میکند. مسئله این است که فقط پیشِ تو کار میکند — با آن ترتیبِ اجرایی که یادت هست، با آن متغیرهایی که چهل دقیقه پیش ساختی، روی همان ماشین.
این فصل همان تحلیل را به یک بستهٔ سهفایلی تبدیل میکند و بعد در یک مفسرِ کاملاً تازه اجرایش میکند — مفسری که هیچکدام از متغیرهای تو را ندیده. عددِ خروجی باید مو به مو همان باشد: ۰٫۹۰۱۶ در نوتبوک، ۰٫۹۰۱۶ در مفسرِ تازه. و در همان مسیر میبینی که همان کد، وقتی کسِ دیگری بدونِ ترتیبِ سلولها اجرایش میکند، با NameError میمیرد.

آخر این فصل میتوانی:
- کدِ یک نوتبوک را به ماژولهایی با مسئولیتِ روشن تقسیم کنی
- ماژولِ خودت را
importکنی و بدانی پایتون از کجا پیدایش میکند - بگویی چرا ویرایشِ یک فایل بعد از
importهیچ اثری ندارد - مرزِ «کد» و «اجرا» را با
if __name__ == "__main__"بکشی
قبل از شروع#
از سرنخ ترمِ ۵: فایلِ .py در برابرِ نوتبوک، pathlib، و subprocess.run. اینجا هر سه را جدیتر بهکار میبریم.
از سرنخ ترمِ ۳: train_test_split، Pipeline و accuracy. مدل در این فصل فقط بهانه است — چیزی که میسازیم ساختار است، نه مدل.
دادهٔ این ترم: یک پیکرهٔ ساختگیِ تیکتِ پشتیبانیِ فارسی که سلولِ راهاندازی میسازد. متن، برچسب، شناسهٔ کاربر و زمان. هر نُه فصلِ این ترم روی همین داده کار میکنند، تا عددها با هم مقایسهشدنی بمانند.
💡 نکته: سلولِ راهاندازی یک پوشهٔ کارِ خالی میسازد و مفسر را داخلش میبرد. هر فایلی که در این فصل مینویسی آنجا ساخته میشود، و هر بار که سلولِ راهاندازی را دوباره بزنی، آن پوشه از نو خالی میشود.
و پنج اصطلاحِ پایتونی که در سراسرِ این شش ترم برمیگردند. هیچکدام مفهومِ تازهای نیستند و هیچ فصلی وقتش را صرفشان نمیکند، ولی اگر تا حالا ندیدهایشان، همینجا یک بار ببینشان و بعد آسوده رد شو:
| اصطلاح | چه میکند |
|---|---|
f"{name:<18}{value:>10.4f}" |
زبانِ قالببندیِ خروجی، و در تقریباً هر جدولِ این دوره هست: < چپچین، > راستچین، عدد پهنای ستون، و .4f یعنی چهار رقمِ اعشار. عرض ثابت است تا ستونها زیرِ هم بیفتند |
str.maketrans و str.translate |
یک جدولِ تبدیل میسازد و در یک گذر روی رشته اعمالش میکند؛ جایگزینِ چند replace پشتِ سرِ هم |
global |
به تابع میگوید «این نام مالِ بیرون است، محلی نساز» — تنها جایی لازم میشود که تابعی به یک شمارندهٔ بیرونی مقدار میدهد |
dict1 | dict2 |
یک دیکشنریِ تازه از ترکیبِ دوتا؛ هر کلیدِ مشترک از دومی برداشته میشود. هیچکدام از دو تای اصلی عوض نمیشوند |
iter(callable, sentinel) |
تابعِ بیآرگومان را بارها صدا میزند تا وقتی جوابش با sentinel برابر شود؛ الگوی خواندنِ یک فایل تکهتکه تا رسیدن به انتهایش |
| اجرا | زمانِ تقریبی |
|---|---|
| CPU (پیشفرضِ Colab) | کمتر از یک دقیقه |
هیچ فصلی از این ترم GPU نمیخواهد.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
۱. یک نوتبوکِ سالم، و کاری که با آن نمیشود کرد#
این همان کدی است که هر کسی در پایانِ سرنخ مینویسد. تمیز است، کار میکند، و عددش هم بد نیست.
import re
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
def clean(text):
text = text.replace("ي", "ی")
return re.sub(r"\s+", " ", text).strip()
rows = [t for t in TICKETS if t["text"]]
X = [clean(t["text"]) for t in rows]
y = [t["label"] for t in rows]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=0, stratify=y)
model = make_pipeline(CountVectorizer(), LogisticRegression(max_iter=1000))
model.fit(X_train, y_train)
print("نمونهها:", len(X), "| دقت:", round(model.score(X_test, y_test), 4))
نمونهها: 769 | دقت: 0.9016
۰٫۹۰۱۶ روی چهار دستهٔ تیکت. این عدد را نگه دار؛ کلِ ترم به آن برمیگردیم و در فصلِ ۳ معلوم میشود بخشی از آن دروغ است.
حالا فرض کن کسی از تو میخواهد همین را روی دادههای خودش اجرا کند. دو سلولِ آخر را برایش کپی میکنی. ببینیم چه میشود — با subprocess یک مفسرِ کاملاً تازه بالا میآوریم، مفسری که هیچچیز از نوتبوکِ ما نمیداند:
import subprocess
import sys
from pathlib import Path
Path("wrong_order.py").write_text(
"rows = [t for t in TICKETS if t['text']]\n"
"X = [clean(t['text']) for t in rows]\n"
"print('نمونهها:', len(X))\n",
encoding="utf-8")
done = subprocess.run([sys.executable, "wrong_order.py"],
capture_output=True, text=True, encoding="utf-8")
print("کدِ خروج:", done.returncode)
print(done.stderr.strip().splitlines()[-1])
کدِ خروج: 1
NameError: name 'TICKETS' is not defined
این خطا احمقانه بهنظر میرسد ولی دقیقاً همان چیزی است که سرِ همه میآید. کدِ نوتبوک به یک حالتِ نامرئی تکیه دارد: متغیرهایی که سلولهای قبلی ساختهاند و هیچجا نوشته نشده که کداماند. تا وقتی خودت پشتِ همان runtime نشستهای، این حالت هست. لحظهای که کد از آن runtime بیرون میرود، نیست.
پس مسئله این نیست که نوتبوک بد است. مسئله این است که نوتبوک جای فکر کردن است، نه جای نگهداشتن. هر چیزی که قرار است دوباره اجرا شود باید جایی باشد که وابستگیهایش را خودش اعلام کند.
۲. اولین مرز: داده از حافظه بیرون میآید#
TICKETS یک متغیر در حافظهٔ نوتبوک است. اولین کاری که میکنیم بیرون آوردنش است — روی دیسک، در قالبی که هر برنامهای بتواند بخواند:
import json
with open("tickets.jsonl", "w", encoding="utf-8") as f:
for t in TICKETS:
f.write(json.dumps(t, ensure_ascii=False) + "\n")
print("خطوطِ فایل:", sum(1 for _ in open("tickets.jsonl", encoding="utf-8")))
خطوطِ فایل: 769
قالبِ jsonl یعنی «هر خط یک شیءِ JSON». انتخابش تصادفی نیست: میشود خطبهخط خواندش بدونِ اینکه کلِ فایل در حافظه بیاید، و اضافه کردن یک ردیفِ تازه یعنی یک خط به آخرِ فایل. ترمِ ۲ قالبهای ذخیره را با عدد مقایسه میکند؛ فعلاً همینقدر بدان که این قالب برای دادهٔ خطیِ نهچندان بزرگ انتخابِ بیدردسری است.
۳. سه فایل، سه مسئولیت#
حالا کد را تقسیم میکنیم. قاعدهای که تقسیم را هدایت میکند این است: هر فایل به یک سؤال جواب بدهد.
text.py— متن را چطور تمیز میکنیم؟data.py— داده از کجا میآید و کدام ردیف قابلِاستفاده است؟model.py— مدل چطور ساخته و سنجیده میشود؟
Path("tickets").mkdir(exist_ok=True)
Path("tickets/__init__.py").write_text("", encoding="utf-8")
Path("tickets/text.py").write_text(r'''import re
def normalise(text):
# ی عربی را به فارسی برمیگردانیم و فاصلههای اضافه را جمع میکنیم
text = text.replace("ي", "ی")
return re.sub(r"\s+", " ", text).strip()
''', encoding="utf-8")
Path("tickets/data.py").write_text(r'''import json
from pathlib import Path
# مسیر را از محلِ خودِ فایل حساب میکنیم، نه از پوشهٔ جاری
DATA = Path(__file__).resolve().parent.parent / "tickets.jsonl"
def load_tickets(path=DATA):
with open(path, encoding="utf-8") as f:
return [json.loads(line) for line in f if line.strip()]
def usable(rows):
return [r for r in rows if r.get("text") and r.get("label")]
''', encoding="utf-8")
Path("tickets/model.py").write_text(r'''from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from tickets.text import normalise
def build():
return make_pipeline(CountVectorizer(), LogisticRegression(max_iter=1000))
def train_and_score(rows, seed=0):
X = [normalise(r["text"]) for r in rows]
y = [r["label"] for r in rows]
X_tr, X_te, y_tr, y_te = train_test_split(
X, y, test_size=0.25, random_state=seed, stratify=y)
model = build()
model.fit(X_tr, y_tr)
return model, model.score(X_te, y_te)
''', encoding="utf-8")
print(sorted(p.name for p in Path("tickets").glob("*.py")))
['__init__.py', 'data.py', 'model.py', 'text.py']
چهار تصمیمِ ریز در این سه فایل هست و هر چهارتا عمدیاند:
۱) __init__.py خالی است و باید باشد. حضورش به پایتون میگوید «این پوشه یک بسته است». بدونِ آن هم در نسخههای تازهٔ پایتون معمولاً کار میکند، ولی رفتارِ import در حالتهای مرزی عوض میشود — پس بگذارش و فکر نکن.
۲) مسیرِ داده از __file__ حساب میشود، نه از پوشهٔ جاری. اگر مینوشتیم open("tickets.jsonl")، کد فقط وقتی کار میکرد که مفسر دقیقاً از همان پوشه اجرا شده باشد. این یکی از رایجترین دلیلهای «پیشِ من کار میکرد» است.
۳) load_tickets یک پارامترِ path دارد با مقدارِ پیشفرض. یعنی در استفادهٔ روزمره ساده است و در تست میشود فایلِ دیگری به آن داد. فصلِ بعد دقیقاً از همین دَر وارد میشود.
۴) train_and_score هم مدل را برمیگرداند و هم عدد را. تابعی که فقط چاپ میکند، برای کسِ دیگری بیفایده است — چاپ کارِ لایهٔ اجراست، نه کارِ کتابخانه.
۴. حالا import کن — و همان عدد را بگیر#
from tickets import data, model
rows = data.usable(data.load_tickets())
fitted, score = model.train_and_score(rows)
print("ردیفِ قابلِاستفاده:", len(rows))
print("دقت:", round(score, 4))
ردیفِ قابلِاستفاده: 769
دقت: 0.9016
همان ۰٫۹۰۱۶. این عددِ یکسان تصادفی نیست و مهمترین چیزی است که تا اینجا ساختیم: کد جابهجا شد، رفتار عوض نشد.
ولی پایتون این فایلها را از کجا پیدا کرد؟ از sys.path — فهرستی از پوشههایی که هنگامِ import در آنها میگردد. سلولِ راهاندازیِ این ترم پوشهٔ کار را به ابتدای همین فهرست اضافه کرده است. این تنها ذرهجادوی این فصل است و بهتر است بدانی کجاست: بدونِ آن، همان import با ModuleNotFoundError میشکند.
🔧 اگر کار نکرد: اگر دیدی
ModuleNotFoundError: No module named 'tickets'، تقریباً همیشه یکی از این دوتاست: یا سلولِ راهاندازی را نزدهای (پس مفسر داخلِ پوشهٔ کار نیست)، یا پوشهٔticketsرا جای دیگری ساختهای. باprint(Path.cwd())وprint(sys.path[0])هر دو را در دو ثانیه میبینی.
۵. تلهٔ import: پایتون فایل را دو بار نمیخواند#
این تله در Colab هر روز آدم میگیرد: فایل را عوض میکنی، سلولِ import را دوباره میزنی، و هیچ اتفاقی نمیافتد.
import importlib
from tickets import text
path = Path("tickets/text.py")
path.write_text(path.read_text(encoding="utf-8") + "\nMARK = 'v2'\n", encoding="utf-8")
from tickets import text
print("بعد از import دوباره:", hasattr(text, "MARK"))
importlib.reload(text)
print("بعد از reload :", hasattr(text, "MARK"))
بعد از import دوباره: False
بعد از reload : True
import دومی اصلاً به دیسک نگاه نکرد. پایتون هر ماژولی را که یک بار خوانده در sys.modules نگه میدارد و دفعهٔ بعد همان شیء را برمیگرداند. این برای سرعت است و در یک برنامهٔ معمولی درست است — ولی در نوتبوک، که ساعتها زنده میماند و تو مدام فایل را عوض میکنی، یعنی داری کدِ قدیمی را تست میکنی و خبر نداری.
⚠️ مواظب باش:
importlib.reloadهم کاملِ کامل نیست: اگر جای دیگریfrom tickets.text import normaliseنوشته باشی، آن نامِ محلی همچنان به تابعِ قدیمی اشاره میکند. مطمئنترین راه در Colab یکی است:Runtime → Restart session and run all. همان آزمونی که در تمامِ این دوره میگوییم تنها آزمونِ صداقتِ یک نوتبوک است.
۶. مرزِ «کد» و «اجرا»#
آخرین قطعه: فایلی که هم بشود importش کرد و هم بشود اجرایش کرد، بدونِ اینکه این دو کارِ هم را خراب کنند.
Path("report.py").write_text(r'''from tickets import data, model
def main():
rows = data.usable(data.load_tickets())
_, score = model.train_and_score(rows)
print(f"ردیف: {len(rows)} دقت: {score:.4f}")
print("این خط همیشه اجرا میشود")
if __name__ == "__main__":
main()
''', encoding="utf-8")
import report # فقط بارش میکنیم، اجرایش نمیکنیم
print("---")
done = subprocess.run([sys.executable, "report.py"],
capture_output=True, text=True, encoding="utf-8")
print(done.stdout.strip())
این خط همیشه اجرا میشود
---
این خط همیشه اجرا میشود
ردیف: 769 دقت: 0.9016
سه خطِ خروجی، سه چیزِ متفاوت میگویند:
- خطِ اول موقعِ
importچاپ شد. هر چیزی که در سطحِ بالای فایل بنویسی، همان لحظه که کسی فایلت راimportمیکند اجرا میشود. اگر آنجا آموزشِ مدل گذاشته باشی، هر کسی که فقط یک تابعِ کوچکِ تو را میخواهد، مجبور میشود مدل را آموزش بدهد. - خطِ دوم و سوم از مفسرِ تازه آمدند. آنجا
__name__برابرِ"__main__"بود، پسmain()صدا زده شد. - و آن
0.9016عیناً همان عددِ نوتبوک است — این بار در فرآیندی که هیچکدام از متغیرهای ما را ندیده بود.
📏 اندازه بگیر: با چه چیزی مقایسه شد؟ با عددِ همان تحلیل در نوتبوک (
0.9016). روی کدام داده؟ همان ۷۶۹ تیکت، از فایلِ روی دیسک. با چندseed؟ یکی — و اینجا کافی است، چون ادعای ما «بهتر شد» نیست، ادعای ما «مو به مو همان شد» است. برای ادعای بهبود، از فصلِ ۳ به بعد چندseedاجباری میشود. این تفاوت را نگه دار: بازتولید یکseedمیخواهد، مقایسه چند تا.
✅ چک کن: عددِ مفسرِ تازه باید دقیقاً با عددِ سلولِ اول یکی باشد. اگر یکی نشد، یعنی جایی از کدِ ماژولی با کدِ نوتبوکی فرق دارد — احتمالاً
cleanوnormaliseیکی نیستند، یاrandom_stateرا در یکی جا انداختهای. این «تقریباً همان» نیست؛ یا دقیقاً همان است یا اشتباهی هست که هنوز پیدایش نکردهای.
۷. چه ساختیم و چه هنوز نداریم#
for p in sorted(Path(".").rglob("*.py")):
lines = len(p.read_text(encoding="utf-8").splitlines())
print(f"{p.as_posix():<22} {lines:>3} خط")
report.py 13 خط
tickets/__init__.py 0 خط
tickets/data.py 14 خط
tickets/model.py 20 خط
tickets/text.py 9 خط
wrong_order.py 3 خط
جمعِ ستونِ راست ۵۹ خط است، ولی wrong_order.py بخشِ تحویل نیست — آن را در بخشِ ۱ ساختیم تا خرابی را نشان بدهیم و همانجا کارش تمام شد. چیزی که تحویل میشود ۵۶ خط است، و حالا قابلِتحویل هم هست: کسِ دیگری میتواند پوشه را بردارد، python report.py بزند، و همان عدد را بگیرد.
ولی صادق باشیم — این هنوز خیلی چیزها ندارد:
- هیچ تستی ندارد. اگر فردا
normaliseرا عوض کنم و خرابش کنم، هیچچیز به من نمیگوید. فصلِ ۲. - هیچکس نگفته ورودیِ این توابع چه شکلی باید باشد.
load_ticketsروی فایلی که ستونِlabelندارد چه میکند؟ فصلِ ۴. - هیچ نسخهای سنجاق نشده. فصلِ ۹.
- و آن
0.9016هنوز اثبات نشده. فصلِ ۳.
🤖 از دستیارت بپرس: «تفاوتِ
import tickets.modelباfrom tickets import modelباfrom tickets.model import buildچیست؟» بعد خودت امتحانش کن: هر سه را در یک نوتبوکِ تازه بزن و بعد از هرکدامprint(sys.modules.keys())را نگاه کن. کدامشان ماژول را واقعاً بار میکند و کدامشان فقط یک نام میسازد؟
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| module | ماژول | یک فایلِ .py که میشود importش کرد |
| package | پکیج | پوشهای از ماژولها با __init__.py |
sys.path |
سیسپث | فهرستِ پوشههایی که پایتون هنگامِ import در آنها میگردد |
sys.modules |
سیسماژولز | حافظهٔ پنهانِ ماژولهای بارشده در همین فرآیند |
| entry point | اینتری پوینت | جایی که اجرا از آن شروع میشود (اینجا main()) |
jsonl |
جیسونال | فایلی که هر خطش یک شیءِ JSON کامل است |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
حالا کدی داریم که جابهجا میشود. ولی هیچ راهی نداریم بفهمیم سالم است یا نه — تنها آزمونِ فعلیمان این است که برنامه خطا ندهد و عددی چاپ کند، و کدی که عددِ غلط چاپ میکند هم دقیقاً همین کار را میکند.
فصلِ بعد اولین تست را مینویسیم، اجراکنندهٔ تستِ خودمان را در بیست خط میسازیم، و همان تستها یک باگِ واقعی در normalise پیدا میکنند که این فصل با خیالِ راحت از کنارش رد شد.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.