شرح Copy-on-Write في Pandas 3.0: كيف تعمل النسخ والتعديلات في DataFrame؟

شرح Copy-on-Write في Pandas 3.0 وتأثيره على DataFrame والنسخ والتعديل

إذا كنت تستخدم Pandas منذ فترة، فربما اعتدت سؤالًا مزعجًا: هل العملية التي نفذتها أعادت نسخة مستقلة من DataFrame أم مجرد View تشارك البيانات مع الأصل؟ وهل تعديل النتيجة سيغير الجدول الأصلي أم لا؟

مع Pandas 3.0 تغيرت القاعدة جذريًا. أصبح نظام Copy-on-Write أو CoW هو السلوك الافتراضي والوحيد، والهدف منه جعل النسخ والتعديل أكثر وضوحًا للمستخدم مع تجنب نسخ البيانات في الذاكرة قبل الحاجة الفعلية إليها.

في هذا الدرس سنشرح Copy-on-Write في Pandas من الصفر بأمثلة عملية: ماذا يعني أن الكائن «يتصرف كنسخة»؟ متى تحدث النسخة الحقيقية؟ ماذا يحدث عند تعديل جزء مأخوذ من DataFrame؟ وكيف تغير سلوك copy(deep=False) وto_numpy()؟

{alertInfo}المهم قبل البدء: هذا المقال يشرح مفهوم Copy-on-Write نفسه. إذا كانت مشكلتك هي ظهور ChainedAssignmentError أو عدم تعديل DataFrame بعد كود متسلسل، راجع درس حل ChainedAssignmentError في Pandas 3.0 باستخدام loc.

تحميل مشروع Copy-on-Write في Pandas

أمثلة قابلة للتشغيل توضح استقلال النسخ، وdeep=False، والتعديل المباشر باستخدام loc، ومصفوفات NumPy للقراءة فقط، مع اختبارات تلقائية.

تحميل ملفات الدرس ZIP

{getToc} $title={محتوى المقال}


ما هو Copy-on-Write في Pandas؟

Copy-on-Write تعني أن Pandas تستطيع مشاركة نفس البيانات الداخلية بين أكثر من كائن ما دام لا يوجد تعديل يحتاج إلى فصلها. عندما تحاول الكتابة على أحد هذه الكائنات، تنشئ Pandas النسخة المطلوبة في ذلك الوقت فقط حتى يبقى الكائن الآخر مستقلًا.

الفكرة الأساسية يمكن تبسيطها هكذا:

إنشاء DataFrame
      ↓
استخراج جزء أو إنشاء كائن مشتق
      ↓
يمكن مشاركة البيانات داخليًا مؤقتًا
      ↓
لا يوجد تعديل؟ لا حاجة إلى نسخة فعلية
      ↓
حدث تعديل؟
      ↓
تنسخ Pandas البيانات اللازمة قبل الكتابة

لكن هناك نقطة دقيقة جدًا: من منظور المستخدم، الكائن المشتق يتصرف كما لو كان نسخة مستقلة حتى لو كانت Pandas تشارك الذاكرة داخليًا مؤقتًا.

{alertWarning}لا تخلط بين واجهة الاستخدام والذاكرة الداخلية: عبارة «يتصرف كنسخة» لا تعني أن Pandas تنسخ كل البيانات فورًا عند كل عملية. Copy-on-Write صُممت تحديدًا لتأخير النسخ حتى تصبح ضرورية.

لماذا احتاجت Pandas إلى Copy-on-Write؟

قبل هذا النظام كان سلوك النسخ والـViews في Pandas مربكًا. بعض عمليات الاختيار قد تعيد View مرتبطة بالبيانات الأصلية، بينما تعيد عمليات أخرى نسخة مستقلة، وقد يعتمد السلوك على تفاصيل داخلية لا يريد المستخدم التفكير فيها عند تحليل البيانات.

مثلًا، كان من الممكن أن تختار عمودًا ثم تعدله، وفي بعض الحالات تنتقل التغييرات إلى DataFrame الأصلية، بينما لا يحدث ذلك في حالات أخرى.

