ضمان الكود النظيف: نظرة على بايثون ، ذات معلمات

نشرت: 2022-03-11

في هذا المنشور ، سأتحدث عن ما أعتبره الأسلوب أو النمط الأكثر أهمية في إنتاج كود Pythonic نظيف - أي تحديد المعلمات. هذا المنشور مخصص لك إذا:

  • أنت جديد نسبيًا على شيء أنماط التصميم بالكامل وربما تشعر بالحيرة قليلاً من القوائم الطويلة لأسماء الأنماط ومخططات الفئات. الخبر السار هو أنه يوجد بالفعل نمط تصميم واحد فقط يجب أن تعرفه تمامًا عن Python. والأفضل من ذلك ، أنك ربما تعرف ذلك بالفعل ، ولكن ربما لا تعرف كل الطرق التي يمكن بها تطبيقها.
  • لقد أتيت إلى Python من لغة OOP أخرى مثل Java أو C # وتريد معرفة كيفية ترجمة معرفتك بأنماط التصميم من تلك اللغة إلى Python. في Python واللغات الأخرى المكتوبة ديناميكيًا ، فإن العديد من الأنماط الشائعة في لغات OOP المكتوبة بشكل ثابت "غير مرئية أو أبسط" ، كما قال المؤلف بيتر نورفيج.

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

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

أبسط حالة لبايثون ذات معلمات

بالنسبة لمعظم الأمثلة لدينا ، سنستخدم وحدة سلحفاة المكتبة التعليمية المعيارية لعمل بعض الرسومات.

إليك بعض التعليمات البرمجية التي سترسم مربعًا بحجم 100 × 100 باستخدام turtle :

 from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)

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

 def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)

يمكننا الآن رسم مربعات بأي حجم باستخدام draw_square . هذا كل ما في الأسلوب الأساسي للمعلمات ، وقد رأينا للتو أول استخدام رئيسي - التخلص من برمجة النسخ واللصق.

مشكلة فورية في الكود أعلاه هي أن draw_square يعتمد على متغير عالمي. هذا له الكثير من العواقب السيئة ، وهناك طريقتان سهلتان لإصلاحه. الأول سيكون لـ draw_square إنشاء نسخة Turtle نفسها (والتي سأناقشها لاحقًا). قد لا يكون هذا مرغوبًا إذا أردنا استخدام Turtle واحدة لجميع رسوماتنا. حتى الآن ، سنستخدم المعلمات مرة أخرى لنجعل turtle معلمة لـ draw_square :

 from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)

هذا له اسم مزخرف - حقن التبعية. هذا يعني فقط أنه إذا احتاجت الوظيفة إلى نوع من الكائن للقيام بعملها ، مثل draw_square يحتاج إلى Turtle ، فإن المتصل مسؤول عن تمرير هذا الكائن كمعامل. لا ، حقًا ، إذا كنت تشعر بالفضول حيال إدخال تبعية Python ، فهذا كل شيء.

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

أي شيء هو كائن

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

  • مثيلات من الأنواع المضمنة ، مثل السلسلة "I'm a string" والعدد الصحيح 42 أو القاموس
  • مثيلات من أنواع وفئات أخرى ، على سبيل المثال ، كائن datetime.datetime
  • الوظائف والطرق
  • الأنواع المضمنة والفئات المخصصة

الأخيران هما الأكثر إثارة للدهشة ، خاصة إذا كنت قادمًا من لغات أخرى ، ويحتاجان إلى مزيد من المناقشة.

وظائف كمعلمات

تقوم جملة الدالة في بايثون بأمرين:

  1. يقوم بإنشاء كائن دالة.
  2. يقوم بإنشاء اسم في النطاق المحلي يشير إلى هذا الكائن.

يمكننا اللعب بهذه العناصر في REPL:

 > >> def foo(): ... return "Hello from foo" > >> > >> foo() 'Hello from foo' > >> print(foo) <function foo at 0x7fc233d706a8> > >> type(foo) <class 'function'> > >> foo.name 'foo'

ومثل جميع الكائنات ، يمكننا إسناد وظائف لمتغيرات أخرى:

 > >> bar = foo > >> bar() 'Hello from foo'

لاحظ أن bar هو اسم آخر لنفس الكائن ، لذلك له نفس خاصية __name__ الداخلية كما في السابق:

 > >> bar.name 'foo' > >> bar <function foo at 0x7fc233d706a8>

لكن النقطة الحاسمة هي أنه نظرًا لأن الوظائف هي مجرد كائنات ، في أي مكان ترى فيه وظيفة يتم استخدامها ، فقد تكون معلمة.

لذا ، لنفترض أننا قمنا بتوسيع وظيفة الرسم المربّع أعلاه ، والآن عندما نرسم مربعات في بعض الأحيان ، نريد أن نتوقف مؤقتًا عند كل زاوية - استدعاء إلى time.sleep() .

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

