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

کلاس‌ها و متدها (فصل ۱۵ Think Python)

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

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

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

توضیح: خروجی‌های نمایش‌داده‌شده‌ی زیر کدها را با اجرای همین کدها به دست آورده‌ام. در نوت‌بوک اصلی کتاب، متدها یکی‌یکی با دستور ویژه‌ی %%add_method_to Time به کلاس Time اضافه می‌شوند تا بتوان کلاس را گام‌به‌گام ساخت. این دستور بخشی از پایتون نیست و فقط در نوت‌بوک Jupyter کار می‌کند؛ من آن را همان‌طور که هست نگه داشته‌ام و در توضیح همان بخش آن را شرح داده‌ام. در اجرای عادی پایتون، همه‌ی متدها باید داخل خود تعریف کلاس بیایند.

پایتون یک زبان شیءگرا (object-oriented language) است – یعنی امکاناتی دارد که از برنامه‌نویسی شیءگرا پشتیبانی می‌کند، که این ویژگی‌های اصلی را دارد:

  • بیشتر محاسبه بر حسب عملیات روی شیءها بیان می‌شود.

  • شیءها اغلب چیزهای دنیای واقعی را نشان می‌دهند و متدها اغلب با شیوه‌هایی که چیزهای دنیای واقعی با هم تعامل دارند متناظرند.

  • برنامه‌ها شامل تعریف کلاس و متد هستند.

برای مثال، در فصل قبل کلاس Time را تعریف کردیم که با شیوه‌ی ثبت زمان روز توسط مردم متناظر است، و توابعی تعریف کردیم که با کارهایی که مردم با زمان‌ها می‌کنند متناظرند. اما هیچ ارتباط صریحی بین تعریف کلاس Time و تعریف توابعی که بعد از آن آمدند نبود. می‌توانیم این ارتباط را با بازنویسی یک تابع به‌صورت متد (method) صریح کنیم؛ متد داخل تعریف یک کلاس تعریف می‌شود.

۱۵.۱. تعریف متدها

در فصل قبل کلاسی به نام Time تعریف کردیم و تابعی به نام print_time نوشتیم که یک زمان از روز را نمایش می‌دهد.

class Time:
    """Represents the time of day."""

def print_time(time):
    s = f'{time.hour:02d}:{time.minute:02d}:{time.second:02d}'
    print(s)

برای تبدیل print_time به متد، فقط کافی است تعریف تابع را داخل تعریف کلاس ببریم. به تغییر تورفتگی دقت کنید.

همزمان، نام پارامتر را از time به self تغییر می‌دهیم. این تغییر ضروری نیست، اما قراردادی است که اولین پارامتر یک متد self نامیده شود.

class Time:
    """Represents the time of day."""    

    def print_time(self):
        s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
        print(s)

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

def make_time(hour, minute, second):
    time = Time()
    time.hour = hour
    time.minute = minute
    time.second = second
    return time

و این یک نمونه‌ی Time است.

start = make_time(9, 40, 0)

حالا دو راه برای فراخوانی print_time وجود دارد. راه اول (و کمتر رایج) استفاده از نحو تابعی است.

Time.print_time(start)
09:40:00

در این نسخه، Time نام کلاس، print_time نام متد و start به‌عنوان پارامتر داده شده است. راه دوم (و idiomatic‌تر) استفاده از نحو متدی است:

start.print_time()
09:40:00

در این نسخه، start شیئی است که متد روی آن احضار می‌شود، که گیرنده (receiver) نامیده می‌شود، بر اساس این قیاس که احضار یک متد مثل فرستادن پیام به یک شیء است.

صرف‌نظر از نحو، رفتار متد یکسان است. گیرنده به اولین پارامتر نسبت داده می‌شود، پس داخل متد، self به همان شیئی اشاره می‌کند که start اشاره می‌کند.

۱۵.۲. متد دیگر

این تابع time_to_int از فصل قبل است.

def time_to_int(time):
    minutes = time.hour * 60 + time.minute
    seconds = minutes * 60 + time.second
    return seconds

و این نسخه‌ای است که به‌صورت متد بازنویسی شده.

%%add_method_to Time

    def time_to_int(self):
        minutes = self.hour * 60 + self.minute
        seconds = minutes * 60 + self.second
        return seconds

خط اول از دستور ویژه‌ی add_method_to استفاده می‌کند که متدی را به کلاسی که قبلاً تعریف شده اضافه می‌کند. این دستور در نوت‌بوک Jupyter کار می‌کند، اما بخشی از پایتون نیست، پس در محیط‌های دیگر کار نمی‌کند. معمولاً همه‌ی متدهای یک کلاس داخل تعریف کلاس هستند، پس همزمان با کلاس تعریف می‌شوند. اما برای این کتاب مفید است که متدها را یکی‌یکی تعریف کنیم.

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

start.time_to_int()
34800

معمولاً می‌گویند یک تابع را «فراخوانی» (call) و یک متد را «احضار» (invoke) می‌کنیم، اما معنایشان یکی است.

۱۵.۳. متدهای ایستا (Static methods)

به‌عنوان مثالی دیگر، تابع int_to_time را در نظر بگیرید. این نسخه‌ی فصل قبل است.

def int_to_time(seconds):
    minute, second = divmod(seconds, 60)
    hour, minute = divmod(minute, 60)
    return make_time(hour, minute, second)

این تابع seconds را به‌عنوان پارامتر می‌گیرد و یک شیء Time جدید برمی‌گرداند. اگر آن را به متد کلاس Time تبدیل کنیم، باید آن را روی یک شیء Time احضار کنیم. اما اگر می‌خواهیم یک شیء Time جدید بسازیم، قرار است آن را روی چه چیزی احضار کنیم؟

می‌توانیم این مسئله‌ی «مرغ و تخم‌مرغ» را با متد ایستا (static method) حل کنیم، که متدی است که برای احضار شدن نیازی به نمونه‌ای از کلاس ندارد. این‌طور این تابع را به‌صورت متد ایستا بازنویسی می‌کنیم.

%%add_method_to Time

    def int_to_time(seconds):
        minute, second = divmod(seconds, 60)
        hour, minute = divmod(minute, 60)
        return make_time(hour, minute, second)

چون متد ایستا است، self را به‌عنوان پارامتر ندارد. برای احضارش از Time، یعنی شیء کلاس، استفاده می‌کنیم.

start = Time.int_to_time(34800)

نتیجه شیء جدیدی است که ۹:۴۰ را نشان می‌دهد.

start.print_time()
09:40:00

حالا که Time.int_to_time را داریم، می‌توانیم از آن برای نوشتن add_time به‌صورت متد استفاده کنیم. این تابع فصل قبل است.

def add_time(time, hours, minutes, seconds):
    duration = make_time(hours, minutes, seconds)
    seconds = time_to_int(time) + time_to_int(duration)
    return int_to_time(seconds)

و این نسخه‌ای است که به‌صورت متد بازنویسی شده.

%%add_method_to Time

    def add_time(self, hours, minutes, seconds):
        duration = make_time(hours, minutes, seconds)
        seconds = Time.time_to_int(self) + Time.time_to_int(duration)
        return Time.int_to_time(seconds)

add_time پارامتر self دارد چون متد ایستا نیست. متد معمولی است – که متد نمونه (instance method) هم نامیده می‌شود. برای احضارش به یک نمونه‌ی Time نیاز داریم.

end = start.add_time(1, 32, 0)
end.print_time()
11:12:00

۱۵.۴. مقایسه‌ی شیءهای Time

به‌عنوان یک مثال دیگر، is_after را به‌صورت متد می‌نویسیم. این تابع is_after است، که راه‌حل یکی از تمرین‌های فصل قبل است.

def is_after(t1, t2):
    return time_to_int(t1) > time_to_int(t2)

و این به‌صورت متد است.

%%add_method_to Time

    def is_after(self, other):
        return self.time_to_int() > other.time_to_int()

چون دو شیء را مقایسه می‌کنیم و اولین پارامتر self است، پارامتر دوم را other می‌نامیم. برای استفاده از این متد، باید آن را روی یک شیء احضار کنیم و دیگری را به‌عنوان آرگومان بدهیم.

end.is_after(start)
True

یک چیز خوب در این نحو این است که تقریباً مثل یک پرسش خوانده می‌شود: «end بعد از start است؟»

۱۵.۵. متد __str__

وقتی متدی می‌نویسید، می‌توانید تقریباً هر نامی که می‌خواهید انتخاب کنید. اما بعضی نام‌ها معنای ویژه‌ای دارند. برای مثال، اگر یک شیء متدی به نام __str__ داشته باشد، پایتون از آن متد برای تبدیل شیء به رشته استفاده می‌کند. برای مثال، این یک متد __str__ برای شیء زمان است.

%%add_method_to Time

    def __str__(self):
        s = f'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
        return s

این متد شبیه print_time فصل قبل است، با این تفاوت که رشته را برمی‌گرداند به‌جای اینکه چاپ کند.

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

end.__str__()
'11:12:00'

اما پایتون هم می‌تواند آن را برایتان احضار کند. اگر از تابع داخلی str برای تبدیل یک شیء Time به رشته استفاده کنید، پایتون از متد __str__ در کلاس Time استفاده می‌کند.

str(end)
'11:12:00'

و اگر یک شیء Time را چاپ کنید هم همین کار را می‌کند.

print(end)
11:12:00

متدهایی مثل __str__ متدهای ویژه (special methods) نامیده می‌شوند. می‌توانید آن‌ها را با اینکه نامشان با دو زیرخط شروع و با دو زیرخط تمام می‌شود تشخیص دهید.

۱۵.۶. متد init

ویژه‌ترین متد از میان متدهای ویژه __init__ است، که به این دلیل چنین نامیده می‌شود که صفت‌های یک شیء جدید را مقداردهی اولیه (initialize) می‌کند. متد __init__ برای کلاس Time ممکن است این‌طور باشد:

%%add_method_to Time

    def __init__(self, hour=0, minute=0, second=0):
        self.hour = hour
        self.minute = minute
        self.second = second

حالا وقتی یک شیء Time نمونه‌سازی می‌کنیم، پایتون __init__ را احضار می‌کند و آرگومان‌ها را به آن می‌دهد. پس می‌توانیم همزمان شیء بسازیم و صفت‌ها را مقداردهی اولیه کنیم.

time = Time(9, 40, 0)
print(time)
09:40:00

در این مثال، پارامترها اختیاری‌اند، پس اگر Time را بدون آرگومان فراخوانی کنید، مقدارهای پیش‌فرض را می‌گیرید.

time = Time()
print(time)
00:00:00

اگر یک آرگومان بدهید، hour را بازنویسی می‌کند:

time = Time(9)
print(time)
09:00:00

اگر دو آرگومان بدهید، hour و minute را بازنویسی می‌کنند.

time = Time(9, 45)
print(time)
09:45:00

و اگر سه آرگومان بدهید، هر سه مقدار پیش‌فرض را بازنویسی می‌کنند.

وقتی کلاس جدیدی می‌نویسم، تقریباً همیشه با نوشتن __init__ شروع می‌کنم، که ساختن شیءها را آسان‌تر می‌کند، و __str__، که برای اشکال‌زدایی مفید است.

۱۵.۷. سربارگذاری عملگر (Operator overloading)

با تعریف متدهای ویژه‌ی دیگر، می‌توانید رفتار عملگرها را روی نوع‌های تعریف‌شده توسط برنامه‌نویس مشخص کنید. برای مثال، اگر متدی به نام __add__ برای کلاس Time تعریف کنید، می‌توانید عملگر + را روی شیءهای Time به کار ببرید.

این یک متد __add__ است.

%%add_method_to Time

    def __add__(self, other):
        seconds = self.time_to_int() + other.time_to_int()
        return Time.int_to_time(seconds)

می‌توانیم آن را این‌طور به کار ببریم.

duration = Time(1, 32)
end = start + duration
print(end)
11:12:00

وقتی این سه خط کد را اجرا می‌کنیم، اتفاق‌های زیادی می‌افتد:

  • وقتی یک شیء Time را نمونه‌سازی می‌کنیم، متد __init__ احضار می‌شود.

  • وقتی عملگر + را با یک شیء Time به کار می‌بریم، متد __add__ آن احضار می‌شود.

  • و وقتی یک شیء Time را چاپ می‌کنیم، متد __str__ آن احضار می‌شود.

تغییر رفتار یک عملگر طوری که با نوع‌های تعریف‌شده توسط برنامه‌نویس کار کند سربارگذاری عملگر (operator overloading) نامیده می‌شود. برای هر عملگر، مثل +، یک متد ویژه‌ی متناظر وجود دارد، مثل __add__.

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

یک شیء Time معتبر است اگر مقدارهای minute و second بین 0 و 60 باشند – با احتساب 0 اما بدون احتساب 60 – و اگر hour مثبت باشد. همچنین hour و minute باید مقدار صحیح باشند، اما ممکن است اجازه دهیم second بخش کسری داشته باشد. نیازمندی‌هایی مثل این پایا (invariant) نامیده می‌شوند چون باید همیشه درست باشند. به عبارت دیگر، اگر درست نباشند، چیزی اشتباه شده است.

نوشتن کد برای بررسی پایاها می‌تواند به شناسایی خطاها و پیدا کردن علتشان کمک کند. برای مثال، ممکن است متدی مثل is_valid داشته باشید که یک شیء Time را بگیرد و اگر پایایی را نقض کند False برگرداند.

%%add_method_to Time

    def is_valid(self):
        if self.hour < 0 or self.minute < 0 or self.second < 0:
            return False
        if self.minute >= 60 or self.second >= 60:
            return False
        if not isinstance(self.hour, int):
            return False
        if not isinstance(self.minute, int):
            return False
        return True

