جدول المحتويات

  • عمل نسخ ضحلة
  • عمل نسخ عميقة
  • نسخ كائنات بايثون التعسفية
  • 3 أشياء يجب تذكرها

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

ولكن للعمل مع كائنات قابلة للتغيير أو مجموعات من الكائنات القابلة للتغيير ، قد تبحث عن طريقة لإنشاء “نسخ حقيقية” أو “نسخ” من هذه الكائنات.

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

ملاحظة: تمت كتابة هذا البرنامج التعليمي مع وضع Python 3 في الاعتبار ولكن هناك اختلاف بسيط بين Python 2 و 3 عندما يتعلق الأمر بنسخ الكائنات. عندما تكون هناك اختلافات سأشير إليها في النص.

لنبدأ بالنظر في كيفية نسخ مجموعات Python المضمنة. يمكن نسخ المجموعات القابلة للتغيير المضمنة في Python مثل القوائم والإملاءات والمجموعات من خلال استدعاء وظائف المصنع الخاصة بها في مجموعة موجودة:

new_list = list(original_list)
new_dict = dict(original_dict)
new_set = set(original_set)

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

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

  • نسخة عميقة تجعل عملية النسخ متكررة. وهذا يعني أولاً إنشاء كائن مجموعة جديد ثم تعبئته بشكل متكرر بنسخ من الكائنات الفرعية الموجودة في الأصل. نسخ كائن بهذه الطريقة يمشي في شجرة الكائن بأكملها لإنشاء نسخة مستقلة تمامًا من الكائن الأصلي وجميع العناصر الفرعية.

أعلم أن هذا كان قليلاً من الفم. لذلك دعونا نلقي نظرة على بعض الأمثلة لتوضيح هذا الاختلاف بين النسخ العميقة والضحلة.

عمل نسخ ضحلة

في المثال أدناه ، سننشئ قائمة متداخلة جديدة ثم ننسخها بشكل سطحي باستخدام وظيفة المصنع list ():

>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys = list(xs)  # Make a shallow copy

هذا يعني أن y سيكون الآن كائنًا جديدًا ومستقلًا له نفس محتويات xs. يمكنك التحقق من ذلك من خلال فحص كلا الكائنين:

>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

للتأكد من أن ys مستقلة حقًا عن الأصل ، فلنبتكر تجربة صغيرة. يمكنك محاولة إضافة قائمة فرعية جديدة إلى (xs) الأصلية ثم التحقق للتأكد من أن هذا التعديل لم يؤثر على النسخة (ys):

>>> xs.append(['new sublist'])
>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

كما ترى ، كان لهذا التأثير المتوقع. لم يكن تعديل القائمة المنسوخة على مستوى “سطحي” مشكلة على الإطلاق.

ومع ذلك ، نظرًا لأننا أنشأنا فقط نسخة سطحية من القائمة الأصلية ، فلا يزال ys يحتوي على مراجع للكائنات الفرعية الأصلية المخزنة في xs.

لم يتم نسخ هؤلاء الأطفال. تمت الإشارة إليهم مرة أخرى في القائمة المنسوخة.

لذلك ، عند تعديل أحد الكائنات الفرعية في xs ، سينعكس هذا التعديل في y أيضًا ، وذلك لأن كلا القائمتين تشتركان في نفس الكائنات الفرعية. النسخة ليست سوى نسخة عميقة من مستوى واحد ضحلة:

>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9], ['new sublist']]
>>> ys
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]

في المثال أعلاه (على ما يبدو) قمنا فقط بإجراء تغيير على xs. ولكن اتضح أنه تم تعديل كلتا القائمتين الفرعيتين في الفهرس 1 في x و y. مرة أخرى ، حدث هذا لأننا أنشأنا فقط نسخة ضحلة من القائمة الأصلية.

لو أنشأنا نسخة عميقة من xs في الخطوة الأولى ، لكان كلا الكائنين مستقلين تمامًا. هذا هو الفرق العملي بين النسخ الضحلة والعميقة من الكائنات.

أنت الآن تعرف كيفية إنشاء نسخ ضحلة لبعض فئات المجموعات المضمنة ، وأنت تعرف الفرق بين النسخ الضحل والنسخ العميق.

الأسئلة التي ما زلنا نريد إجابات عنها هي:

  • كيف يمكنك إنشاء نسخ عميقة من المجموعات المضمنة؟
  • كيف يمكنك إنشاء نسخ (ضحلة وعميقة) من الكائنات العشوائية ، بما في ذلك الفئات المخصصة؟

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

عمل نسخ عميقة

دعنا نكرر مثال نسخ القائمة السابق ، ولكن مع اختلاف واحد مهم. هذه المرة سننشئ نسخة عميقة باستخدام وظيفة deepcopy () المحددة في وحدة النسخ بدلاً من ذلك:

>>> import copy
>>> xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs = copy.deepcopy(xs)

عندما تفحص xs واستنساخها z الذي أنشأناه باستخدام copy.deepcopy () ، سترى أنهما يبدوان متطابقين مرة أخرى — تمامًا كما في المثال السابق:

>>> xs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

ومع ذلك ، إذا قمت بإجراء تعديل على أحد الكائنات الفرعية في الكائن الأصلي (xs) ، فسترى أن هذا التعديل لن يؤثر على النسخة العميقة (zs).

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

>>> xs[1][0] = 'X'
>>> xs
[[1, 2, 3], ['X', 5, 6], [7, 8, 9]]
>>> zs
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]

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

بالمناسبة ، يمكنك أيضًا إنشاء نسخ ضحلة باستخدام وظيفة في وحدة النسخ. تُنشئ الوظيفة copy.copy () نسخًا ضحلة من الكائنات.

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

نسخ كائنات بايثون التعسفية

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

مرة أخرى ، تأتي وحدة النسخ لإنقاذنا. يمكن استخدام وظيفتي copy.copy () و copy.deepcopy () لتكرار أي كائن.

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

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Point({self.x!r}, {self.y!r})'

آمل أن توافق على أن هذا كان واضحًا جدًا. أضفت تنفيذ __repr __ () حتى نتمكن من فحص الكائنات التي تم إنشاؤها من هذه الفئة بسهولة في مترجم Python.

ملاحظة: يستخدم المثال أعلاه سلسلة Python 3.6 لتكوين السلسلة التي يتم إرجاعها بواسطة __repr__. في Python 2 وإصدارات Python 3 قبل 3.6 ، يمكنك استخدام تعبير تنسيق سلسلة مختلف ، على سبيل المثال:

def __repr__(self):
    return 'Point(%r, %r)' % (self.x, self.y)

بعد ذلك ، سننشئ مثيل Point ثم ننسخه (بشكل سطحي) باستخدام وحدة النسخ:

>>> a = Point(23, 42)
>>> b = copy.copy(a)

إذا فحصنا محتويات كائن Point الأصلي واستنساخه (الضحل) ، فإننا نرى ما نتوقعه:

>>> a
Point(23, 42)
>>> b
Point(23, 42)
>>> a is b
False

إليك شيء آخر يجب مراعاته. نظرًا لأن الكائن النقطي يستخدم أنواعًا غير قابلة للتغيير (ints) لإحداثياته ​​، فلا فرق بين النسخة الضحلة والنسخة العميقة في هذه الحالة. لكنني سأوسع المثال في ثانية.

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

class Rectangle:
    def __init__(self, topleft, bottomright):
        self.topleft = topleft
        self.bottomright = bottomright

    def __repr__(self):
        return (f'Rectangle({self.topleft!r}, '
                f'{self.bottomright!r})')

مرة أخرى ، سنحاول أولاً إنشاء نسخة سطحية من مثيل المستطيل:

rect = Rectangle(Point(0, 1), Point(5, 6))
srect = copy.copy(rect)

إذا قمت بفحص المستطيل الأصلي ونسخته ، فسترى مدى جودة عمل تجاوز __repr __ () ، وأن عملية النسخ السطحي عملت كما هو متوقع:

>>> rect
Rectangle(Point(0, 1), Point(5, 6))
>>> srect
Rectangle(Point(0, 1), Point(5, 6))
>>> rect is srect
False

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

>>> rect.topleft.x = 999
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

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

>>> drect = copy.deepcopy(srect)
>>> drect.topleft.x = 222
>>> drect
Rectangle(Point(222, 1), Point(5, 6))
>>> rect
Rectangle(Point(999, 1), Point(5, 6))
>>> srect
Rectangle(Point(999, 1), Point(5, 6))

هاهو! هذه المرة ، تكون النسخة العميقة (صحيحة) مستقلة تمامًا عن النسخة الأصلية (المستقيمة) والنسخة السطحية (صحيحة).

لقد غطينا الكثير من الأرضية هنا ، ولا تزال هناك بعض النقاط الدقيقة لنسخ الكائنات.

من المفيد التعمق في هذا الموضوع ، لذلك قد ترغب في دراسة وثائق وحدة النسخ. على سبيل المثال ، يمكن للكائنات التحكم في كيفية نسخها من خلال تحديد الطرق الخاصة __copy __ () و __deepcopy __ () عليها.

3 أشياء يجب تذكرها

  • لن يؤدي عمل نسخة ضحلة من كائن إلى استنساخ كائنات فرعية. لذلك ، فإن النسخة ليست مستقلة تمامًا عن الأصل.
  • ستقوم نسخة عميقة من كائن باستنساخ الكائنات الفرعية بشكل متكرر. الاستنساخ مستقل تمامًا عن الأصل ، ولكن إنشاء نسخة عميقة يكون أبطأ.
  • يمكنك نسخ كائنات عشوائية (بما في ذلك الفئات المخصصة) باستخدام وحدة النسخ.

إذا كنت ترغب في التعمق في تقنيات برمجة Python الأخرى ذات المستوى المتوسط ​​، تحقق من هذه المكافأة المجانية: