در این فصل چه یاد میگیری#
یک اسکریپتِ کاملاً درست را دو بار اجرا میکنیم. بارِ اول ۱۲۳۵ ردیف مینویسد، بارِ دوم فایل ۲۴۷۰ ردیف دارد. هیچ خطایی هم نمیدهد — و این بدترین قسمتش است.
بعد همان کار را به پنج مرحله میشکنیم و راهاندازی مینویسیم که خودش میفهمد چه چیزی کهنه شده. اجرای دوم صفر مرحله اجرا میکند. یک ستون را عوض میکنیم: یک مرحله. یک فیلتر را عوض میکنیم: دو مرحله. و در یک مورد کد را کاملاً بازنویسی میکنیم و باز هم مرحلهٔ پاییندستی اجرا نمیشود — چون خروجی بایتبهبایت همان است.

آخر این فصل میتوانی:
- تفاوتِ یک اسکریپت و یک مسیرِ داده را با یک آزمایش نشان بدهی
- مرحله را با ورودی و خروجیِ اعلامشده تعریف کنی
- ترتیبِ اجرا را از وابستگیها بسازی، نه از ترتیبی که کد را نوشتهای
- برای هر ستون بگویی از کدام مرحله و از چند ردیف آمده است
قبل از شروع#
از ترمِ ۱: پروژهای که ماژول دارد و تست دارد. اینجا همان انضباط را روی داده میگذاریم.
از سرنخ ترمِ ۲: خواندن و نوشتنِ csv. در این فصل عمداً از pandas استفاده نمیکنیم تا هیچ چیزی پشتِ یک تابعِ آماده پنهان نماند.
💡 نکته: پیکرهٔ ترم را سلولِ راهاندازی میسازد: ۱۲۴۰ تیکتِ پشتیبانیِ فارسی که عمداً کثیفاند. هر نُه فصلِ این ترم روی همین داده کار میکنند، پس عددهایت با کتاب و با فصلهای بعدی قابلِ مقایسه است.
📓 نوتبوک: نوتبوک این فصل را در Colab باز کن — همهٔ کدهای این فصل آماده و بهترتیب داخلش هست.
| اجرا | زمانِ تقریبی |
|---|---|
| کلِ نوتبوک روی CPU | کمتر از یک دقیقه |
| GPU | لازم نیست |
۱. اسکریپتی که کار میکند — تا وقتی دوباره اجرایش کنی#
این کدْ کارِ درستی میکند: تیکتها را میخواند، ردیفِ بیمتن را کنار میگذارد، و برای هر تیکت تعدادِ کلمه را مینویسد.
import csv
RAW = WORK / "raw" / "tickets.csv"
OUT = WORK / "naive_features.csv"
def naive_script():
"""همان کاری که همه اولین بار میکنند: بخوان، تمیز کن، بنویس."""
with open(RAW, encoding="utf-8") as fh:
rows = list(csv.DictReader(fh))
keep = [r for r in rows if r["text"].strip()]
exists = OUT.exists()
with open(OUT, "a", encoding="utf-8", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=["ticket_id", "words", "category"])
if not exists:
w.writeheader()
for r in keep:
w.writerow({"ticket_id": r["ticket_id"],
"words": len(r["text"].split()),
"category": r["category"]})
return len(rows), len(keep)
def count_rows(path):
with open(path, encoding="utf-8") as fh:
return sum(1 for _ in fh) - 1
read, kept = naive_script()
print("اجرای اول:", read, "خوانده،", kept, "نوشته · فایل:", count_rows(OUT), "ردیف")
naive_script()
print("اجرای دوم:", read, "خوانده،", kept, "نوشته · فایل:", count_rows(OUT), "ردیف")
اجرای اول: 1240 خوانده، 1235 نوشته · فایل: 1235 ردیف
اجرای دوم: 1240 خوانده، 1235 نوشته · فایل: 2470 ردیف
هر دو اجرا گزارش میدهند «۱۲۳۵ ردیف نوشتم». فایل ۲۴۷۰ ردیف دارد.
اسکریپت دروغ نمیگوید؛ واقعاً ۱۲۳۵ ردیف نوشته. مشکل این است که نمیداند قبلاً هم نوشته بوده. یک اسکریپت حافظه ندارد: هر بار از صفر شروع میکند و همان دستورها را از اول اجرا میکند.
و این خرابی از آن دستهای است که هفتهها میماند. هیچ traceback ای نمیآید. فقط یک روز کسی میپرسد چرا میانگینِ ستون عوض شده.
نامِ فنیِ چیزی که این اسکریپت ندارد، «همتوانی» است: اجرای دوباره باید همان وضعیتی را بسازد که اجرای اول ساخت، نه چیزی روی آن.
۲. مرحله: کوچکترین واحدی که میشود دوباره اجرایش کرد#
یک مرحله سه چیز را اعلام میکند: چه فایلهایی میخوانَد، چه فایلهایی میسازد، و با چه کدی. همین اعلامکردن است که همهچیزِ بعدی را ممکن میکند.
import hashlib
import inspect
import json
import time
STAGE_DIR = WORK / "stage"
STAGE_DIR.mkdir(exist_ok=True)
STATE = WORK / "state.json"
LOG = WORK / "provenance.jsonl"
def file_hash(path):
"""اثرِ انگشتِ محتوای یک فایل — پایهٔ همهٔ تصمیمهای این راهانداز."""
h = hashlib.sha256()
with open(path, "rb") as fh:
for block in iter(lambda: fh.read(1 << 16), b""):
h.update(block)
return h.hexdigest()
class Stage:
def __init__(self, name, inputs, outputs, fn):
self.name, self.inputs, self.outputs, self.fn = name, inputs, outputs, fn
def key(self):
"""ورودیها + خودِ کد. اگر هرکدام عوض شود، مرحله دیگر تازه نیست."""
parts = [file_hash(p) for p in self.inputs] + [inspect.getsource(self.fn)]
return hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()[:12]
def read_csv(path):
with open(path, encoding="utf-8") as fh:
return list(csv.DictReader(fh))
def write_csv(path, rows, fields):
with open(path, "w", encoding="utf-8", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=fields)
w.writeheader()
w.writerows(rows)
print("اثرِ انگشتِ فایلِ خام:", file_hash(RAW)[:12])
اثرِ انگشتِ فایلِ خام: fa917db4b366
به key دقت کن: کدِ خودِ مرحله هم بخشی از ورودی است. اگر فقط محتوای فایلها را حساب کنیم، عوض کردنِ منطقِ یک مرحله هیچ اثری ندارد و راهانداز با اطمینان میگوید «تازه است». این دقیقاً همان اشتباهی است که آدم را ساعتها سرِ کار میگذارد.
inspect.getsource متنِ تابع را برمیگرداند — همان چیزی که در سلولِ نوتبوک نوشتهای. اثرِ انگشتِ محتوا هم sha256 است: دو فایل با بایتهای یکسان همیشه یک اثرِ انگشت دارند، و یک بایت اختلاف کلِ رشته را عوض میکند. فصلِ ۴ کاملاً روی همین ساخته میشود.
💡 از یک اثرِ انگشت سه چیز میخواهیم، و هر سه دلیلِ یک تصمیمِ بعدیاند. ۱) قطعی باشد: همان بایتها، همیشه همان رشته — روی هر ماشین و هر اجرا. ۲) یکنواخت پخش شود: هیچ الگویی در ورودی نباید مقدارها را در یک گوشه جمع کند، وگرنه دو دادهٔ بیربط یک اثرِ انگشت میگیرند. ۳) با یک بایت اختلاف، کاملاً عوض شود — نه کمی، بلکه بهشکلی که هیچ شباهتی به قبلی نداشته باشد؛ وگرنه «تقریباً شبیه» با «یکسان» قاطی میشود. و چرا
[:12]کافی است؟ هر نویسهٔ hex ۱۶ حالت دارد، پس ۱۲ نویسه یعنی16 ** 12— بیش از ۲۸۱ هزار میلیارد مقدارِ ممکن. برای فهرستی که چند هزار نسخه دارد این عدد آنقدر بزرگ است که برخوردِ اتفاقی عملاً ناممکن باشد. این حساب را نگه دار: ترمِ ۴ فصلِ ۶ دقیقاً روی همین سه خاصیت تصمیم میگیرد کهhash()پایتون را برای تخصیصِ ترافیک کنار بگذارد وsha256بگذارد — چونhash()در خاصیتِ اول میافتد، نه در دومی.
۳. پنج مرحله، و ترتیبی که اهمیت ندارد#
RAW_FIELDS = ["ticket_id", "customer_id", "created_at", "channel", "branch_code",
"text", "category", "escalated", "rating", "handling_minutes"]
def dedup(inputs, outputs):
rows, seen, out = read_csv(inputs[0]), set(), []
for r in rows:
key = tuple(r[f] for f in RAW_FIELDS)
if key not in seen:
seen.add(key)
out.append(r)
write_csv(outputs[0], out, RAW_FIELDS)
return len(rows), len(out)
def typed(inputs, outputs):
rows = read_csv(inputs[0])
for r in rows:
r["rating"] = r["rating"] if r["rating"].isdigit() else ""
r["handling_minutes"] = int(r["handling_minutes"])
write_csv(outputs[0], rows, RAW_FIELDS)
return len(rows), len(rows)
def normalized(inputs, outputs):
rows = read_csv(inputs[0])
for r in rows:
r["text"] = " ".join(r["text"].replace("ي", "ی").replace("ك", "ک").split())
write_csv(outputs[0], rows, RAW_FIELDS)
return len(rows), len(rows)
def valid(inputs, outputs):
rows = read_csv(inputs[0])
out = [r for r in rows
if r["text"] and int(r["handling_minutes"]) > 0
and r["created_at"] < "2032-01-01"]
write_csv(outputs[0], out, RAW_FIELDS)
return len(rows), len(out)
def features(inputs, outputs):
rows = read_csv(inputs[0])
out = [{"ticket_id": r["ticket_id"], "customer_id": r["customer_id"],
"words": len(r["text"].split()), "channel": r["channel"],
"category": r["category"], "escalated": r["escalated"]}
for r in rows]
write_csv(outputs[0], out, ["ticket_id", "customer_id", "words",
"channel", "category", "escalated"])
return len(rows), len(out)
S = STAGE_DIR
PIPELINE = [
Stage("features", [S / "valid.csv"], [S / "features.csv"], features),
Stage("dedup", [RAW], [S / "dedup.csv"], dedup),
Stage("typed", [S / "dedup.csv"], [S / "typed.csv"], typed),
Stage("normalized", [S / "typed.csv"], [S / "normalized.csv"], normalized),
Stage("valid", [S / "normalized.csv"], [S / "valid.csv"], valid),
]
BY_NAME = {s.name: s for s in PIPELINE}
print("به ترتیبی که نوشته شدند:", [s.name for s in PIPELINE])
به ترتیبی که نوشته شدند: ['features', 'dedup', 'typed', 'normalized', 'valid']
عمداً بد نوشتمشان: features اول است و ورودیاش را مرحلهای میسازد که آخر آمده. حالا سادهترین راهانداز را امتحان کن — یعنی «به ترتیب اجرا کن».
from pathlib import Path
try:
for stage in PIPELINE:
stage.fn(stage.inputs, stage.outputs)
except FileNotFoundError as e:
print("FileNotFoundError:", Path(e.filename).name)
FileNotFoundError: valid.csv
🔧 اگر کار نکرد: پیامِ کاملی که پایتون میدهد این است:
FileNotFoundError: [Errno 2] No such file or directory: '/tmp/darbast-t2/stage/valid.csv'. ما فقط نامِ فایل را چاپ کردیم چون مسیرِ کاملْ روی ماشینِ تو فرق دارد. این خطا خوب است؛ خرابیِ بیصدای بخشِ ۱ خیلی بدتر بود. اگر بهجای این خطا هیچ اتفاقی نیفتاد، یعنی از اجرای قبلی فایلی روی دیسک مانده — پوشهٔstageرا پاک کن و دوباره امتحان کن.
۴. ترتیب را اعلام کن، ننویس#
ترتیبِ درست را نباید دستی نوشت. ترتیب از وابستگیها درمیآید و وابستگیها را همان موقعِ ساختنِ Stage اعلام کردهایم.
def order(stages):
"""مرتبسازی بر پایهٔ وابستگی: هیچ مرحلهای قبل از سازندهٔ ورودیاش اجرا نمیشود."""
made_by = {out: s.name for s in stages for out in s.outputs}
done, out, todo = set(), [], list(stages)
while todo:
ready = [s for s in todo
if all(i not in made_by or made_by[i] in done for i in s.inputs)]
if not ready:
raise ValueError("حلقه در وابستگیها: " + ", ".join(s.name for s in todo))
ready.sort(key=lambda s: s.name)
for s in ready:
out.append(s)
done.add(s.name)
todo.remove(s)
return out
print("ترتیبِ وابستگی :", [s.name for s in order(PIPELINE)])
ترتیبِ وابستگی : ['dedup', 'typed', 'normalized', 'valid', 'features']
هر دور، مرحلههایی را برمیداریم که همهٔ ورودیهایشان یا فایلِ بیرونی است یا مرحلهای که قبلاً اجرا شده. اگر هیچ مرحلهای آماده نبود، یعنی حلقه داری و راهانداز صریح شکایت میکند بهجای اینکه تا ابد بچرخد.
ready.sort هم بیدلیل نیست: بدونِ آن، ترتیبِ بینِ مرحلههای همرتبه به ترتیبِ ورودی وابسته میشود و دو اجرا با دو ترتیبِ متفاوت، دو سیاههٔ متفاوت میسازند. بازتولیدپذیری از همین جزئیاتِ کوچک شروع میشود.
۵. اجرای مجدد: فقط چیزی که کهنه شده#
def run(stages):
state = json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {}
ran, skipped = [], []
for s in order(stages):
if state.get(s.name) == s.key() and all(p.exists() for p in s.outputs):
skipped.append(s.name)
continue
t0 = time.perf_counter()
n_in, n_out = s.fn(s.inputs, s.outputs)
record = {"stage": s.name, "key": s.key(),
"inputs": [p.name for p in s.inputs],
"outputs": [p.name for p in s.outputs],
"rows_in": n_in, "rows_out": n_out,
"seconds": round(time.perf_counter() - t0, 3)}
with open(LOG, "a", encoding="utf-8") as fh:
fh.write(json.dumps(record, ensure_ascii=False) + "\n")
state[s.name] = s.key()
ran.append(s.name)
STATE.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
return ran, skipped
ran, skipped = run(PIPELINE)
print("اجرای اول — اجرا:", len(ran), "· رد:", len(skipped), "·", ran)
ran, skipped = run(PIPELINE)
print("اجرای دوم — اجرا:", len(ran), "· رد:", len(skipped), "·", ran)
اجرای اول — اجرا: 5 · رد: 0 · ['dedup', 'typed', 'normalized', 'valid', 'features']
اجرای دوم — اجرا: 0 · رد: 5 · []
اجرای دوم هیچ کاری نکرد — و این همان چیزی است که اسکریپتِ بخشِ ۱ نداشت. فایلها هم دستنخورده ماندند، پس اجرای مجدد دیگر خطرناک نیست.
✅ چک کن: فایلِ
state.jsonرا باز کن. برای هر مرحله یک رشتهٔ دوازدهحرفی میبینی: همانkey. راهانداز فقط این را با مقدارِ الان مقایسه میکند. اگر اجرای دومت هم پنج مرحله اجرا کرد، تقریباً همیشه یک دلیل دارد:state.jsonنوشته نشده — مثلاً چون پوشهٔ کار در اجرای قبلی پاک شده است.
حالا ستونی به features اضافه میکنیم.
def features(inputs, outputs):
rows = read_csv(inputs[0])
out = [{"ticket_id": r["ticket_id"], "customer_id": r["customer_id"],
"words": len(r["text"].split()), "chars": len(r["text"]),
"channel": r["channel"], "category": r["category"],
"escalated": r["escalated"]}
for r in rows]
write_csv(outputs[0], out, ["ticket_id", "customer_id", "words", "chars",
"channel", "category", "escalated"])
return len(rows), len(out)
BY_NAME["features"].fn = features
ran, skipped = run(PIPELINE)
print("تغییرِ features:", ran, "· رد شد:", skipped)
تغییرِ features: ['features'] · رد شد: ['dedup', 'typed', 'normalized', 'valid']
یک مرحله از پنج. چون کدِ features عوض شد، key اش عوض شد؛ و چون هیچکدام از بالادستیها دست نخورده بودند، دست نخوردند.
حالا سراغِ وسطِ مسیر برویم: به valid یک شرطِ تازه اضافه میکنیم که امتیازهای ناممکن را دور بریزد.
def valid(inputs, outputs):
rows = read_csv(inputs[0])
out = [r for r in rows
if r["text"] and int(r["handling_minutes"]) > 0
and r["created_at"] < "2032-01-01"
and r["rating"] not in ("0", "9")]
write_csv(outputs[0], out, RAW_FIELDS)
return len(rows), len(out)
BY_NAME["valid"].fn = valid
before = file_hash(S / "valid.csv")[:12]
ran, skipped = run(PIPELINE)
print("تغییرِ valid :", ran, "· رد شد:", skipped)
print("اثرِ انگشتِ valid.csv:", before, "→", file_hash(S / "valid.csv")[:12])
تغییرِ valid : ['valid', 'features'] · رد شد: ['dedup', 'typed', 'normalized']
اثرِ انگشتِ valid.csv: b78bff9d6f53 → 2ea05e333de4
دو مرحله: خودش و پاییندستیاش. تغییر بهدرستی موج زد و به features رسید، ولی از سه مرحلهٔ بالادستی رد نشد. این همان «چه چیزی باید دوباره اجرا شود» است که هیچ اسکریپتی جوابش را ندارد.
۶. وقتی کد عوض میشود ولی خروجی نه#
حالا valid را کاملاً بازنویسی میکنیم — همان شرطها، این بار با یک حلقه بهجای یک list comprehension.
def valid(inputs, outputs):
rows = read_csv(inputs[0])
ok = []
for r in rows: # همان شرطها، فقط با حلقه
if not r["text"] or r["rating"] in ("0", "9"):
continue
if int(r["handling_minutes"]) <= 0 or r["created_at"] >= "2032-01-01":
continue
ok.append(r)
write_csv(outputs[0], ok, RAW_FIELDS)
return len(rows), len(ok)
BY_NAME["valid"].fn = valid
before = file_hash(S / "valid.csv")[:12]
ran, skipped = run(PIPELINE)
print("بازنویسیِ valid:", ran, "· رد شد:", skipped)
print("اثرِ انگشتِ valid.csv:", before, "→", file_hash(S / "valid.csv")[:12])
بازنویسیِ valid: ['valid'] · رد شد: ['dedup', 'typed', 'normalized', 'features']
اثرِ انگشتِ valid.csv: 2ea05e333de4 → 2ea05e333de4
valid دوباره اجرا شد — کدش عوض شده بود — ولی features اجرا نشد.
دلیلش در خطِ دوم است: اثرِ انگشتِ valid.csv عوض نشده. بازنویسی خروجی را تغییر نداد، پس مرحلهٔ پاییندستی هیچ کارِ تازهای ندارد.
و این تفاوتِ «اثرِ انگشتِ محتوا» با «زمانِ آخرین تغییرِ فایل» است. ابزارهای قدیمیترِ ساخت به زمان نگاه میکنند؛ برای آنها فایلی که دوباره نوشته شده حتماً تازه است، حتی اگر بایتهایش یکی باشد. آنها اینجا features را هم دوباره اجرا میکردند.
📏 اندازه بگیر: با چه چیزی مقایسه شد؟ با همان مسیر، بدونِ حافظه — پنج مرحله در برابرِ یک مرحله. روی کدام داده؟ همان ۱۲۴۰ ردیف در هر پنج آزمایش. با چند
seed؟ این فصل مدل ندارد، پسseedمعنا ندارد؛ ولی معادلِ سختگیرترش را داریم: اجرای دوباره باید عیناً همان اثرِ انگشت را بدهد — و در خطِ آخر همین را دیدی.
۷. ثبتِ منشأ — قاعدهٔ سختِ این ترم#
هر بار که مرحلهای اجرا شد، یک ردیف در provenance.jsonl نوشته شد. حالا از آن سیاهه بپرسیم داده در مسیر چه کشیده است.
last = {}
for line in LOG.read_text(encoding="utf-8").strip().split("\n"):
r = json.loads(line)
last[r["stage"]] = r
print(f"{'مرحله':<12}{'ورودی':>7}{'خروجی':>7}{'افت':>6} از")
for s in order(PIPELINE):
r = last[s.name]
print(f"{s.name:<12}{r['rows_in']:>7}{r['rows_out']:>7}"
f"{r['rows_in'] - r['rows_out']:>6} {', '.join(r['inputs'])}")
مرحله ورودی خروجی افت از
dedup 1240 1200 40 tickets.csv
typed 1200 1200 0 dedup.csv
normalized 1200 1200 0 typed.csv
valid 1200 934 266 normalized.csv
features 934 934 0 valid.csv
۱۲۴۰ ردیف وارد شد، ۹۳۴ ردیف بیرون آمد. یکچهارمِ داده در راه گم شد و حالا دقیقاً میدانیم کجا.
چهل تا تکراری بودند و ۲۶۶ تا از فیلترِ valid رد نشدند. بدونِ این جدول، فقط عددِ آخر را میدیدی و «۹۳۴ تیکت» بهنظر یک واقعیت میآمد، نه نتیجهٔ چهار تصمیم.
اینجاست که قاعدهٔ این ترم معنا پیدا میکند: هیچ تبدیلی روی داده بدونِ ثبتِ منشأ انجام نمیشود. سیاههٔ ما هنوز ساده است — نامِ مرحله، فایلها، شمارِ ردیف، و key. ولی همین کافی است که برای هر ستونِ features.csv بگویی کدام مرحله ساختش و از چند ردیف.
🤖 از دستیارت بپرس: «تفاوتِ ‹اثرِ انگشتِ محتوا› و ‹زمانِ آخرین تغییرِ فایل› برای تشخیصِ کهنگی چیست و هرکدام کجا شکست میخورند؟» بعد این را هم بپرس: «اگر مرحلهای به ساعتِ سیستم یا به یک عددِ تصادفی وابسته باشد، خروجیاش هر بار عوض میشود؛ راهاندازِ من چه رفتاری نشان میدهد؟» — جوابش تمرینِ ستارهدارِ همین فصل است.
۸. چه چیزی این را از یک اسکریپت جدا کرد#
سه چیز، و هیچکدام به کتابخانهٔ خاصی نیاز نداشت:
- ۱) هر مرحله ورودی و خروجیاش را اعلام میکند. بدونِ این، نه ترتیب قابلِ محاسبه است و نه کهنگی.
- ۲) تازگی از محتوا میآید، نه از زمان و نه از حافظهٔ آدم.
- ۳) هر اجرا رد میگذارد. سیاهه بخشی از خروجی است، نه یک لاگِ اضافی.
و صادقانه: راهاندازِ ما هنوز خیلی چیزها ندارد — موازیسازی، اجرای روی چند ماشین، از سرگیری بعد از قطعی، رابطِ گرافیکی. ابزارهایی مثلِ Airflow و Dagster و Prefect دقیقاً همینها را اضافه میکنند. ولی همهشان روی همین سه ایده ساخته شدهاند، و کسی که این سه را خودش نوشته باشد، هر کدامشان را در یک بعدازظهر یاد میگیرد.
و سؤالِ درست این نیست که «کدام ابزار بهتر است»، این است که «کِی این طراحیِ تکماشینه و درونحافظهای میشکند». سه علامتِ قابلِ مشاهده هست و هر سهشان را بدونِ حدس میبینی: ۱) دادهٔ یک مرحله در حافظه جا نمیشود — MemoryError میگیری یا ماشین شروع به swap میکند و زمانِ اجرا ناگهان دهها برابر میشود. ۲) یک مرحله از عمرِ نشست بلندتر میشود — در Colab یعنی runtime وسطِ کار قطع میشود و تو هیچ خروجیِ نیمهکارهای نداری. ۳) دو نفر یا دو زمانبند همزمان روی یک STATE مینویسند — آنوقت فایلِ وضعیت خراب میشود یا یکی کارِ دیگری را دوباره انجام میدهد. تا وقتی هیچکدام از این سه را ندیدهای، آوردنِ یک زمانبندِ توزیعشده فقط پیچیدگیِ اضافه است.
t0 = time.perf_counter()
STATE.unlink() # «همهچیز را از نو» — همان کاری که اسکریپت هر بار میکند
run(PIPELINE)
full = time.perf_counter() - t0
t0 = time.perf_counter()
run(PIPELINE)
again = time.perf_counter() - t0
print(f"اجرای کامل : {full:.2f} ثانیه")
print(f"اجرای مجدد : {again:.3f} ثانیه")
print("نسبت :", round(full / again))
اجرای کامل : 0.06 ثانیه
اجرای مجدد : 0.004 ثانیه
نسبت : 14
عددها روی ماشینِ تو فرق میکنند و مهم هم نیستند؛ نسبت مهم است. روی ۱۲۴۰ ردیف این صرفهجویی مسخره است. روی دادهای که هر مرحلهاش چند دقیقه طول بکشد، همین نسبت تفاوتِ «روزی سه بار آزمایش میکنم» و «روزی یک بار» است.
واژههای تازهٔ این فصل#
| کلمه | تلفظ به حروف فارسی | یعنی چه |
|---|---|---|
| pipeline | پایپلاین | زنجیرهٔ مرحلههایی که داده را از منبع به خروجیِ نهایی میبرد |
| stage | استیج | یک واحدِ کار با ورودی و خروجیِ اعلامشده |
| idempotent | آیدمپوتنت | کاری که اجرای دوبارهاش وضعیت را عوض نمیکند |
| topological order | توپولوژیکال اوردر | ترتیبی که از وابستگیها ساخته میشود، نه از ترتیبِ نوشتن |
| content hash | کانتنت هش | اثرِ انگشتی که فقط از بایتهای محتوا ساخته میشود |
| staleness | استیلنس | کهنه بودنِ خروجی نسبت به ورودی یا کد |
| provenance | پرونانس | سیاههٔ اینکه هر داده از کجا و با چه مرحلهای آمده |
تمرینها
اول خودت فکر کن یا امتحان کن — بعد اینجا را باز کن.
در فصل بعد#
مسیرمان کار میکند ولی همهچیز را در csv مینویسد، چون csv تنها چیزی است که همه بلدند. فصلِ بعد همان جدول را در سه قالبِ متفاوت مینویسد و حجم و زمانِ خواندن را اندازه میگیرد — و نشان میدهد csv یک ستونِ کاملاً درست را چطور بیسروصدا خراب میکند.
به آخر این فصل رسیدی!
اگر ساختی و جواب داد، این دکمه مال توست.