المراجع
أهداف التعلّم
بعد هذا القسم
- ستعرف المقصود بالمرجع إلى متغيّر
- ستفهم أنه يمكن أن تتعدّد المراجع إلى الكائن نفسه
- ستكون قادرًا على استخدام القوائم كمعاملات في الدوال
- ستعرف المقصود بالأثر الجانبي للدالة
إلى الآن كنا نتخيّل المتغيّر نوعًا من «الصندوق» الذي يحتوي قيمة المتغيّر. وتقنيًّا هذا غير صحيح في Python. فما يُخزَّن في المتغيّر ليس القيمة بحد ذاتها، بل مرجع إلى الكائن الذي يمثّل القيمة الفعلية للمتغيّر. ويمكن أن يكون الكائن رقمًا أو سلسلة نصية أو قائمة مثلًا.
وفي الممارسة العملية يعني هذا أن قيمة المتغيّر لا تُخزَّن في المتغيّر نفسه. بل توجد معلومات عن الموضع في ذاكرة الحاسوب الذي يمكن العثور فيه على القيمة.
ويُمثَّل المرجع غالبًا بسهم يمتدّ من المتغيّر إلى القيمة الفعلية في الذاكرة:

إذًا، يخبرنا المرجع أين يمكن العثور على القيمة. ويمكن استخدام الدالة id لمعرفة الموضع الدقيق الذي يشير إليه المتغيّر:
a = [1, 2, 3]
print(id(a))
b = "This is a reference, too"
print(id(b))
4538357072
4537788912
المرجع، أو معرّف المتغيّر (ID)، هو عدد صحيح يمكن التخيّل أنه العنوان في ذاكرة الحاسوب حيث تُخزَّن قيمة المتغيّر. وإذا نفّذت الشيفرة أعلاه على حاسوبك، فغالبًا ستكون النتيجة مختلفة، لأن متغيّراتك ستشير إلى مواضع مختلفة - أي أن المراجع ستكون مختلفة.
وكما رأينا في القسم السابق، تُظهر أداة التصوير المرئي في Python Tutor المراجع أيضًا كسهم من المتغيّر إلى المحتوى الفعلي. غير أن الأداة «تغشّ» قليلًا حين يتعلق الأمر بالسلاسل النصية، فتعرض السلاسل النصية وكأنها مخزّنة في المتغيّرات نفسها:

في الواقع، تُعالَج السلاسل النصية في Python معالجة شبه مماثلة للقوائم، مع مراجع إلى مواضع في الذاكرة.
كثير من الأنواع المدمجة في Python، مثل str، غير قابلة للتغيير (immutable). أي أن قيمة الكائن، أو أي جزء منها، لا يمكن أن تتغيّر. غير أنه يمكن استبدال القيمة بقيمة جديدة:

وبعض أنواع Python قابلة للتغيير (mutable). فمثلًا، يمكن أن تتغيّر محتويات قائمة دون الحاجة إلى إنشاء قائمة جديدة بأكملها:

قد يُدهشك أن أنواع البيانات الأساسية int وfloat وbool غير قابلة للتغيير في Python أيضًا. فلننظر إلى الشيفرة التالية:
number = 1
number = 2
number += 10
يبدو أن الأوامر أعلاه لا تغيّر سوى القيمة المخزّنة في المتغيّر، لكنها في الحقيقة تنشئ عددًا جديدًا كليًّا في ذاكرة الحاسوب في كل مرة.
وتوضّح مخرجات البرنامج التالي هذا الوضع:
number = 1
print(id(number))
number += 10
print(id(number))
a = 1
print(id(a))
4535856912
4535856944
4535856912
في البداية، يشير المتغيّر number إلى موضع الذاكرة 4535856912. وحين تُسنَد إلى number قيمة جديدة، يشير إلى الموضع 4535856944. والآن، حين تُسنَد إلى المتغيّر a القيمة 1، يشير a إلى الموضع نفسه تمامًا الذي كان يشير إليه number حين أُسندت إليه القيمة 1 أيضًا.
يبدو أن Python قد خزّنت القيمة 1 في موضع الذاكرة 4535856912. فكلما أُسندت إلى متغيّر القيمة 1، فإن المتغيّر يشير إلى ذلك الموضع في ذاكرة الحاسوب.
ومن الجيد أن نضع في اعتبارنا أن كل شيء تقريبًا مرجع في Python، لكن كل هذا نادرًا ما يكون ذا صلة بمهام البرمجة اليومية. فلنعد إلى أمثلة أكثر عملية.
مراجع متعدّدة إلى القائمة نفسها
ما الذي يحدث فعلًا حين تُسنِد متغيّر قائمة إلى متغيّر جديد - هل تُنسخ القائمة؟
a = [1, 2, 3]
b = a
b[0] = 10
ينسخ الإسناد b = a القيمة المخزّنة في المتغيّر a إلى المتغيّر b. غير أن القيمة المخزّنة في a ليست القائمة بنفسها، بل مرجعًا إلى القائمة.
إذًا، ينسخ الإسناد b = a المرجع. ونتيجة لذلك يوجد الآن مرجعان إلى موضع الذاكرة نفسه الذي يحتوي القائمة.