Copy-on-Write وضعت قاعدة أبسط:

{alertSuccess}القاعدة الأساسية في Pandas 3.0: أي كائن DataFrame أو Series ناتج من عملية اختيار أو دالة تُرجع كائنًا جديدًا يتصرف بالنسبة للمستخدم كأنه نسخة، ولا تعتمد على تعديل الكائن المشتق لتغيير الأصل.

إذا أردت تعديل الأصل، عدّل الأصل نفسه مباشرة.

متى ظهر Copy-on-Write وماذا تغير في Pandas 3.0؟

لم يظهر CoW فجأة في الإصدار 3.0. بدأ العمل عليه تدريجيًا في الإصدارات السابقة، ثم أصبح السلوك النهائي الملزم في Pandas 3.0.

الإصدارحالة Copy-on-Write
Pandas 1.5ظهرت آلية Copy-on-Write بصورة تجريبية.
Pandas 2.0أصبحت معظم التحسينات اللازمة متوفرة ويمكن تفعيل CoW.
Pandas 2.1اكتملت التحسينات الرئيسية المرتبطة بالآلية.
Pandas 2.2يمكن اختبار السلوك واستخدام وضع التحذيرات للمساعدة في الهجرة.
Pandas 3.0Copy-on-Write أصبح السلوك الافتراضي والوحيد.

في Pandas 3.0 لم يعد الخيار mode.copy_on_write وسيلة لإيقاف النظام والعودة إلى السلوك القديم؛ CoW أصبح جزءًا من قواعد المكتبة الجديدة.

مثال بسيط: تعديل نسخة مشتقة لا يغير DataFrame الأصلية

أنشئ الجدول التالي:

import pandas as pd

df = pd.DataFrame({
    "name": ["Ali", "Sara", "Omar"],
    "score": [70, 90, 80],
})

subset = df[["name", "score"]]

subset.loc[0, "score"] = 100

print(subset["score"].tolist())
print(df["score"].tolist())

الناتج المتوقع مع Copy-on-Write:

[100, 90, 80]
[70, 90, 80]

تغير subset، لكن df الأصلية بقيت كما هي.

هذه هي الفكرة التي تجعل سلوك Pandas أكثر قابلية للتوقع: لا تعتمد على كون الجزء المحدد View أو Copy داخليًا. إذا عدلت الكائن المشتق، فالتعديل يخصه وحده.

مثال يوضح أن تعديل subset في Pandas 3.0 لا يغير DataFrame الأصلية

إذا أردت تعديل الأصل فاستخدمه مباشرة

عندما يكون هدفك تعديل df نفسها، نفّذ التعديل عليها مباشرة:

df.loc[
    df["score"] < 75,
    "score"
] = 75

print(df["score"].tolist())

الناتج:

[75, 90, 80]

إذا أردت مراجعة الاختيار والتصفية بتفصيل أكبر، راجع اختيار الصفوف والأعمدة في Pandas باستخدام loc وiloc.

ما علاقة Copy-on-Write بـ Chained Assignment؟

من أشهر النتائج العملية للنظام الجديد أن Chained Assignment لم يعد طريقة صالحة لتعديل الأصل.

لا تعتمد على هذا النمط:

df["score"][df["score"] < 75] = 75

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

لن نكرر تفاصيل الخطأ هنا؛ لديك دليل مخصص يشرح ChainedAssignmentError في Pandas 3.0 والحل باستخدام loc.

Copy-on-Write لا تعني deepcopy

قد يبدو اسم Copy-on-Write قريبًا من مفاهيم copy وdeepcopy في بايثون، لكنهما ليسا الشيء نفسه.

deepcopy في بايثون تعني إنشاء نسخ مستقلة بصورة متكررة للكائنات المتداخلة حسب بنية الكائن. أما Copy-on-Write في Pandas فهي استراتيجية لإدارة مشاركة البيانات والنسخ عند الحاجة، مع قواعد سلوك واضحة للكائنات المشتقة.

