منابع ترجمه · Think Python · فصل ۱۶

کلاس‌ها و شیءها (فصل ۱۶ Think Python)

۱۸ دقیقه مطالعه · به‌روزرسانی ۲ آبان ۱۴۰۵

منبع اصلی: Think Python, Chapter 16: Classes and Objects — Allen B. Downey

💡 این صفحه مثل یک نوت‌بوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلول‌های بعدی همین صفحه در دسترس است. سلول‌ها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلول‌های قبلیِ اجرانشده را یک‌جا اجرا کنید.

توضیح‌ها:

  • خروجی‌های زیر کدها را با اجرای همین کدها به دست آورده‌ام (نوت‌بوک اصلی خروجی ذخیره‌شده ندارد).
  • نمودارهای شیء و شکل‌هایی که با ماژول jupyturtle رسم می‌شوند در این فایل نیامده‌اند؛ به‌جای آن‌ها توضیح کوتاهی نوشته‌ام. کد ساخت نمودارها (ماژول diagram) هم حذف شده چون جزو آموزش نیست.
  • دستور %%add_method_to فقط در Jupyter کار می‌کند و متد را به کلاسی که قبلاً تعریف شده اضافه می‌کند. در برنامه‌ی عادی، همه‌ی متدها باید داخل تعریف کلاس بیایند.

تا اینجا کلاس‌هایی تعریف کرده‌ایم و شیءهایی ساخته‌ایم که زمان روز و روز سال را نشان می‌دهند. همچنین متدهایی تعریف کرده‌ایم که این شیءها را می‌سازند، تغییر می‌دهند و با آن‌ها محاسبه می‌کنند.

در این فصل گشت‌وگذارمان در برنامه‌نویسی شیءگرا (OOP) را ادامه می‌دهیم و کلاس‌هایی تعریف می‌کنیم که شیءهای هندسی را نشان می‌دهند، از جمله نقطه، خط، مستطیل و دایره. متدهایی می‌نویسیم که این شیءها را می‌سازند و تغییر می‌دهند، و از ماژول jupyturtle برای رسم آن‌ها استفاده می‌کنیم.

از این کلاس‌ها برای نمایش مباحث OOP استفاده می‌کنم، از جمله هویت و هم‌ارزی شیءها، کپی سطحی و عمیق، و چندریختی.

۱۶.۱. ساختن یک نقطه (Point)

در گرافیک کامپیوتری، یک مکان روی صفحه اغلب با یک جفت مختصات در صفحه‌ی x-y نشان داده می‌شود. طبق قرارداد، نقطه‌ی (0, 0) معمولاً گوشه‌ی بالا-چپ صفحه است و (x, y) نقطه‌ای است که x واحد به راست و y واحد پایین‌تر از مبدأ است. نسبت به دستگاه مختصات دکارتی که ممکن است در کلاس ریاضی دیده باشید، محور y وارونه است.

چند راه برای نمایش یک نقطه در پایتون وجود دارد:

  • می‌توانیم مختصات را جداگانه در دو متغیر x و y ذخیره کنیم.

  • می‌توانیم مختصات را به‌عنوان عناصر یک فهرست یا چندتایی ذخیره کنیم.

  • می‌توانیم یک نوع جدید بسازیم تا نقطه‌ها را به‌صورت شیء نشان دهد.

در برنامه‌نویسی شیءگرا، ساختن نوع جدید متداول‌ترین راه است. برای این کار با تعریف کلاس Point شروع می‌کنیم.

class Point:
    """Represents a point in 2-D space."""
    
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
    def __str__(self):
        return f'Point({self.x}, {self.y})'

متد __init__ مختصات را به‌عنوان پارامتر می‌گیرد و به صفت‌های x و y نسبت می‌دهد. متد __str__ یک نمایش رشته‌ای از Point برمی‌گرداند.

حالا می‌توانیم یک شیء Point را این‌طور نمونه‌سازی و نمایش دهیم.

start = Point(0, 0)
print(start)
Point(0, 0)

(در کتاب اصلی اینجا نمودار حالت شیء رسم شده است: جعبه‌ای با برچسب Point که دو صفت x و y با مقدار ۰ دارد و متغیر start به آن اشاره می‌کند.)

مثل همیشه، یک نوع تعریف‌شده توسط برنامه‌نویس با جعبه‌ای نشان داده می‌شود که نام نوع بیرون و صفت‌ها داخل آن‌اند.

به‌طور کلی، نوع‌های تعریف‌شده توسط برنامه‌نویس تغییرپذیرند، پس می‌توانیم متدی مثل translate بنویسیم که دو عدد dx و dy را می‌گیرد و به صفت‌های x و y اضافه می‌کند.

%%add_method_to Point

    def translate(self, dx, dy):
        self.x += dx
        self.y += dy

این تابع Point را از یک مکان در صفحه به مکان دیگر می‌برد (انتقال می‌دهد). اگر نخواهیم Point موجود را تغییر دهیم، می‌توانیم از copy برای کپی کردن شیء اصلی و بعد تغییر دادن کپی استفاده کنیم.

from copy import copy

end1 = copy(start)
end1.translate(300, 0)
print(end1)
Point(300, 0)

می‌توانیم این مراحل را در متد دیگری به نام translated کپسوله کنیم.

%%add_method_to Point

    def translated(self, dx=0, dy=0):
        point = copy(self)
        point.translate(dx, dy)
        return point

همان‌طور که متد sort یک فهرست را تغییر می‌دهد و تابع sorted فهرست جدیدی می‌سازد، حالا هم متد translate را داریم که یک Point را تغییر می‌دهد و متد translated که یکی جدید می‌سازد.

این یک مثال است:

end2 = start.translated(0, 150)
print(end2)
Point(0, 150)

در بخش بعد، از این نقطه‌ها برای تعریف و رسم یک خط استفاده می‌کنیم.

۱۶.۲. ساختن یک خط (Line)

حالا کلاسی تعریف می‌کنیم که پاره‌خط بین دو نقطه را نشان می‌دهد. مثل همیشه با یک متد __init__ و یک متد __str__ شروع می‌کنیم.

class Line:
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2
        
    def __str__(self):
        return f'Line({self.p1}, {self.p2})'

با این دو متد می‌توانیم یک شیء Line را نمونه‌سازی و نمایش دهیم که برای نمایش محور x به کار می‌بریم.

line1 = Line(start, end1)
print(line1)
Line(Point(0, 0), Point(300, 0))

وقتی print را فراخوانی می‌کنیم و line را به‌عنوان پارامتر می‌دهیم، print متد __str__ را روی line احضار می‌کند. متد __str__ از یک f-string برای ساختن نمایش رشته‌ای line استفاده می‌کند.

f-string دو عبارت داخل آکولاد دارد، self.p1 و self.p2. وقتی این عبارت‌ها ارزیابی می‌شوند، نتیجه شیءهای Point است. بعد، وقتی به رشته تبدیل می‌شوند، متد __str__ از کلاس Point احضار می‌شود.

به همین دلیل، وقتی یک Line را نمایش می‌دهیم، نتیجه شامل نمایش رشته‌ای شیءهای Point است.

(نمودار شیء زیر در کتاب اصلی حالت این شیء Line را نشان می‌دهد: جعبه‌ی Line که دو صفت p1 و p2 دارد، و هر کدام به یک جعبه‌ی Point اشاره می‌کنند؛ اولی x=0, y=0 و دومی x=300, y=0.)

نمایش‌های رشته‌ای و نمودارهای شیء برای اشکال‌زدایی مفیدند، اما هدف این مثال تولید گرافیک است، نه متن! پس از ماژول jupyturtle برای رسم خط‌ها روی صفحه استفاده می‌کنیم.

همان‌طور که در فصل ۴ کردیم، از make_turtle برای ساختن یک شیء Turtle و یک بوم کوچک استفاده می‌کنیم تا روی آن بکشد. برای کشیدن خط‌ها از دو تابع جدید ماژول jupyturtle استفاده می‌کنیم:

  • jumpto، که دو مختصات می‌گیرد و Turtle را بدون کشیدن خط به مکان داده‌شده می‌برد، و

  • moveto، که Turtle را از مکان فعلی‌اش به مکان داده‌شده می‌برد و بین آن‌ها یک پاره‌خط می‌کشد.

این‌طور وارد‌شان می‌کنیم.

from jupyturtle import make_turtle, jumpto, moveto

و این متدی است که یک Line می‌کشد.

