المدخلات نفسها، الجواب نفسه
تكون النتيجة قابلة لإعادة الإنتاج حين تستطيع أنت أو زميل أو مراجع تشغيل المشروع نفسه لاحقًا والحصول على الأرقام نفسها. والاختبار الخلفي الذي لا يمكن إعادة بنائه لا يمكن تدقيقه ولا تنقيحه ولا الوثوق به. تنحرف النتائج لأسباب قليلة: أرقام عشوائية، وإصدارات مكتبات مختلفة، وبيانات تغيّرت أو عُدّلت يدويًا، وإعدادات مبعثرة في الكود، وحالة خفية كدفتر شُغّل بغير ترتيب. ويزيل سير العمل القابل لإعادة الإنتاج كل مصدر للانحراف ويسجّل ما استُخدم.
import random
# Same seed, same "random" numbers
a = random.Random(42)
b = random.Random(42)
print([round(a.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
print([round(b.random(), 3) for _ in range(3)]) # [0.639, 0.025, 0.275]
# A different seed gives a different but equally repeatable sequence
print(round(random.Random(7).random(), 3)) # 0.324البذور وأي مولّد يملكها
تستخدم المحاكاة وإعادة العيّنات أرقامًا عشوائية، وهي في الحقيقة متتالية حتمية تبدأ من بذرة (seed). ثبّت البذرة فيتكرر التشغيل بالضبط. يهيّئ random.seed(42) المولّد العام الوحيد الذي يتشاركه البرنامج كله، فأي كود آخر يسحب أرقامًا يزيح متتاليتك. والأسلم أن تنشئ مولّدك الخاص rng = random.Random(42) (أو numpy.random.default_rng(42)) وتمرّره وتسحب منه. فتعتمد نتائجك على بذرتك وحدها، ولا تُخلّ تهيئتك بعشوائية غيرك.
import random
# The global generator is shared: seeding it changes everyone's numbers
random.seed(1)
first = random.random()
random.seed(1)
print(random.random() == first) # True: global seeding repeats...
# ...but a private generator leaves the global one untouched
random.seed(1)
before = random.getstate()
mine = random.Random(99)
mine.random()
print(random.getstate() == before) # True: your draws did not disturb itبصمات للبيانات والإعدادات
كيف تعرف أن ملف البيانات هو الذي استخدمته الشهر الماضي؟ احسب مجموع تحقق: تحوّل دالة تجزئة مثل SHA-256 أي ملف إلى نص قصير ثابت الطول يتغير إن تغيّر بايت واحد. يعطيها hashlib.sha256(data).hexdigest()؛ واقرأ الملفات الكبيرة على دفعات. وتستحق الإعدادات المعاملة نفسها: سلسِل الإعداد بمفاتيح مرتّبة (json.dumps(config, sort_keys=True)) ثم جزّئ النص، فتكون البصمة واحدة لإعدادين لهما المعنى نفسه مهما كان ترتيب المفاتيح، وتختلف بمجرد تغيّر قيمة.
import hashlib
import json
# A checksum of bytes
print(hashlib.sha256(b"AAPL,184.25").hexdigest()[:12]) # 06ea4aa73ec1 (12 characters of 64)
# A fingerprint of settings, independent of key order
def fingerprint(config):
text = json.dumps(config, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(text.encode("utf-8")).hexdigest()[:12]
one = {"fee": 0.001, "window": 20}
two = {"window": 20, "fee": 0.001}
print(fingerprint(one) == fingerprint(two)) # True
print(fingerprint(one) == fingerprint({"fee": 0.002, "window": 20})) # Falseدوّن ما شغّلته
ينبغي أن يترك كل تشغيل بيانًا (manifest) صغيرًا بجانب نتائجه: البذرة وبصمة الإعداد وإصدارات بايثون والمكتبات ومجموع التحقق لكل ملف إدخال وإيداع Git للكود. بهذا السجل تستطيع القول بالضبط ما الذي أنتج رقمًا وإعادة بنائه لاحقًا. اجمعه مع العادات السابقة: بيئة مثبّتة في requirements.txt والكود في Git والإعدادات في ملف واحد وأمر واحد يشغّل كل شيء من البيانات الخام إلى الجداول النهائية، ولا تعديلات يدوية على البيانات في الأثناء.
import json
import platform
# A manifest records what a run used
manifest = {
"seed": 42,
"config_fingerprint": "a1b2c3d4e5f6",
"python": platform.python_version().split(".")[0] + ".x",
"inputs": {"prices.csv": "9f86d081884c"},
}
print(json.dumps(manifest, indent=2, sort_keys=True))
# {
# "config_fingerprint": "a1b2c3d4e5f6",
# "inputs": {
# "prices.csv": "9f86d081884c"
# },
# "python": "3.x",
# "seed": 42
# }- استخدم
random.Random(seed)خاصًا لتعتمد النتائج على بذرتك وحدها. - بصمة للبيانات (مجموع تحقق ملف) وللإعدادات (JSON مرتّب ثم SHA-256).
- احفظ بيانًا مع كل تشغيل؛ بيئة مثبّتة + إيداع Git + أمر واحد لإعادة البناء.
التالي: الأصناف والكائنات، لجمع البيانات والدوال التي تعالجها في وحدة مرتبة.
المختبر: ما الذي يُقيَّم
- ولّد عوائد عشوائية قابلة للتكرار باستخدام
seeded_returns25 نقطة - خذ بصمة ملف بيانات باستخدام
file_checksum25 نقطة - خذ بصمة إعداد باستخدام
config_fingerprint25 نقطة - سجّل ما استخدمه تشغيل ما باستخدام
run_manifest25 نقطة