إذا أردت مراجعة مفهوم النسخ في بايثون نفسه، راجع الفرق بين copy وdeepcopy في بايثون.

ماذا يحدث مع copy(deep=False) في Pandas 3.0؟

هذه من أكثر النقاط التي تغير فهمها مع CoW.

عندما تكتب:

df2 = df.copy(deep=False)

فأنت تنشئ كائن DataFrame جديدًا يمكنه مشاركة البيانات الداخلية مع الأصل في البداية. لكن مع Copy-on-Write لا يعني ذلك أن تعديل df2 سيغير df.

import pandas as pd

df = pd.DataFrame({
    "a": [1, 2, 3],
    "b": [4, 5, 6],
})

df2 = df.copy(deep=False)
df2.loc[0, "a"] = 99

print(df2["a"].tolist())
print(df["a"].tolist())

الناتج:

[99, 2, 3]
[1, 2, 3]

إذًا deep=False في عصر Copy-on-Write أقرب إلى نسخة مؤجلة Lazy Copy: يمكن مشاركة البيانات أولًا، لكن التعديل لا ينتشر إلى الكائن الآخر.

متى تستخدم copy() إذًا؟

ما زالت .copy() مفيدة عندما تريد التعبير بوضوح عن نيتك أو فرض نسخة فعلية مستقلة باستخدام السلوك الافتراضي deep=True.

لكن لم يعد من الضروري إضافة .copy() عشوائيًا فقط لإسكات SettingWithCopyWarning القديم. في Pandas 3.0 أصبح السلوك أكثر وضوحًا، وتحول الاهتمام من «هل هذه View أم Copy؟» إلى «أي كائن أريد تعديله فعلًا؟».

الحالةالتصرف المناسب
أريد تعديل DataFrame الأصليةعدّل الأصل مباشرة باستخدام loc أو iloc.
أريد كائنًا مستقلًا واضحًااستخدم .copy().
أريد كائنًا جديدًا مع نسخ مؤجل.copy(deep=False) يعمل مع CoW كنسخة مؤجلة.
أستخدم .copy() فقط لإخفاء Warning قديمراجع الكود؛ غالبًا لم يعد هذا النسخ الدفاعي ضروريًا.

كيف يحسن Copy-on-Write استخدام الذاكرة؟

تخيل DataFrame كبيرة ثم نفذت عملية مثل إعادة تسمية عمود أو حذف عمود وأنت لا تعدل البيانات نفسها بعد. إذا نسخت Pandas كامل البيانات مباشرة في كل مرحلة، يمكن أن يزداد استهلاك الذاكرة بلا حاجة.

CoW تسمح بتأجيل النسخة:

df2 = df.drop(columns=["unused"])

يمكن لبعض العمليات أن تستفيد من مشاركة البيانات داخليًا بدل نسخ كل شيء فورًا، ثم تنفصل البيانات عند حدوث كتابة تستلزم ذلك.

لهذا لا يجب تفسير Copy-on-Write على أنها «نسخ أكثر». الهدف الفعلي هو تحقيق سلوك يشبه النسخة للمستخدم مع تجنب النسخ غير الضروري داخليًا.

مخطط النسخ المؤجل ومشاركة الذاكرة في Pandas Copy-on-Write

هل Copy-on-Write تجعل كل العمليات أسرع؟

لا. لا توجد قاعدة تقول إن كل كود سيصبح أسرع لمجرد تشغيل CoW.

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

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

نمط قد يسبب نسخًا غير ضرورية

إذا لم تعد تحتاج إلى النسخة القديمة من الجدول، تجنب الاحتفاظ بمراجع إضافية دون سبب.

مثلًا:

df2 = df.reset_index(drop=True)
df2.iloc[0, 0] = 100

إذا كنت تحتاج df2 فقط ولا تحتاج df القديمة، فإعادة الإسناد قد تكون أوضح:

df = df.reset_index(drop=True)
df.iloc[0, 0] = 100

بهذا لا تبقي مرجعًا إضافيًا إلى البيانات من دون حاجة.

ماذا يحدث عند تحويل DataFrame إلى NumPy؟