%%add_method_to Line

    def draw(self):
        jumpto(self.p1.x, self.p1.y)
        moveto(self.p2.x, self.p2.y)

برای نشان دادن طرز استفاده، خط دومی می‌سازم که محور y را نشان می‌دهد.

line2 = Line(start, end2)
print(line2)
Line(Point(0, 0), Point(0, 150))

و بعد محورها را می‌کشیم.

make_turtle()
line1.draw()
line2.draw()

(در کتاب اصلی اینجا شکل رسم شده است: یک خط افقی به طول ۳۰۰ و یک خط عمودی به طول ۱۵۰ که از گوشه‌ی بالا-چپ بوم شروع می‌شوند.)

همین‌طور که شیءهای بیشتری تعریف و رسم می‌کنیم، دوباره از این خط‌ها استفاده می‌کنیم. اما اول درباره‌ی هم‌ارزی و هویت شیءها صحبت کنیم.

۱۶.۳. هم‌ارزی و هویت

فرض کنید دو نقطه با مختصات یکسان بسازیم.

p1 = Point(200, 100)
p2 = Point(200, 100)

اگر از عملگر == برای مقایسه‌شان استفاده کنیم، رفتار پیش‌فرض نوع‌های تعریف‌شده توسط برنامه‌نویس را می‌گیریم – نتیجه فقط وقتی True است که هر دو یک شیء باشند، که نیستند.

p1 == p2
False

اگر بخواهیم این رفتار را تغییر دهیم، می‌توانیم متد ویژه‌ای به نام __eq__ فراهم کنیم که تعریف می‌کند برابر بودن دو شیء Point یعنی چه.

%%add_method_to Point

def __eq__(self, other):
    return (self.x == other.x) and (self.y == other.y)

این تعریف دو Point را وقتی برابر می‌داند که صفت‌هایشان برابر باشد. حالا وقتی از عملگر == استفاده می‌کنیم، متد __eq__ احضار می‌شود، که نشان می‌دهد p1 و p2 برابر در نظر گرفته می‌شوند.

p1 == p2
True

اما عملگر is هنوز نشان می‌دهد که شیءهای متفاوتی هستند.

p1 is p2
False

بازنویسی عملگر is ممکن نیست – این عملگر همیشه بررسی می‌کند که شیءها یکسان (identical) هستند یا نه. اما برای نوع‌های تعریف‌شده توسط برنامه‌نویس، می‌توانید عملگر == را بازنویسی کنید تا بررسی کند که شیءها هم‌ارز (equivalent) هستند یا نه. و می‌توانید تعریف کنید که هم‌ارز یعنی چه.

۱۶.۴. ساختن یک مستطیل (Rectangle)

حالا کلاسی تعریف می‌کنیم که مستطیل‌ها را نشان می‌دهد و می‌کشد. برای ساده ماندن، فرض می‌کنیم مستطیل‌ها یا عمودی‌اند یا افقی، نه کج. به نظرتان چه صفت‌هایی را برای مشخص کردن مکان و اندازه‌ی یک مستطیل باید به کار ببریم؟

دست‌کم دو امکان وجود دارد:

  • می‌توانید عرض و ارتفاع مستطیل و مکان یک گوشه را مشخص کنید.

  • می‌توانید دو گوشه‌ی مقابل را مشخص کنید.

در این مرحله سخت است بگوییم کدام بهتر است، پس اولی را پیاده‌سازی می‌کنیم. این هم تعریف کلاس.

class Rectangle:
    """Represents a rectangle. 

    attributes: width, height, corner.
    """
    def __init__(self, width, height, corner):
        self.width = width
        self.height = height
        self.corner = corner
        
    def __str__(self):
        return f'Rectangle({self.width}, {self.height}, {self.corner})'

مثل همیشه، متد __init__ پارامترها را به صفت‌ها نسبت می‌دهد و __str__ یک نمایش رشته‌ای از شیء برمی‌گرداند. حالا می‌توانیم یک شیء Rectangle نمونه‌سازی کنیم و یک Point را به‌عنوان مکان گوشه‌ی بالا-چپ به کار ببریم.

corner = Point(30, 20)
box1 = Rectangle(100, 50, corner)
print(box1)
Rectangle(100, 50, Point(30, 20))

(نمودار کتاب حالت این شیء را نشان می‌دهد: جعبه‌ی Rectangle با صفت‌های width=100 و height=50، و صفت corner که به یک جعبه‌ی Point با x=30 و y=20 اشاره می‌کند.)

برای کشیدن یک مستطیل، از متد زیر استفاده می‌کنیم که چهار شیء Point برای گوشه‌ها می‌سازد.

%%add_method_to Rectangle

    def make_points(self):
        p1 = self.corner
        p2 = p1.translated(self.width, 0)
        p3 = p2.translated(0, self.height)
        p4 = p3.translated(-self.width, 0)
        return p1, p2, p3, p4

بعد چهار شیء Line برای ضلع‌ها می‌سازیم.

%%add_method_to Rectangle

    def make_lines(self):
        p1, p2, p3, p4 = self.make_points()
        return Line(p1, p2), Line(p2, p3), Line(p3, p4), Line(p4, p1)

و بعد ضلع‌ها را می‌کشیم.

%%add_method_to Rectangle

    def draw(self):
        lines = self.make_lines()
        for line in lines:
            line.draw()

این یک مثال است.

make_turtle()
line1.draw()
line2.draw()
box1.draw()

(در کتاب اصلی اینجا شکل رسم شده است: دو محور و یک مستطیل ۱۰۰×۵۰ که گوشه‌ی بالا-چپ آن در نقطه‌ی (۳۰، ۲۰) است.)

شکل شامل دو خط است که محورها را نشان می‌دهند.

۱۶.۵. تغییر دادن مستطیل‌ها

حالا دو متد را بررسی می‌کنیم که مستطیل‌ها را تغییر می‌دهند، grow و translate. خواهیم دید که grow همان‌طور که انتظار می‌رود کار می‌کند، اما translate یک باگ ظریف دارد. پیش از اینکه توضیح بدهم، ببینید می‌توانید آن را پیدا کنید.

grow دو عدد dwidth و dheight می‌گیرد و آن‌ها را به صفت‌های width و height مستطیل اضافه می‌کند.

%%add_method_to Rectangle

    def grow(self, dwidth, dheight):
        self.width += dwidth
        self.height += dheight

این مثالی است که با کپی کردن box1 و احضار grow روی کپی، اثر آن را نشان می‌دهد.

box2 = copy(box1)
box2.grow(60, 40)
print(box2)
Rectangle(160, 90, Point(30, 20))

اگر box1 و box2 را بکشیم، می‌توانیم تأیید کنیم که grow همان‌طور که انتظار می‌رود کار می‌کند.

make_turtle()
line1.draw()
line2.draw()
box1.draw()
box2.draw()

(شکل: دو مستطیل تودرتو با گوشه‌ی بالا-چپ مشترک؛ مستطیل بزرگ‌تر ۱۶۰×۹۰ و کوچک‌تر ۱۰۰×۵۰ است.)

حالا translate را ببینیم. این متد دو عدد dx و dy می‌گیرد و مستطیل را به اندازه‌ی داده‌شده در جهت‌های x و y جابه‌جا می‌کند.

%%add_method_to Rectangle

    def translate(self, dx, dy):
        self.corner.translate(dx, dy)

برای نشان دادن اثر، box2 را به راست و پایین منتقل می‌کنیم.

box2.translate(30, 20)
print(box2)
Rectangle(160, 90, Point(60, 40))

حالا ببینیم اگر دوباره box1 و box2 را بکشیم چه می‌شود.

make_turtle()
line1.draw()
line2.draw()
box1.draw()
box2.draw()

(شکل: این بار مستطیل‌ها هر دو جابه‌جا شده‌اند و باز گوشه‌ی بالا-چپ مشترک دارند، این بار در نقطه‌ی (۶۰، ۴۰).)

به نظر می‌رسد هر دو مستطیل جابه‌جا شدند، که چیزی نیست که قصد داشتیم! بخش بعد توضیح می‌دهد چه اشتباهی رخ داد.

۱۶.۶. کپی عمیق (Deep copy)

وقتی از copy برای تکثیر box1 استفاده می‌کنیم، شیء Rectangle را کپی می‌کند اما شیء Point داخل آن را نه. پس box1 و box2 شیءهای متفاوتی هستند، همان‌طور که می‌خواستیم.

box1 is box2
False

اما صفت‌های corner آن‌ها به یک شیء اشاره می‌کنند.

box1.corner is box2.corner
True

(نمودار کتاب حالت این شیءها را نشان می‌دهد: دو جعبه‌ی Rectangle مجزا، هر کدام با width و height خودش، اما صفت corner هر دو به همان یک جعبه‌ی Point اشاره می‌کند.)

کاری که copy می‌کند کپی سطحی (shallow copy) نامیده می‌شود چون شیء را کپی می‌کند اما شیءهایی را که در بر دارد نه. در نتیجه، تغییر width یا height یک Rectangle روی دیگری اثر نمی‌گذارد، اما تغییر صفت‌های Point مشترک روی هر دو اثر می‌گذارد! این رفتار گیج‌کننده و مستعد خطا است.

خوشبختانه ماژول copy تابع دیگری به نام deepcopy دارد که نه‌تنها شیء، بلکه شیءهایی را که به آن‌ها اشاره می‌کند، و شیءهایی را که آن‌ها به آن‌ها اشاره می‌کنند و همین‌طور الی آخر، کپی می‌کند. این عملیات کپی عمیق (deep copy) نامیده می‌شود.

برای نمایش، با یک Rectangle تازه شروع می‌کنیم که یک Point تازه دارد.

corner = Point(20, 20)
box3 = Rectangle(100, 50, corner)
print(box3)
Rectangle(100, 50, Point(20, 20))

و یک کپی عمیق می‌سازیم.

from copy import deepcopy

box4 = deepcopy(box3)

می‌توانیم تأیید کنیم که این دو شیء Rectangle به شیءهای Point متفاوتی اشاره می‌کنند.

box3.corner is box4.corner
False

چون box3 و box4 شیءهای کاملاً جدا از هم هستند، می‌توانیم یکی را بدون اثر روی دیگری تغییر دهیم. برای نمایش، box3 را جابه‌جا و box4 را بزرگ می‌کنیم.

box3.translate(50, 30)
box4.grow(100, 60)

و می‌توانیم تأیید کنیم که اثر همان است که انتظار داریم.

make_turtle()
line1.draw()
line2.draw()
box3.draw()
box4.draw()

(شکل: box3 با گوشه‌ی (۷۰، ۵۰) و اندازه‌ی ۱۰۰×۵۰، و box4 با گوشه‌ی (۲۰، ۲۰) و اندازه‌ی ۲۰۰×۱۱۰ رسم شده‌اند. حالا هر دو مستقل از هم‌اند.)

برای اطمینان، مقدارها این‌ها هستند:

print(box3)
print(box4)
Rectangle(100, 50, Point(70, 50))
Rectangle(200, 110, Point(20, 20))

۱۶.۷. چندریختی (Polymorphism)

در مثال قبل، متد draw را روی دو شیء Line و دو شیء Rectangle احضار کردیم. می‌توانیم همین کار را با ساختن فهرستی از شیءها مختصرتر انجام دهیم.

shapes = [line1, line2, box3, box4]

عناصر این فهرست از نوع‌های متفاوتی‌اند، اما همه متد draw دارند، پس می‌توانیم فهرست را پیمایش و روی هر یک draw را احضار کنیم.

make_turtle()

for shape in shapes:
    shape.draw()

بار اول و دوم که حلقه اجرا می‌شود، shape به یک شیء Line اشاره می‌کند، پس وقتی draw احضار می‌شود، متدی که اجرا می‌شود همانی است که در کلاس Line تعریف شده است.

بار سوم و چهارم، shape به یک شیء Rectangle اشاره می‌کند، پس وقتی draw احضار می‌شود، متدی که اجرا می‌شود همانی است که در کلاس Rectangle تعریف شده است.

به یک معنا، هر شیء می‌داند چطور خودش را بکشد. این ویژگی چندریختی (polymorphism) نامیده می‌شود. این واژه از ریشه‌های یونانی‌ای می‌آید که به معنای «دارای شکل‌های بسیار» است. در برنامه‌نویسی شیءگرا، چندریختی توانایی نوع‌های متفاوت برای ارائه‌ی متدهای یکسان است، که انجام محاسبه‌های بسیار – مثل کشیدن شکل‌ها – را با احضار یک متد یکسان روی نوع‌های متفاوتی از شیءها ممکن می‌کند.

در تمرینی در پایان این فصل، کلاس جدیدی تعریف می‌کنید که یک دایره را نشان می‌دهد و متد draw دارد. بعد می‌توانید با چندریختی خط‌ها، مستطیل‌ها و دایره‌ها را بکشید.

۱۶.۸. اشکال‌زدایی

در این فصل با باگ ظریفی روبه‌رو شدیم که چون یک Point ساختیم که بین دو شیء Rectangle مشترک بود و بعد Point را تغییر دادیم رخ داد. به‌طور کلی دو راه برای پرهیز از مشکل‌هایی مثل این وجود دارد: می‌توانید از به اشتراک گذاشتن شیءها پرهیز کنید، یا از تغییر دادن آن‌ها.

برای پرهیز از اشتراک شیءها، می‌توانید از کپی عمیق استفاده کنید، همان‌طور که در این فصل کردیم.

برای پرهیز از تغییر شیءها، می‌توانید به جای توابع ناخالص مثل translate از توابع خالص مثل translated استفاده کنید. برای مثال، این نسخه‌ای از translated است که یک Point جدید می‌سازد و هرگز صفت‌هایش را تغییر نمی‌دهد.

    def translated(self, dx=0, dy=0):
        x = self.x + dx
        y = self.y + dy
        return Point(x, y)

پایتون امکاناتی دارد که پرهیز از تغییر دادن شیءها را آسان‌تر می‌کند. این امکانات خارج از حوصله‌ی این کتاب‌اند، اما اگر کنجکاو هستید، از یک دستیار مجازی بپرسید: “How do I make a Python object immutable?” (چطور یک شیء پایتون را تغییرناپذیر کنم؟)

ساختن شیء جدید زمان بیشتری از تغییر شیء موجود می‌برد، اما این تفاوت به‌ندرت در عمل اهمیت دارد. برنامه‌هایی که از شیءهای مشترک و توابع ناخالص پرهیز می‌کنند اغلب توسعه، آزمون و اشکال‌زدایی‌شان آسان‌تر است – و بهترین نوع اشکال‌زدایی همان است که مجبور نباشید انجامش دهید.

۱۶.۹. واژه‌نامه

کپی سطحی (shallow copy): عملیات کپی که شیءهای تودرتو را کپی نمی‌کند.

کپی عمیق (deep copy): عملیات کپی که شیءهای تودرتو را هم کپی می‌کند.

چندریختی (polymorphism): توانایی یک متد یا عملگر برای کار با چند نوع شیء.

۱۶.۱۰. تمرین‌ها

# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.

%xmode Verbose

از یک دستیار مجازی بپرسید

برای همه‌ی تمرین‌های زیر، از یک دستیار مجازی کمک بگیرید. اگر این کار را می‌کنید، تعریف کلاس‌های Point، Line و Rectangle را در متن درخواست بگنجانید – وگرنه دستیار درباره‌ی صفت‌ها و توابع آن‌ها حدس می‌زند و کدی که تولید می‌کند کار نخواهد کرد.

تمرین ۱

برای کلاس Line متد __eq__ بنویسید که اگر شیءهای Line به شیءهای Point هم‌ارز اشاره کنند – به هر ترتیبی – True برگرداند.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to Line

def __eq__(self, other):
    return None
# Solution goes here

می‌توانید از این مثال‌ها برای آزمودن کدتان استفاده کنید.

start1 = Point(0, 0)
start2 = Point(0, 0)
end = Point(200, 100)

این مثال باید True باشد چون شیءهای Line به شیءهای Point هم‌ارز، به همان ترتیب، اشاره می‌کنند.

line_a = Line(start1, end)
line_b = Line(start2, end)
line_a == line_b    # should be True

این مثال باید True باشد چون شیءهای Line به شیءهای Point هم‌ارز، به ترتیب معکوس، اشاره می‌کنند.

line_c = Line(end, start1)
line_a == line_c     # should be True

هم‌ارزی باید همیشه تعدی‌پذیر (transitive) باشد – یعنی اگر line_a و line_b هم‌ارز باشند، و line_a و line_c هم‌ارز باشند، آنگاه line_b و line_c هم باید هم‌ارز باشند.

line_b == line_c     # should be True

این مثال باید False باشد چون شیءهای Line به شیءهای Point ناهم‌ارز اشاره می‌کنند.

line_d = Line(start1, start2)
line_a == line_d    # should be False

تمرین ۲

برای Line متدی به نام midpoint بنویسید که نقطه‌ی میانی یک پاره‌خط را حساب کند و نتیجه را به‌صورت شیء Point برگرداند.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to Line

    def midpoint(self):
        return Point(0, 0)
# Solution goes here

می‌توانید از مثال‌های زیر برای آزمودن کدتان و رسم نتیجه استفاده کنید.

start = Point(0, 0)
end1 = Point(300, 0)
end2 = Point(0, 150)
line1 = Line(start, end1)
line2 = Line(start, end2)
mid1 = line1.midpoint()
print(mid1)
Point(150.0, 0.0)
mid2 = line2.midpoint()
print(mid2)
Point(0.0, 75.0)
line3 = Line(mid1, mid2)
make_turtle()

for shape in [line1, line2, line3]:
    shape.draw()

(خروجی‌های بالا مربوط به یک راه‌حل درست‌اند که من اجرا کردم؛ شکل باید دو محور و یک خط مورب بین نقطه‌های میانی آن‌ها باشد.)

تمرین ۳

برای Rectangle متدی به نام midpoint بنویسید که نقطه‌ی مرکزی یک مستطیل را پیدا کند و نتیجه را به‌صورت شیء Point برگرداند.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to Rectangle

    def midpoint(self):
        return Point(0, 0)
# Solution goes here

می‌توانید از مثال زیر برای آزمودن کدتان استفاده کنید.

corner = Point(30, 20)
rectangle = Rectangle(100, 80, corner)
mid = rectangle.midpoint()
print(mid)
Point(80.0, 60.0)
diagonal = Line(corner, mid)
make_turtle()

for shape in [line1, line2, rectangle, diagonal]:
    shape.draw()

تمرین ۴

برای Rectangle متدی به نام make_cross بنویسید که:

  1. از make_lines برای گرفتن فهرستی از شیءهای Line که چهار ضلع مستطیل را نشان می‌دهند استفاده کند.

  2. نقطه‌های میانی چهار خط را حساب کند.

  3. فهرستی از دو شیء Line بسازد و برگرداند که نقطه‌های میانی مقابل را به هم وصل می‌کنند و یک صلیب از میان مستطیل می‌سازند.

می‌توانید از این طرح اولیه شروع کنید.

%%add_method_to Rectangle

    def make_diagonals(self):
        return []
# Solution goes here

می‌توانید از مثال زیر برای آزمودن کدتان استفاده کنید.

corner = Point(30, 20)
rectangle = Rectangle(100, 80, corner)
lines = rectangle.make_cross()
make_turtle()

rectangle.draw()
for line in lines:
    line.draw()

تمرین ۵

تعریف کلاسی به نام Circle بنویسید که صفت‌های center و radius دارد، که center یک شیء Point و radius یک عدد است. متدهای ویژه‌ی __init__ و __str__ و متدی به نام draw را که با توابع jupyturtle دایره را می‌کشد در آن بگنجانید.

می‌توانید از تابع زیر استفاده کنید، که نسخه‌ای از تابع circle است که در فصل ۴ نوشتیم.

from jupyturtle import make_turtle, forward, left, right
import math
    
def draw_circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    angle = 360 / n
    left(angle / 2)
    for i in range(n):
        forward(length)
        left(angle)
# Solution goes here

می‌توانید از مثال زیر برای آزمودن کدتان استفاده کنید. با یک Rectangle مربعی با عرض و ارتفاع 100 شروع می‌کنیم.

corner = Point(20, 20)
rectangle = Rectangle(100, 100, corner)

کد زیر باید Circleی بسازد که داخل مربع جا می‌شود.

center = rectangle.midpoint()
radius = rectangle.height / 2

circle = Circle(center, radius)
print(circle)
Circle(Point(70.0, 70.0), 50.0)

اگر همه‌چیز درست کار کرده باشد، کد زیر باید دایره را داخل مربع بکشد (به هر چهار ضلع مماس).

make_turtle(delay=0.01)

rectangle.draw()
circle.draw()