شیءگرایی: کلاس و شیء
شروع ترم دوم. تا اینجا داده و رفتار جدا بودند: لیستی از نمرهها و تابعی که میانگین میگرفت. شیءگرایی این دو را کنار هم میگذارد و برنامه را شبیه دنیای واقعی میکند.
۱. چرا شیءگرایی
فرض کنید میخواهید اطلاعات سی دانشجو را نگه دارید: نام، سن و نمرهها. با ابزارهای ترم اول، یا سه لیست موازی میسازید (که به هم ریختنشان فقط یک اشتباه فاصله دارد) یا لیستی از دیکشنری. راه دوم بهتر است، ولی توابعی که روی این داده کار میکنند جای دیگری هستند و هیچ چیز آنها را به هم وصل نمیکند.
کلاس یک نقشه است: میگوید یک دانشجو چه دادههایی دارد و چه کارهایی میتواند بکند. شیء یک نمونه واقعی ساختهشده از روی آن نقشه است.
کلاس مثل نقشه ساختمان است و شیء مثل ساختمانهای ساختهشده از روی آن. یک نقشه، صد ساختمان. هر ساختمان رنگ و ساکنان خودش را دارد ولی همه همان ساختار را دارند.
۲. اولین کلاس
# 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
شرح مثال
- کلمه کلیدی
class، بعد نام، بعد دونقطه. بدنه تو رفته است. - نام کلاس با قرارداد PascalCase نوشته میشود: هر کلمه با حرف بزرگ شروع میشود و زیرخط ندارد.
BankAccountنهbank_account. - نوشتن کلاس هیچ شیئی نمیسازد. برای ساختن شیء، کلاس را مثل تابع صدا میزنید:
MyClass(). - به صفتهای شیء با نقطه دسترسی دارید، مثل
p1.x. - کلاس یک نوع تازه است، دقیقاً مثل
intوstr. تابعtypeهمانطور که در فصل اول دیدید، اینجا هم کار میکند.
۳. صفت کلاس در برابر صفت نمونه
# 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 دقیقاً چیست
selfیعنی همین شیئی که متد رویش صدا زده شده.- وقتی مینویسید
p1.hello()، پایتون در عملMyClass.hello(p1)را اجرا میکند. پسselfهمانp1است. - برای همین است که
selfدر تعریف نوشته میشود ولی موقع صدا زدن نه: پایتون خودش پرش میکند. - نام
selfاجباری نیست ولی قرارداد جهانی است. هرگز عوضش نکنید. - بدون
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}
شرح مثال
__init__بلافاصله پس از ساخته شدن شیء و به طور خودکار اجرا میشود. به آن سازنده میگویند.- آرگومانهایی که به
Person(...)میدهید، به پارامترهای__init__میرسند. اولی همیشهselfاست و خودکار پر میشود. - خط
self.name = nameصفت نمونه میسازد: یک نسخه مخصوص همین شیء. - آن دو زیرخط در دو طرف نام، یعنی این متد خاص است و پایتون خودش صدایش میزند. در فصل بیست و چهارم خواهرها و برادرهایش را میبینید.
__dict__همه صفتهای نمونه را به شکل دیکشنری نشان میدهد. برای عیبیابی مفید است.
۶. مقدار پیشفرض در سازنده
# 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
شرح مثال
- شیء هم یک مقدار است: در لیست میرود، به تابع داده میشود و برگردانده میشود.
- الگوی «قهرمان فعلی» فصل دهم اینجا هم کار میکند، فقط به جای مقایسه عددها، صفت
ageاشیاء را مقایسه میکنیم. - برای مرتبسازی اشیاء باید بگویید بر اساس چه چیزی. آن
lambdaدر پارامترkeyدقیقاً همین را میگوید و موضوع فصل بیست و ششم است. در فصل بیست و چهارم راه دیگری میبینید که به خود کلاس یاد میدهد چطور مقایسه شود.
۱۰. شیئی که خودش را ذخیره میکند
'''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
شرح مثال
- متد
saveروی یک شیء کار میکند، پسselfدارد. - متد
loadفرق دارد: هنوز شیئی وجود ندارد که بخواهد بارگذاری شود. پسselfندارد و با@staticmethodعلامتگذاری شده است. صدا زدنش هم از راه خود کلاس است:Dog.load('Jessy'). - آن دکوراتور و شکل کامل متدهای ایستا و کلاسی، موضوع فصل بعد است.
- راهنمای نوع
'Dog | None'داخل گیومه است، چون در آن لحظه کلاسDogهنوز کامل تعریف نشده است.
۱۱. حساب بانکی
'''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
شرح مثال
- این کلاس نمونه خوبی از چرا شیءگرایی است: موجودی فقط از راه دو متد قابل تغییر است و هر دو متد قواعد را بررسی میکنند.
withdrawمقدار بولی برمیگرداند تا صداکننده بفهمد موفق شد یا نه. این بهتر از چاپ کردن تنهاست.- تاریخچه یک لیست داخل
__init__است، پس هر حساب تاریخچه خودش را دارد. - ولی یک مشکل باقی است: هنوز کسی میتواند بنویسد
account.balance = 999999و همه قواعد را دور بزند. حل این مشکل، موضوع فصل بعد است.
۱۲. برنامه مدیریت دانشجو
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
شرح مثال
- مقایسه کنید با نسخهای که با دیکشنری مینوشتید: اینجا
averageوdescribeداخل خود دانشجو هستند، نه در جای دیگری از برنامه. - تابع
findبیرون کلاس است، چون روی مجموعه دانشجوها کار میکند نه روی یک دانشجو. این تصمیم درستی است. - خروجی
Student | Noneبه صداکننده هشدار میدهد که ممکن است چیزی پیدا نشود، پس باید بررسی کند. - ورودیها با
tryمحافظت شدهاند، درس فصل نوزدهم.
پوشه S21-oop-classes-and-objects شامل نمونههای
01_first_class.py تا 11_student_manager.py.
خلاصه فصل
| مفهوم | توضیح |
|---|---|
| کلاس | نقشه. با class Name: و نام PascalCase |
| شیء | نمونه ساختهشده. با Name() |
| صفت | داده شیء. دسترسی با نقطه |
| متد | تابع داخل کلاس. اولین پارامترش self |
self | خود شیئی که متد رویش صدا زده شده |
__init__ | سازنده. خودکار اجرا میشود، صفتهای نمونه را میسازد |
| صفت کلاس | در بدنه کلاس. مشترک بین همه اشیاء |
| صفت نمونه | با self. در سازنده. مخصوص هر شیء |
| لیست تغییرپذیر در بدنه کلاس | دام. همیشه داخل __init__ بسازیدش |
obj.__dict__ | دیدن همه صفتهای نمونه |
تمرینهای فصل بیست و یک
- کلاس
Bookرا با صفتهایtitleوauthorوyearو متدdescribe()بنویسید. - کلاس
Rectangleرا باwidthوheightو متدهایarea()وperimeter()بنویسید. - کلاس
Circleرا بنویسید و مساحتش را باmath.piحساب کنید. - کلاس
Timerرا با متدهایstart()وstop()وelapsed()بنویسید. - لیستی از پنج شیء
Bookبسازید و قدیمیترینشان را چاپ کنید. - متد
save()بهBookاضافه کنید که آن را در فایل متنی بنویسد. - با مثال خودتان فرق صفت کلاس و صفت نمونه را توضیح دهید.
- کلاس
Playlistرا بنویسید که لیستی از آهنگها دارد، با متدهایaddوremoveوshow.