بعد، در ابتدای هر متد می‌توانید آرگومان‌ها را بررسی کنید تا مطمئن شوید معتبرند.

%%add_method_to Time

    def is_after(self, other):
        assert self.is_valid(), 'self is not a valid Time'
        assert other.is_valid(), 'other is not a valid Time'
        return self.time_to_int() > other.time_to_int()

دستور assert عبارت بعد از خود را ارزیابی می‌کند. اگر نتیجه True باشد، کاری نمی‌کند؛ اگر نتیجه False باشد، باعث AssertionError می‌شود. این یک مثال است.

duration = Time(minute=132)
print(duration)
00:132:00
start.is_after(duration)
AssertionError: other is not a valid Time

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

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

زبان شیءگرا (object-oriented language): زبانی که امکاناتی برای پشتیبانی از برنامه‌نویسی شیءگرا دارد، به‌ویژه نوع‌های تعریف‌شده توسط کاربر.

متد (method): تابعی که داخل تعریف یک کلاس تعریف می‌شود و روی نمونه‌های آن کلاس احضار می‌شود.

گیرنده (receiver): شیئی که یک متد روی آن احضار می‌شود.

متد ایستا (static method): متدی که می‌توان آن را بدون شیء به‌عنوان گیرنده احضار کرد.

متد نمونه (instance method): متدی که باید با یک شیء به‌عنوان گیرنده احضار شود.

متد ویژه (special method): متدی که شیوه‌ی کار عملگرها و بعضی توابع را با یک شیء تغییر می‌دهد.

سربارگذاری عملگر (operator overloading): فرایند استفاده از متدهای ویژه برای تغییر شیوه‌ی کار عملگرها با نوع‌های تعریف‌شده توسط کاربر.

پایا (invariant): شرطی که باید در طول اجرای برنامه همیشه درست باشد.

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

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

%xmode Verbose

۱۵.۱۰.۱. از یک دستیار مجازی بپرسید

برای اطلاعات بیشتر درباره‌ی متدهای ایستا، از یک دستیار مجازی بپرسید:

  • “What’s the difference between an instance method and a static method?” (تفاوت متد نمونه و متد ایستا چیست؟)

  • “Why are static methods called static?” (چرا متدهای ایستا «ایستا» نامیده می‌شوند؟)

اگر از یک دستیار مجازی بخواهید یک متد ایستا تولید کند، نتیجه احتمالاً با @staticmethod شروع می‌شود، که یک «دکوراتور» (decorator) است و نشان می‌دهد متد ایستا است. دکوراتورها در این کتاب پوشش داده نشده‌اند، اما اگر کنجکاوید، می‌توانید از یک دستیار مجازی اطلاعات بیشتری بخواهید.

در این فصل چند تابع را به‌صورت متد بازنویسی کردیم. دستیارهای مجازی عموماً در این نوع تبدیل کد خوب‌اند. به‌عنوان مثال، تابع زیر را در یک دستیار مجازی بچسبانید و بگویید: «Rewrite this function as a method of the Time class.» (این تابع را به‌صورت متدی از کلاس Time بازنویسی کن.)

def subtract_time(t1, t2):
    return time_to_int(t1) - time_to_int(t2)

۱۵.۱۰.۲. تمرین

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

  1. تعریفی برای کلاس Date بنویسید که یک تاریخ را نشان می‌دهد – یعنی سال، ماه و روز ماه.

  2. متد __init__ بنویسید که year، month و day را به‌عنوان پارامتر بگیرد و پارامترها را به صفت‌ها نسبت دهد. شیئی بسازید که ۲۲ ژوئن ۱۹۳۳ را نشان می‌دهد.

  3. متد __str__ بنویسید که با f-string صفت‌ها را قالب‌بندی کند و نتیجه را برگرداند. اگر آن را با Date‌ای که ساختید آزمایش کنید، نتیجه باید 1933-06-22 باشد.

  4. متدی به نام is_after بنویسید که دو شیء Date بگیرد و اگر اولی بعد از دومی می‌آید True برگرداند. شیء دومی بسازید که ۱۷ سپتامبر ۱۹۳۳ را نشان می‌دهد و بررسی کنید که آیا بعد از شیء اول می‌آید.

راهنمایی: ممکن است نوشتن متدی به نام to_tuple مفید باشد که چندتایی‌ای شامل صفت‌های یک شیء Date به ترتیب سال-ماه-روز برگرداند.

# Solution goes here

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

birthday1 = Date(1933, 6, 22)
print(birthday1)
birthday2 = Date(1933, 9, 17)
print(birthday2)
birthday1.is_after(birthday2)  # should be False
birthday2.is_after(birthday1)  # should be True