أن تستمر. تبدأ في بيثون كحالة C ++ النهائية. الجزء 1/2 ".
المتغيرات وأنواع البيانات
الآن وبعد أن توصلنا في النهاية إلى الرياضيات ، فلنقرر ما يجب أن تعني المتغيرات في لغتنا.
في C ++ ، للمبرمج خيار: استخدام المتغيرات التلقائية الموضوعة على المكدس ، أو الاحتفاظ بالقيم في ذاكرة بيانات البرنامج ، ووضع مؤشرات فقط على هذه القيم على المكدس. ماذا لو اخترنا واحدًا فقط من هذه الخيارات لبيثون؟
بالطبع ، لا يمكننا دائمًا استخدام قيم المتغيرات فقط ، نظرًا لأن هياكل البيانات الكبيرة لن تتلاءم مع الحزمة ، أو أن حركتها المستمرة في المجموعة ستؤدي إلى مشاكل في الأداء. لذلك ، سوف نستخدم المؤشرات فقط في Python. وهذا تبسيط مفهوم اللغة.
لذلك التعبير
a = 3
يعني أننا أنشأنا كائنًا "3" في ذاكرة بيانات البرنامج (ما يسمى "الكومة") وجعلنا الاسم "a" مرجعًا له. والتعبير
b = a
في هذه الحالة ، سيعني ذلك أننا أجبرنا المتغير "b" على الإشارة إلى نفس الكائن في الذاكرة الذي يشير إليه "a" ، بمعنى آخر ، قمنا بنسخ المؤشر.
إذا كان كل شيء مؤشرًا ، فما عدد أنواع القوائم التي نحتاج إلى تنفيذها بلغتنا؟ بالطبع ، واحد فقط هو قائمة المؤشرات! يمكنك استخدامه لتخزين أعداد صحيحة ، سلاسل ، قوائم أخرى ، أيا كان - بعد كل شيء ، هذه هي مؤشرات.
كم عدد أنواع جداول التجزئة التي نحتاج إلى تنفيذها؟ (في بيثون ، يسمى هذا النوع "القاموس" - dict
.) واحد! دعه يربط المؤشرات بالمفاتيح مع المؤشرات للقيم.
وبالتالي ، لا نحتاج إلى تطبيق جزء كبير من مواصفات C ++ بلغتنا ، حيث إننا نقوم بجميع العمليات على الكائنات ، ويمكن الوصول إلى الكائنات دائمًا عن طريق المؤشر. بالطبع ، ليس من الضروري أن تقتصر البرامج المكتوبة في Python على العمل مع المؤشرات: فهناك مكتبات مثل NumPy تساعد العلماء على التعامل مع صفائف من البيانات في الذاكرة ، كما هو الحال في Fortran. لكن أساس اللغة - التعبيرات مثل "a = 3" - يعمل دائمًا مع المؤشرات.
يبسط مفهوم "كل شيء مؤشر" أيضًا تكوين الأنواع إلى الحد الأقصى. نريد قائمة من القواميس؟ مجرد إنشاء قائمة ووضع القواميس هناك! لا تحتاج إلى طلب إذن من بيثون ، لا تحتاج إلى الإعلان عن أنواع إضافية ، كل شيء يعمل خارج الصندوق.
ولكن ماذا لو أردنا استخدام الكائنات المركبة كمفاتيح؟ يجب أن يكون للمفتاح في القاموس قيمة ثابتة ، وإلا كيف يمكن البحث عن القيم به؟ القوائم قابلة للتغيير ، وبالتالي لا يمكن استخدامها بهذه الصفة. في مثل هذه المواقف ، يحتوي Python على نوع بيانات ، مثل القائمة ، عبارة عن سلسلة من الكائنات ، ولكن على عكس القائمة ، لا يتغير هذا التسلسل. يُطلق على هذا النوع اسم tuple أو tuple
(يُطلق عليه "tuple" أو "tuple").
يحل التلاميذ في بايثون مشكلة لغة البرمجة النصية القديمة. إذا لم تكن معجبًا بهذه الميزة ، فمن المحتمل أنك لم تحاول أبدًا استخدام لغات البرمجة النصية للعمل الجاد مع البيانات ، حيث يمكنك فقط استخدام سلاسل أو أنواع بدائية فقط كمفتاح في جداول التجزئة.
الاحتمال الآخر الذي يعطينا tuples هو إرجاع عدة قيم من دالة دون الحاجة إلى إعلان أنواع بيانات إضافية لهذا ، كما يجب عليك القيام به في C و C ++. علاوة على ذلك ، لتسهيل استخدام هذه الميزة ، تم تزويد مشغل المهمة بالقدرة على تفكيك tuples تلقائيًا إلى متغيرات منفصلة.
def get_address(): ... return host, port host, port = get_address()
يحتوي فك الحزم على العديد من الآثار الجانبية المفيدة ، على سبيل المثال ، يمكن كتابة تبادل القيم المتغيرة على النحو التالي:
x, y = y, x
كل شيء مؤشر ، مما يعني أنه يمكن استخدام الوظائف وأنواع البيانات كبيانات. إذا كنت على دراية بكتاب "Design Patterns" من قبل مؤلفي "The Gang of Four" ، فيجب أن تتذكر الأساليب المعقدة والمربكة التي يقدمها من أجل تحديد نوع الكائن الذي تم إنشاؤه بواسطة البرنامج في وقت التشغيل. في الواقع ، في العديد من لغات البرمجة من الصعب القيام بذلك! في Python ، تختفي كل هذه الصعوبات ، لأننا نعلم أن الوظيفة يمكنها إرجاع نوع البيانات ، وأن كلا من الوظائف وأنواع البيانات ليست سوى روابط ، ويمكن تخزين الروابط ، على سبيل المثال ، في القواميس. هذا يبسط المهمة إلى الحد الأقصى.
وقال ديفيد ويلر: "يتم حل جميع مشاكل البرمجة من خلال خلق مستوى إضافي من عدم الاتصال." استخدام الارتباطات في Python هو مستوى غير مباشر تم استخدامه تقليديًا لحل العديد من المشكلات في العديد من اللغات ، بما في ذلك C ++. لكن إذا تم استخدامه بشكل صريح هناك ، وهذا يعقد البرامج ، فسيتم استخدامه في Python بشكل ضمني وموحد فيما يتعلق بالبيانات من جميع الأنواع ، ويكون سهل الاستخدام.
ولكن إذا كان كل شيء رابطًا ، فما الذي تشير إليه هذه الروابط؟ لغات مثل C ++ لها أنواع عديدة. دعونا نترك في Python نوع بيانات واحد فقط - كائن! المتخصصون في مجال نظرية الكتابة يهزون رؤوسهم برفض ، لكنني أعتقد أن نوع بيانات مصدر واحد ، والتي تستمد منها جميع الأنواع الأخرى في اللغة ، فكرة جيدة تضمن توحيد اللغة وسهولة استخدامها.
بالنسبة لمحتويات الذاكرة المحددة ، يمكن لمختلف تطبيقات Python (PyPy أو Jython أو MicroPython) إدارة الذاكرة بطرق مختلفة. ولكن من أجل فهم أفضل لكيفية تنفيذ بساطة Python وتوحيده ، لتشكيل النموذج العقلي الصحيح ، من الأفضل أن ننتقل إلى تنفيذ مرجع Python في C يسمى CPython ، والذي يمكننا تنزيله على python.org .
struct { struct _typeobject *ob_type; }
ما سنراه في شفرة مصدر CPython هو هيكل يتكون من مؤشر إلى معلومات حول نوع متغير معين وحمولة محددة تحدد القيمة المحددة للمتغير.
كيف تعمل معلومات النوع؟ دعونا حفر في شفرة المصدر CPython مرة أخرى.
struct _typeobject { getattrfunc tp_getattr; setattrfunc tp_setattr; newfunc tp_new; freefunc tp_free; binaryfunc nb_add; binaryfunc nb_subtract; richcmpfunc tp_richcompare; }
نرى مؤشرات إلى الوظائف التي توفر جميع العمليات الممكنة لنوع معين: الجمع والطرح والمقارنة والوصول إلى السمات والفهرسة والتقطيع ، وما إلى ذلك. تعرف هذه العمليات كيفية التعامل مع الحمولة النافعة الموجودة في الذاكرة أسفل مؤشر لكتابة المعلومات ، سواء كان عددًا صحيحًا أو سلسلة أو كائنًا من نوع أنشأه المستخدم.
هذا يختلف اختلافًا جذريًا عن C و C ++ ، حيث ترتبط معلومات النوع بالأسماء ، وليس قيم المتغيرات. في Python ، ترتبط جميع الأسماء بالروابط. القيمة حسب المرجع ، بدورها ، من النوع. هذا هو جوهر اللغات الديناميكية.
لتحقيق كل ميزات اللغة ، يكفي أن نحدد عمليتين على الروابط. واحدة من أكثر وضوحا هو النسخ. عندما نقوم بتعيين قيمة إلى متغير أو فتحة في قاموس أو سمة لكائن ما ، نقوم بنسخ الارتباطات. هذه عملية بسيطة وسريعة وآمنة تمامًا: لا يؤدي نسخ الارتباطات إلى تغيير محتويات الكائن.
العملية الثانية هي وظيفة أو استدعاء الأسلوب. كما أوضحنا أعلاه ، لا يمكن لبرنامج Python التفاعل مع الذاكرة إلا من خلال الأساليب المطبقة في الكائنات المدمجة. لذلك ، لا يمكن أن يسبب خطأ يتعلق بالوصول إلى الذاكرة.
قد يكون لديك سؤال: إذا كانت جميع المتغيرات تحتوي على مراجع ، فكيف يمكنني حماية قيمة المتغير من التغييرات عن طريق تمريره إلى الدالة كمعلمة؟
n = 3 some_function(n)
الجواب هو أن الأنواع البسيطة في بيثون غير قابلة للتغيير: إنها ببساطة لا تطبق الطريقة المسؤولة عن تغيير قيمتها. توفر int
(غير الثابتة) int
أو float
أو tuple
أو str
بلغات مثل "كل شيء مؤشر" نفس التأثير الدلالي الذي توفره المتغيرات التلقائية في C.
تعمل الأنواع والأساليب الموحدة على تبسيط استخدام البرمجة المعممة ، أو الأدوية العامة ، قدر الإمكان. الدالات min()
و max()
و sum()
وما شابهها مدمجة ، ليست هناك حاجة لاستيرادها. وهم يعملون مع أي من أنواع البيانات التي يتم فيها تنفيذ عمليات المقارنة لـ min()
و max()
، والإضافات لـ sum()
، إلخ.
إنشاء كائنات
وجدنا بعبارات عامة كيف ينبغي أن تتصرف الأشياء. الآن سوف نحدد كيف سنقوم بإنشائها. هذه مسألة بناء جملة لغوية. يدعم C ++ ثلاث طرق على الأقل لإنشاء كائن:
- تلقائي ، بإعلان متغير من هذه الفئة:
my_class c(arg);
- باستخدام المشغل
new
:
my_class *c = new my_class(arg);
- مصنع ، عن طريق استدعاء وظيفة تعسفية تقوم بإرجاع مؤشر:
my_class *c = my_factory(arg);
كما قد تكون خمنت بالفعل ، بعد أن درست طريقة تفكير المبدعين من بيثون في الأمثلة المذكورة أعلاه ، يجب علينا الآن اختيار واحد منهم.
من نفس كتاب The Gangs of Four ، علمنا أن المصنع هو أكثر الطرق مرونة وشمولية لإنشاء الأشياء. لذلك ، يتم تنفيذ هذه الطريقة فقط في بيثون.
بالإضافة إلى العالمية ، تعد هذه الطريقة جيدة بحيث لا تحتاج إلى زيادة تحميل اللغة باستخدام بناء الجملة غير الضروري للتأكد من ذلك: تم بالفعل استدعاء دالة في لغتنا ، والمصنع ليس أكثر من دالة.
قاعدة أخرى لإنشاء كائنات في بيثون هي: أي نوع بيانات هو مصنعه الخاص. بالطبع ، يمكنك كتابة أي عدد من المصانع الإضافية المخصصة (والتي ستكون وظائف أو طرق عادية ، بالطبع) ، ولكن القاعدة العامة ستظل سارية:
تسمى جميع الأنواع كائنات ، وكلها تُرجع قيمًا من نوعها ، تحددها الوسائط التي تم تمريرها في المكالمة.
وبالتالي ، باستخدام الصيغة الأساسية للغة فقط ، يمكن تغليف أي معالجة عند إنشاء كائنات ، مثل أنماط "Arena" أو "Adaptation" ، نظرًا لأن فكرة رائعة أخرى مستعارة من C ++ هي أن النوع نفسه يحدد كيفية حدوثه تفرخ الأشياء ، وكيف يعمل المشغل new
بالنسبة له.
ماذا عن NULL؟
معالجة مؤشر لاغية يضيف تعقيد إلى البرنامج ، لذلك نحن تحظر NULL. بناء جملة Python يجعل من المستحيل إنشاء مؤشر فارغ. يتم تعريف عمليتين أساسيتين على المؤشرات ، والتي تحدثنا عنها سابقًا ، بحيث يشير أي متغير إلى كائن ما.
نتيجة لذلك ، لا يمكن للمستخدم استخدام Python لإنشاء خطأ يتعلق بالوصول إلى الذاكرة ، مثل خطأ تجزئة أو خارج حدود المخزن المؤقت. بمعنى آخر ، لا تتأثر برامج Python بأخطر نوعين من نقاط الضعف التي تهدد أمان الإنترنت على مدار العشرين عامًا الماضية.
قد تسأل: "إذا كان هيكل العمليات على الكائنات لم يتغير ، كما رأينا سابقًا ، فكيف سينشئ المستخدمون فصولهم الخاصة ، مع الأساليب والسمات غير المدرجة في هذه البنية؟"
يكمن السحر في حقيقة أن Python للفئات المخصصة "إعداد" بسيط للغاية مع عدد قليل من الأساليب المطبقة. فيما يلي أهمها:
struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; newfunc tp_new; }
ينشئ tp_new()
جدول تجزئة لفئة المستخدم ، كما هو الحال بالنسبة لنوع dict
. tp_getattr()
شيئًا من جدول التجزئة هذا ، و tp_setattr()
، على العكس ، يضع شيئًا هناك. وبالتالي ، يتم توفير قدرة الطبقات التعسفية على تخزين أي أساليب وسمات ليس على مستوى بنية اللغة C ، ولكن على مستوى أعلى - جدول التجزئة. (بالطبع ، باستثناء بعض الحالات المتعلقة بتحسين الأداء.)
معدلات الوصول
ماذا نفعل مع كل تلك القواعد والمفاهيم المبنية حول C ++ الكلمات الأساسية private
protected
؟ بيثون ، كونها لغة البرمجة ، لا تحتاج إليها. لدينا بالفعل أجزاء "محمية" من اللغة - هذه بيانات عن أنواع مدمجة. لن يسمح Python بأي حال من الأحوال لأي برنامج ، على سبيل المثال ، بمعالجة أجزاء رقم الفاصلة العائمة! هذا المستوى من التغليف يكفي للحفاظ على سلامة اللغة نفسها. نحن ، مبدعو Python ، نعتقد أن تكامل اللغة هو الذريعة الجيدة الوحيدة لإخفاء المعلومات. جميع الهياكل الأخرى وبيانات برنامج المستخدم تعتبر عامة.
يمكنك كتابة تسطير سفلي ( _
) في بداية اسم سمة الفصل لتحذير زميل له: يجب ألا تعتمد على هذه السمة. لكن بقية بيثون تعلمت دروس أوائل التسعينيات: ثم اعتقد الكثيرون أن السبب الرئيسي وراء قيامنا بكتابة برامج منتفخة وغير قابلة للقراءة وعربات التي تجرها الدواب هو عدم وجود متغيرات خاصة. أعتقد أن السنوات العشرين المقبلة قد أقنعت الجميع في صناعة البرمجة: المتغيرات الخاصة ليست هي الوحيدة ، وبعيدًا عن العلاج الأكثر فعالية للبرامج المتضخمة والعربات التي تجرها الدواب. لذلك ، قرر مبدعو Python ألا يقلقوا حتى بشأن المتغيرات الخاصة ، وكما ترون ، لم يفشلوا.
إدارة الذاكرة
ماذا يحدث لأجسامنا والأرقام والسلاسل في مستوى أقل؟ كيف يتم تخزينها بالضبط في الذاكرة ، وكيف يوفر CPython الوصول المشترك إليها ومتى وتحت أي ظروف يتم تدميرها؟
وفي هذه الحالة ، اخترنا الطريقة الأكثر عمومية وقابلية للتنبؤ والإنتاجية للعمل مع الذاكرة: من جانب البرنامج C ، كل الكائنات هي مؤشرات مشتركة .
مع وضع هذه المعرفة في الاعتبار ، ينبغي استكمال هياكل البيانات التي درسناها مسبقًا في قسم "المتغيرات وأنواع البيانات" على النحو التالي:
struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; } }
لذلك ، كل كائن في بيثون (نعني تنفيذ CPython ، بالطبع) لديه عداد مرجع خاص به. بمجرد أن يصبح صفرا ، يمكن حذف الكائن.
لا تعتمد آلية عد الارتباط على حسابات إضافية أو عمليات خلفية - يمكن إتلاف كائن على الفور. بالإضافة إلى ذلك ، فإنه يوفر موقع البيانات عالية: في كثير من الأحيان ، يبدأ استخدام الذاكرة مرة أخرى فور تحريرها. على الأرجح تم استخدام الكائن الذي تم إتلافه مؤخرًا ، مما يعني أنه كان في ذاكرة التخزين المؤقت للمعالج. لذلك ، سيبقى الكائن المنشأ حديثًا في ذاكرة التخزين المؤقت. هذان العاملان - البساطة والمكان - يجعلان من ارتباط طريقة مثمرة للغاية لجمع القمامة.
(نظرًا لأن الكائنات الموجودة في البرامج الحقيقية غالباً ما تشير إلى بعضها البعض ، فإن عداد المرجع في بعض الحالات لا يمكن أن ينخفض إلى الصفر حتى في حالة عدم استخدام الكائنات في البرنامج. لذلك ، يحتوي CPython أيضًا على آلية ثانية لجمع البيانات المهملة - خلفية واحدة ، بناءً على على أجيال من الأشياء - تقريبا الترجمة. )
أخطاء مطور بايثون
حاولنا تطوير لغة تكون بسيطة بما يكفي للمبتدئين ، ولكنها جذابة أيضًا بدرجة كافية للمحترفين. في الوقت نفسه ، لم نتمكن من تجنب الأخطاء في فهم واستخدام الأدوات التي أنشأناها نحن أنفسنا.
حاول Python 2 ، بسبب الجمود في التفكير المرتبط بلغات البرمجة النصية ، تحويل أنواع السلاسل ، كما تفعل اللغة ذات الكتابة الضعيفة. إذا حاولت دمج سلسلة بايت مع سلسلة في Unicode ، يقوم المترجم ضمنياً بتحويل سلسلة البايت إلى Unicode باستخدام جدول الرموز المتاح على النظام ويعرض النتيجة في Unicode:
>>> 'byte string ' + u'unicode string' u'byte string unicode string'
نتيجةً لذلك ، عملت بعض مواقع الويب جيدًا أثناء استخدام مستخدميها للغة الإنجليزية ، لكنهم تسببوا في أخطاء مشفرة عند استخدام أحرف من الحروف الهجائية الأخرى.
تم إصلاح خطأ تصميم اللغة هذا في Python 3:
>>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str
كان هناك خطأ مشابه في بيثون 2 يتعلق بالفرز "الساذج" للقوائم التي تتكون من عناصر لا تضاهى:
>>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b']
يوضح Python 3 في هذه الحالة للمستخدم أنه يحاول فعل شيء غير ذي معنى:
>>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str()
انتهاكات
يستخدم المستخدمون من حين لآخر أحيانًا الطبيعة الديناميكية للغة بيثون ، ثم ، في التسعينيات ، عندما لم تكن أفضل الممارسات معروفة على نطاق واسع ، حدث هذا غالبًا خاصة:
class Address(object): def __init__(self, host, port): self.host = host self.port = port
"ولكن هذا ليس الأمثل!" - قال البعض: "ماذا لو كان المنفذ لا يختلف عن القيمة الافتراضية؟ على أي حال ، فإننا ننفق سمة فئة كاملة على تخزينها! " والنتيجة شيء من هذا القبيل
class Address(object): def __init__(self, host, port=None): self.host = host if port is not None:
لذلك ، تظهر كائنات من نفس النوع في البرنامج ، ومع ذلك ، لا يمكن تشغيلها بشكل موحد ، لأن بعضها لديه سمة معينة ، بينما البعض الآخر لا! ولا يمكننا لمس هذه السمة دون التحقق من وجودها مقدمًا:
حاليًا ، تعد وفرة hasattr()
و isinstance()
والتأمل الآخر علامة أكيدة على التعليمات البرمجية السيئة ، ويعتبر أفضل ممارسة لجعل السمات موجودة دائمًا في الكائن. يوفر هذا بناء جملة أبسط عند الوصول إليه:
لذلك ، انتهت التجارب المبكرة مع السمات المضافة والمُحذوفة ديناميكيًا ، والآن ننظر إلى الفئات في Python بنفس طريقة C ++.
كانت العادة السيئة الأخرى لبايثون المبكرة استخدام الوظائف التي يمكن أن يكون للحجة أنواع مختلفة تمامًا. على سبيل المثال ، قد تعتقد أنه قد يكون من الصعب للغاية على المستخدم إنشاء قائمة بأسماء الأعمدة في كل مرة ، ويجب أن تسمح له بتمريرها أيضًا كسطر واحد ، حيث تكون أسماء الأعمدة الفردية مفصولة بفاصلة:
class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns
ولكن هذا النهج يمكن أن تؤدي إلى مشاكلها. على سبيل المثال ، ماذا لو قدم لنا مستخدم عن طريق الخطأ صفًا غير مقصود استخدامه كقائمة بأسماء الأعمدة؟ أو إذا كان يجب أن يحتوي اسم العمود على فاصلة؟
أيضًا ، من الصعب الحفاظ على هذا الرمز وتصحيحه وخصوصًا الاختبار: في الاختبارات ، قد يتم التحقق من نوع واحد فقط من النوعين المدعومين من قبلنا ، لكن التغطية ستبقى 100٪ ، ولن نختبر النوع الآخر.
نتيجة لذلك ، توصلنا إلى استنتاج مفاده أن Python يسمح للمستخدم بتمرير وسيطات من أي نوع إلى وظائف ، ولكن معظمها في معظم الحالات ستستخدم دالة بنفس الطريقة التي تستخدمها في C: تمرير وسيطة من نفس النوع إليها.
تعتبر الحاجة إلى استخدام eval()
في البرنامج خطأً معماريًا صريحًا. على الأرجح ، أنت فقط لم تحدد كيفية القيام بنفس الشيء بطريقة طبيعية. − , Jupyter notebook - − eval()
, Python ! , C++ .
, ( getattr()
, hasattr()
, isinstance()
) . , , , , : , , , !
: , . 20 , C++ Python. , , . .
, shared_ptr
TensorFlow 2016 2018 .
TensorFlow − C++-, Python- ( C++ − TensorFlow, ).

TensorFlow, shared_ptr
, . , .
C++? . , ? , , C++ Python!