ومع ذلك ، اكتشفنا لاحقًا أننا في بعض الأحيان نريد فعلاً فعل شيء مختلف تمامًا في الزوايا. ربما نريد رسم شكل آخر في كل زاوية ، وتغيير لون القلم ، وما إلى ذلك. قد نميل إلى إضافة المزيد من المعلمات ، واحدة لكل شيء نحتاج إلى القيام به. ومع ذلك ، فإن أفضل حل هو السماح بتمرير أي وظيفة كإجراء يجب اتخاذه. بالنسبة للإعداد الافتراضي ، سنقوم بعمل وظيفة لا تفعل شيئًا. سنجعل هذه الوظيفة أيضًا تقبل المعلمات المحلية turtle size ، في حالة طلبها:

 def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)

أو يمكننا القيام بشيء أكثر برودة مثل رسم مربعات أصغر بشكل متكرر في كل زاوية:

 def smaller_square(turtle, size): if size < 10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square) 

رسم توضيحي لمربعات أصغر مرسومة بشكل متكرر كما هو موضح في كود معلمات بيثون أعلاه

هناك ، بالطبع ، اختلافات في هذا. في العديد من الأمثلة ، سيتم استخدام القيمة المرجعة للدالة. هنا ، لدينا أسلوب أكثر إلحاحًا في البرمجة ، ويتم استدعاء الوظيفة فقط لآثارها الجانبية.

بلغات أخرى ...

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

يتمثل أحد الحلول في تحويل draw_square إلى فئة ، SquareDrawer :

 class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass

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

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

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

 class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())

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

العناصر الأخرى القابلة للاستدعاء

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

افترض أن لدينا قائمة foo :

 foo = [1, 2, 3]

يحتوي foo الآن على مجموعة كاملة من الطرق المرفقة به ، مثل .append() و .count() . يمكن تمرير هذه "الطرق المقيدة" واستخدامها مثل الوظائف:

 > >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]

بالإضافة إلى عمليات المثيل هذه ، هناك أنواع أخرى من الكائنات القابلة للاستدعاء - الأساليب الثابتة وطرق classmethods ، ومثيلات الفئات التي تنفذ staticmethods ، __call__ / الأنواع نفسها.

الفئات كمعلمات

في بايثون ، تعتبر الفئات "من الدرجة الأولى" - فهي كائنات وقت التشغيل تمامًا مثل الإملاء ، والسلاسل ، وما إلى ذلك. قد يبدو هذا أكثر غرابة من كون الوظائف كائنات ، ولكن لحسن الحظ ، من الأسهل في الواقع إثبات هذه الحقيقة أكثر من الوظائف.

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

 class Foo: pass Foo = type('Foo', (), {})

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

  1. على الجانب الأيمن من علامة يساوي ، أنشأنا فصلًا جديدًا ، باسم داخلي هو Foo . هذا هو الاسم الذي ستحصل عليه إذا قمت بعمل Foo.__name__ .
  2. باستخدام المهمة ، أنشأنا اسمًا في النطاق الحالي ، Foo ، والذي يشير إلى كائن الفئة الذي أنشأناه للتو.

لقد قدمنا ​​نفس الملاحظات لما يفعله بيان الوظيفة.

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

يمكننا تقسيم ذلك إلى عدد من الاستخدامات:

فئات كمصانع

الفئة هي كائن قابل للاستدعاء يُنشئ مثيلًا لنفسه:

 > >> class Foo: ... pass > >> Foo() <__main__.Foo at 0x7f73e0c96780>

وككائن ، يمكن تخصيصه لمتغيرات أخرى:

 > >> myclass = Foo > >> myclass() <__main__.Foo at 0x7f73e0ca93c8>

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

 def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

ومع ذلك ، لدينا الآن مشكلة التخصيص. افترض أن المتصل أراد تعيين بعض سمات السلحفاة أو استخدم نوعًا مختلفًا من السلاحف التي لها نفس الواجهة ولكن لديها بعض السلوك الخاص؟

يمكننا حل هذا عن طريق حقن التبعية ، كما فعلنا من قبل - سيكون المتصل مسؤولاً عن إعداد كائن Turtle . ولكن ماذا لو كانت وظيفتنا تحتاج أحيانًا إلى صنع العديد من السلاحف لأغراض الرسم المختلفة ، أو إذا كانت تريد إطلاق أربعة خيوط ، لكل منها سلحفاة خاصة بها لرسم جانب واحد من المربع؟ الجواب ببساطة هو جعل فئة Turtle معلمة للوظيفة. يمكننا استخدام وسيطة الكلمات الرئيسية بقيمة افتراضية ، لتبسيط الأمور للمتصلين الذين لا يهتمون:

 def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

