قابلية التغيير والنسخ في Python: تجنّب التعديلات غير المقصودة
تفسّر قابلية التغيير في Python سبب ظهور تعديل القائمة عند الوصول إليها بأسماء متغيّرات مختلفة. فالتعليمة b = a لا تنسخ الكائن الذي يشير إليه a، بل تربط اسمًا آخر بالكائن نفسه. ولتجهيز البيانات مع الاحتفاظ بحالتها الأصلية، تحتاج إلى التمييز بين الإسناد، النسخ السطحي والنسخ العميق.
يبني هذا المقال على أساسيات Python. ستتعلّم كيف تتعرّف على كائن قابل للتغيير، وتتحقّق مما إذا كان اسمان يشيران إلى الكائن نفسه، وتختار النسخ المناسب لقائمة أو لقاموس يحتوي على قائمة. يساعدك هذا الفهم قبل تحويل البيانات التي يستخدمها نموذج لتعلّم الآلة.
قبل أن تبدأ
تحتاج إلى معرفة كيفية إسناد قيمة إلى متغيّر، والوصول إلى عنصر في قائمة، واستدعاء دالة. إذا لم تكن بيئتك جاهزة، اتبع دليل تثبيت Python باستخدام uv وإعداد Jupyter في VS Code. افتح بعد ذلك ملف MutabiliteCopiesPython.ipynb أو أنشئه، واختر نواة Python الخاصة بمشروعك من أعلى يمين VS Code، ثم نفّذ الخلايا بالترتيب.
يستخدم هذا الـNotebook لغة Python ووحدة copy من المكتبة القياسية فقط. جرى تنفيذ الأمثلة باستخدام Python 3.14.8.
نزّل الـNotebook مع مخرجاته المتحقّق منها في ملف ZIP. فكّ ضغط الملف ثم افتح MutabiliteCopiesPython.ipynb في VS Code. يمكنك أيضًا نسخ كلّ خلية معروضة أدناه على حدة وتنفيذها في الـNotebook الخاص بك.
المحتويات
الكائنات والأنواع وقابلية التغيير في Python
تمثّل Python البيانات باستخدام الكائنات. لكلّ كائن قيمة ونوع وهوية. يحدّد النوع العمليات المتاحة، مثل جمع عددين صحيحين، أو إضافة عنصر إلى قائمة، أو البحث عن مفتاح في قاموس. أمّا الهوية فتميّز الكائن عن غيره، حتى عندما تتساوى القيم. يشرح نموذج البيانات في توثيق Python الرسمي هذه المفاهيم.
يكون الكائن قابلًا للتغيير إذا كان من الممكن تعديل حالته بعد إنشائه. وتحتفظ القائمة بهويتها عندما تغيّر أحد عناصرها أو ترتيب عناصرها. تتعلّق قابلية التغيير في Python إذن بتعديل الكائن الموجود نفسه.
أمّا الكائن غير القابل للتغيير فلا يسمح بهذا التعديل. لاستخدام قيمة أخرى، تعيد إسناد اسم المتغيّر إلى كائن آخر. عدم قابلية التغيير صفة للكائن، وليست للاسم الذي يشير إليه.
| الأنواع | كائنات قابلة للتغيير وغير قابلة للتغيير |
|---|---|
int، float، bool وstr | هذه الكائنات غير قابلة للتغيير، لكن يمكنك إعادة إسناد اسم المتغيّر إلى كائن آخر. |
list | القائمة قابلة للتغيير: يمكن تعديل عناصرها وطولها وترتيبها. |
dict | القاموس قابل للتغيير: يمكنك إضافة مدخلات أو حذفها، أو تعديل القيمة المرتبطة بمفتاح. |
set | المجموعة من نوع set قابلة للتغيير: يمكنك إضافة عنصر أو حذفه. |
tuple | الكائن من نوع tuple غير قابل للتغيير، لكن يمكنك تعديل كائن قابل للتغيير يحتوي عليه. |
تحدّد الدالة type() نوع الكائن. وفي الخلية الأولى، يعرض .__name__ اسم هذا النوع فقط. يحتفظ scores_reference، وهو كائن من نوع tuple، بالدرجات الخمس الأصلية؛ وتُنشئ الأمثلة التالية قوائمها انطلاقًا من هذا المرجع.
# Create a tuple of reference scores
scores_reference = (67, 61, 74, 71, 70)
# Display the type of a score, the tuple and its list conversion
print("score:", type(scores_reference[0]).__name__)
print("collection:", type(scores_reference).__name__)
print("list:", type(list(scores_reference)).__name__)score: int
collection: tuple
list: listإعادة إسناد الاسم لا تعدّل العدد الصحيح
في البداية، يشير كلّ من score_initial وscore_selectionne إلى القيمة 67. وعندما تعيد إسناد score_selectionne إلى 74، تبقى قيمة score_initial مساوية لـ67. إعادة إسناد متغيّر في Python لا تعدّل الكائن الذي كان يشير إليه.
# Keep a reference to the first score
score_initial = scores_reference[0]
score_selectionne = score_initial
# Reassign the second name to another score
score_selectionne = scores_reference[2]
# Display the values associated with both names
print("initial:", score_initial)
print("selected:", score_selectionne)initial: 67
selected: 74لماذا يمكن لمتغيّرين أن يشيرا إلى القائمة نفسها؟
يسمح المرجع لاسم أو لحاوية بالإشارة إلى كائن. بعد تنفيذ notes_a_trier = notes_examens، يتيح الاسمان الوصول إلى القائمة نفسها. ويُسمّى هذا الاسم الآخر أحيانًا اسمًا بديلًا (alias). يؤكّد توثيق وحدة copy أن الإسناد لا ينسخ الكائنات.
يقارن العامل is هوية الكائنات. أمّا .sort() فهي دالة مرتبطة بالقائمة (method)، وتستدعيها على القائمة باستخدام النقطة. ترتّب القائمة في مكانها، أي بتعديل الكائن الموجود نفسه. يمكنك لذلك التحقّق من مشاركة القائمة قبل الترتيب، ثم ملاحظة النتيجة عند الوصول إليها باسمها الأصلي.
# Create a list and give it a second name
notes_examens = list(scores_reference)
notes_a_trier = notes_examens
# Check whether both names refer to the same object
print("same object:", notes_a_trier is notes_examens)
# Sort the list in place, then inspect the original list
notes_a_trier.sort()
print("original list:", notes_examens)same object: True
original list: [61, 67, 70, 71, 74]تؤكّد النتيجة True أن الاسمين يشيران إلى الكائن نفسه. تصبح القائمة بعد الترتيب [61, 67, 70, 71, 74]، بما في ذلك عند عرض notes_examens. إذا كنت تريد الاحتفاظ بترتيب الدرجات الأصلي في قائمة مستقلة، فهذا الإسناد لا يحقّق ذلك. لم تتغيّر الدرجات، لكن ترتيبها في قائمة العمل تغيّر.
اختيار النسخ المناسب للقائمة
تُنشئ .copy()، وهي دالة مرتبطة بالقائمة (method)، نسخة سطحية: تُنشئ حاوية جديدة، لكن الحاوية الجديدة تظلّ تشير إلى الكائنات الموجودة داخل الحاوية الأصلية. يمكنك أيضًا نسخ القائمة باستخدام list(notes_examens) أو notes_examens[:]. ولهذه العمليات القيد نفسه عندما تحتوي القائمة على كائنات أخرى قابلة للتغيير.
في قائمة الأعداد الصحيحة المستخدمة هنا، تكفي النسخة السطحية للاحتفاظ بالقائمة الأصلية عند الترتيب. الأعداد الصحيحة غير قابلة للتغيير، ولكلّ قائمة ترتيب مستقل لعناصرها. قارن القيم باستخدام ==، والهويات باستخدام is، قبل تعديل النسخة.
# Create the original list and a separate copy
notes_examens = list(scores_reference)
notes_triees = notes_examens.copy()
# Compare the lists' values and identities
print("same values:", notes_triees == notes_examens)
print("same object:", notes_triees is notes_examens)
# Sort the copy, then display both lists
notes_triees.sort()
print("original:", notes_examens)
print("sorted copy:", notes_triees)same values: True
same object: False
original: [67, 61, 74, 71, 70]
sorted copy: [61, 67, 70, 71, 74]تبدأ القائمتان بالقيم نفسها، ولذلك يُعيد == القيمة True. لكنهما كائنان مختلفان، ولذلك يُعيد is القيمة False. بعد الترتيب، يحتفظ notes_examens بالترتيب الأصلي. وإذا استبدلت عددًا صحيحًا في النسخة، فلن يتغيّر العنصر المقابل في القائمة الأخرى.
يمكن أن يحتوي tuple على كائن قابل للتغيير
عدم قابلية tuple للتغيير يعني ثبات المراجع إلى عناصره، ولا يجعل القائمة التي يحتوي عليها غير قابلة للتغيير. في المثال التالي، يحتوي tuple على اسم العمود وقائمة من ثلاث درجات. لذلك يغيّر ترتيب هذه القائمة ما تراه عند عرض tuple.
# Place a list inside a tuple
notes = list(scores_reference[:3])
serie = ("Exam_Score", notes)
# Change the list held by the tuple
notes.sort()
# Display the tuple and check whether the list is shared
print(serie)
print("shared list:", serie[1] is notes)('Exam_Score', [61, 67, 74])
shared list: Trueيحتفظ tuple بمرجعيه: مرجع إلى السلسلة النصية "Exam_Score" ومرجع إلى القائمة. لا يمكنك استبدال serie[1] مباشرةً بقائمة أخرى، لكن يمكنك تعديل القائمة التي يشير إليها هذا العنصر بالفعل. لذلك لا يعزل تحويل قائمة إلى tuple الكائنات التي تحتوي عليها تلقائيًا.

