اذهب إلى المحتوى

فضاءات الأسماء Namespaces في البرمجة


أسامة دمراني

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

لقد ظهرت فضاءات الأسماء لأن لغات البرمجة القديمة -مثل BASIC- لم تكن فيها إلا متغيرات عامة Global Variables، أي متغيرات يمكن رؤيتها في البرنامج كله وداخل الدوال أيضًا، لكن هذا جعل متابعة أداء البرامج الكبيرة وصيانتها صعبًا، لأن التعديل على متغير في جزء ما من البرنامج سيتسبب في تغير وظيفة جزء آخر دون أن يدرك المبرمج ذلك، وهو ما يسمى بالآثار الجانبية، وقد قدمت اللغات التالية -بما فيها النسخ الأحدث من BASIC- مبدأ فضاء الأسماء إلى البرمجة، بل إن لغةً مثل C++‎ تسمح للمبرمج بإنشاء فضاء اسم خاص به في أي مكان داخل البرنامج، وهذا مفيد لمنشئي المكتبات الذين يرغبون في الاحتفاظ بأسماء دوالهم فريدةً عند اختلاطها مع مكتبات أخرى.

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

سيشرح هذا المقال:

  • مفهوم فضاء الاسم أو النطاق وأهميته.
  • كيفية عمل فضاء الاسم في بايثون.
  • مفهوم فضاء الاسم في لغتي جافاسكربت وVBScript.

فضاء الاسم في بايثون

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

تتيح بايثون خمسة فضاءات أسماء -أو نطاقات- هي:

  1. النطاق المضمَّن: الأسماء المعرفة داخل بايثون نفسها، وهي متاحة دائمًا من أي مكان في البرنامج.
  2. نطاق الوحدة: وهي أسماء معرَّفة ومرئية داخل ملف أو وحدة، لكن هذا النطاق يشار إليه في بايثون باسم النطاق العام global scope، في حين أن المعنى الذي يتبادر للذهن عند سماع الاسم هو أن النطاق العام يمكن رؤيته في أي جزء من البرنامج.
  3. النطاق المحلي: وهي الأسماء المعرَّفة داخل دالة أو تابع صنف، بما في ذلك المعامِلات.
  4. نطاق الصنف: الأسماء المعرفة داخل الأصناف، وسننظر فيها في مقال لاحق.
  5. النطاق المتشعب nested Scope: هذا موضوع معقد قليلًا تستطيع تجاهله حاليًا.

لننظر الآن في الشيفرة التالية التي تحتوي على أمثلة لأول ثلاثة نطاقات:

def square(x):
   return x*x

data = int(input('Type a number to be squared: '))
print( data, 'squared is: ', square(data) )

يسرد الجدول التالي كلًا من الاسم والنطاق الذي ينتمي إليه:

الاسم فضاء الاسم
square الوحدة-عام
x محلي للنطاق square
data الوحدة-عام
int مضمَّن
input مضمَّن
print مضمَّن

لاحظ أننا لا نَعُد def وreturn من الأسماء، وذلك لأنهما كلمتان مفتاحيتان أو جزء من تعريف اللغة نفسها، وسنحصل على خطأ إذا استخدمنا كلمةً مفتاحيةً اسمًا لمتغير.

لنطرح سؤالًا جديدًا الآن، ماذا يحدث عندما يكون للمتغيرات التي في فضاءات أسماء مختلفة نفس الاسم؟ وماذا يحدث عند الإشارة إلى اسم ليس موجودًا في فضاء الاسم الحالي؟

الوصول إلى الأسماء التي خارج النطاق الحالي

تحدد بايثون الأسماء حتى لو لم تكن في فضاء الاسم الحالي بالنظر إلى:

  1. فضاء الاسم المحلي -الدالة الحالية- أو الدالة المغلِّفة أو الصنف المغلِّف إذا كانت دالةً متشعبةً أو تابعًا متشعبًا.
  2. نطاق الوحدة، أي الملف الحالي.
  3. النطاق المضمَّن.

فإذا كان الاسم في وحدة أخرى، فسنستورد الوحدة باستخدام import كما رأينا في المقالات السابقة، وعند استيرادها سيكون اسم الوحدة مرئيًا في نطاق تلك الوحدة، وسنستطيع حينئذ أن نستخدم اسم الوحدة للوصول إلى أسماء المتغيرات فيها باستخدام نمط module.name المعتاد، ويتبين من هذا أن استيراد جميع الأسماء من وحدة إلى الملف الحالي ليس صحيحًا، إذ قد يتطابق اسم وحدة ما مع اسم أحد متغيراتنا، مما سيتسبب في سلوك غريب للبرنامج لأن أحد الاسمين سيغطي على الآخر.

