الوحدات النمطية والاستيراد
استخدام ملف كتبته من داخل ملف آخر، ما يحدث فعلياً عند الاستيراد، لماذا نحتاج `if __name__ == "__main__"`، المكتبة القياسية، وفخ حجب الوحدات النمطية بملفاتك الخاصة.
- 1المشكلة
- 2الفهم
- 3أمثلة محلولة
- 4التوقع
- 5التطبيق
- 6التحدي
المشكلة التي نقوم بحلها
في الفصل السابق كتبنا دالة باسم line_total. والآن هناك برنامج ثانٍ يحتاج إلى نفس الدالة تماماً.
الطريقة الأسهل هي نسخها ولصقها في الملف الجديد. هذا يفي بالغرض، في اليوم الأول فقط.
ثم تتغير نسبة الضريبة. فتقوم بتعديل أحد الملفين وتنسى الآخر. والآن أصبح لديك برنامجان يعرضان إجماليين مختلفين للطلب نفسه، ولا سبيل لمعرفة أيهما صحيح — لأن كلاهما صحيح وفقاً للملف الخاص به!
المشكلة ليست في الجهد المبذول في النسخ. المشكلة هي أن الحقيقة أصبحت موجودة في مكانين منفصلين.
import pricing
print(pricing.TAX_RATE)
print(pricing.line_total(15.0, 3))0.15
51.75ملف واحد، ويمكن لعدد لا نهائي من البرامج استخدامه كما تشاء. وعندما تتغير النسبة، تتغير في مكان واحد فقط.
يدور هذا الفصل حول هذا السطر الواحد — import — وما يحدث فعلياً خلف الكواليس عند كتابته.
في نهاية هذا الدرس ستكون قادراً على
- استيراد ملف برمجته بنفسك من داخل ملف آخر
- التمييز بين
import xوfrom x import yوimport x as z - شرح ما يحدث لحظة الاستيراد وعدد المرات التي يُنفذ فيها
- تفسير سبب كتابة
if __name__ == "__main__": - استخدام الوحدات النمطية من المكتبة القياسية لبايثون
- معرفة أين يبحث بايثون عن الملفات، وتجنب حجب الوحدات النمطية بأسماء ملفاتك الخاصة
المتطلبات السابقة: نطاق المتغيرات والقيم الافتراضية القابلة للتعديل.
الوحدة النمطية هي مجرد ملف عادي
الملف المسمى pricing.py هو وحدة نمطية (Module) تسمى pricing. لا تحتاج للإعلان عن أي شيء خاص ولا لكتابة سطر سحري — مجرد وجود الملف كافٍ تماماً.
العبارة import pricing تجلب الوحدة النمطية بأكملها، ويمكنك الوصول إلى محتوياتها عبر علامة النقطة: pricing.TAX_RATE، و pricing.line_total(...).
قد تبدو النقطة كتابة إضافية، لكنها تؤدي وظيفة بالغة الأهمية — فعند قراءة الكود، توضح بدقة من أين أتى هذا العنصر. وفي المشاريع الكبيرة، هذا الأمر له قيمة هائلة.
استيراد ما تحتاجه فقط
from pricing import line_total
print(line_total(15.0, 3))
print(TAX_RATE)51.75
NameError: name 'TAX_RATE' is not definedالصيغة from ... import ... تجلب فقط الاسم المطلوب تحديداً، ويمكن استخدام ذلك الاسم مباشرة دون الحاجة لعلامة النقطة.
لكن بقية الأسماء لا تنتقل. لم نطلب TAX_RATE، لذا فهو غير موجود هنا، رغم وجوده في نفس الملف الأصلي.
أيهما تختار؟ كقاعدة عامة: استخدم from عندما تحتاج إلى اسمين أو ثلاثة أسماء محددة، واستورد الوحدة بأكملها عندما تحتاج إلى أسماء كثيرة. وتجنب تماماً كتابة from pricing import *، لأنها تستورد كل الأسماء بخفاء، وبعدها يستحيل معرفة مصدر line_total أثناء قراءة الكود.
إعادة التسمية أثناء الاستيراد
import pricing as p
print(p.line_total(15.0, 3))51.75الكلمة المفتاحية as تمنح الوحدة النمطية اسماً مختصراً داخل هذا الملف.
استخدمها عندما يكون الاسم طويلاً بالفعل، أو عندما يكون هناك عرف شائع متفق عليه في المجتمع البرمجي. أما اختصار الاسم إلى حرف واحد بدافع الكسل فهو تخلٍّ عن وضوح الكود ومقروئيته — فلن يعرف من يقرأ p.line_total(...) من أين جاءت.
الاستيراد يعني تشغيل الملف فعلياً
هذا هو الجانب الذي يكتشفه معظم الناس متأخرين، وغالباً في أوقات غير مناسبة.
ملف tools.py:
print("tools is being set up")
def ping():
return "pong"ملف main.py:
print("main starts")
import tools
import tools
print(tools.ping())
print("main ends")main starts
tools is being set up
pong
main endsهناك نقطتان جديرتان بالملاحظة:
الاستيراد يعني تنفيذ الملف بالكامل، من أول سطر لآخره. أسطر def تنشئ الدوال، وأي شيء آخر — مثل عبارة print تلك — يُنفذ فعلياً. إذا وضعت أمراً مثل print أو input في المستوى الرئيسي للوحدة، فسيظهر في وجه كل من يستورد تلك الوحدة.
عبارة import tools الثانية لم تفعل أي شيء. يتم تشغيل الوحدة النمطية مرة واحدة فقط لكل برنامج؛ بعد ذلك يعيد بايثون استخدام النسخة التي قام بتحميلها بالفعل في الذاكرة. لذلك، استيراد نفس الوحدة في عشرة أماكن مختلفة يُعد أمراً آمناً وغير مكلف في الأداء.
المتغير __name__ والسطر الشهير
تمتلك كل وحدة نمطية متغيراً داخلياً باسم __name__ يقوم بايثون بتحديده تلقائياً.
ملف tools.py:
def ping():
return "pong"
print("module name is", __name__)
if __name__ == "__main__":
print("running directly")عند تشغيله عبر python tools.py:
module name is __main__
running directlyلكن عند استيراده عبر import tools من ملف آخر:
module name is tools
imported, name was toolsالقاعدة بسيطة للغاية: الملف الذي تقوم بتشغيله مباشرة يحمل القيمة "__main__" في __name__؛ أما الملفات التي يتم استيرادها فتحمل اسمها الفعلي.
لذا، فإن أي كود يقع داخل if __name__ == "__main__": لن يعمل إلا عند تشغيل الملف مباشرة، ولن يعمل أبداً عند استيراده من ملف آخر.
سبب الحاجة إلى ذلك ينبع من الفقرة السابقة. فالاستيراد يعني تشغيل الملف كاملاً — فإذا كانت الدالة main() في نهاية الملف دون هذا الشرط، فإن أي شخص يستورد ملفك سيشغّل برنامجك بالكامل تلقائياً. هذا السطر يغلق الباب أمام حدوث ذلك.
المكتبة القياسية (Standard Library)
تأتي بايثون مجهزة بالعديد من الوحدات النمطية المضمنة دون الحاجة لتثبيت أي شيء إضافي.
import math
import statistics
print(math.sqrt(144))
print(math.floor(3.7), math.ceil(3.2))
print(statistics.mean([10, 20, 60]))12.0
3 4
30وهناك وحدة أخرى تُستخدم بكثرة:
import random
random.seed(7)
print(random.randint(1, 100))
print(random.choice(["pen", "bag", "ink"]))42
penدون كتابة random.seed(7)، كان كل تشغيل سيعطي أرقاماً مختلفة — وهذا هو الغرض الأساسي من العشوائية. لكن تحديد البذرة (Seed) يثبت تسلسل الأرقام الناتجة، مما يجعل المخرجات الموضحة أعلاه مطابقة تماماً لما سيظهر على جهازك. وهذا أمر لا غنى عنه عند كتابة الاختبارات: لاختبار شيء عشوائي، يجب أولاً جعله قابلاً للتكرار والتنبؤ.
هذه ليست عشوائية مطلقة، بل "شبه عشوائية". تولد الوحدةrandomأرقاماً عبر معادلات حسابية، لذا فإن نفس البذرة ستعطي دائماً نفس التسلسل. لا تستخدمها لتوليد كلمات المرور أو الرموز الأمنية (Tokens) — إذ توجد وحدة مخصصة لذلك اسمهاsecrets.
أين يبحث بايثون عن الملفات؟
عند تنفيذ import pricing، يبحث بايثون بالترتيب التالي: أولاً في الوحدات النمطية المضمنة في المفسر نفسه، ثم في المجلد الذي يوجد به السكربت الجاري تشغيله، ثم في المكتبة القياسية، ثم في الحزم المثبتة خارجياً.
هذا الترتيب له نتيجة غير متوقعة قد توقع المبرمجين في الفخ.
افترض أن لديك ملفاً باسم random.py في مجلدك، وكتبت:
import random
print(random.choice([1, 2, 3]))AttributeError: module 'random' has no attribute 'choice'ملفك جاء قبل وحدة random الحقيقية في الترتيب، فقام بايثون باستيراد ملفك أنت — وبالتأكيد لا يحتوي ملفك على دالة choice.
رسالة الخطأ محيرة، لأنها تقول إن random لا يملك السمة choice، وهو أمر يبدو مستحيلاً. لكنه يتحدث عن ملفك أنت.
القاعدة: إياك أن تسمي ملفاً باسم وحدة نمطية موجودة. أسماء مثل random.py و string.py و json.py و csv.py تبدو أسماء بريئة، ولهذا السبب تحديداً يقع الناس في الفخ.
استثناء واحد يوضح القاعدة. إذا أنشأت ملفاً باسمmath.pyوكتبتimport math، فستحصل على الوحدة الحقيقية وليس ملفك، لأنmathمدمجة مباشرة في نواة المفسر وتأتي في المرتبة الأولى في الترتيب. ولهذا ينصب الفخ لبعض الأسماء دون غيرها — وهو أمر يصعب تذكره مقارنة بالقاعدة البسيطة: اختر أسماء مختلفة لملفاتك.
تنظيم الملفات في مجلدات (Packages)
مع زيادة عدد الملفات، يمكن تجميعها داخل مجلدات. ضع ملفاً باسم __init__.py داخل المجلد — حتى لو كان فارغاً — وسيتحول المجلد إلى حزمة برمجية (Package).
shop/
__init__.py
pricing.py
main.pyيمكن بعد ذلك كتابة المسار مفصولاً بنقاط:
from shop.pricing import line_total
print(line_total(15.0, 3))51.75هذا يكفي في الوقت الحالي. وفيما تبقى من هذا الفصل، سنكتفي بالملفات الموجودة في نفس المجلد.
مثال متكامل
ثلاثة ملفات تعتمد على بعضها في اتجاه واحد فقط:
ملف pricing.py:
"""Prices and tax. Nothing here prints, and nothing here asks for input."""
TAX_RATE = 0.15
def line_total(price, quantity):
"""One line of an order, tax included."""
return round(price * quantity * (1 + TAX_RATE), 2)
def order_total(lines):
"""`lines` is a list of (name, price, quantity) tuples."""
return round(sum(line_total(price, qty) for _, price, qty in lines), 2)ملف report.py:
"""Turning numbers into lines of text. It imports pricing; pricing imports nothing."""
import pricing
def receipt(lines):
rows = [f"{name:<8} {pricing.line_total(price, qty):>8.2f}"
for name, price, qty in lines]
rows.append("-" * 17)
rows.append(f"{'total':<8} {pricing.order_total(lines):>8.2f}")
return rows
if __name__ == "__main__":
print("report.py has no data of its own to show")ملف main.py:
"""The only file that is meant to be run."""
import statistics
from report import receipt
ORDER = [
("pen", 15.0, 3),
("bag", 850.0, 1),
("ink", 120.0, 2),
]
def main():
for row in receipt(ORDER):
print(row)
print()
quantities = [qty for _, _, qty in ORDER]
print("mean quantity:", statistics.mean(quantities))
if __name__ == "__main__":
main()عند التشغيل عبر python main.py:
pen 51.75
bag 977.50
ink 276.00
-----------------
total 1305.25
mean quantity: 2أربعة أمور جديرة بالدراسة والتأمل:
الاعتماديات تسير في اتجاه واحد فقط. الملف main يعرف report، والملف report يعرف pricing، بينما pricing لا يعرف أحداً منهما. يشكل هذا الترتيب سلسلة أحادية الاتجاه، والملف الموجود في نهايتها — وهو pricing — يمكن اختباره بالكامل بمفرده وبمعزل عن الآخرين. لو احتاج pricing إلى استيراد report بالمقابل، لما استطاع أي منهما العمل بمفرده.
pricing.py لا يطبع أي شيء ولا يطلب أي مدخلات. إنه يتلقى قيماً ويرجع قيماً — وهي الدوال النقية التي درسناها في الفصل الحادي والعشرين. وبما أن الاستيراد يعني تشغيل الملف، فإن هذا الانضباط ليس مجرد تنظيم أنيق، بل هو ضرورة حتمية.
يحتوي report.py على سطر if __name__ == "__main__": أيضاً، وهو لا يقدم وظيفة كبيرة — فالملف لا يملك بيانات خاصة به ليعرضها. كان بالإمكان حذفه، لكنه وُضع لكي يعرض رسالة توضيحية إذا قام أحد بتشغيل الملف مباشرة، بدلاً من أن ينتهي في صمت مطبق.
لا يوجد ملف يقوم بعمل فعلي من تلقاء نفسه باستثناء main.py. للبرنامج بأكمله نقطة دخول واحدة ومحددة، وقراءتها توضح لك مسار البرنامج كاملاً.
حالات الخطأ الشائعة
ModuleNotFoundError: No module named 'priceing' خطأ في كتابة الاسم، أو أن الملف في مجلد آخر. تأكد من وجوده بجوار السكربت الذي تشغّله.
ImportError: cannot import name 'line_totl' from 'pricing' تم العثور على الوحدة النمطية، لكن الاسم غير موجود داخلها. وغالباً ما تقترح الرسالة الاسم الصحيح — Did you mean: 'line_total'?
AttributeError: module 'random' has no attribute 'choice' غالباً ما يكون لديك ملف يحجب الوحدة النمطية الحقيقية. ابحث عن ذلك الاسم في مجلدك وغيّر اسمه، واحذف مجلد __pycache__ أيضاً.
ImportError: cannot import name 'A' from partially initialized module 'a' (most likely due to a circular import) ملفان يستورد كل منهما الآخر (استيراد دائري). والحل المعتاد هو نقل الجزء المشترك الذي يحتاجه كلاهما إلى ملف ثالث لا يستورد أياً منهما.
طُبعت نصوص أو طُلبت مدخلات بمجرد استيراد الملف يوجد كود في المستوى الرئيسي للوحدة النمطية. ضعه داخل دالة واستدعه من داخل شرط if __name__ == "__main__":.
عدّلت الملف لكن البرنامج ما زال يعمل بالسلوك القديم أعد تشغيل البرنامج — فالبرنامج الذي يعمل بالفعل لا يعيد قراءة الملف من القرص. وفي دفاتر جوبيتر (Notebooks)، أعد تشغيل النواة (Kernel).
Step 4 of 6 — Predict
Check your understanding
line_total was asked for, TAX_RATE was not. What happens?
# pricing.py
TAX_RATE = 0.15
def line_total(price, quantity):
return round(price * quantity * (1 + TAX_RATE), 2)
# main.py
from pricing import line_total
print(line_total(15.0, 3))
print(TAX_RATE)- A`51.75` prints, then a `NameError`
- BBoth `51.75` and `0.15` print
- CAn `ImportError`
- D`51.75` prints, then `None`
import tools is written twice. What is printed?
# tools.py
print("tools is being set up")
def ping():
return "pong"
# main.py
print("main starts")
import tools
import tools
print(tools.ping())
print("main ends")- Amain starts tools is being set up pong main ends
- Bmain starts tools is being set up tools is being set up pong main ends
- Ctools is being set up main starts pong main ends
- Dmain starts pong main ends
python main.py is being run. What is printed?
# tools.py
print("name:", __name__)
if __name__ == "__main__":
print("direct")
# main.py
import tools
print("done")- Aname: tools done
- Bname: __main__ direct done
- Cname: tools direct done
- Dname: main done
Answering needs an account
Sign in to check your answers
The questions are above, and working them out in your head is the part that matters. Sign in to see the answers, the explanations and the three-level hints.
دورك الآن
أنشئ ثلاثة ملفات في مجلد واحد:
textutil.py — يحتوي على دالتين: initials(name) لتحويل "rafi ahmed" إلى "RA"، و shorten(text, limit) لاقتطاع النصوص الطويلة وإضافة ... في نهايتها. لا تضع أي أمر print في هذا الملف.
table.py — يستورد textutil، ويقدم دالة rows(people) التي تأخذ قائمة أسماء وترجع قائمة من الأسطر المنسقة.
main.py — يستورد table، ويستدعي الدالة ممرراً بعض الأسماء، ثم يطبع النتائج. ضع سطر if __name__ == "__main__": في الملفات الثلاثة جميعاً.
ثم أجرِ هذه التجارب الأربع العملية بنفسك:
- أضف
print("loaded")في السطر الأول تماماً من ملفtextutil.pyوشغّلpython main.py. هل يظهر السطر؟ هل يظهر قبل أولprintفيmain.pyأم بعده؟ - اكتب
import tableمرتين متتاليتين فيmain.py. كم مرة ستُطبع كلمةloaded؟ - أنشئ ملفاً فارغاً باسم
statistics.pyفي نفس المجلد، ثم استوردstatisticsفيmain.pyواستدعِstatistics.mean([1, 2]). اقرأ رسالة الخطأ — عن أي ملفstatisticsتتحدث؟ - أضف
import tableإلى ملفtextutil.pyبحيث يستورد كل منهما الآخر. ما هي رسالة الخطأ التي ستظهر لك؟
الخطآن الأخيران قد يكلّفان ساعات طويلة من البحث إن لم تكن معتاداً على رسائلهما، ولكن حلهما لا يستغرق سوى ثوانٍ بمجرد أن تختبرهما بنفسك.
Step 6 of 6
التحدي — the chapter quiz
عشرة أسئلة متدرجة من السهل إلى الصعب. الأسئلة الأخيرة صعبة عن قصد.
Sign in to take the quiz