المجموعات من نوع set في Python
المجموعة من نوع set في Python هي مجموعة عناصر دون تكرار. وعلى خلاف القائمة، لا تضمن ترتيب عناصرها. لإنشاء مجموعة غير فارغة، ضع عناصرها بين القوسين المعقوفين {} وافصل بينها بفواصل. في هذا المثال، يحتوي champs_a_verifier على اسمي الحقلين Hours_Studied وAttendance. باستدعاء .add() على المجموعة، تضيف اسم حقل ثالث هو Exam_Score.
المجموعة من نوع set قابلة للتغيير: يمكنك إضافة عناصر أو حذفها بعد إنشائها. وتوفّر Python أيضًا النوع frozenset للمجموعات غير القابلة للتغيير. يشرح فصل هياكل البيانات في دليل Python الرسمي هذه الاستخدامات.
# Create a set containing two field names
champs_a_verifier = {"Hours_Studied", "Attendance"}
# Add another field name to the set
champs_a_verifier.add("Exam_Score")
# Count the elements
print("number of fields:", len(champs_a_verifier))
# Check whether a field name is present
print("Exam_Score present:", "Exam_Score" in champs_a_verifier)number of fields: 3
Exam_Score present: Trueالنسخ السطحي والعميق للقاموس
يظهر الفرق عندما تكون إحدى قيم القاموس قائمة. تُنشئ original.copy() قاموسًا مستقلًا، لكن القيمة المرتبطة بالمفتاح "scores" هي القائمة نفسها المستخدمة في original. نسخ الحاوية الخارجية لا ينسخ الكائنات المتداخلة. وهذه أيضًا من نتائج قابلية التغيير في Python.