هذه حالة متقدمة لكنها مهمة إذا كنت تتنقل بين Pandas وNumPy.

عندما تعيد to_numpy() مصفوفة تشارك البيانات مع كائن Pandas، تحمي Pandas قواعد Copy-on-Write بجعل مصفوفة NumPy للقراءة فقط.

import pandas as pd

df = pd.DataFrame({
    "a": [1, 2, 3],
    "b": [4, 5, 6],
})

arr = df.to_numpy()

print(arr.flags.writeable)

في الحالة التي تكون فيها المصفوفة View تشارك البيانات، ستجد:

False

وإذا حاولت التعديل:

arr[0, 0] = 100

قد يظهر:

ValueError: assignment destination is read-only

إذا كنت تريد مصفوفة NumPy مستقلة قابلة للتعديل، اطلب نسخة:

arr = df.to_numpy(copy=True)
arr[0, 0] = 100

{alertInfo} ليس كل استدعاء لـto_numpy() يعيد View؛ قد تضطر Pandas أصلًا إلى إنشاء مصفوفة جديدة بسبب اختلاف أنواع الأعمدة. لذلك تعامل مع copy=True كطلب صريح لنسخة، ولا تفترض أن copy=False يضمن عدم النسخ.

ماذا تغير في SettingWithCopyWarning؟

كان SettingWithCopyWarning مرتبطًا بالغموض القديم: هل الكائن الذي تعدله View أم Copy؟ مع قواعد Copy-on-Write الجديدة لم يعد هذا السؤال هو الأساس في Pandas 3.0، وأزيل التحذير القديم.

بدلًا من ذلك، الأنماط غير الصالحة مثل Chained Assignment لا ينبغي الاعتماد عليها لتعديل الأصل.

هذا لا يعني تجاهل التحذيرات الجديدة، بل يعني أن طريقة التفكير تغيرت: حدد الكائن المطلوب تعديله ونفّذ التعديل عليه مباشرة.

كيف تختبر كود قديم قبل الانتقال إلى Pandas 3.0؟

إذا كنت ما زلت تستخدم Pandas 2.2 في مشروع قديم، كان من الممكن استخدام وضع التحذيرات للمساعدة في اكتشاف العمليات التي يتغير سلوكها:

import pandas as pd

pd.options.mode.copy_on_write = "warn"

هذا الأسلوب يخص مرحلة الهجرة في Pandas 2.x وقد يكون كثير الرسائل. في Pandas 3.0 أصبح Copy-on-Write دائمًا، والخيار نفسه لم يعد وسيلة لتشغيل أو إيقاف السلوك.

خطة عملية لمراجعة مشروع Pandas قديم

  1. اعرف إصدار Pandas المستخدم حاليًا.
  2. شغّل الاختبارات الموجودة في المشروع قبل الترقية.
  3. ابحث عن Chained Assignment وعمليات inplace=True على أعمدة مختارة.
  4. تأكد أن التعديلات التي تريدها على الأصل تتم مباشرة باستخدام loc أو iloc.
  5. راجع الاستخدامات القديمة لـ.copy() التي أضيفت فقط لتجنب SettingWithCopyWarning.
  6. راجع الأكواد التي تعتمد على تعديل مصفوفة ناتجة من to_numpy().
  7. اختبر النتائج الفعلية للبيانات بعد الترقية، لا تكتفِ بأن الكود يعمل دون Exception.

وإذا كان المشروع يتعامل مع Excel، فقد يفيدك أيضًا دليل Pandas أم openpyxl؟ أيهما تستخدم مع Excel في Python؟ لتحديد حدود دور DataFrame ودور مكتبة التعامل المباشر مع ملف Excel.

مثال كامل يجمع القواعد الجديدة

import pandas as pd


students = pd.DataFrame({
    "name": ["Ali", "Sara", "Omar", "Mona"],
    "score": [65, 90, 70, 82],
})


# كائن مشتق مستقل سلوكيًا
low_scores = students.loc[
    students["score"] < 75
]

low_scores.loc[:, "score"] = 75

