المساحات الاسمية في بايثون

مساحة أسماء بايثون هي ربط بين الأسماء والكائنات. تعمل كقاموس، حيث المفاتيح هي أسماء الكائنات والقيم هي الكائنات نفسها. تُنظّم مساحات الأسماء المتغيرات والدوال في مساحة مخصصة، مما يسمح باستخدام نسخ متعددة تحمل الاسم نفسه دون تعارض، طالما أنها في مساحات أسماء مختلفة.

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

إن فهم آلية عمل مساحات الأسماء سيُحسّن قدرتك على إدارة وتنظيم الشيفرة البرمجية بكفاءة في برامج بايثون، مما يُساعد على تجنب تضارب الأسماء وغيرها من المشاكل. لتحقيق أقصى استفادة من هذا البرنامج التعليمي، يجب أن تكون مُلِمًّا بمتغيرات ودوال بايثون. كما تُعدّ معرفة الدوال الداخلية والإغلاقات ميزة إضافية.

التعرف على مساحات الأسماء في بايثون

مساحة الأسماء هي حاوية تحتوي على الأسماء الرمزية المُعرّفة حاليًا والكائنات التي يشير إليها كل اسم. يمكنك اعتبار مساحة الأسماء قاموسًا، حيث تكون المفاتيح أسماء الكائنات والقيم هي الكائنات نفسها. يربط كل زوج مفتاح-قيمة اسمًا بالكائن المقابل له.

تتيح لك مساحات الأسماء استخدام الاسم نفسه في سياقات مختلفة دون تضارب. يشبه الأمر تخصيص مساحة صغيرة لكل شيء في منزل منظم جيدًا. فهي تسمح لبايثون بالحفاظ على تنظيم الأشياء، ومنع تضارب الأسماء، ودعم مفهوم النطاق، وتعزيز النمطية.

تعتبر مساحات الأسماء بالغة الأهمية في بايثون لدرجة أنها تم تخليدها في كتاب Zen of Python:

إن المساحات الاسمية هي فكرة رائعة للغاية – فلنعمل على المزيد منها!

إن مساحات الأسماء هي رائعة بالفعل، ويستخدمها بايثون على نطاق واسع. وحسب هيكلة الكود، يمكن لبرنامج بايثون أن يحتوي على ما يصل إلى أربعة أنواع مختلفة من مساحات الأسماء:

  • مُدمج
  • عالمي
  • محلي
  • مغلق أو محلي

تختلف أعمار هذه المساحات. عند تنفيذ بايثون لبرنامج، يُنشئ مساحات أسماء حسب الحاجة، ثم يُزيلها عند عدم الحاجة إليها. عادةً، توجد العديد من مساحات الأسماء في أي وقت.

تُنفَّذ مساحات الأسماء العالمية والمحلية وغير المحلية في بايثون كقواميس. في المقابل، لا تُعتبر مساحة الأسماء المُدمجة قاموسًا، بل وحدة تُسمى “builtins”. تعمل هذه الوحدة كحاوية لمساحة الأسماء المُدمجة.

في الأقسام التالية، ستتعرف على هذه المساحات الأربعة وما هو محتواها وسلوكها.

مساحة الاسم المُدمجة

تحتوي مساحة الأسماء المُدمجة على أسماء جميع كائنات بايثون المُدمجة. تكون هذه المساحة مُتاحة أثناء تشغيل مُفسّر بايثون. لذا، يُمكنك الوصول إلى الأسماء الموجودة فيها في أي وقت ضمن الكود دون الحاجة إلى استيرادها صراحةً.

يمكنك إدراج الكائنات في مساحة الاسم المضمنة باستخدام الدالة ()dir باستخدام الكائن __builtins__ كحجة:

>>> dir(__builtins__)
[
    'ArithmeticError',
    'AssertionError',
    'AttributeError',
    'BaseException',
    ...
    'super',
    'tuple',
    'type',
    'vars',
    'zip'
]

