أمثلة الإجراءات: التغليف والتعميم وإعادة الهيكلة

ليكون التطبيق أقرب للحس والبصر، سنطبق مفاهيم الدوال باستعمال مكتبة jupyturtle؛ وهي ترسم “سحلفاة” وتحركها في لوحة بالطريقة التي نريد تحريكها فيه.

uv add jupyturtle
from jupyturtle import (
    make_turtle,
    Turtle,
)

أمر الإنشاء:

t = make_turtle()
'

ثم أمر التحريك:

t.forward(20)

أو الميل:

t.left(90)

ثم التراجع

t.back(40)

ويمكنك معرفة جميع ما تستطيع عمله بهذه السلحفاة بإجراء المساعدة:

help(t)
Help on Turtle in module jupyturtle.jupyturtle object:

class Turtle(builtins.object)
 |  Turtle(
 |      *,
 |      animate=True,
 |      delay: float | None = None,
 |      drawing: jupyturtle.jupyturtle.Drawing | None = None
 |  )
 |
 |  Methods defined here:
 |
 |  __enter__(self)
 |
 |  __exit__(self, exc_type, exc_value, traceback)
 |
 |  __init__(
 |      self,
 |      *,
 |      animate=True,
 |      delay: float | None = None,
 |      drawing: jupyturtle.jupyturtle.Drawing | None = None
 |  )
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  back(self, units: float)
 |      Move the turtle backward by units, drawing if the pen is down.
 |
 |  draw(self)
 |
 |  forward(self, units: float, degrees: float = 0)
 |      Move turtle forward by units; draw path if pen is down.
 |      If `degrees` is given, turn left after moving.
 |
 |  get_SVG(self)
 |
 |  hide(self)
 |      Hide turtle. It will still leave trail if the pen is down.
 |
 |  jump_to(self, x: float, y: float)
 |      Teleport the turtle to coordinates (x, y) without drawing.
 |
 |  left(self, degrees: float)
 |      Turn turtle left by degrees.
 |
 |  move_to(self, x: float, y: float)
 |      Move the turtle to coordinates (x, y), drawing if the pen is down.
 |
 |  pen_down(self)
 |      Lower the pen, so turtle starts drawing.
 |
 |  pen_up(self)
 |      Lift the pen, so turtle stops drawing.
 |
 |  right(self, degrees: float)
 |      Turn turtle right by degrees.
 |
 |  show(self)
 |      Show turtle.
 |
 |  toggle_pen(self)
 |      Lower the pen if it's up; raises if it's down.
 |
 |  ----------------------------------------------------------------------
 |  Readonly properties defined here:
 |
 |  x
 |
 |  y
 |
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |
 |  __dict__
 |      dictionary for instance variables
 |
 |  __weakref__
 |      list of weak references to the object
 |
 |  delay
 |
 |  heading
 |
 |  pen_color
 |
 |  pen_width
Member Category Action / Description
forward(units, degrees) Method Moves forward; optionally turns left.
back(units) Method Moves backward.
left(degrees) Method Rotates turtle counter-clockwise.
right(degrees) Method Rotates turtle clockwise.
move_to(x, y) Method Moves to specific coordinates; draws if pen is down.
jump_to(x, y) Method Teleports to coordinates without drawing.
pen_down() Method Lowers the pen to start drawing.
pen_up() Method Lifts the pen to stop drawing.
toggle_pen() Method Flips the pen state between up and down.
show() / hide() Method Toggles visibility of the turtle icon.
draw() Method Renders the drawing.
get_SVG() Method Exports the drawing as SVG data.
x, y Property Current position coordinates (Read-only).
heading Descriptor Current direction/angle of the turtle.
pen_color Descriptor Sets or gets the drawing color.
pen_width Descriptor Sets or gets the line thickness.
delay Descriptor Controls the animation speed.

رسم مربع

t = make_turtle()

t.forward(50)
t.left(90)

t.forward(50)
t.left(90)

t.forward(50)
t.left(90)

t.forward(50)
t.left(90)
'

نلاحظ تكرار أمري التقدم والدوران، فنستطيع استعمال حلقة التكرار:

t = make_turtle()

for i in range(4):
    t.forward(50)
    t.left(90)
'

يُسمى وضع الكود داخل دالة بـ التغليف (Encapsulation). ميزته منح الكود اسماً للتوثيق، واختصار تكراره؛ فاستدعاء الدالة أوجز من نسخ محتواها يدوياً.