لاستخدام هذا ، يمكننا كتابة دالة make_turtle تُنشئ سلحفاة وتعديلها. لنفترض أننا نريد إخفاء السلحفاة عند رسم المربعات:

 def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)

أو يمكننا تصنيف Turtle فرعية لجعل هذا السلوك مدمجًا وتمرير الفئة الفرعية كمعامل:

 class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)

بلغات أخرى ...

تفتقر العديد من لغات OOP الأخرى ، مثل Java و C # ، إلى فصول من الدرجة الأولى. لإنشاء فئة ، يجب عليك استخدام الكلمة الأساسية new متبوعة باسم فئة فعلي.

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

الفئات كفئة أساسية

لنفترض أننا وجدنا أنفسنا ننشئ فئات فرعية لإضافة نفس الميزة إلى فئات مختلفة. على سبيل المثال ، نريد فئة Turtle فرعية تكتب في السجل عند إنشائها:

 import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Turtle got created")

ولكن بعد ذلك ، نجد أنفسنا نفعل الشيء نفسه بالضبط مع فصل آخر:

 class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("Hippo got created")

الأشياء الوحيدة التي تختلف بين هذين الأمرين هي:

  1. الطبقة الأساسية
  2. اسم الفئة الفرعية - لكننا لا نهتم بذلك حقًا ويمكن أن ننشئها تلقائيًا من الصنف الأساسي __name__ .
  3. الاسم المستخدم داخل استدعاء debug — ولكن مرة أخرى ، يمكننا إنشاء هذا من اسم الفئة الأساسية.

في مواجهة اثنين من أجزاء التعليمات البرمجية المتشابهة جدًا مع متغير واحد فقط ، ماذا يمكننا أن نفعل؟ تمامًا كما في مثالنا الأول ، نقوم بإنشاء دالة وسحب الجزء المتغير كمعامل:

 def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug("{0} got created".format(cls.__name__)) LoggingThing.__name__ = "Logging{0}".format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)

هنا ، لدينا عرض لفصول الدرجة الأولى:

  • لقد مررنا فئة إلى دالة ، مع إعطاء المعلمة اسمًا تقليديًا cls لتجنب التعارض مع class الكلمات الرئيسية (سترى أيضًا class_ و klass لهذا الغرض).
  • داخل الوظيفة ، أنشأنا فئة — لاحظ أن كل استدعاء لهذه الوظيفة ينشئ فئة جديدة .
  • أعدنا هذه الفئة كقيمة إرجاع للدالة.

قمنا أيضًا بتعيين LoggingThing.__name__ وهو اختياري تمامًا ولكن يمكن أن يساعد في تصحيح الأخطاء.

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

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

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

الفئات كاستثناءات

هناك مكان آخر ترى فيه الفئات المستخدمة في عبارة " except " في عبارة try / except / final. لا توجد مفاجآت في التخمين أنه يمكننا تحديد معايير هذه الفئات أيضًا.

على سبيل المثال ، تطبق الكود التالي إستراتيجية عامة جدًا لمحاولة إجراء قد يفشل وإعادة المحاولة مع التراجع الأسي حتى الوصول إلى الحد الأقصى لعدد المحاولات:

 import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)

لقد سحبنا كلاً من الإجراء الذي يجب اتخاذه والاستثناءات التي يجب تحديدها كمعلمات. يمكن أن تكون المعلمة exceptions_to_catch to_catch إما فئة واحدة ، مثل IOError أو httplib.client.HTTPConnectionError أو مجموعة من هذه الفئات. (نريد تجنب عبارات "bare except" أو حتى except Exception لأن هذا معروف بإخفاء أخطاء البرمجة الأخرى).

تحذيرات وخاتمة

تعد المعلمات تقنية قوية لإعادة استخدام التعليمات البرمجية وتقليل تكرار الكود. لا يخلو من بعض العيوب. في السعي وراء إعادة استخدام الكود ، غالبًا ما تظهر العديد من المشكلات:

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

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

في هذا المنشور ، قمنا بتغطية أنماط التصميم المعروفة باسم حقن التبعية ، والاستراتيجية ، وطريقة القالب ، والمصنع المجرد ، وطريقة المصنع ، والديكور . في Python ، يتضح أن العديد من هذه العناصر عبارة عن تطبيق بسيط للمعلمات أو أصبحت بالتأكيد غير ضرورية بسبب حقيقة أن المعلمات في Python يمكن أن تكون كائنات أو فئات قابلة للاستدعاء. نأمل أن يساعد هذا في تخفيف العبء المفاهيمي "للأشياء التي من المفترض أن تعرفها كمطور Python حقيقي" ويمكّنك من كتابة كود Python مختصر!

قراءة متعمقة:

  • أنماط تصميم Python: للحصول على كود أنيق وعصري
  • أنماط بايثون: لأنماط تصميم بيثون
  • تسجيل بيثون: برنامج تعليمي متعمق