قد تتعرف هنا على بعض الكائنات، مثل الاستثناءات المضمنة، والدوال المضمنة، وأنواع البيانات المضمنة. يُنشئ بايثون مساحة الاسم المضمنة عند بدء تشغيله، ويبقيها نشطة حتى انتهاء المُفسِّر.

مساحة الاسم العالمية

تحتوي مساحة الأسماء العالمية على الأسماء المُعرّفة على مستوى الوحدة. يُنشئ بايثون مساحة أسماء عالمية رئيسية عند بدء تشغيل نص البرنامج الرئيسي. تبقى هذه المساحة موجودة حتى انتهاء عمل المُفسّر.

بالإضافة إلى ذلك، لكل وحدة مساحة اسم عالمية خاصة بها. يُنشئ المُفسّر مساحة اسم عالمية لأي وحدة يُحمّلها برنامجك باستخدام عبارة الاستيراد.

في الوقت الحالي، عندما ترى مصطلح مساحة الاسم العالمية، يمكنك أن تفكر فيها باعتبارها المساحة التي تنتمي إلى البرنامج الرئيسي.

لتوضيح ذلك، ارجع إلى جلسة REPL وقم بتشغيل الكود التالي:

>>> number = 42
>>> dir()
[
    '__annotations__',
    '__builtins__',
    '__cached__',
    '__doc__',
    '__file__',
    ...
    'number'
]

في هذا المقطع البرمجي، تُعرّف متغير number كاسم عام. ثم تستدعي الدالة المُدمجة ()dir للتحقق من قائمة الأسماء المُعرّفة في نطاقك العام الحالي. في نهاية القائمة، ستجد مفتاح “number”، المُطابق لمتغيرك العام.

مساحة الاسم المحلية

يُنشئ مُفسّر بايثون مساحة اسم جديدة ومُخصّصة عند استدعاء دالة. هذه المساحة محلية للدالة:

>>> def double_number(number):
...     result = number * 2
...     print(dir())
...     return result
...

>>> double_number(4)
['number', 'result']
8

>>> result
Traceback (most recent call last):
    ...
NameError: name 'result' is not defined

>>> number
Traceback (most recent call last):
    ...
NameError: name 'number' is not defined

في هذا المثال، وسيطة الرقم ومتغير result محليان لـ ()double_number. لاحظ أنه إذا حاولت الوصول إليهما بعد إرجاع الدالة، فستحصل على استثناءات NameError.

مساحة الأسماء المغلقة أو غير المحلية

يمكنك أيضًا تعريف دالة داخل أخرى. في هذه الحالة، ستحصل على ما يُعرف بالدالة الداخلية. في المثال أدناه، لديك ثلاثة نطاقات:

>>> global_variable = "global"

>>> def outer_func():
...     # Nonlocal scope
...     nonlocal_variable = "nonlocal"
...     def inner_func():
...         # Local scope
...         local_variable = "local"
...         print(f"Hi from the '{local_variable}' scope!")
...         print(f"Hi from the '{nonlocal_variable}' scope!")
...         print(f"Hi from the '{global_variable}' scope!")
...     inner_func()
...

>>> outer_func()
Hi from the 'local' scope!
Hi from the 'nonlocal' scope!
Hi from the 'global' scope!

في هذا المثال، تُنشئ أولاً متغيرًا عامًا على مستوى الوحدة. ثم تُعرّف دالة تُسمى ()external_func. داخل هذه الدالة، يوجد متغير nonlocal_variable، وهو محلي لـ ()external_func وغير محلي لـ ()inner_func. في ()inner_func، تُنشئ متغيرًا آخر يُسمى local_variable، وهو محلي للدالة نفسها.

تبقى كلٌّ من هذه المساحات قائمةً حتى عودة دوالها. قد لا يستعيد بايثون الذاكرة المخصصة لتلك المساحات فورًا عند انتهاء دوالها، ولكن جميع الإشارات إلى الكائنات التي تحتويها تُصبح غير صالحة.

فهم نطاق الأسماء

يتيح لك وجود مساحات أسماء متعددة ومتميزة استخدام عدة نسخ مختلفة من اسم معين في آنٍ واحد أثناء تشغيل برنامج بايثون. طالما أن كل نسخة في مساحة أسماء مختلفة، فسيتم صيانتها بشكل منفصل ولن تتداخل مع بعضها البعض.

هذا يثير سؤالاً. لنفترض أنك تشير إلى الاسم x في شيفرتك، وأن x موجود في عدة مساحات أسماء. كيف يعرف بايثون أي مساحة أسماء تقصد في كل مرة؟

تكمن الإجابة في مفهوم النطاق. النطاق هو منطقة البرنامج التي يحمل فيها الاسم معنى. يُحدد المُفسّر هذا المعنى أثناء التشغيل بناءً على مكان ظهور تعريف الاسم ومكان الإشارة إليه في الكود.

ملاحظة: راجع صفحة ويكيبيديا حول النطاق في البرمجة الحاسوبية لمناقشة متعمقة حول نطاق المتغير في لغات البرمجة.

يرتبط مفهوما مساحة الأسماء والنطاق ارتباطًا وثيقًا. عمليًا، تُطبّق بايثون مفهوم النطاق على عملية البحث عن الأسماء من خلال مساحات الأسماء.

قاعدة LEGB للبحث عن الأسماء

بالعودة إلى السؤال السابق، لنفترض أنك تُشير إلى x من داخل دالة داخلية. في هذه الحالة، يبحث بايثون عن x بالترتيب التالي:

  • محليًا: يبحث بايثون عن x داخل الدالة الداخلية. إذا لم يجد x هناك، ينتقل إلى النطاق المُحيط.
  • مغلق: يبحث بايثون بعد ذلك عن x في نطاق دالة مغلقة. إذا لم يُعثر عليه هناك، ينتقل إلى النطاق العام.
  • عالمي: يبحث بايثون عن x في النطاق العالمي. إذا لم يجده، ينتقل إلى النطاق المدمج.
  • مُدمج: أخيرًا، يبحث بايثون عن x في النطاق المُدمج. إذا لم يجده، يُطلق استثناء NameError.

هذا ما يُعرف بقاعدة LEGB. يبحث المُفسّر عن اسم من الداخل إلى الخارج، باحثًا في النطاق المحلي، والمحيط، والعالمي، وأخيرًا، النطاق المُضمّن:

قاعدة LEGB في بايثون

إذا لم يعثر بايثون على الاسم في أيٍّ من هذه المساحات، فسيُطلق استثناء NameError، كما تعلمتَ سابقًا. في الأقسام التالية، ستستكشف بعض الأمثلة التي توضح كيفية عمل قاعدة LEGB عمليًا.

قاعدة LEGB في العمل

في المثال أدناه، يمكنك تعريف المتغير x خارج كل من ()external و ()inner حتى يكون موجودًا في نطاقك العالمي الحالي:

>>> x = "global"

>>> def outer():
...     def inner():
...         print(x)
...     inner()
...

>>> outer()
global

لا يمكن لدالة ()print الإشارة إلا إلى كائن x واحد. يعرض هذا الكائن x المُعرّف في مساحة الاسم العالمية، والتي تحتوي على السلسلة “global”.

في المثال التالي، يمكنك تعريف x في مكانين: مرة في النطاق العالمي ومرة ​​داخل الدالة ()external:

>>> x = "global"

>>> def outer():
...     x = "enclosing"
...     def inner():
...         print(x)
...     inner()
...

>>> outer()
enclosing

كما هو الحال في المثال السابق، يشير ()inner إلى x، ولكن هذه المرة، لديه تعريفان للاختيار من بينهما:

  • x في النطاق العالمي
  • x في النطاق المغلق

وفقًا لقاعدة LEGB، يبحث المُفسِّر عن القيمة من النطاق المغلق قبل البحث في النطاق العالمي. لذا، تعرض دالة ()print القيمة المغلقة بدلًا من العالمية.

بعد ذلك، تُعرّف x في كل مكان. التعريف الأول يقع ضمن النطاق العالمي. التعريف الثاني يقع داخل دالة ()external وخارج دالة ()inner. أما التعريف الثالث، فيقع داخل دالة ()inner:

>>> x = "global"

>>> def outer():
...     x = "enclosing"
...     def inner():
...         x = "local"
...         print(x)
...     inner()
...

>>> outer()
local

الآن، يجب على ()print التمييز بين ثلاثة احتمالات مختلفة:

  • x في النطاق العالمي
  • علامة x في النطاق المغلق
  • x في النطاق المحلي لـ ()inner

في هذه الحالة، تنص قاعدة LEGB على أن ()inner ترى أولاً قيمتها المحلية المحددة وهي x، وبالتالي تعرض دالة ()print القيمة المحلية.

بعد ذلك، لديك حالة تحاول فيها دالة ()inner طباعة قيمة x، لكن x غير مُعرَّفة في أي مكان. ينتج عن هذا خطأ:

>>> def outer():
...     def inner():
...         print(x)
...     inner()
...

>>> outer()
Traceback (most recent call last):
    ...
NameError: name 'x' is not defined

هذه المرة، لا يجد Python x في أي من مساحات الأسماء، لذا تقوم دالة ()print بإنشاء استثناء NameError.

تظليل الأسماء المضمنة في بايثون

قد يكون إخفاء أو تجاوز الأسماء من مساحة الأسماء المُدمجة مشكلة شائعة للمبتدئين في بايثون. الأسماء المُدمجة متاحة دائمًا، وقد يكون بعضها مناسبًا للمتغيرات العملية.

على سبيل المثال، لنفترض أنك تتعلم عن القوائم وقم بتشغيل الكود التالي:

>>> list = [1, 2, 3, 4]
>>> list
[1, 2, 3, 4]

في هذا المثال، استخدمتَ list كاسم لكائن قائمة يحتوي على بعض الأرقام. هذا التعيين يُلغي دالة ()list المُدمجة:

>>> list(range(10))
Traceback (most recent call last):
    ...
TypeError: 'list' object is not callable

الآن، يفشل استدعاء ()list لأنك تجاوزت الاسم في الكود السابق. الحل السريع لهذه المشكلة هو استخدام عبارة del لإزالة المتغير المخصص واستعادة الاسم الأصلي:

>>> del list  # Remove the redefined name
>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

إذا قمت بإعادة تعيين اسم مضمن عن طريق الخطأ، فيمكنك تشغيل عبارة del name السريعة لإزالة إعادة التعريف من نطاقك واستعادة الاسم المضمن الأصلي في نطاق عملك.

سيكون النهج الأكثر موثوقية هو استيراد وحدة builtins واستخدام الأسماء المؤهلة بالكامل:

>>> list = [1, 2, 3, 4]

>>> import builtins
>>> builtins.list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

هذه المرة، على الرغم من أنك قمت بتجاوز اسم القائمة، فلا يزال بإمكانك استخدام الدالة المضمنة الأصلية بسبب الاسم المؤهل بالكامل ()builtins.list، والذي يشير بشكل لا لبس فيه إلى الكائن في مساحة الاسم المضمنة.

إدارة قواميس مساحة الأسماء

يوفر بايثون دالتين مدمجتين، ()globals و()locals، تتيحان الوصول إلى قواميس النطاقين العالمي والمحلي. تتيح لك هاتان الدالتان الوصول المباشر إلى محتوى كلا النطاقين، واللذين يتم تنفيذهما كقواميس.

في الأقسام التالية، ستتعلم كيفية استخدام ()globals و()locals في برمجتك. ستتعرف أيضًا على الفرق الجوهري بين هاتين الدالتين المدمجتين.

دالة ()globals

تُرجع دالة ()globals المُدمجة مرجعًا إلى قاموس مساحة الأسماء العالمية الحالي. يمكنك استخدامها للوصول إلى الكائنات في مساحة الأسماء العالمية. هكذا تبدو عند بدء جلسة REPL:

>>> type(globals())
<class 'dict'>

>>> globals()
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': '_pyrepl',
    ...
}

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

الآن، انظر ماذا يحدث عندما تقوم بتعريف متغير في النطاق العالمي:

>>> number = 42
>>> globals()
{
    '__name__': '__main__',
    '__doc__': None,
    ...
    'number': 42
}

بعد عبارة التعيين number = 42، يظهر عنصر جديد في قاموس مساحة الأسماء العالمية. مفتاح القاموس هو اسم الكائن ورقمه، وقيمة القاموس هي قيمة الكائن 42.

عادةً ما يمكنك الوصول إلى هذا الكائن بالطريقة المعتادة بالإشارة إلى اسمه الرمزي، وهو رقم. ولكن يمكنك أيضًا الوصول إليه بشكل غير مباشر من خلال قاموس مساحة الأسماء العالمية:

>>> number
42
>>> globals()["number"]
42

>>> number is globals()["number"]
Trueي

يمكنك الوصول إلى قيمة الرقم باستخدام صيغة البحث عن مفتاح القاموس. يؤكد عامل التشغيل is في المثال الأخير أن هذين العنصرين هما نفس الكائن.

يمكنك أيضًا استخدام ()globals لإنشاء وتعديل الإدخالات في مساحة الاسم العالمية:

>>> globals()["message"] = "Welcome to Real Python!"

>>> globals()
{
    '__name__': '__main__',
    '__doc__': None,
    ...
    'number': 42,
    'message': 'Welcome to Real Python!'
}
>>> message
'Welcome to Real Python!'

>>> globals()["message"] = "Hello, World!"
>>> message
'Hello, World!'

في هذا المثال، تُنشئ أولاً متغيرًا عالميًا جديدًا باسم “message” وتُخصص له سلسلة نصية. ثم تستدعي الدالة ()globals لفحص مساحة الأسماء والعثور على المتغير الجديد في آخر موضع في القاموس.

دالة ()locals

يوفر بايثون أيضًا دالة مدمجة تُسمى ()locals. وهي مشابهة لـ ()globals، ولكنها تتيح لك الوصول إلى الكائنات في مساحة الأسماء المحلية.

>>> def func(x, y):
...     message = "Hello!"
...     print(locals())
...

>>> func(10, 0.5)
{'x': 10, 'y': 0.5, 'message': 'Hello!'}

عند استدعائها داخل دالة ()func، تُرجع ()locals قاموسًا يُمثل مساحة الأسماء المحلية للدالة. لاحظ أنه بالإضافة إلى المتغيرات المُعرّفة محليًا، تتضمن مساحة الأسماء المحلية وسيطتي الدالة x وy، لأنهما أيضًا محليتان لدالة ()func.

عندما تقوم باستدعاء ()locals خارج دالة في البرنامج الرئيسي، فإنها تتصرف بنفس الطريقة التي يتصرف بها ()globals، حيث تقوم بإرجاع مرجع إلى مساحة الاسم العالمية:

>>> locals()
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': '_pyrepl',
    ...
    'func': <function func at 0x104c87c40>
}

>>> globals()
{
    '__name__': '__main__',
    '__doc__': None,
    '__package__': '_pyrepl',
    ...
    'func': <function func at 0x104c87c40>
}

>>> locals() is globals()
True

عند استدعاء ()locals في نطاق الأسماء العالمي، ستحصل على مرجع للقاموس المقابل. عمليًا، تعمل ()locals بنفس طريقة ()globals في هذا السياق.

الفرق بين ()globals و ()locals

هناك فرق بسيط بين ()globals و()locals من المفيد معرفته. تُرجع دالة ()globals مرجعًا إلى القاموس الذي يحتوي على مساحة الاسم العالمية. لذلك، إذا استدعيت ()globals وحفظت قيمة إرجاعها، يمكنك إضافة متغيرات جديدة وتعديل المتغيرات الموجودة كما تعلمت.

في المقابل، تُرجع الدالة ()locals قاموسًا يُمثل نسخة من القاموس الذي يحتوي على مساحة الاسم المحلية الحالية، وليس مرجعًا لها. وبسبب هذا الاختلاف الدقيق، فإن إضافة قيمة إرجاع ()locals لن تُغير مساحة الاسم الفعلية:

>>> def func():
...     message = "Hello!"
...     loc = locals()
...     print(f"{loc = }")
...     number = 42
...     print(f"{loc = }")
...     loc["message"] = "Welcome!"
...     print(f"{loc = }")
...     print(f"{locals() = }")
...

>>> func()
loc = {'message': 'Hello!'}
loc = {'message': 'Hello!'}
loc = {'message': 'Welcome!'}
locals() = {'message': 'Hello!', 'loc': {'message': 'Welcome!'}, 'number': 42}

في هذا المثال، يشير loc إلى قيمة الإرجاع الخاصة بـ ()locals، وهي نسخة من قاموس المساحة الاسمية المحلية.

التعيين number = 42 يضيف متغيرًا جديدًا إلى مساحة الاسم المحلية، ولكن ليس إلى النسخة التي يشير إليها loc. وبالمثل، يُعدّل التعيين loc[“message”] = ‘Welcome!’ قيمة المفتاح “message” في loc، ولكنه لا يؤثر على مساحة الاسم المحلية.

أخيرًا، تجدر الإشارة إلى أن دالة ()locals تُرجع نسخة سطحية من مساحة الاسم، لذا فإن أي اسم يُشير إلى كائن قابل للتغيير يُتيح لك تغيير هذا الكائن في مكانه. ينعكس هذا التغيير في مساحة الاسم المحلية:

>>> def func():
...     fruits = ["apple", "banana"]
...     loc = locals()
...     loc["fruits"].append("orange")
...     print(f"{loc = }")
...     print(f"{locals() = }")
...

>>> func()
loc = {
    'fruits': ['apple', 'banana', 'orange']
}
locals() = {
    'fruits': ['apple', 'banana', 'orange'],
    'loc': {'fruits': ['apple', 'banana', 'orange']}
}

في هذا المثال، ستحتفظ بإشارة إلى قيمة الإرجاع ()locals في المتغير loc. ثم تستخدم هذا المتغير لإضافة قيمة جديدة إلى قائمة fruits، وهي قابلة للتغيير. يؤثر هذا التغيير على محتوى مساحة الأسماء المحلية، كما هو موضح في السطر الأخير.

لقد تعلمتَ عن مساحات الأسماء في بايثون، والتي تربط الأسماء بالكائنات. كما استكشفتَ أنواعًا مختلفة من مساحات الأسماء، مثل المدمجة، والعالمية، والمحلية، والمغلقة، وتعلمتَ كيفية تحديد نطاق الاسم في بايثون.

بالإضافة إلى ذلك، تعمقتَ في قاعدة LEGB، التي تُحدد كيفية بحث بايثون عن الأسماء عبر مساحات الأسماء الفعلية.


اكتشاف المزيد من بايثون العربي

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *

Scroll to Top

اكتشاف المزيد من بايثون العربي

اشترك الآن للاستمرار في القراءة والحصول على حق الوصول إلى الأرشيف الكامل.

Continue reading