رفتن به محتوا
پایتون از صفرپایتون در کار واقعیدرس ۸ از ۱۰

کلاس و شیء

داده و رفتار را کنار هم بگذار: کلاس و شیء، __init__ و self، ویژگی و متد، __str__ و __repr__ برای چاپ خوانا، اعتبارسنجی با raise، ویژگی کلاس، وراثت و super()، بازنویسی متد، و dataclass برای کلاس‌های داده‌ای؛ همراه خطاهای AttributeError و فراموش کردن self.

  • حدود ۱۸ دقیقه مطالعه
  • ۵ مثال اجراشده
  • ۳ تمرین با جواب
  • مقدماتی تا متوسط
  • به‌روزرسانی: ۲۹ مهر ۱۴۰۵

توی این درس یاد می‌گیری با کلاس نوع داده‌ی خودت را بسازی. تا اینجا یک سرور را با یک دیکشنری ({"name": ..., "disk": ...}) نشان می‌دادی و تابع‌هایش جدا بودند. کلاس داده (ویژگی‌ها) و رفتار (متدها) را در یک جا جمع می‌کند: web.is_full() به‌جای is_full(web). با __init__ و self آشنا می‌شوی، شیءها را خوانا چاپ می‌کنی (__str__)، جلوی داده‌ی غلط را با raise می‌گیری، با وراثت کلاس‌های تخصصی‌تر می‌سازی (WebServer از Server)، و با dataclass کلاس‌های داده‌ای را در چند خط می‌نویسی.

مسئله: دیکشنری‌هایی که هر چیزی می‌پذیرند

Section titled “مسئله: دیکشنری‌هایی که هر چیزی می‌پذیرند”

یک سرور به شکل دیکشنری را هر کسی می‌تواند هر طور بخواهد بسازد: یکی "disk" می‌نویسد و یکی "Disk"، یکی عدد می‌گذارد و یکی متن. تابع‌هایی که روی سرورها کار می‌کنند هم در ده فایل پراکنده‌اند. وقتی برنامه بزرگ شود، دیگر معلوم نیست «یک سرور» دقیقاً چه کلیدهایی دارد و چه کارهایی رویش می‌شود کرد.

کلاس مثل نقشه‌ی معماری یک خانه است: می‌گوید هر خانه چند اتاق دارد و چه کارهایی در آن می‌شود کرد، ولی خودش خانه نیست. شیء (object یا instance) یک خانه‌ی واقعی است که از روی نقشه ساخته شده. از یک نقشه می‌شود صد خانه ساخت؛ همه یک شکل‌اند ولی هر کدام صاحب، رنگ و اثاث خودش را دارد. self یعنی «همین خانه‌ای که الان درباره‌اش حرف می‌زنیم».

کلاس یک بار تعریف می‌شود؛ هر شیء ساخته‌شده از آن ویژگی‌های خودش را دارد ولی متدها مشترک‌اند.
server.py
class Server:
def __init__(self, name, ip, disk=0):
self.name = name
self.ip = ip
self.disk = disk
def is_full(self, limit=90):
return self.disk > limit
def describe(self):
return f"{self.name} ({self.ip}) دیسک {self.disk}٪"
web = Server("web1", "10.0.0.1", 72)
db = Server("db1", "10.0.0.5", 95)
print(web.name, db.ip)
print(web.describe())
print(web.is_full(), db.is_full())
db.disk = 60
print(db.describe(), db.is_full())
print(type(web))
خروجی
web1 10.0.0.5
web1 (10.0.0.1) دیسک 72٪
False True
db1 (10.0.0.5) دیسک 60٪ False
<class '__main__.Server'>

قدم به قدم:

  • class Server: یک نوع جدید به اسم Server تعریف می‌کند. رسم پایتون: اسم کلاس با حرف بزرگ و بدون _ (WebServer).
  • __init__ متد ویژه‌ای است که هنگام ساختن هر شیء خودکار اجرا می‌شود. Server("web1", "10.0.0.1", 72) یک شیء می‌سازد و __init__ را با این آرگومان‌ها صدا می‌زند.
  • self پارامتر اول هر متد است و یعنی «همین شیء». self.name = name یعنی «ویژگی name همین شیء را برابر آرگومان بگذار».
  • ویژگی‌ها (attribute) با نقطه خوانده و نوشته می‌شوند: web.name، db.disk = 60.
  • متدها تابع‌هایی‌اند که داخل کلاس تعریف شده‌اند. web.is_full() را صدا می‌زنی و پایتون خودش web را به‌جای self می‌دهد؛ برای همین self را در صدا زدن نمی‌نویسی.

type(web) می‌گوید این شیء از نوع Server است؛ همان‌طور که type(5) می‌گفت int. در واقع str، list و dict هم کلاس‌اند، و s.upper() و x.append() که تا حالا می‌زدی همین متدها بودند.

مثال ۲: چاپ خوانا، __str__ و __repr__

Section titled “مثال ۲: چاپ خوانا، __str__ و __repr__”

یک شیء ساده را چاپ کنی، چیز به‌دردبخوری نمی‌بینی:

خروجی print(s)
<__main__.Server object at 0x7f0182dff8f0>

یعنی «یک شیء Server در این آدرس حافظه». با دو متد ویژه، خودت تعیین می‌کنی چطور نشان داده شود:

server_str.py
class Server:
def __init__(self, name, disk):
self.name = name
self.disk = disk
def __str__(self):
return f"Server {self.name} ({self.disk}%)"
def __repr__(self):
return f"Server(name={self.name!r}, disk={self.disk})"
s = Server("web1", 72)
print(s)
print([s, Server("db1", 95)])
خروجی
Server web1 (72%)
[Server(name='web1', disk=72), Server(name='db1', disk=95)]
  • __str__ برای آدم‌ها: print(s) و str(s) از آن استفاده می‌کنند.
  • __repr__ برای برنامه‌نویس‌ها: وقتی شیء داخل یک لیست چاپ می‌شود یا در حالت تعاملی اسمش را می‌نویسی. رسم است شکلی داشته باشد که شبیه کد ساختن همان شیء باشد. !r در f-string یعنی «repr این مقدار» (برای همین 'web1' با کوتیشن آمد).

متدهایی که با دو زیرخط شروع و تمام می‌شوند (__init__، __str__) را متدهای ویژه (dunder) می‌گویند: پایتون خودش در موقعیت‌های خاص صدایشان می‌زند.

مثال ۳: شیئی که از خودش محافظت می‌کند

Section titled “مثال ۳: شیئی که از خودش محافظت می‌کند”

کلاس جای خوبی برای قانون‌گذاری است: هر تغییری از راه متد، و متد بررسی می‌کند که تغییر معتبر باشد:

account.py
class Account:
bank = "LoopX Bank"
def __init__(self, owner, balance=0):
self.owner = owner
self.balance = balance
self.history = []
def deposit(self, amount):
if amount <= 0:
raise ValueError("مبلغ باید مثبت باشد")
self.balance += amount
self.history.append(("+", amount))
def withdraw(self, amount):
if amount > self.balance:
raise ValueError("موجودی کافی نیست")
self.balance -= amount
self.history.append(("-", amount))
acc = Account("علی", 100_000)
acc.deposit(50_000)
acc.withdraw(30_000)
print(acc.owner, acc.balance, acc.history, Account.bank)
try:
acc.withdraw(1_000_000)
except ValueError as e:
print("خطا:", e)
print(acc.balance)
خروجی
علی 120000 [('+', 50000), ('-', 30000)] LoopX Bank
خطا: موجودی کافی نیست
120000
  • withdraw نمی‌گذارد موجودی منفی شود؛ raise (درس قبل) خطا را به صدازننده می‌دهد و موجودی دست‌نخورده می‌ماند.
  • history یک لیست است که هر حساب مال خودش را دارد، چون در __init__ (یعنی برای هر شیء جدا) ساخته شده.
  • bank بیرون از __init__ و مستقیم داخل کلاس تعریف شده: یک ویژگی کلاس است که بین همه‌ی شیءها مشترک است (Account.bank).
⚡ بررسی سریع

در متد def withdraw(self, amount)، هنگام صدا زدن acc.withdraw(500) پارامتر self چیست؟

مثال ۴: وراثت، کلاس‌های تخصصی‌تر

Section titled “مثال ۴: وراثت، کلاس‌های تخصصی‌تر”

همه‌ی سرورها اسم و گزارش دارند، ولی «سالم بودن» یک وب‌سرور با یک دیتابیس فرق دارد. وراثت یعنی کلاس جدید همه‌چیز کلاس قبلی را به ارث ببرد و فقط تفاوت‌ها را بنویسد:

inherit.py
class Server:
def __init__(self, name):
self.name = name
def health(self):
return "ok"
def report(self):
return f"{self.name}: {self.health()}"
class WebServer(Server):
def __init__(self, name, port=80):
super().__init__(name)
self.port = port
def health(self):
return f"http://{self.name}:{self.port} پاسخ داد"
class DBServer(Server):
def health(self):
return "اتصال دیتابیس برقرار است"
for s in [Server("cache1"), WebServer("web1", 8080), DBServer("db1")]:
print(s.report())
print(isinstance(WebServer("x"), Server))
خروجی
cache1: ok
web1: http://web1:8080 پاسخ داد
db1: اتصال دیتابیس برقرار است
True
  • class WebServer(Server): یعنی WebServer فرزند Server است و همه‌ی متدهای آن (report) را دارد.
  • WebServer و DBServer متد health را بازنویسی (override) کرده‌اند؛ report که در کلاس پدر است، self.health() را صدا می‌زند و برای هر شیء نسخه‌ی مخصوص نوع خودش اجرا می‌شود.
  • super().__init__(name) کار __init__ پدر را انجام می‌دهد (گذاشتن name) و بعد WebServer ویژگی اضافه‌ی خودش (port) را می‌گذارد. DBServer __init__ ننوشته، پس همان مال پدر استفاده می‌شود.
  • isinstance(x, Server) می‌گوید x از نوع Server یا یکی از فرزندانش است.

حلقه‌ی آخر نکته‌ی اصلی است: کد صدازننده (s.report()) نمی‌داند و لازم نیست بداند هر سرور چه نوعی است؛ هر شیء خودش می‌داند چه کند.

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

مثال ۵: dataclass، کلاس داده‌ای در چند خط

Section titled “مثال ۵: dataclass، کلاس داده‌ای در چند خط”

خیلی از کلاس‌ها فقط داده نگه می‌دارند و __init__ و __repr__شان تکراری است. dataclass آن‌ها را خودکار می‌سازد:

task.py
from dataclasses import dataclass, field
@dataclass
class Task:
title: str
done: bool = False
tags: list = field(default_factory=list)
t = Task("بکاپ دیتابیس")
t2 = Task("به‌روزرسانی سرور", tags=["ops"])
print(t)
print(t2.title, t2.tags)
print(t == Task("بکاپ دیتابیس"))
خروجی
Task(title='بکاپ دیتابیس', done=False, tags=[])
به‌روزرسانی سرور ['ops']
True
  • @dataclass (یک «دکوراتور»؛ کلاس زیرش را تغییر می‌دهد) از روی فهرست ویژگی‌ها و نوعشان، __init__، __repr__ و __eq__ (مقایسه با ==) را خودکار می‌سازد.
  • field(default_factory=list) همان راه‌حل تله‌ی «لیست به‌عنوان پیش‌فرض» در درس تابع است: هر Task لیست خودش را می‌گیرد.
  • دو dataclass با مقدارهای یکسان برابرند (True)؛ دو شیء کلاس معمولی بدون __eq__، فقط اگر همان یک شیء باشند.

برای «یک رکورد با چند فیلد» (کاربر، سفارش، تنظیمات) dataclass معمولاً بهترین انتخاب است.

پشت پرده: self و جستجوی ویژگی‌ها

Section titled “پشت پرده: self و جستجوی ویژگی‌ها”

web.is_full() در واقع کوتاه‌شده‌ی Server.is_full(web) است: پایتون متد را در کلاس پیدا می‌کند و شیء را به‌عنوان آرگومان اول می‌دهد. برای همین self اسم خاصی نیست (فقط یک رسم بسیار قوی است) و برای همین اگر در تعریف متد فراموشش کنی، خطای «۰ آرگومان می‌گیرد ولی ۱ داده شد» می‌گیری (اشتباهات رایج ۲).

وقتی web.name را می‌خوانی، پایتون به ترتیب می‌گردد: اول در ویژگی‌های خود شیء (که در __init__ با self. گذاشته شده‌اند)، بعد در کلاس (برای همین acc.bank هم کار می‌کند)، بعد در پدرها به ترتیب وراثت. اولین جایی که پیدا شود برنده است؛ بازنویسی متد در فرزند همین‌طور کار می‌کند. اگر هیچ‌جا نبود: AttributeError.

کد معنی
class Name: تعریف کلاس
def __init__(self, ...): ساختن شیء (خودکار)
self.x = x ویژگی شیء
def method(self, ...): متد
obj = Name(...) ساختن شیء
obj.x / obj.method() ویژگی / صدا زدن متد
class Child(Parent): وراثت
super().__init__(...) صدا زدن نسخه‌ی پدر
isinstance(obj, Name) بررسی نوع (با فرزندها)
@dataclass ساختن خودکار init و repr و eq
متد ویژه کی صدا زده می‌شود
__init__ Name(...)
__str__ print(obj)، str(obj)، f-string
__repr__ حالت تعاملی، داخل لیست، repr(obj)
__eq__ a == b
__len__ len(obj)

۱) ویژگی‌ای که وجود ندارد

Section titled “۱) ویژگی‌ای که وجود ندارد”
class Server:
def __init__(self, name):
self.name = name
s = Server("web1")
print(s.ip)
خروجی
Traceback (most recent call last):
File "/home/ali/py/attr.py", line 5, in <module>
print(s.ip)
^^^^
AttributeError: 'Server' object has no attribute 'ip'

یا غلط تایپی است، یا آن ویژگی در __init__ ساخته نشده. راه‌حل: همه‌ی ویژگی‌ها را در __init__ بساز (حتی با مقدار None) تا شکل شیء همیشه یکی باشد.

۲) فراموش کردن self در تعریف متد

Section titled “۲) فراموش کردن self در تعریف متد”
class Server:
def __init__(self, name):
self.name = name
def hello():
return "hi"
s = Server("web1")
print(s.hello())
خروجی
TypeError: Server.hello() takes 0 positional arguments but 1 was given

«۱ داده شد» همان شیئی است که پایتون خودکار به‌عنوان self فرستاد. راه‌حل: def hello(self):.

۳) name = name به‌جای self.name = name

Section titled “۳) name = name به‌جای self.name = name”
class Server:
def __init__(self, name):
name = name
s = Server("web1")
print(s.name)
خروجی
AttributeError: 'Server' object has no attribute 'name'

بدون self.، فقط یک متغیر محلی ساخته شد که با تمام شدن __init__ از بین رفت. راه‌حل: self.name = name.

۴) لیست به‌عنوان ویژگی کلاس

Section titled “۴) لیست به‌عنوان ویژگی کلاس”

اگر history = [] را بیرون از __init__ (مثل bank) بنویسی، همه‌ی حساب‌ها یک لیست مشترک می‌گیرند و تراکنش‌های همه قاطی می‌شود. راه‌حل: چیزهای قابل‌تغییر (لیست، دیکشنری) را در __init__ با self. بساز.

برای یک کار ساده (مثلاً تبدیل دما) یک تابع کافی است؛ کلاسی که فقط یک متد و هیچ داده‌ای ندارد، کد را بی‌دلیل پیچیده می‌کند. راه‌حل: وقتی داده و رفتار به هم گره خورده‌اند یا چند شیء هم‌شکل داری، کلاس؛ وگرنه تابع.

تمرینآسان

کلاس Rectangle بساز با width و height، و دو متد area() و perimeter(). یک __str__ هم بنویس که Rectangle 3×4 چاپ کند. دو مستطیل بساز و مساحت و محیطشان را چاپ کن.

دیدن جواب
rect.py
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def perimeter(self):
return 2 * (self.width + self.height)
def __str__(self):
return f"Rectangle {self.width}×{self.height}"
for r in [Rectangle(3, 4), Rectangle(10, 2)]:
print(r, "مساحت:", r.area(), "محیط:", r.perimeter())
خروجی
Rectangle 3×4 مساحت: 12 محیط: 14
Rectangle 10×2 مساحت: 20 محیط: 24
تمرینمتوسط

تمرین اصلی: کلاس Inventory (انبار) بساز که یک دیکشنری {کالا: تعداد} نگه دارد و سه متد داشته باشد: add(item, qty)، remove(item, qty) (اگر کالا نبود یا تعداد کافی نبود، ValueError با پیام فارسی) و report() که فهرست را مرتب‌شده برگرداند. یک برداشت نامعتبر را با try/except امتحان کن.

دیدن جواب
inventory.py
class Inventory:
def __init__(self):
self.items = {}
def add(self, item, qty):
self.items[item] = self.items.get(item, 0) + qty
def remove(self, item, qty):
have = self.items.get(item, 0)
if qty > have:
raise ValueError(f"از {item} فقط {have} عدد موجود است")
self.items[item] = have - qty
def report(self):
return [f"{name}: {n}" for name, n in sorted(self.items.items())]
inv = Inventory()
inv.add("کابل شبکه", 20)
inv.add("هارد", 5)
inv.remove("کابل شبکه", 8)
try:
inv.remove("هارد", 9)
except ValueError as e:
print("خطا:", e)
for line in inv.report():
print(line)
خروجی
خطا: از هارد فقط 5 عدد موجود است
هارد: 5
کابل شبکه: 12

برداشت نامعتبر هیچ تغییری نداد (هارد هنوز ۵ است)، چون raise قبل از تغییر موجودی اجرا شد. (ترتیب sorted روی متن فارسی، ترتیب کد یونیکد حرف‌هاست؛ برای همین «ه» قبل از «ک» آمده.)

تمرینسخت

با وراثت یک سلسله‌مراتب «اعلان» (notification) بساز: کلاس پایه‌ی Notifier با متد send(message) که format(message) را صدا می‌زند و نتیجه را چاپ می‌کند؛ و دو فرزند: EmailNotifier(address) که پیام را با To: آدرس قالب‌بندی کند و SlackNotifier(channel) با #کانال. یک لیست از هر دو بساز و با یک حلقه پیام «دیسک db1 پر شد» را به همه بفرست.

دیدن جواب
notify.py
class Notifier:
def format(self, message):
return message
def send(self, message):
print(self.format(message))
class EmailNotifier(Notifier):
def __init__(self, address):
self.address = address
def format(self, message):
return f"[email] To: {self.address} | {message}"
class SlackNotifier(Notifier):
def __init__(self, channel):
self.channel = channel
def format(self, message):
return f"[slack] #{self.channel} | {message}"
targets = [EmailNotifier("ops@example.com"), SlackNotifier("alerts")]
for t in targets:
t.send("دیسک db1 پر شد")
خروجی
[email] To: ops@example.com | دیسک db1 پر شد
[slack] #alerts | دیسک db1 پر شد

send فقط یک بار در کلاس پایه نوشته شده؛ هر فرزند فقط قالب خودش را تعریف کرده. فردا اگر TelegramNotifier لازم شد، فقط یک کلاس کوچک اضافه می‌کنی و حلقه دست نمی‌خورد.

آزمونک
  1. فرق کلاس و شیء؟

  2. __init__ کی اجرا می‌شود؟

  3. چرا print(obj) چیزی مثل <__main__.Server object at 0x...> چاپ می‌کند؟

  4. در class B(A):، اگر B متد m را تعریف کند و A هم، b.m() کدام را اجرا می‌کند؟

  5. خطای «takes 0 positional arguments but 1 was given» در صدا زدن متد معمولاً یعنی…

  6. @dataclass چه چیزی را خودکار می‌سازد؟

  • کلاس نقشه است و شیء نمونه‌ی واقعی آن؛ کلاس داده (ویژگی) و رفتار (متد) را کنار هم می‌گذارد.
  • __init__(self, ...) هنگام ساختن اجرا می‌شود؛ self.x = ... ویژگی می‌سازد؛ متدها همیشه self را اول دارند.
  • __str__ برای چاپ خوانا و __repr__ برای اشکال‌زدایی؛ متدها جای خوبی برای اعتبارسنجی با raise هستند.
  • وراثت: class Child(Parent):، بازنویسی متد، super().__init__(...)؛ کد صدازننده لازم نیست نوع دقیق را بداند.
  • @dataclass برای رکوردهای داده‌ای؛ لیست‌ها را در __init__ (یا با field(default_factory=list)) بساز، نه در سطح کلاس.
برگه‌ی تقلب این درس
دستورکاری که می‌کند
class Server:تعریف کلاس
def __init__(self, name): self.name = nameساختن ویژگی‌ها
def is_full(self): return self.disk > 90متد
s = Server("web1"); s.is_full()ساختن شیء و صدا زدن متد
def __str__(self): return ...چاپ خوانا
class WebServer(Server):وراثت
super().__init__(name)صدا زدن init پدر
isinstance(s, Server)بررسی نوع
@dataclass class Task: title: strکلاس داده‌ای سریع