كيفية تسطيح القاموس في بايثون بأربع طرق عملية مختلفة

دقائق القراءة: 7

مقدمة: ما المقصود بتسطيح القاموس في Python؟

يُقصد بتسطيح القاموس في Python تحويل القاموس المتداخل إلى بنية مسطّحة ذات مستوى واحد فقط، بحيث تصبح المفاتيح المتداخلة مفاتيح نصية متصلة بفاصل مثل .. هذه العملية مفيدة في تحليل البيانات، ومقارنة القواميس، وتسهيل الوصول إلى القيم، وتجهيز البيانات للتخزين أو العرض أو المعالجة اللاحقة.

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

توضيح بصري لعملية تحويل قاموس متداخل إلى قاموس مسطح في بايثون

لماذا تحتاج إلى تسطيح القواميس في Python؟

هناك عدة حالات عملية تجعل تسطيح القاموس مفيداً:

  • تسهيل مقارنة قاموسين يحتويان على بنى متداخلة.
  • تبسيط الوصول إلى القيم دون التنقل عبر مستويات متعددة.
  • تهيئة البيانات لتخزينها في ملفات أو قواعد بيانات أو جداول تحليلية.
  • تسهيل تحويل البيانات إلى صيغ مناسبة للأدوات التحليلية.

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

فهرس الطرق التي سنغطيها

  • استخدام دالة递归 مخصّصة.
  • استخدام دالة递归 مع generators.
  • استخدام pandas.json_normalize.
  • استخدام مكتبة flatdict.

الطريقة الأولى: تسطيح القاموس عبر دالة递归 مخصّصة

من أكثر الحلول شيوعاً كتابة دالة تمرّ على عناصر القاموس، وإذا وجدت قاموساً داخلياً تتابع النزول داخله حتى تصل إلى القيم النهائية. هذه الطريقة واضحة ومباشرة، ويمكن تحسينها باستخدام type hints لزيادة الوضوح.

from collections.abc import MutableMapping

def flatten_dict(d: MutableMapping, parent_key: str = '', sep: str = '.') -> MutableMapping:
    items = []
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, MutableMapping):
            items.extend(flatten_dict(v, new_key, sep=sep).items())
        else:
            items.append((new_key, v))
    return dict(items)

>>> flatten_dict({
    'a': 1,
    'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}},
    'd': [6, 7, 8]
})
{'a': 1, 'c.a': 2, 'c.b.x': 3, 'c.b.y': 4, 'c.b.z': 5, 'd': [6, 7, 8]}

تحليل الأداء

تم اختبار هذه الطريقة باستخدام IPython عبر timeit وmemit من مكتبة memory_profiler. وإذا كنت تريد تشغيل %memit فعليك أولاً تحميل الامتداد بواسطة %load_ext memory_profiler.

In [4]: %timeit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
7.28 µs ± 54.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [5]: %load_ext memory_profiler
In [6]: %memit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
peak memory: 84.94 MiB, increment: 0.29 MiB

المزايا

  • سهلة الفهم والتنفيذ.
  • لا تحتاج إلى مكتبات خارجية.
  • مناسبة للتعلّم والحالات البسيطة.

العيوب

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

الطريقة الثانية: استخدام الدالة递归 مع generators

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

from collections.abc import MutableMapping

def _flatten_dict_gen(d, parent_key, sep):
    for k, v in d.items():
        new_key = parent_key + sep + k if parent_key else k
        if isinstance(v, MutableMapping):
            yield from flatten_dict(v, new_key, sep=sep).items()
        else:
            yield new_key, v

def flatten_dict(d: MutableMapping, parent_key: str = '', sep: str = '.'):
    return dict(_flatten_dict_gen(d, parent_key, sep))

>>> flatten_dict({
    'a': 1,
    'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}},
    'd': [6, 7, 8]
})
{'a': 1, 'c.a': 2, 'c.b.x': 3, 'c.b.y': 4, 'c.b.z': 5, 'd': [6, 7, 8]}

تحليل الأداء

In [9]: %timeit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
7.39 µs ± 78.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [7]: %memit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
peak memory: 45.27 MiB, increment: 0.25 MiB

المزايا

  • سهلة الفهم تقريباً مثل الطريقة الأولى.
  • أكثر كفاءة في استهلاك الذاكرة.
  • تقلل الحاجة إلى بنية مؤقتة غير ضرورية.

العيوب

  • قد لا تتعامل بسلاسة مع بعض الحالات الخاصة إذا لم تكن البنية من نوع MutableMapping.
  • ما زالت تعتمد على حل مخصّص من تطويرك، ما يعني أنك مسؤول عن اختباره وصيانته.

متى تكون هذه الطريقة مناسبة؟

إذا كنت تريد حلاً خفيفاً وسريعاً دون إضافة تبعيات خارجية، فهذه الطريقة تُعد من أفضل الخيارات العملية، خصوصاً عندما يكون استهلاك الذاكرة عاملاً مهماً.

الطريقة الثالثة: استخدام pandas.json_normalize

بدلاً من إعادة اختراع الحل من الصفر، يمكنك الاستفادة من مكتبة راسخة مثل pandas. توفر هذه المكتبة الدالة json_normalize التي تتعامل مع الكائنات الشبيهة بـ JSON وتحولها إلى بنية أكثر تسطيحاً، وهو ما يجعلها مناسبة لهذا النوع من المعالجة.