يمكن الوصول إلى القائمة عبر أيٍّ من المرجعين الاثنين:
list1 = [1, 2, 3, 4]
list2 = list1
list1[0] = 10
list2[1] = 20
print(list1)
print(list2)
[10, 20, 3, 4]
[10, 20, 3, 4]
إذا وُجد أكثر من مرجع إلى القائمة نفسها، فقد يُستخدم أيٌّ منها للوصول إلى القائمة. وفي المقابل، يؤثّر التغيير المُجرى عبر أيّ من المراجع في المراجع الأخرى أيضًا، لأن وجهتها واحدة.
وأداة التصوير المرئي مفيدة جدًا مرة أخرى في معرفة ما يحدث:

نسخ قائمة
إن أردت إنشاء نسخة منفصلة فعلًا من قائمة، فيمكنك إنشاء قائمة جديدة وإضافة كل عنصر من القائمة الأصلية إليها بالتناوب:
my_list = [1, 2, 3, 3, 5]
new_list = []
for item in my_list:
new_list.append(item)
new_list[0] = 10
new_list.append(6)
print("the original:", my_list)
print("the copy:", new_list)
my_list [1, 2, 3, 3, 5]
new_list [10, 2, 3, 3, 5, 6]
لقطة من عملية النسخ في أداة التصوير المرئي:

يشير المتغيّر new_list إلى قائمة مختلفة عن تلك التي يشير إليها المتغيّر my_list.
وطريقة أسهل لنسخ قائمة هي مشغّل الأقواس [] الذي استخدمناه سابقًا في التقطيعات. وتُحدّد الصيغة [:] جميع عناصر التجمُّع. وكأثر جانبي، تنشئ الصيغة نسخة من القائمة:
my_list = [1,2,3,4]
new_list = my_list[:]
my_list[0] = 10
new_list[1] = 20
print(my_list)
print(new_list)
[10, 2, 3, 4]
[1, 20, 3, 4]
استخدام القوائم كمعاملات في الدوال
حين تمرّر قائمة كوسيط إلى دالة، فأنت تمرّر مرجعًا إلى تلك القائمة. وهذا يعني أن الدالة يمكنها تعديل القائمة مباشرة.
تأخذ الدالة التالية قائمة كوسيط لها، وتضيف عنصرًا جديدًا إلى نهاية القائمة:
def add_item(my_list: list):
new_item = 10
my_list.append(new_item)
a_list = [1,2,3]
print(a_list)
add_item(a_list)
print(a_list)
[1, 2, 3]
[1, 2, 3, 10]
لاحظ أن الدالة add_item ليس لها قيمة معادة. إنها تغيّر القائمة التي تأخذها كوسيط فقط.
قد تساعدك أداة التصوير المرئي على فهم ما يحدث هنا:

يُشير الإطار العام (Global frame) إلى المتغيّرات المعرَّفة في الدالة الرئيسية، بينما يمثّل الإطار add_item ذو الخلفية الزرقاء المعاملات والمتغيّرات داخل تلك الدالة. وكما يبيّن التصوير المرئي، تشير الدالة add_item إلى القائمة نفسها تمامًا التي تشير إليها الدالة الرئيسية. والتغييرات المُجراة داخل الدالة add_item تؤثّر في الدالة الرئيسية أيضًا.
طريقة أخرى لتنفيذ هذه الوظيفة هي إنشاء قائمة جديدة داخل الدالة، ثم إعادتها:
def add_item(my_list: list) -> list:
new_item = 10
my_list_copy = my_list[:]
my_list_copy.append(new_item)
return my_list_copy
numbers = [1, 2, 3]
numbers2 = add_item(numbers)
print("original list:", numbers)
print("new list:", numbers2)
original list: [1, 2, 3]
new list: [1, 2, 3, 10]
إن لم تكن متأكّدًا تمامًا من أنك تفهم ما يحدث في الشيفرة أعلاه، فيُرجى تتبّعها في أداة التصوير المرئي.
تعديل قائمة مُعطاة كوسيط
فيما يلي محاولة لدالة يُفترض أن تزيد كل عنصر في قائمة بمقدار عشرة:
def augment_all(my_list: list):
new_list = []
for item in my_list:
new_list.append(item + 10)
my_list = new_list
numbers = [1, 2, 3]
print("in the beginning:", numbers)
augment_all(numbers)
print("after the function is executed:", numbers)
in the beginning: [1, 2, 3]
after the function is executed: [1, 2, 3]
لسبب ما لا تعمل الدالة، فما الذي يحدث؟
تأخذ الدالة مرجعًا إلى قائمة كوسيط، ويُخزَّن هذا المرجع في المتغيّر my_list. وينسب الإسناد my_list = new_list قيمة جديدة إلى ذلك المتغيّر نفسه. ويصبح المتغيّر my_list الآن مشيرًا إلى القائمة الجديدة التي أُنشئت داخل الدالة، ولم يعد المرجع إلى القائمة الأصلية متاحًا داخل الدالة. غير أن هذا الإسناد لا تأثير له خارج الدالة.
وإضافةً إلى ذلك، لا يمكن الوصول من خارج الدالة إلى المتغيّر new_list الذي يحتوي القيم الجديدة المزيدة. فهو «يضيع» حين ينتهي تنفيذ الدالة ويعود التركيز إلى الدالة الرئيسية. بينما يشير المتغيّر numbers في الدالة الرئيسية دائمًا إلى القائمة الأصلية.
وأداة التصوير المرئي هي صديقك هنا أيضًا. فيُرجى تتبّع المراحل بعناية، ومشاهدة كيف لا تتأثر القائمة الأصلية بتنفيذ الدالة إطلاقًا:

إحدى طرق إصلاح ذلك هي نسخ جميع العناصر من القائمة الجديدة إلى القائمة القديمة واحدًا واحدًا:
def augment_all(my_list: list):
new_list = []
for item in my_list:
new_list.append(item + 10)
# copy items from the new list into the old list
for i in range(len(my_list)):
my_list[i] = new_list[i]
لدى Python أيضًا اختصار أنيق لإسناد عدة عناصر في تجمُّع دفعة واحدة:
>>> my_list = [1, 2, 3, 4]
>>> my_list[1:3] = [10, 20]
>>> my_list
[1, 10, 20, 4]
في المثال أعلاه تُسنَد قيم من تجمُّع آخر إلى شريحة من القائمة.
وكما نعلم، يمكن أن تشمل الشريحة التجمُّع كله:
>>> my_list = [1, 2, 3, 4]
>>> my_list[:] = [100, 99, 98, 97]
>>> my_list
[100, 99, 98, 97]
تُستبدل محتويات القائمة القديمة بأكملها. واستنادًا إلى ذلك، قد تبدو نسخة عاملة من دالة الزيادة هكذا:
def augment_all(my_list: list):
new_list = []
for item in my_list:
new_list.append(item + 10)
my_list[:] = new_list
في الواقع، لا حاجة على الإطلاق إلى إنشاء قائمة جديدة داخل الدالة. يمكننا أن نُسنِد القيم الجديدة مباشرةً إلى القائمة الأصلية:
def augment_all(my_list: list):
for i in range(len(my_list)):
my_list[i] += 10
تمرين برمجي: العناصر مضروبة في اثنين
يُرجى كتابة دالة اسمها
double_items(numbers: list)، تأخذ قائمة من الأعداد الصحيحة كوسيط لها.
ينبغي أن تُرجع الدالة قائمة جديدة تحوي جميع قيم القائمة الأصلية مضاعفة. ولا ينبغي للدالة أن تغيّر القائمة الأصلية.
مثال على عمل الدالة:
if __name__ == "__main__":
numbers = [2, 4, 5, 3, 11, -4]
numbers_doubled = double_items(numbers)
print("original:", numbers)
print("doubled:", numbers_doubled)
original: [2, 4, 5, 3, 11, -4]
doubled: [4, 8, 10, 6, 22, -8]
تمرين برمجي: إزالة الأصغر
يُرجى كتابة دالة اسمها
remove_smallest(numbers: list)، تأخذ قائمة من الأعداد الصحيحة كوسيط لها.
ينبغي للدالة أن تجد أصغر عنصر في القائمة وتزيله. ويمكنك افتراض أن في القائمة عنصرًا واحدًا هو الأصغر.
لا ينبغي أن تكون للدالة قيمة معادة - بل ينبغي أن تُعدّل القائمة مباشرةً التي تتلقّاها كمعامل.
مثال على كيفية عمل الدالة:
if __name__ == "__main__":
numbers = [2, 4, 6, 1, 3, 5]
remove_smallest(numbers)
print(numbers)
[2, 4, 6, 3, 5]
تمرين برمجي: سودوكو: طبع الشبكة وإضافة رقم
في هذا التمرين سنُكمل دالتين أخريين لمشروع سودوكو من القسم السابق:
print_sudokuوadd_number.
تأخذ الدالة print_sudoku(sudoku: list) مصفوفة ثنائية الأبعاد تمثّل شبكة سودوكو كوسيط لها. وينبغي أن تطبع الدالة الشبكة بالصيغة المحدَّدة في الأمثلة أدناه.
تأخذ الدالة add_number(sudoku: list, row_no: int, column_no: int, number:int) كوسيطات لها مصفوفة ثنائية الأبعاد تمثّل شبكة سودوكو، وعدين صحيحين يشيران إلى فهرسي الصف والعمود لمربع واحد، ورقمًا واحدًا بين 1 و9. وينبغي أن تضيف الدالة الرقم إلى الموضع المحدَّد في الشبكة.
sudoku = [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]
]
print_sudoku(sudoku)
add_number(sudoku, 0, 0, 2)
add_number(sudoku, 1, 2, 7)
add_number(sudoku, 5, 7, 3)
print()
print("Three numbers added:")
print()
print_sudoku(sudoku)
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
Three numbers added:
2 _ _ _ _ _ _ _ _
_ _ 7 _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ 3 _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
تلميح
تذكّر أنه من الممكن استدعاء الدالة print دون إحداث تغيير في السطر:
print("characters ", end="")
print("without carriage returns", end="")
characters without carriage returns
في بعض الأحيان تحتاج سطرًا جديدًا فقط، وهو ما تُحققه جملة print الخالية من أي وسيط:
print()
تمرين برمجي: سودوكو: إضافة رقم إلى نسخة من الشبكة
هذه هي مهمة سودوكو الأخيرة تمامًا. وهذه المرة سننشئ نسخة مختلفة قليلًا من دالة إضافة أرقام جديدة إلى الشبكة.
تأخذ الدالة copy_and_add(sudoku: list, row_no: int, column_no: int, number: int) كوسيطات لها مصفوفة ثنائية الأبعاد تمثّل شبكة سودوكو، وعدين صحيحين يشيران إلى فهرسي الصف والعمود لمربع واحد، ورقمًا واحدًا بين 1 و9. وينبغي أن تُعيد الدالة نسخة من الشبكة الأصلية أُضيف إليها الرقم الجديد في الموضع الصحيح. ولا ينبغي للدالة أن تغيّر الشبكة الأصلية المُستلمة كمعامل.
قد تكون الدالة print_sudoku من التمرين السابق مفيدة للاختبار، وهي مستخدَمة في المثال أدناه:
sudoku = [
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0]
]
grid_copy = copy_and_add(sudoku, 0, 0, 2)
print("Original:")
print_sudoku(sudoku)
print()
print("Copy:")
print_sudoku(grid_copy)
Original:
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
Copy:
2 _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
_ _ _ _ _ _ _ _ _
تلميح حين تتعامل مع قوائم متداخلة، كُن حريصًا للغاية عند النسخ. فما الذي يجب نسخه صراحةً، وأين يكون للتغييرات تأثير فعلي؟ وأداة التصوير المرئي مساعدة كبيرة هنا أيضًا، وإن كان حجم شبكة سودوكو يجعل العرض أقلّ انتظامًا من المعتاد.
تمرين برمجي: تيك تاك تو
تُلعب لعبة إكس-أو على شبكة 3 في 3، بلاعبين يتناوبان على إدخال الأصفار وعلامات X. فإذا تمكّن أيّ من اللاعبين من وضع ثلاثة من رموزه في أي صف أو عمود أو قطر، فقد فاز. وإذا لم يتمكّن أيّ منهما من ذلك، كانت النتيجة تعادلًا.
يُرجى كتابة دالة اسمها play_turn(game_board: list, x: int, y: int, piece: str)، تضع الرمز المحدَّد عند الإحداثيات المحدَّدة على اللوحة. وقيم الإحداثيات على اللوحة بين 0 و2.
ملاحظة: عند مقارنة هذا بما تقدّم من تمارين سودوكو، فإن الوسائط التي تأخذها الدالة معكوسة هنا. فالعمود x يأتي أولًا، والصف y ثانيًا.
تتكوّن اللوحة من السلاسل النصية التالية:
"": مربع فارغ"X": رمز اللاعب 1"O": رمز اللاعب 2
ينبغي أن تُرجع الدالة True إذا كان المربع فارغًا ووُضِع الرمز على لوحة اللعب بنجاح. وينبغي أن تُرجع الدالة False إذا كان المربع محتلًّا، أو إذا لم تكن الإحداثيات صالحة.
مثال على تنفيذ الدالة:
game_board = [["", "", ""], ["", "", ""], ["", "", ""]]
print(play_turn(game_board, 2, 0, "X"))
print(game_board)
True
[['', '', 'X'], ['', '', ''], ['', '', '']]
تمرين برمجي: نقل المصفوفة
يُرجى كتابة دالة اسمها
transpose(matrix: list)، تأخذ مصفوفة ثنائية الأبعاد من الأعداد الصحيحة، أي ما يُسمّى مصفوفة، كوسيط لها. وينبغي أن تُجري الدالة النقل على المصفوفة. والنقل يعني في جوهره قلب المصفوفة على قطرها: تصبح الأعمدة صفوفًا، وتصبح الصفوف أعمدة.
يمكنك افتراض أن المصفوفة مصفوفة مربعة، لذا سيكون عدد صفوفها مساويًا لعدد أعمدتها.
المصفوفة التالية
1 2 3
4 5 6
7 8 9
تبدو بعد نقلها هكذا:
1 4 7
2 5 8
3 6 9
لا ينبغي أن تكون للدالة قيمة معادة. بل يجب تعديل المصفوفة مباشرةً عبر المرجع.
الآثار الجانبية للدوال
إذا أخذت دالة مرجعًا إلى قائمة كوسيط، فقد تستطيع تعديل تلك القائمة. وإذا لم يكن المبرمج يقصد التعديلات المباشرة، فقد يسبّب تعديل القائمة المُستلمة كمعامل عن طريق الخطأ مشكلات في مواضع أخرى من البرنامج.
لننظر إلى دالة يُفترض أنها تجد ثاني أصغر عنصر في قائمة:
def second_smallest(my_list: list) -> int:
# in an ordered list, the second smallest item is at index 1
my_list.sort()
return my_list[1]
numbers = [1, 4, 2, 5, 3, 6, 4, 7]
print(second_smallest(numbers))
print(numbers)
2
[1, 2, 3, 4, 4, 5, 6, 7]
تجد الدالة ثاني أصغر عنصر بشكل موثوق، لكنها إضافةً إلى ذلك تُرتّب القائمة في مكانها، فتُغيّر ترتيب العناصر. وإذا كان الترتيب ذا أهمية في مواضع أخرى من البرنامج، فقد يسبّب استدعاء الدالة أخطاء. وتُسمّى التعديلات غير المقصودة على كائن يُوصَل إليه عبر مرجع أثرًا جانبيًا (side effect) للدالة.
يمكننا تجنّب الأثر الجانبي بإجراء تغيير صغير في الدالة:
def second_smallest(my_list: list) -> int:
list_copy = sorted(my_list)
return list_copy[1]
numbers = [1, 4, 2, 5, 3, 6, 4, 7]
print(second_smallest(numbers))
print(numbers)
2
[1, 4, 2, 5, 3, 6, 4, 7]
تُعيد الدالة sorted نسخة جديدة مرتّبة من القائمة، لذا لم يعد البحث عن ثاني أصغر عنصر يعبث بترتيب القائمة الأصلية.
ويُعتبر تجنّب إحداث آثار جانبية بالدوال ممارسة برمجية جيدة على نحو عام. فالآثار الجانبية قد تجعل التحقق من أن البرنامج يعمل كما هو مقصود في جميع الظروف أصعب.
وتُسمّى الدوال الخالية من الآثار الجانبية أيضًا دوال نقية (pure functions). ولا سيّما عند الالتزام بأسلوب البرمجة الدالية، فهذا مَثَل أعلى شائع يُستحسن اتّباعه. وسنستعرض هذا الموضوع بمزيد من التفصيل في الدورة المتقدمة في البرمجة، وهي الدورة التي تلي هذه.