لنعرِّف وحدتين تستورد الثانية فيهما الأولى على سبيل المثال:

##### module first.py #########
spam = 42
def print42(): print( spam )
###############################
##### module second.py ########
from first import *  # استورد جميع الأسماء من الأولى

spam = 101   # لتخفي النسخة الأولى spam أنشئ متغير 
print42()    # ما الذي سيُطبع، هل 42 أم 101

################################

إذا ظننت أن هذا المثال سيطبع 101 فستكون مخطئًا، لأنه سيطبع 42 بسبب تعريف المتغير في بايثون، فكما شرحنا -في مقال: البيانات وأنواعها-؛ الاسم هو عنوان يُستخدم للإشارة إلى كائن، وقد كان الاسم print42 في الوحدة الأولى يشير إلى كائن الدالة المعرَّف في الوحدة، (وسننظر في ذلك بالتفصيل حين نشرح تعبير لامدا في مقال لاحق)، ومع أننا استوردنا الاسم إلى وحدتنا، إلا أننا لم نستورد الدالة التي لا تزال تشير إلى نسخة الوحدة الخاصة بها من spam، وعلى ذلك فقد أنشأنا متغير spam جديد ليس لديه تأثير على الدالة المشار إليها بالاسم print42. يشرح المخطط التالي هذا لكلا النوعين من الاستيراد، ويمكنك ملاحظة كيف يكرر الاستيراد الثاني الاسم print42 من first.py إلى second.py:

namespaces.png

ينبغي أن يكون هذا اللبس قد شرح سهولة الوصول إلى الأسماء في الوحدات المستوردة باستخدام ترميز النقطة dot notation. رغم أننا سنكتب شيفرةً أكثر، لكن توجد وحدات قليلة -مثل Tkinter الذي سنتعرض لها لاحقًا-، تُستخدم لاستيراد جميع الأسماء، لكنها تُكتب بطريقة تقلل خطر تعارض الأسماء رغم وجود الخطورة على أي حال، كما قد تنشأ عنها زلات برمجية bugs يصعب العثور عليها، وعلى أي حال توجد طريقة أكثر أمانًا لاستيراد اسم وحيد من وحدة ما، كما يلي:

from sys import exit

وهنا نأتي بدالة exit فقط إلى فضاء الاسم المحلي، لكن لا نستطيع استخدام أسماء أخرى من sys، ولا حتى sys نفسها.

تجنب تعارض الأسماء

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

ولن نواجه مشكلةً إذا أردنا قراءة متغير عام داخل دالة، إذ ستبحث بايثون عن الاسم محليًا، فإذا لم تجده فستبحث في النطاق العام، بل وفي النطاق المضمَّن كذلك إذا دعت الحاجة، وإنما ستظهر المشكلة عندما نريد إسناد قيمة إلى متغير عام، إذ سينشئ هذا متغيرًا محليًا جديدًا داخل الدالة، فكيف نسند القيمة إلى متغير عام دون إنشاء متغير محلي بنفس الاسم إذًا؟ يمكن تنفيذ هذا باستخدام الكلمة المفتاحية global:

var = 42
def modGlobal():
   global var  # محلي var تمنع إنشاء
   var = var - 21

def modLocal():
   var = 101

print( var )   # تطبع 42
modGlobal()
print( var )  # تطبع 21
modLocal()
print( var )  # تطبع 21

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

var = 42
def modGlobal(aVariable):
    return aVariable - 21

print( var )
var = modGlobal(var)
print( var )

في هذه الحالة نسند القيمة التي أعادتها الدالة إلى المتغير الأصلي في نفس الوقت الذي نمررها فيه وسيطًا، وتكون النتيجة نفسها، لكن لا تعتمد الدالة الآن على أي شيفرة خارجها، مما يسهل إعادة استخدامها في برامج أخرى، كما يسهل رؤية كيف تتغير القيمة العامة global value، حيث نستطيع أن نرى حدوث الإسناد الصريح هنا، ويطبق المثال التالي كل ذلك عمليًا، إذ يوضح النقاط التي شرحناها إلى الآن، لهذا ادرسه جيدًا حتى تعرف استخدام الأسماء والقيم في كل خطوة فيه:

# متغيرات نطاقها الوحدة
W = 5
Y = 3

# المعامِلات تشبه متغيرات الدوال 
# نطاق محلي X وعليه يكون لـ 
def spam(X):

   # أخبر الدالة أن تنظر في مستوى الوحدة 
   # خاصة بها w وألا تنشئ وحدة 
   global W

   Z = X*2 # الذي له نطاق محلي Z أنشأنا متغير 
   W = X+5 # كما شرحنا أعلاه w استخدم الوحدة 

   if Z > W:
      # هو اسم نطاق مضمَّن pow
      print( pow(Z,W) )
      return Z
   else:
      return Y # محلي Y لا يوجد
                 # لذا نستخدم نسخة الوحدة

print("W,Y = ", W, Y )
for n in [2,4,6]:
   print( "Spam(%d) returned: " % n, spam(n) )
   print( "W,Y = ", W, Y )

فضاء الاسم في VBScript

إذا صرحنا عن متغير خارج الدالة أو البرنامج الفرعي في VBScript، فسيكون عامًا globally أما إذا صرحنا عنه داخل الدالة أو البرنامج الفرعي، فسيكون محليًا في الوحدة وسيخفي أي متغير عام له نفس الاسم، كما سيكون المبرمج هنا هو المسؤول عن إدارة التعارض بين هذه الأسماء، وبما أن متغيرات VBScript تُنشأ باستخدام تعليمة Dim فلن يحدث غموض أو لبس حول المتغير المقصود على عكس بايثون، وبهذا نرى أن منظور VBScript لقواعد النطاقات أبسط وأوضح من بايثون، لكن هناك بعض الأمور الخاصة بصفحات الويب، حيث ستكون المتغيرات العامة مرئيةً في كامل الملف، وليس داخل حدود الوسم script الذي عُرَّفت فيه فقط، وتوضح الشيفرة التالية ذلك:

<script type="text/vbscript">
Dim aVariable
Dim another
aVariable = "This is global in scope"
another = "A Global can be visible from a function"
</script>

<script type="text/vbscript">
Sub aSubroutine
   Dim aVariable
   aVariable = "Defined within a subroutine"
   MsgBox aVariable
   MsgBox another  ' uses global name
End Sub
</script>

<script type="text/vbscript">
MsgBox aVariable
aSubroutine
MsgBox aVariable
</script>

توجد بعض مزايا النطاقات في VBScript، والتي نتيح بها إمكانية الوصول إلى المتغيرات بين الملفات المختلفة في صفحة ويب -من الفهرس مثلًا إلى المحتوى والعكس-، لكن لن نتحدث عن هذا المستوى من برمجة صفحات الويب هنا، لذا سنكتفي بالإشارة إلى وجود كلمات مفتاحية مثل Public وPrivate.

فضاء الاسم في جافاسكربت

تتبع جافاسكربت نفس القواعد تقريبًا، إذ تكون المتغيرات المصرح عنها داخل الدوال مرئيةً داخل تلك الدوال فقط؛ أما المتغيرات التي خارج الدوال فيمكن أن تراها الشيفرة التي خارج الدوال، إضافةً إلى إمكانية رؤيتها داخل الدوال أيضًا. وكما هو الحال في VBScript؛ ليس هناك تعارض أو غموض بشأن المتغير المقصود، لأن المتغيرات تُنشأ صراحةً باستخدام تعليمة var، والمثال التالي شبيه بمثال VBScript السابق لكنه مكتوب بلغة جافاسكربت:

<script type="text/javascript">
var aVariable, another;  // متغيرات عامة
aVariable = "This is Global in scope<BR>";
another = "A global variable can be seen inside a function<BR>";

function aSubroutine(){
   var aVariable;        // متغير محلي
   aVariable = "Defined within a function<BR>";
   document.write(aVariable);
   document.write(another);  // يستخدم متغيرًا عامًا
}

document.write(aVariable);
aSubroutine();
document.write(aVariable);

</script>

ولا أظننا بحاجة إلى تكرار شرح المثال مرةً أخرى هنا.

خاتمة

نرجو في نهاية هذا المثال أن تكون تعلمت ما يلي:

  • النطاقات وفضاءات الأسماء وجهان لعملة واحدة، ويشيران إلى نفس الشيء.
  • المفاهيم واحدة بين اللغات المختلفة، لكن الذي يختلف هو التطبيق الدقيق لها وفق قواعد كل لغة.
  • تحتوي بايثون على خمسة نطاقات هي: النطاق المضمَّن built in، ونطاق الصنف class، والنطاق المتشعب nested، ونطاق الملف أو النطاق العام global، ونطاق الدالة أو النطاق المحلي function، وهذه النطاقات الثلاثة الأخيرة هي أهم نطاقات فيها من حيث كثرة الاستخدام في البرمجة.
  • تحتوي كل من جافاسكربت وVBScript على نطاقين لكل واحدة منهما، هما نطاق الملف أو النطاق العام file، ونطاق الدالة أو النطاق المحلي function.

ترجمة -بتصرف- للفصل الخامس عشر: Namespaces من كتاب Learn To Program لصاحبه Alan Gauld.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...