from collections.abc import MutableMapping
import pandas as pd

def flatten_dict(d: MutableMapping, sep: str = '.') -> MutableMapping:
    [flat_dict] = pd.json_normalize(d, sep=sep).to_dict(orient='records')
    return flat_dict

>>> flatten_dict({
    'a': 1,
    'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}},
    'd': [6, 7, 8]
})
{'a': 1, 'd': [6, 7, 8], 'c.a': 2, 'c.b.x': 3, 'c.b.y': 4, 'c.b.z': 5}

تحليل الأداء

In [5]: %timeit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
779 µs ± 10.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

In [6]: %memit flatten_dict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]})
peak memory: 86.30 MiB, increment: 0.90 MiB

المزايا

  • تعتمد على مكتبة قوية وشائعة الاستخدام.
  • مفيدة جداً إذا كان مشروعك يستخدم pandas بالفعل.
  • تنتج حلاً أنيقاً في سطر واحد تقريباً.

العيوب

  • تُعد مبالغة إذا كان هدفك الوحيد هو تسطيح قاموس بسيط.
  • أبطأ بكثير من الحلول اليدوية في هذا السيناريو.
  • تستهلك ذاكرة أكبر نسبياً.

متى تختار هذه الطريقة؟

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

الطريقة الرابعة: استخدام مكتبة flatdict

توفر مكتبة flatdict حلاً متخصصاً لتسطيح القواميس المتداخلة في Python. وهي أخف من pandas، ومصممة خصيصاً لهذه المهمة، كما تسمح بتخصيص الفاصل بين المفاتيح مثل ..

ومن أبرز ميزاتها أنك لا تحصل فقط على بنية مسطحة، بل يمكنك أيضاً الوصول إلى القيم باستخدام المفاتيح الجديدة أو عبر المسار المتداخل التقليدي.

>>> import flatdict
>>> d = flatdict.FlatDict(data, delimiter='.')  # d is a FlatDict instance
>>> d
<FlatDict id=140665244199904 {'a': 1, 'c.a': 2, 'c.b.x': 3, 'c.b.y': 4, 'c.b.z': 5, 'd': [6, 7, 8]}>

# and it allows accessing flat keys
>>> d['c.b.y']
4

# but also nested ones
>>> d['c']['b']['y']
4

# and can be converted to a flatten dict
>>> dict(d)
{'a': 1, 'c.a': 2, 'c.b.x': 3, 'c.b.y': 4, 'c.b.z': 5, 'd': [6, 7, 8]}

تحليل الأداء

In [3]: %timeit flatdict.FlatDict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]}, delimiter='.')
8.97 µs ± 21.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

In [4]: %memit flatdict.FlatDict({'a': 1, 'c': {'a': 2, 'b': {'x': 3, 'y': 4, 'z': 5}}, 'd': [6, 7, 8]}, delimiter='.')
peak memory: 45.21 MiB, increment: 0.14 MiB

المزايا

  • مكتبة خفيفة ومتخصصة في هذه المهمة.
  • أداء جيد جداً واستهلاك منخفض للذاكرة.
  • تمنحك مرونة في الوصول إلى البيانات بالمفاتيح المسطحة أو المتداخلة.
  • تدعم تخصيص delimiter بسهولة.

العيوب

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

مقارنة سريعة بين الطرق الأربع

الطريقة السهولة الأداء استهلاك الذاكرة الاعتماد على مكتبات خارجية
دالة递归 مع list مرتفعة جيد متوسط إلى مرتفع لا
دالة递归 مع generators مرتفعة جيد جداً أفضل من السابقة لا
pandas.json_normalize مرتفعة أبطأ أعلى نعم
flatdict مرتفعة جيد جداً منخفض نعم

كيف تختار الحل المناسب في مشروعك؟

  1. إذا أردت حلاً بسيطاً بلا تبعيات خارجية، فابدأ بالدالة المخصّصة.
  2. إذا كان استهلاك الذاكرة مهماً، فاختر نسخة generators.
  3. إذا كان مشروعك يستخدم pandas بالفعل في معالجة البيانات، فقد يكون json_normalize خياراً منطقياً.
  4. إذا كنت تبحث عن مكتبة متخصصة وخفيفة وجاهزة للإنتاج، فغالباً ستجد ضالتك في flatdict.

الخلاصة التقنية

تسطيح القواميس في Python ليس مجرد عملية تجميلية للبنية، بل خطوة عملية تساعد في المقارنة والمعالجة والتخزين والتحليل. من الناحية التقنية، فإن الحل القائم على generators يحقق توازناً ممتازاً بين البساطة والكفاءة، بينما تمنح مكتبة flatdict مرونة أكبر عند الحاجة إلى حل جاهز ومجرب. أما pandas فتبقى مناسبة فقط عندما تكون ضمن منظومة العمل لديك بالفعل. الاختيار الأفضل هنا لا يعتمد على فكرة واحدة ثابتة، بل على سياق مشروعك وحجم البيانات وطبيعة الاعتمادات البرمجية التي تقبلها.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *