برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۱

شیءگرایی: کلاس و شیء

شروع ترم دوم. تا اینجا داده و رفتار جدا بودند: لیستی از نمره‌ها و تابعی که میانگین می‌گرفت. شیءگرایی این دو را کنار هم می‌گذارد و برنامه را شبیه دنیای واقعی می‌کند.

در این فصل می‌خوانید
  1. چرا شیءگرایی
  2. اولین کلاس
  3. صفت کلاس در برابر صفت نمونه
  4. متد و پارامتر self
  5. سازنده __init__
  6. مقدار پیش‌فرض در سازنده
  7. یک کلاس کامل
  8. کلاس با راهنمای نوع
  9. لیستی از اشیاء
  10. شیئی که خودش را ذخیره می‌کند
  11. حساب بانکی
  12. برنامه مدیریت دانشجو

۱. چرا شیءگرایی

فرض کنید می‌خواهید اطلاعات سی دانشجو را نگه دارید: نام، سن و نمره‌ها. با ابزارهای ترم اول، یا سه لیست موازی می‌سازید (که به هم ریختنشان فقط یک اشتباه فاصله دارد) یا لیستی از دیکشنری. راه دوم بهتر است، ولی توابعی که روی این داده کار می‌کنند جای دیگری هستند و هیچ چیز آنها را به هم وصل نمی‌کند.

کلاس یک نقشه است: می‌گوید یک دانشجو چه داده‌هایی دارد و چه کارهایی می‌تواند بکند. شیء یک نمونه واقعی ساخته‌شده از روی آن نقشه است.

یک مقایسه

کلاس مثل نقشه ساختمان است و شیء مثل ساختمان‌های ساخته‌شده از روی آن. یک نقشه، صد ساختمان. هر ساختمان رنگ و ساکنان خودش را دارد ولی همه همان ساختار را دارند.

۲. اولین کلاس

# A class is a blueprint. Writing it creates NOTHING by itself.
class MyClass:
    x = 5
    y = 'Hello'


def main():
    # Calling the class like a function creates an OBJECT (an instance).
    p1 = MyClass()
    print(p1.x)          # 5

    # Every object is independent: changing p2 does not touch p1.
    p2 = MyClass()
    p2.x = 12
    print(p2.x)          # 12
    print(p1.x)          # 5

    # A class is a type, exactly like int or str.
    a = 2
    print(type(a))       # <class 'int'>
    print(type(p1))      # <class '__main__.MyClass'>
    print(isinstance(p1, MyClass))   # True


if __name__ == '__main__':
    main()

اولین کلاس: نمونه 01_first_class.py

شرح مثال

۳. صفت کلاس در برابر صفت نمونه

# An attribute written in the body of the class belongs to the CLASS:
# it is shared by every object that does not define its own.
class Counter:
    total = 0            # class attribute - ONE copy for everybody


def main():
    a = Counter()
    b = Counter()

    print(a.total, b.total)        # 0 0

    # Assigning through an object creates an INSTANCE attribute that hides
    # the class one - only for that object.
    a.total = 10
    print(a.total, b.total)        # 10 0

    # Changing it on the class itself changes it for everyone who has no
    # instance attribute of that name.
    Counter.total = 99
    print(a.total, b.total)        # 10 99

    # This is why mutable class attributes are dangerous:
    class Basket:
        items = []                 # shared by ALL the baskets!

    b1 = Basket()
    b2 = Basket()
    b1.items.append('apple')
    print(b2.items)                # ['apple'] - surprise!

صفت کلاس و نمونه: نمونه 02_class_vs_instance.py

صفت کلاسصفت نمونه
کجا تعریف می‌شودبدنه کلاسداخل __init__، با self
چند نسخهیکی، مشترک بین همهیکی برای هر شیء
مناسب برایثابت‌ها، شمارنده کلداده مخصوص هر شیء
دام صفت کلاس تغییرپذیر

این همان دام پیش‌فرض تغییرپذیر فصل سیزدهم است، در لباس تازه. لیستی که در بدنه کلاس تعریف شود، بین همه اشیاء مشترک است. یک سبد خرید که کالای همه مشتری‌ها را در خودش دارد. راه حل همیشه یکی است: لیست را داخل __init__ بسازید.

۴. متد و پارامتر self

# A method is a function written inside a class.
# Its first parameter is always 'self': the object the method was called on.
class MyClass:
    x = 5

    def hello(self):
        print(f'Hello {self.x}')

    def bye(self):
        print(f'Goodbye {self.x}')


if __name__ == '__main__':
    p1 = MyClass()
    p2 = MyClass()

    p1.x = 15
    p2.x = -7

    # p1.hello() is really MyClass.hello(p1): that is where 'self' comes from.
    p1.hello()
    p2.hello()

متد و self: نمونه 03_methods_and_self.py

Hello 15
Hello -7

self دقیقاً چیست

۵. سازنده __init__

# __init__ runs automatically right after the object is created.
# It is where the instance attributes are born.
class Person:

    def __init__(self, name, age):
        self.name = name       # instance attribute, one per object
        self.age = age


if __name__ == '__main__':
    p1 = Person('Ali', 36)
    p2 = Person(name='Reza', age=22)     # keyword arguments work here too

    print(p1.name, p1.age)
    print(p2.name, p2.age)

    # Each object keeps its own values.
    p1.age = 37
    print(p1.age, p2.age)

    # __dict__ shows the instance attributes of an object.
    print(p1.__dict__)

سازنده: نمونه 04_init.py

Ali 36
Reza 22
37 22
{'name': 'Ali', 'age': 36}

شرح مثال

۶. مقدار پیش‌فرض در سازنده

# The parameters of __init__ can have default values, like any function.
class Person:
    def __init__(self, name, age=18):
        self.name = name
        self.age = age


if __name__ == '__main__':
    p1 = Person('Ali')             # age takes its default
    p2 = Person('Reza', 22)

    # Remember the trap of session 13: never use a list as a default value.
    class Basket:
        def __init__(self, items=None):
            # a NEW list for every basket
            self.items = items if items is not None else []

    b1 = Basket()
    b2 = Basket()
    b1.items.append('apple')
    print(b1.items, b2.items)      # ['apple'] []

پیش‌فرض در سازنده: نمونه 05_init_defaults.py

همان قواعد فصل دوازدهم و سیزدهم اینجا هم برقرارند: پیش‌فرض بعد از پارامترهای بدون پیش‌فرض، و هرگز لیست به عنوان پیش‌فرض.

۷. یک کلاس کامل

# A class puts the data (name, age) and the behaviour (greet) together.
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def greet(self):
        print(f'Hello, my name is {self.name}!')

    def have_birthday(self):
        'A method may change the state of the object.'
        self.age += 1
        print(f'{self.name} is now {self.age}.')

    def is_adult(self):
        'A method may also just answer a question.'
        return self.age >= 18


if __name__ == '__main__':
    p1 = Person('Ali', 32)
    p1.greet()
    p1.have_birthday()
    print(p1.is_adult())

    p2 = Person('Reza', 12)
    p2.greet()
    print(p2.is_adult())

کلاس کامل: نمونه 06_person.py

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

۸. کلاس با راهنمای نوع

'''The same ideas, written with type hints and a main() function.'''


class Car:
    def __init__(self, brand: str, model: str, year: int) -> None:
        self.brand: str = brand
        self.model: str = model
        self.year: int = year

    def display_info(self) -> None:
        'Print a readable description of the car.'
        print(f'{self.year} {self.brand} {self.model}')

    def age(self, current_year: int = 2026) -> int:
        'Return how old the car is.'
        return current_year - self.year


def main() -> None:
    car1: Car = Car(brand='Mazda', model='6', year=2018)
    car1.display_info()
    print('Age:', car1.age())


if __name__ == '__main__':
    main()

با راهنمای نوع: نمونه 07_car_with_hints.py

نکته: __init__ خروجی -> None دارد، چون سازنده هیچ‌وقت چیزی برنمی‌گرداند. اگر داخلش return با مقدار بنویسید، پایتون خطا ایجاد می‌کند.

۹. لیستی از اشیاء

# Objects are values like any other: they can live in a list, in a dictionary,
# be passed to a function and be returned from one.
class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name} says vagh! And its age is {self.age}.")


def oldest(dogs):
    'Return the oldest dog of a list.'
    best = dogs[0]
    for dog in dogs:
        if dog.age > best.age:
            best = dog
    return best


if __name__ == '__main__':
    dogs = [
        Dog('Jessy', 3),
        Dog('Blacky', 5),
        Dog('Rex', 1),
    ]

    for dog in dogs:
        dog.bark()

    print('The oldest is', oldest(dogs).name)

    # Sorting a list of objects needs a key that says WHAT to compare.
    for dog in sorted(dogs, key=lambda d: d.age):
        print(dog.name, dog.age)

لیستی از اشیاء: نمونه 08_list_of_objects.py

شرح مثال

۱۰. شیئی که خودش را ذخیره می‌کند

'''An object can carry the code that saves and loads it.'''

import os


class Dog:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def save(self) -> None:
        'Write this dog into its own text file.'
        with open(f'{self.name}.txt', 'w', encoding='utf-8') as file:
            file.write(f'{self.name}\n{self.age}\n')

    @staticmethod
    def load(name: str) -> 'Dog | None':
        'Read a dog back from its file (None when the file is missing).'
        filename = f'{name}.txt'
        if not os.path.exists(filename):
            return None

        with open(filename, encoding='utf-8') as file:
            saved_name = file.readline().strip()
            saved_age = int(file.readline().strip())

        return Dog(saved_name, saved_age)


if __name__ == '__main__':
    dogs = [Dog('Jessy', 3), Dog('Blacky', 5)]

    for dog in dogs:
        dog.save()

    loaded = Dog.load('Jessy')
    if loaded is not None:
        print('Loaded from the file:', loaded.name, loaded.age)

ذخیره و بازیابی: نمونه 09_object_saves_itself.py

شرح مثال

۱۱. حساب بانکی

'''A class whose whole point is to protect a value that changes.'''


class BankAccount:
    def __init__(self, owner: str, balance: float = 0.0) -> None:
        self.owner = owner
        self.balance = balance
        self.history: list[str] = []      # a NEW list for every account

    def deposit(self, amount: float) -> None:
        'Add money to the account.'
        if amount <= 0:
            print('The amount must be positive.')
            return
        self.balance += amount
        self.history.append(f'+{amount}')

    def withdraw(self, amount: float) -> bool:
        'Take money out. Return False when there is not enough.'
        if amount <= 0:
            print('The amount must be positive.')
            return False
        if amount > self.balance:
            print('Not enough money.')
            return False
        self.balance -= amount
        self.history.append(f'-{amount}')
        return True

    def show(self) -> None:
        print(f'{self.owner}: {self.balance:,.0f}')
        print('  history:', ', '.join(self.history) or '(empty)')


if __name__ == '__main__':
    account = BankAccount('Ali', 100_000)
    account.deposit(50_000)
    account.withdraw(30_000)
    account.withdraw(1_000_000)      # refused
    account.show()

حساب بانکی: نمونه 10_bank_account.py

شرح مثال

۱۲. برنامه مدیریت دانشجو

class Student:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age
        self.scores: list[float] = []

    def add_score(self, score: float) -> None:
        'Store one score.'
        self.scores.append(score)

    def average(self) -> float:
        'Return the average score (0 when there is none).'
        if len(self.scores) == 0:
            return 0.0
        return sum(self.scores) / len(self.scores)

    def describe(self) -> str:
        'Return a one-line description.'
        return f'{self.name} ({self.age}) - average {self.average():.2f}'


def find(students: list[Student], name: str) -> Student | None:
    'Return the student with that name, or None.'
    for student in students:
        if student.name.lower() == name.lower():
            return student
    return None

کلاس دانشجو: نمونه 11_student_manager.py

def main() -> None:
    students: list[Student] = []

    while True:
        print('\n1) Add a student')
        print('2) Add a score')
        print('3) Show all the students')
        print('0) Exit')

        choice = input('Your choice: ')

        if choice == '1':
            name = input('Name: ')
            try:
                age = int(input('Age: '))
            except ValueError:
                print('The age must be a whole number.')
                continue
            students.append(Student(name, age))
            print('Added.')

        elif choice == '2':
            student = find(students, input('Name: '))
            if student is None:
                print('No such student.')
                continue
            try:
                student.add_score(float(input('Score: ')))
            except ValueError:
                print('The score must be a number.')

        elif choice == '3':
            for student in students:
                print(' -', student.describe())

        elif choice == '0':
            break

منوی برنامه: نمونه 11_student_manager.py

شرح مثال

فایل‌های تمرینی این فصل

پوشه S21-oop-classes-and-objects شامل نمونه‌های 01_first_class.py تا 11_student_manager.py.

خلاصه فصل

مفهومتوضیح
کلاسنقشه. با class Name: و نام PascalCase
شیءنمونه ساخته‌شده. با Name()
صفتداده شیء. دسترسی با نقطه
متدتابع داخل کلاس. اولین پارامترش self
selfخود شیئی که متد رویش صدا زده شده
__init__سازنده. خودکار اجرا می‌شود، صفت‌های نمونه را می‌سازد
صفت کلاسدر بدنه کلاس. مشترک بین همه اشیاء
صفت نمونهبا self. در سازنده. مخصوص هر شیء
لیست تغییرپذیر در بدنه کلاسدام. همیشه داخل __init__ بسازیدش
obj.__dict__دیدن همه صفت‌های نمونه

تمرین‌های فصل بیست و یک

  1. کلاس Book را با صفت‌های title و author و year و متد describe() بنویسید.
  2. کلاس Rectangle را با width و height و متدهای area() و perimeter() بنویسید.
  3. کلاس Circle را بنویسید و مساحتش را با math.pi حساب کنید.
  4. کلاس Timer را با متدهای start() و stop() و elapsed() بنویسید.
  5. لیستی از پنج شیء Book بسازید و قدیمی‌ترینشان را چاپ کنید.
  6. متد save() به Book اضافه کنید که آن را در فایل متنی بنویسد.
  7. با مثال خودتان فرق صفت کلاس و صفت نمونه را توضیح دهید.
  8. کلاس Playlist را بنویسید که لیستی از آهنگ‌ها دارد، با متدهای add و remove و show.
فصل قبل: پروژه‌های ترم اول فصل بعد: کپسوله‌سازی و dataclass