print("subset:", low_scores["score"].tolist())
print("original:", students["score"].tolist())


# تعديل الأصل مباشرة
students.loc[
    students["score"] < 75,
    "score"
] = 75

print("updated original:", students["score"].tolist())

الناتج:

subset: [75, 75]
original: [65, 90, 70, 82]
updated original: [75, 90, 75, 82]

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

مقارنة بين سلوك Pandas القديم وCopy-on-Write في Pandas 3.0

أخطاء شائعة في فهم Copy-on-Write

الفكرة الخاطئةالتصحيح
CoW تنسخ DataFrame كاملة فور كل عمليةالنسخ يؤجل قدر الإمكان حتى توجد كتابة تحتاج إلى الفصل.
أي subset هو نسخة فعلية كاملة في الذاكرةيتصرف كنسخة في واجهة الاستخدام، وقد توجد مشاركة داخلية للبيانات.
تعديل Series مأخوذة من DataFrame سيعدل الأصللا تعتمد على ذلك؛ عدّل DataFrame الأصلية مباشرة.
deep=False يعني أن التغييرات تنتقل بين الكائنينمع CoW يصبح السلوك نسخة مؤجلة ولا تنتقل التغييرات بينهما.
CoW تجعل كل العمليات أسرعقد تقلل النسخ غير الضروري، لكن الأداء يعتمد على نمط الاستخدام.
اختفاء SettingWithCopyWarning يعني أن أي كود قديم أصبح صحيحًابعض الأنماط القديمة تغير سلوكها ويجب تحديثها.

مراجع رسمية ودروس مرتبطة

الأسئلة الشائعة

ما هو Copy-on-Write في Pandas؟

هو نظام يجعل كائنات DataFrame وSeries المشتقة تتصرف كنسخ مستقلة من منظور المستخدم، مع السماح لـPandas بمشاركة البيانات داخليًا وتأخير النسخ الفعلي حتى وقت الكتابة عند الحاجة.

هل Copy-on-Write مفعّل افتراضيًا في Pandas 3.0؟

نعم. في Pandas 3.0 أصبح Copy-on-Write السلوك الافتراضي والوحيد، ولا يمكن العودة إلى قواعد النسخ والعروض القديمة بإيقاف الخيار.

هل Copy-on-Write يعني أن كل DataFrame تُنسخ فورًا؟

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

هل تعديل subset يغير DataFrame الأصلية؟

لا تعتمد على ذلك مع Copy-on-Write. إذا أردت تعديل الأصل فاستخدم الأصل نفسه مباشرة، مثل df.loc[rows, columns] = value.

ما الفرق بين Copy-on-Write وcopy(deep=True)؟

copy(deep=True) تطلب نسخة مستقلة مباشرة من بيانات Pandas، بينما Copy-on-Write تسمح بمشاركة البيانات وتأخير النسخة الفعلية مع الحفاظ على استقلال سلوك الكائنات عند التعديل.

ما معنى copy(deep=False) بعد Pandas 3.0؟

ينشئ كائنًا جديدًا يمكن أن يشارك البيانات مع الأصل مؤقتًا، لكن التغييرات على أحد الكائنين لا تنعكس على الآخر؛ تتم النسخة عند الحاجة إلى الكتابة.

هل SettingWithCopyWarning ما زال موجودًا في Pandas 3.0؟

أزيل التحذير القديم مع قواعد Copy-on-Write الجديدة. لكن الأنماط مثل Chained Assignment لم تصبح صحيحة؛ بل يجب تعديل DataFrame المطلوبة مباشرة.

لماذا تكون مصفوفة to_numpy للقراءة فقط أحيانًا؟

إذا كانت المصفوفة تشارك الذاكرة مع كائن Pandas، قد تعيدها Pandas بحالة read-only لحماية قواعد Copy-on-Write. إذا كنت تريد مصفوفة مستقلة قابلة للتعديل، استخدم to_numpy(copy=True).

هل Copy-on-Write تحسن الأداء دائمًا؟

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

إرسال تعليق

أحدث أقدم