الفصل 22

الوحدات النمطية والاستيراد

استخدام ملف كتبته من داخل ملف آخر، ما يحدث فعلياً عند الاستيراد، لماذا نحتاج `if __name__ == "__main__"`، المكتبة القياسية، وفخ حجب الوحدات النمطية بملفاتك الخاصة.

33 دقيقةPython 3.12
  1. 1المشكلة
  2. 2الفهم
  3. 3أمثلة محلولة
  4. 4التوقع
  5. 5التطبيق
  6. 6التحدي

المشكلة التي نقوم بحلها

في الفصل السابق كتبنا دالة باسم line_total. والآن هناك برنامج ثانٍ يحتاج إلى نفس الدالة تماماً.

الطريقة الأسهل هي نسخها ولصقها في الملف الجديد. هذا يفي بالغرض، في اليوم الأول فقط.

ثم تتغير نسبة الضريبة. فتقوم بتعديل أحد الملفين وتنسى الآخر. والآن أصبح لديك برنامجان يعرضان إجماليين مختلفين للطلب نفسه، ولا سبيل لمعرفة أيهما صحيح — لأن كلاهما صحيح وفقاً للملف الخاص به!

المشكلة ليست في الجهد المبذول في النسخ. المشكلة هي أن الحقيقة أصبحت موجودة في مكانين منفصلين.

python
import pricing

print(pricing.TAX_RATE)
print(pricing.line_total(15.0, 3))
text
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(...).

قد تبدو النقطة كتابة إضافية، لكنها تؤدي وظيفة بالغة الأهمية — فعند قراءة الكود، توضح بدقة من أين أتى هذا العنصر. وفي المشاريع الكبيرة، هذا الأمر له قيمة هائلة.

استيراد ما تحتاجه فقط

python
from pricing import line_total

print(line_total(15.0, 3))
print(TAX_RATE)
text
51.75
NameError: name 'TAX_RATE' is not defined

الصيغة from ... import ... تجلب فقط الاسم المطلوب تحديداً، ويمكن استخدام ذلك الاسم مباشرة دون الحاجة لعلامة النقطة.

لكن بقية الأسماء لا تنتقل. لم نطلب TAX_RATE، لذا فهو غير موجود هنا، رغم وجوده في نفس الملف الأصلي.

أيهما تختار؟ كقاعدة عامة: استخدم from عندما تحتاج إلى اسمين أو ثلاثة أسماء محددة، واستورد الوحدة بأكملها عندما تحتاج إلى أسماء كثيرة. وتجنب تماماً كتابة from pricing import *، لأنها تستورد كل الأسماء بخفاء، وبعدها يستحيل معرفة مصدر line_total أثناء قراءة الكود.

إعادة التسمية أثناء الاستيراد

python
import pricing as p

print(p.line_total(15.0, 3))
text
51.75

الكلمة المفتاحية as تمنح الوحدة النمطية اسماً مختصراً داخل هذا الملف.

استخدمها عندما يكون الاسم طويلاً بالفعل، أو عندما يكون هناك عرف شائع متفق عليه في المجتمع البرمجي. أما اختصار الاسم إلى حرف واحد بدافع الكسل فهو تخلٍّ عن وضوح الكود ومقروئيته — فلن يعرف من يقرأ p.line_total(...) من أين جاءت.

الاستيراد يعني تشغيل الملف فعلياً

هذا هو الجانب الذي يكتشفه معظم الناس متأخرين، وغالباً في أوقات غير مناسبة.

ملف tools.py:

python
print("tools is being set up")


def ping():
    return "pong"

ملف main.py:

python
print("main starts")
import tools
import tools

print(tools.ping())
print("main ends")
text
main starts
tools is being set up
pong
main ends

هناك نقطتان جديرتان بالملاحظة:

الاستيراد يعني تنفيذ الملف بالكامل، من أول سطر لآخره. أسطر def تنشئ الدوال، وأي شيء آخر — مثل عبارة print تلك — يُنفذ فعلياً. إذا وضعت أمراً مثل print أو input في المستوى الرئيسي للوحدة، فسيظهر في وجه كل من يستورد تلك الوحدة.

عبارة import tools الثانية لم تفعل أي شيء. يتم تشغيل الوحدة النمطية مرة واحدة فقط لكل برنامج؛ بعد ذلك يعيد بايثون استخدام النسخة التي قام بتحميلها بالفعل في الذاكرة. لذلك، استيراد نفس الوحدة في عشرة أماكن مختلفة يُعد أمراً آمناً وغير مكلف في الأداء.

المتغير __name__ والسطر الشهير

تمتلك كل وحدة نمطية متغيراً داخلياً باسم __name__ يقوم بايثون بتحديده تلقائياً.

ملف tools.py:

python
def ping():
    return "pong"


print("module name is", __name__)

if __name__ == "__main__":
    print("running directly")

عند تشغيله عبر python tools.py:

text
module name is __main__
running directly

لكن عند استيراده عبر import tools من ملف آخر:

text
module name is tools
imported, name was tools

القاعدة بسيطة للغاية: الملف الذي تقوم بتشغيله مباشرة يحمل القيمة "__main__" في __name__؛ أما الملفات التي يتم استيرادها فتحمل اسمها الفعلي.

لذا، فإن أي كود يقع داخل if __name__ == "__main__": لن يعمل إلا عند تشغيل الملف مباشرة، ولن يعمل أبداً عند استيراده من ملف آخر.

سبب الحاجة إلى ذلك ينبع من الفقرة السابقة. فالاستيراد يعني تشغيل الملف كاملاً — فإذا كانت الدالة main() في نهاية الملف دون هذا الشرط، فإن أي شخص يستورد ملفك سيشغّل برنامجك بالكامل تلقائياً. هذا السطر يغلق الباب أمام حدوث ذلك.

المكتبة القياسية (Standard Library)

تأتي بايثون مجهزة بالعديد من الوحدات النمطية المضمنة دون الحاجة لتثبيت أي شيء إضافي.

python
import math
import statistics

print(math.sqrt(144))
print(math.floor(3.7), math.ceil(3.2))
print(statistics.mean([10, 20, 60]))
text
12.0
3 4
30

وهناك وحدة أخرى تُستخدم بكثرة:

python
import random

random.seed(7)
print(random.randint(1, 100))
print(random.choice(["pen", "bag", "ink"]))
text
42
pen

دون كتابة random.seed(7)، كان كل تشغيل سيعطي أرقاماً مختلفة — وهذا هو الغرض الأساسي من العشوائية. لكن تحديد البذرة (Seed) يثبت تسلسل الأرقام الناتجة، مما يجعل المخرجات الموضحة أعلاه مطابقة تماماً لما سيظهر على جهازك. وهذا أمر لا غنى عنه عند كتابة الاختبارات: لاختبار شيء عشوائي، يجب أولاً جعله قابلاً للتكرار والتنبؤ.

هذه ليست عشوائية مطلقة، بل "شبه عشوائية". تولد الوحدة random أرقاماً عبر معادلات حسابية، لذا فإن نفس البذرة ستعطي دائماً نفس التسلسل. لا تستخدمها لتوليد كلمات المرور أو الرموز الأمنية (Tokens) — إذ توجد وحدة مخصصة لذلك اسمها secrets.

أين يبحث بايثون عن الملفات؟

عند تنفيذ import pricing، يبحث بايثون بالترتيب التالي: أولاً في الوحدات النمطية المضمنة في المفسر نفسه، ثم في المجلد الذي يوجد به السكربت الجاري تشغيله، ثم في المكتبة القياسية، ثم في الحزم المثبتة خارجياً.

هذا الترتيب له نتيجة غير متوقعة قد توقع المبرمجين في الفخ.

افترض أن لديك ملفاً باسم random.py في مجلدك، وكتبت:

python
import random

print(random.choice([1, 2, 3]))
text
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).

text
shop/
    __init__.py
    pricing.py
main.py

يمكن بعد ذلك كتابة المسار مفصولاً بنقاط:

python
from shop.pricing import line_total

print(line_total(15.0, 3))
text
51.75

هذا يكفي في الوقت الحالي. وفيما تبقى من هذا الفصل، سنكتفي بالملفات الموجودة في نفس المجلد.


مثال متكامل

ثلاثة ملفات تعتمد على بعضها في اتجاه واحد فقط:

ملف pricing.py:

python
"""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:

python
"""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:

python
"""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:

text
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).