ملاحظة المشاركة بعد النسخ السطحي
# Create a dictionary containing a list, then make a shallow copy
original = {"scores": list(scores_reference[:3])}
copie = original.copy()
# Compare the dictionaries and the lists they contain
print("same dictionary:", copie is original)
print("same list:", copie["scores"] is original["scores"])
# Sort the shared list and inspect both dictionaries
copie["scores"].sort()
print("original:", original["scores"])
print("copy:", copie["scores"])same dictionary: False
same list: True
original: [61, 67, 74]
copy: [61, 67, 74]القاموسان مختلفان، لكن القائمة الموجودة داخلهما واحدة. لذلك يظهر ترتيب copie["scores"] أيضًا في original["scores"]. أمّا لو استبدلت فقط القيمة المرتبطة بالمفتاح في القاموس الجديد، لاحتفظ القاموس الآخر بارتباطه الخاص. العملية هنا تعدّل القائمة المشتركة.
استخدام النسخ العميق عندما تتطلّبه البنية
يمرّ النسخ العميق عبر البنية المركّبة لنسخ الكائنات المتداخلة المعنيّة، بينما قد تظلّ الكائنات غير القابلة للتغيير مشتركة. في القاموس المستخدم هنا، الذي يحتوي على قوائم وأعداد صحيحة، تُنشئ deepcopy() أيضًا قائمة داخلية مستقلة. تستورد التعليمة from هذه الدالة من وحدة copy، وهي جزء من المكتبة القياسية.
# Import the deep-copy function
from copy import deepcopy
# Copy the dictionary and the list it contains
original = {"scores": list(scores_reference[:3])}
copie = deepcopy(original)
# Check whether the lists are distinct
print("same list:", copie["scores"] is original["scores"])
# Sort the copied list, then display both lists
copie["scores"].sort()
print("original:", original["scores"])
print("copy:", copie["scores"])same list: False
original: [67, 61, 74]
copy: [61, 67, 74]تبقى القائمة الأصلية [67, 61, 74]، وتصبح النسخة وحدها [61, 67, 74]. مع ذلك، اختر النسخ حسب العملية التي تريد تنفيذها؛ فقد ينسخ النسخ العميق بيانات أكثر مما تحتاج. وعندما تعرف البنية، قد يكفي نسخ الكائنات التي ستعدّلها فقط.
الحفاظ على البيانات المُمرّرة إلى دالة
يشير معامل الدالة إلى الكائن المُمرّر إليها. لذلك يمكن للدالة تعديل قائمة يستخدمها الكود الذي استدعاها إذا نفّذت عملية تعدّل القائمة في مكانها. وعندما تريد الاحتفاظ بالمدخلات وإنتاج نتيجة، اختر عملية تُنشئ هذه النتيجة بصورة مستقلة.
تُعيد الدالة المضمّنة sorted() قائمة جديدة مرتّبة، بينما تعدّل .sort() القائمة الموجودة. يوضّح دليل الترتيب الرسمي في Python هذا الفرق. لذلك تستخدم الدالة trier_scores الدالة sorted() للحفاظ على مدخلاتها.
# Define a function that returns a new sorted list
def trier_scores(scores):
return sorted(scores)
# Call the function with the score list
notes_examens = list(scores_reference)
resultat = trier_scores(notes_examens)
# Compare the input with the result without changing the input
print("input:", notes_examens)
print("result:", resultat)input: [67, 61, 74, 71, 70]
result: [61, 67, 70, 71, 74]تكون النتيجة مرتّبة، وتحتفظ القائمة المُدخلة بترتيبها. يساعدك هذا الفصل على التحقّق من التحويلات المتتابعة للبيانات، إذ يمكنك مقارنة الحالة الأصلية بالنتيجة دون إعادة إنشاء المدخلات. لكنه ليس ضمانًا عامًا لكلّ دالة؛ تحتاج إلى فحص العمليات التي تستخدمها الدالة.

تطبيق عملي: تجهيز نسخة للعمل
لديك قاموس original يحفظ اسم الحقل "Exam_Score" وقائمة الدرجات الخمس الأصلية. أنشئ قاموسًا مستقلًا باسم travail، ورتّب درجاته تصاعديًا، واحتفظ بالدرجات الأصلية بترتيبها الأول. يجب أن يظلّ اسم الحقل كما هو.
ابدأ بتحديد الكائنات التي يجب أن تكون مستقلة. ستعدّل القاموس الخارجي لربطه بقائمة أخرى، ثم ترتّب هذه القائمة. ولا تحتاج إلى نسخ السلسلة النصية التي تصف الحقل لتنفيذ هذه العملية.
عرض الحل والتحقّق من نتائجه
# Create the original dictionary
original = {
"champ": "Exam_Score",
"scores": list(scores_reference),
}
# Copy the dictionary and its list before sorting
travail = original.copy()
travail["scores"] = original["scores"].copy()
travail["scores"].sort()
# Display the values and check whether the lists are distinct
print("original:", original["scores"])
print("working copy:", travail["scores"])
print("field preserved:", travail["champ"])
print("distinct lists:", travail["scores"] is not original["scores"])
# Check that the original is unchanged and the copy is sorted
assert original["scores"] == list(scores_reference)
assert travail["scores"] == sorted(scores_reference)
assert travail is not originaloriginal: [67, 61, 74, 71, 70]
working copy: [61, 67, 70, 71, 74]
field preserved: Exam_Score
distinct lists: Trueتُنشئ النسخة الأولى قاموس العمل. وتُنشئ الثانية القائمة التي سترتّبها، ثم يستبدل الإسناد إلى travail["scores"] المرجع المشترك. يمكن للقائمتين الآن أن تتغيّرا بصورة مستقلة. تتحقّق تعليمات assert من النتائج؛ فإذا كان أحد الشروط غير صحيح، يتوقّف تنفيذ الخلية مع ظهور خطأ.
لهذه البنية المحدّدة، يمكنك أيضًا استخدام deepcopy(original). لكن نسخ الأجزاء المطلوبة فقط يوضّح ما تمّ عزله. ولو احتوى القاموس على مستويات متعدّدة من الكائنات القابلة للتغيير التي تريد تعديلها، لاحتجت إلى تكييف النسخ مع تلك البنية.
أخطاء شائعة تحتاج إلى التعرّف عليها
إسناد القيمة التي تعيدها sort إلى متغيّر
عند استدعاء .sort() على القائمة، تُعاد القيمة None، ولا تُعاد قائمة جديدة؛ إذ يكون الاستدعاء قد رتّب القائمة الموجودة. لذلك يجعل إسناد مثل notes_examens = notes_examens.sort() هذا الاسم يفقد مرجعه إلى القائمة. لاحظ القيمة المُعادة والقائمة المرتّبة كلًّا على حدة.
# Create a list of scores
notes_examens = list(scores_reference)
# Sort the list in place and capture the return value
resultat = notes_examens.sort()
# Display the return value of sort() and the changed list
print("sort return value:", resultat)
print("list after sorting:", notes_examens)sort return value: None
list after sorting: [61, 67, 70, 71, 74]للاحتفاظ بالكائن نفسه، استدعِ notes_examens.sort() فقط. وللحصول على قائمة أخرى مع الحفاظ على المدخلات، استخدم notes_triees = sorted(notes_examens).
الخلط بين تساوي القيم وهوية الكائن
يمكن أن تتساوى قيم قائمتين دون أن تكونا الكائن نفسه. استخدم == لمقارنة القيم، وis للتحقّق من مشاركة الكائن. لا تستنتج قاعدة للنسخ من هوية الأعداد الصحيحة الصغيرة أو السلاسل النصية، فقد تعيد Python استخدام بعض الكائنات غير القابلة للتغيير.
إعادة استخدام قائمة عُدّلت سابقًا في الـNotebook
عند تنفيذ خلية أكثر من مرّة، تستخدم المتغيّرات الموجودة بالفعل في النواة. إذا رُتّبت قائمة، فلا يعود ترتيبها السابق تلقائيًا. لذلك تعيد الأمثلة إنشاء قوائمها باستخدام list(scores_reference). لإعادة إنتاج جميع المخرجات، أعد تشغيل النواة ثم اختر Run All، أي تنفيذ جميع الخلايا.
قابلية التغيير في Python: 5 قواعد تحتاج إلى تذكّرها
طبّق قواعد قابلية التغيير في Python الخمس التالية على كائنات هذا الدرس، لاختيار النسخ المناسب والحفاظ على البيانات الأصلية.
- حدّد المراجع المشتركة. يربط الإسناد اسمًا آخر بالكائن نفسه. تحقّق من المشاركة باستخدام
isقبل تعديل القائمة. - انسخ قائمة العناصر غير القابلة للتغيير. تُنشئ النسخة السطحية باستخدام
.copy()قائمة مستقلة. وتكفي هذه النسخة للاحتفاظ بالترتيب الأصلي للدرجات الصحيحة في هذا المثال. - افحص كلّ مستوى من البنية. قد تظلّ النسخة السطحية من القاموس تشترك مع الأصل في القوائم. انسخ أيضًا القائمة التي ستعدّلها، كما في التمرين.
- اختر النسخ العميق عند الحاجة. في القاموس المعروض، تعيد
deepcopy()إنشاء القائمة الداخلية. وقد ينسخ النسخ العميق كائنات أكثر مما تحتاج؛ أحيانًا يكفي نسخ الأجزاء المطلوبة فقط. - فضّل عملية تحافظ على المدخلات. تُعيد
sorted()قائمة جديدة، بينما تعدّل.sort()القائمة الموجودة وتُعيدNone. اختر العملية حسب النتيجة المطلوبة.