حالياً، حجم المربع ثابت (50). ولرسم أحجام متنوعة، نجعل طول الضلع مُعامِلاً (Parameter) للدالة.

def square(t: Turtle, length: float):
    for i in range(4):
        t.forward(length)
        t.left(90)
t = make_turtle()
square(t, 20)
square(t, 50)
'

يُسمى إضافة مُعامِل للدالة التعميم (Generalization)؛ لأنه يجعلها أكثر شمولية. فبينما كانت النسخة السابقة تلتزم بحجم ثابت، تمنحك هذه النسخة حرية اختيار أي حجم.

وبإضافة مُعامِل آخر، نصل لمستوى أعلى من التعميم؛ حيث ترسم الدالة التالية مضلعات منتظمة بأي عدد من الأضلاع.

def polygon(t: Turtle, n: int, length: float):
    angle = 360 / n
    for i in range(n):
        t.forward(length)
        t.left(angle)

والغرض من تعريف angle بقسمة 360 على عدد الأضلاع n هو جعلها تكمل دورة كاملة.

ولاحظ أننا الآن نستطيع مثلث أو مربع أو خماسي أو سداسي وما فوق ذلك، فقط بتغيير عدد الأضلاع:

t = make_turtle()
polygon(t, n=3, length=60)
'
t = make_turtle()
polygon(t, n=4, length=60)
'
t = make_turtle()
polygon(t, n=5, length=40)
'

لنرسم دائرة، يمكننا محاكاتها برسم مضلع بأضلاع كثيرة وصغيرة جداً. تستخدم الدالة التالية polygon لرسم مضلع من 30 ضلعاً لتمثيل الدائرة تقريبياً.

تستقبل الدالة circle نصف القطر (radius) كمُعامِل، وتحسب المحيط (circumference)، ثم تقسمه على عدد الأضلاع n لتعيين طول كل ضلع.

import math

def circle(t: Turtle, radius: float):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(t, n, length)

ولتفادي البطء، نستخدم الوسيط delay في make_turtle لضبط سرعة السلحفاة؛ فتقليل القيمة من 0.2 إلى 0.02 ثانية يسرّع العملية بمقدار 10 أضعاف تقريباً.

t = make_turtle(
    delay=0.02, # speed up animation
)
circle(t, 30)
'

من عيوب هذا الحل ثبات قيمة n؛ فالدوائر الكبيرة تظهر أضلاعها طويلة جداً، والصغيرة تُهدر الوقت في رسم أضلاع قصيرة للغاية. يمكننا تعميم الدالة بجعل n مُعامِلاً، لكن لنبقي الأمر بسيطاً حالياً.

إعادة الهيكلة (Refactoring)

سنكتب نسخة أكثر تعميماً من circle تُسمى arc (القوس)، تأخذ مُعامِلاً ثانياً هو الزاوية angle. فإذا كانت 360 درجة رسمت دائرة كاملة، وإذا كانت 180 رسمت نصف دائرة.

استخدمنا سابقاً polygon لتقريب الدائرة، لكنه لا يصلح لرسم القوس. بدلاً من ذلك، سننشئ نسخة أعمّ من المضلع تُدعى polyline (المسار المتعدد).

def polyline(t: Turtle, n: int, length: float, angle: float):
    for i in range(n):
        t.forward(length)
        t.left(angle)

تستقبل polyline مُعامِلات هي: عدد القطع المستقيمة n وطولها length والزاوية بينها angle. وبذلك، يمكننا إعادة كتابة polygon بالاعتماد على polyline.

def polygon(t: Turtle, n: int, length: float):
    angle = 360.0 / n
    polyline(t, n, length, angle)

ويمكننا استخدام polyline لبرمجة arc.

def arc(t: Turtle, radius: float, angle: float):
    arc_length = 2 * math.pi * radius * angle / 360
    n = 30
    length = arc_length / n
    step_angle = angle / n
    polyline(t, n, length, step_angle)

تتشابه arc مع circle في آلية العمل، لكنها تحسب طول القوس (arc_length) كجزء من محيط الدائرة. وفي الختام، يمكننا إعادة كتابة circle باستخدام arc.

def circle(t: Turtle, radius: float ):
    arc(t, radius, 360)

بدأنا بكود يعمل ثم أعدنا تنظيمه؛ تُسمى عملية تحسين الكود مع الحفاظ على سلوكه بـ إعادة الهيكلة (Refactoring).

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

t = make_turtle(delay=0.02)
polygon(t, n=6, length=20)
arc(t, radius=70, angle=70)
circle(t, radius=10)
'