Python پایتون ۳.۱۴
فصل ۹

کلاس‌ها (Classes)

📖 ۲۵ دقیقه 🎯 پیشرفته 🔄 ۲۰۲۶

کلاس‌ها ابزاری برای ترکیب داده‌ها و عملکردها در کنار یکدیگر فراهم می‌کنند. ایجاد یک کلاس جدید، منجر به خلق یک نوع (Type) جدید از شیء می‌شود و امکان ساخت نمونه‌های (Instances) جدید از آن نوع را میسر می‌سازد. هر نمونه از یک کلاس می‌تواند دارای ویژگی‌هایی (Attributes) برای حفظ وضعیت (State) خود باشد. همچنین، نمونه‌های کلاس می‌توانند متدهایی (که توسط کلاسشان تعریف شده‌اند) برای تغییر وضعیت خود داشته باشند.

در مقایسه با سایر زبان‌های برنامه‌نویسی، مکانیزم کلاس در پایتون، کلاس‌ها را با حداقل نحو (Syntax) و معنای (Semantics) جدیدی اضافه می‌کند. این مکانیزم ترکیبی از مکانیزم‌های کلاس موجود در C++ و Modula-3 است. کلاس‌های پایتون تمام ویژگی‌های استاندارد برنامه‌نویسی شی‌گرا (OOP) را فراهم می‌کنند: مکانیزم وراثت (Inheritance) اجازه می‌دهد چندین کلاس پایه (Base Classes) داشته باشید، یک کلاس مشتق‌شده (Derived Class) می‌تواند هر متدی از کلاس پایه خود را بازنویسی (Override) کند، و یک متد می‌تواند متدی از کلاس پایه را با همان نام فراخوانی نماید. اشیاء می‌توانند حاوی مقادیر و انواع دلخواهی از داده‌ها باشند. همان‌طور که در مورد ماژول‌ها صدق می‌کند، کلاس‌ها نیز در ماهیت پویای پایتون سهیم هستند: آن‌ها در زمان اجرا (Runtime) ایجاد می‌شوند و پس از ایجاد نیز قابل اصلاح هستند.

در اصطلاحات C++، معمولاً اعضای کلاس (از جمله اعضای داده) عمومی (Public) هستند (به‌جز بخش متغیرهای خصوصی که در ادامه می‌آید)، و تمام توابع عضو مجازی (Virtual) هستند. همانند Modula-3، هیچ میانبری برای ارجاع به اعضای شیء از درون متدهای آن وجود ندارد: تابع متد با یک آرگومان اولِ صریح که نشان‌دهنده شیء است اعلان می‌شود و این آرگومان به‌طور ضمنی توسط فراخوانی فراهم می‌گردد. همانند Smalltalk، خود کلاس‌ها نیز شیء هستند. این امر معنایی برای وارد کردن (Import) و تغییر نام (Renaming) فراهم می‌کند. برخلاف C++ و Modula-3، انواع توکار (Built-in Types) می‌توانند به‌عنوان کلاس‌های پایه برای توسعه توسط کاربر مورد استفاده قرار گیرند. همچنین، مانند C++، بیشتر عملگرهای توکار با نحو خاص (عملگرهای حسابی، زیرنویس (Subscripting) و غیره) می‌توانند برای نمونه‌های کلاس بازتعریف (Redefine) شوند.

۹.۱. یک نکته درباره نام‌ها و اشیاء (A Word About Names and Objects)

اشیاء دارای هویت (Individuality) هستند و چندین نام (در چندین حوزه یا Scope) می‌توانند به یک شیء واحد متصل شوند. این مفهوم در زبان‌های دیگر به‌عنوان نام‌گذاری مستعار (Aliasing) شناخته می‌شود. این موضوع معمولاً در نگاه اول در پایتون به چشم نمی‌آید و می‌توان هنگام کار با انواع پایهٔ تغییرناپذیر (Immutable) (مانند اعداد، رشته‌ها، تاپل‌ها) با خیال راحت آن را نادیده گرفت. با این حال، Aliasing می‌تواند تأثیر شگفت‌آوری بر معنای کد پایتون، زمانی که شامل اشیاء تغییرپذیر (Mutable) مانند لیست‌ها، دیکشنری‌ها و بیشتر انواع دیگر باشد، داشته باشد. این موضوع معمولاً به نفع برنامه است، زیرا نام‌های مستعار از برخی جهات مانند اشاره‌گرها (Pointers) رفتار می‌کنند. برای مثال، ارسال یک شیء ارزان است زیرا تنها یک اشاره‌گر توسط پیاده‌سازی ارسال می‌شود؛ و اگر تابعی شیئی را که به‌عنوان آرگومان ارسال شده است اصلاح کند، فراخواننده (Caller) تغییر را خواهد دید — این کار نیاز به دو مکانیزم مختلف برای ارسال آرگومان (مانند آنچه در پاسکال وجود دارد) را از بین می‌برد.

۹.۲. حوزه‌ها (Scopes) و فضاهای نام (Namespaces) پایتون

قبل از معرفی کلاس‌ها، ابتدا باید چیزی در مورد قوانین حوزه (Scope) پایتون به شما بگویم. تعاریف کلاس، ترفندهای جالبی با فضاهای نام انجام می‌دهند و برای درک کامل آنچه در حال وقوع است، باید بدانید که حوزه‌ها و فضاهای نام چگونه کار می‌کنند. به هر حال، دانش در مورد این موضوع برای هر برنامه‌نویس پیشرفتهٔ پایتون مفید است.

بیایید با چند تعریف شروع کنیم.

یک فضای نام (Namespace) نگاشتی (Mapping) از نام‌ها به اشیاء است. بیشتر فضاهای نام در حال حاضر به‌عنوان دیکشنری‌های پایتون پیاده‌سازی می‌شوند، اما این موضوع معمولاً به هیچ وجه قابل توجه نیست (به‌جز برای کارایی)، و ممکن است در آینده تغییر کند. نمونه‌هایی از فضاهای نام عبارتند از: مجموعه نام‌های توکار (شامل توابعی مانند abs() و نام‌های استثنای توکار)؛ نام‌های سراسری (Global) در یک ماژول؛ و نام‌های محلی (Local) در یک فراخوانی تابع. به نوعی، مجموعه ویژگی‌های (Attributes) یک شیء نیز یک فضای نام را تشکیل می‌دهند. نکته مهم در مورد فضاهای نام این است که مطلقاً هیچ رابطه‌ای بین نام‌ها در فضاهای نام مختلف وجود ندارد؛ برای مثال، دو ماژول مختلف ممکن است هر دو یک تابع maximize را بدون ایجاد سردرگمی تعریف کنند — کاربران ماژول‌ها باید آن را با نام ماژول پیشوند کنند.

به هر حال، من از کلمه ویژگی (Attribute) برای هر نامی که پس از یک نقطه می‌آید استفاده می‌کنم — برای مثال، در عبارت z.real، کلمه real یک ویژگی از شیء z است. به‌طور دقیق‌تر، ارجاعات به نام‌ها در ماژول‌ها، ارجاعات ویژگی هستند: در عبارت modname.funcname، عبارت modname یک شیء ماژول است و funcname یک ویژگی از آن است. در این مورد، یک نگاشت مستقیم بین ویژگی‌های ماژول و نام‌های سراسری تعریف‌شده در ماژول وجود دارد: آن‌ها یک فضای نام را به اشتراک می‌گذارند! [1]

ویژگی‌ها می‌توانند فقط‌خواندنی (Read-only) یا قابل‌نوشتن (Writable) باشند. در حالت دوم، انتساب به ویژگی‌ها امکان‌پذیر است. ویژگی‌های ماژول قابل‌نوشتن هستند: می‌توانید بنویسید modname.the_answer = 42. ویژگی‌های قابل‌نوشتن همچنین می‌توانند با دستور del حذف شوند. برای مثال، del modname.the_answer ویژگی the_answer را از شیءِ نام‌گذاری‌شده توسط modname حذف می‌کند.

فضاهای نام در زمان‌های مختلفی ایجاد می‌شوند و طول عمرهای متفاوتی دارند. فضای نام حاوی نام‌های توکار زمانی ایجاد می‌شود که مفسر پایتون راه‌اندازی می‌شود و هرگز حذف نمی‌گردد. فضای نام سراسری برای یک ماژول زمانی ایجاد می‌شود که تعریف ماژول خوانده می‌شود؛ به‌طور معمول، فضاهای نام ماژول نیز تا زمانی که مفسر خارج شود، باقی می‌مانند. دستوراتی که توسط فراخوانی سطح بالای (Top-level) مفسر، که یا از یک فایل اسکریپت خوانده می‌شوند یا به‌صورت تعاملی (Interactively) اجرا می‌شوند، به‌عنوان بخشی از یک ماژول به نام __main__ در نظر گرفته می‌شوند، بنابراین فضای نام سراسری خود را دارند. (نام‌های توکار در واقع همچنین در یک ماژول زندگی می‌کنند؛ این ماژول builtins نامیده می‌شود.)

فضای نام محلی برای یک تابع زمانی ایجاد می‌شود که تابع فراخوانی می‌شود و زمانی که تابع بازگردد (Return) یا استثنایی را ایجاد کند که در داخل تابع مدیریت نشود، حذف می‌شود. (در واقع، فراموش شدن (Forgetting) روش بهتری برای توصیف آنچه واقعاً اتفاق می‌افتد است.) البته، فراخوانی‌های بازگشتی (Recursive) هر کدام فضای نام محلی خود را دارند.

یک حوزه (Scope) یک منطقه متنی (Textual Region) از یک برنامه پایتون است که در آن یک فضای نام به‌طور مستقیم قابل دسترسی است. "دسترسی مستقیم" در اینجا به این معنی است که یک ارجاع بدون صلاحیت (Unqualified Reference) به یک نام، سعی می‌کند آن نام را در فضای نام پیدا کند.

اگرچه حوزه‌ها به‌صورت ایستا (Statically) تعیین می‌شوند، اما به‌صورت پویا (Dynamically) استفاده می‌شوند. در هر زمان در حین اجرا، ۳ یا ۴ حوزهٔ تودرتو (Nested) وجود دارند که فضاهای نام آن‌ها به‌طور مستقیم قابل دسترسی هستند:

  1. درونی‌ترین حوزه، که ابتدا جست‌وجو می‌شود، شامل نام‌های محلی است.
  2. حوزه‌های هر تابع محصورکننده (Enclosing)، که با نزدیک‌ترین حوزهٔ محصورکننده شروع به جست‌وجو می‌کنند، شامل نام‌های غیرمحلی (Non-local) اما همچنین غیرسراسری (Non-global) هستند.
  3. حوزه ماقبل آخر، شامل نام‌های سراسری ماژول جاری است.
  4. بیرونی‌ترین حوزه (که آخرین بار جست‌وجو می‌شود)، فضای نام حاوی نام‌های توکار است.

اگر یک نام به‌عنوان global اعلان شود، تمام ارجاعات و انتساب‌ها مستقیماً به حوزه ماقبل آخر که حاوی نام‌های سراسری ماژول است، می‌روند. برای اتصال مجدد (Rebind) متغیرهای یافت‌شده در خارج از درونی‌ترین حوزه، می‌توان از دستور nonlocal استفاده کرد؛ اگر به‌عنوان nonlocal اعلان نشده باشند، آن متغیرها فقط‌خواندنی هستند (تلاش برای نوشتن در چنین متغیری به‌سادگی یک متغیر محلی جدید در درونی‌ترین حوزه ایجاد می‌کند و متغیر بیرونی با همین نام را بدون تغییر باقی می‌گذارد).

معمولاً، حوزه محلی به نام‌های محلی تابع (متنی) جاری ارجاع می‌دهد. در خارج از توابع، حوزه محلی به فضای نام یکسانی با حوزه سراسری ارجاع می‌دهد: فضای نام ماژول. تعاریف کلاس، فضای نام دیگری را در حوزه محلی قرار می‌دهند.

درک این نکته مهم است که حوزه‌ها به‌صورت متنی (Textually) تعیین می‌شوند: حوزه سراسری یک تابع تعریف‌شده در یک ماژول، فضای نام آن ماژول است، صرف‌نظر از اینکه تابع از کجا یا با چه اسم مستعاری فراخوانی شود. از طرف دیگر، جست‌وجوی واقعی برای نام‌ها به‌صورت پویا، در زمان اجرا انجام می‌شود — با این حال، تعریف زبان به سمت وضوح نام ایستا (Static Name Resolution) در زمان "کامپایل" در حال تکامل است، بنابراین به وضوح نام پویا (Dynamic Name Resolution) تکیه نکنید! (در واقع، متغیرهای محلی در حال حاضر به‌صورت ایستا تعیین می‌شوند.)

یک ویژگی خاص پایتون این است که - اگر هیچ دستور global یا nonlocal در حال اجرا نباشد - انتساب به نام‌ها همیشه به درونی‌ترین حوزه می‌رود. انتساب‌ها داده را کپی نمی‌کنند — آن‌ها فقط نام‌ها را به اشیاء متصل می‌کنند. همین امر در مورد حذف‌ها نیز صادق است: دستور del x اتصال x را از فضایی که توسط حوزه محلی ارجاع داده شده است، حذف می‌کند. در واقع، تمام عملیاتی که نام‌های جدید را معرفی می‌کنند، از حوزه محلی استفاده می‌کنند: به‌طور خاص، دستورات import و تعاریف توابع، نام ماژول یا تابع را در حوزه محلی متصل می‌کنند.

از دستور global می‌توان برای نشان دادن اینکه متغیرهای خاصی در حوزه سراسری زندگی می‌کنند و باید در آنجا دوباره متصل شوند، استفاده کرد؛ دستور nonlocal نشان می‌دهد که متغیرهای خاصی در یک حوزه محصورکننده (Enclosing Scope) زندگی می‌کنند و باید در آنجا دوباره متصل شوند.

۹.۲.۱. مثال حوزه‌ها و فضاهای نام

این مثالی است که نحوه ارجاع به حوزه‌ها و فضاهای نام مختلف و نحوه تأثیر global و nonlocal بر اتصال متغیرها را نشان می‌دهد:

>>> def scope_test():
...     def do_local():
...         spam = "local spam"
...     def do_nonlocal():
...         nonlocal spam
...         spam = "nonlocal spam"
...     def do_global():
...         global spam
...         spam = "global spam"
...     spam = "test spam"
...     do_local()
...     print("After local assignment:", spam)
...     do_nonlocal()
...     print("After nonlocal assignment:", spam)
...     do_global()
...     print("After global assignment:", spam)
...
>>> scope_test()
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
>>> print("In global scope:", spam)
In global scope: global spam

توجه کنید که چگونه انتساب محلی (که پیش‌فرض است) اتصال spam را در scope_test تغییر نداد. انتساب nonlocal اتصال spam را در scope_test تغییر داد، و انتساب global اتصال را در سطح ماژول تغییر داد.

همچنین می‌توانید ببینید که قبل از انتساب global، هیچ اتصال قبلی برای spam وجود نداشت.

۹.۳. نگاه اول به کلاس‌ها

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

۹.۳.۱. نحو تعریف کلاس (Class Definition Syntax)

ساده‌ترین شکل تعریف کلاس به این صورت است:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

تعاریف کلاس، مانند تعاریف توابع (دستورات def) باید قبل از اینکه تأثیری داشته باشند، اجرا شوند. (احتمالاً می‌توانید یک تعریف کلاس را در شاخه‌ای از یک دستور if یا داخل یک تابع قرار دهید.)

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

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

هنگامی که یک تعریف کلاس به‌طور عادی (از طریق پایان) ترک می‌شود، یک شیء کلاس (Class Object) ایجاد می‌شود. این اساساً یک پوشش (Wrapper) در اطراف محتویات فضای نام ایجادشده توسط تعریف کلاس است؛ در بخش بعدی بیشتر در مورد اشیاء کلاس خواهیم آموخت. حوزه محلی اصلی (حوزه‌ای که درست قبل از ورود به تعریف کلاس در حال اجرا بود) دوباره برقرار می‌شود و شیء کلاس در اینجا به نام کلاس داده‌شده در هدر تعریف کلاس (ClassName در مثال) متصل می‌شود.

۹.۳.۲. اشیاء کلاس (Class Objects)

اشیاء کلاس از دو نوع عملیات پشتیبانی می‌کنند: ارجاعات ویژگی (Attribute References) و نمونه‌سازی (Instantiation).

ارجاعات ویژگی از نحو استانداردی استفاده می‌کنند که برای تمام ارجاعات ویژگی در پایتون استفاده می‌شود: obj.name. نام‌های معتبر ویژگی، تمام نام‌هایی هستند که در زمان ایجاد شیء کلاس در فضای نام کلاس وجود داشتند. بنابراین، اگر تعریف کلاس به این شکل بود:

>>> class MyClass:
...     """A simple example class"""
...     i = 12345
...     def f(self):
...         return 'hello world'
...

آنگاه MyClass.i و MyClass.f ارجاعات ویژگی معتبری هستند که به ترتیب یک عدد صحیح و یک شیء تابع را برمی‌گردانند. ویژگی‌های کلاس همچنین قابل انتساب هستند، بنابراین می‌توانید مقدار MyClass.i را با انتساب تغییر دهید. __doc__ نیز یک ویژگی معتبر است که رشته مستندات (Docstring) متعلق به کلاس را برمی‌گرداند: "A simple example class".

نمونه‌سازی (Instantiation) کلاس از نماد تابع (Function Notation) استفاده می‌کند. فقط تصور کنید که شیء کلاس یک تابع بدون پارامتر است که یک نمونه جدید از کلاس را برمی‌گرداند. برای مثال (با فرض کلاس فوق):

>>> x = MyClass()

یک نمونه (Instance) جدید از کلاس ایجاد می‌کند و این شیء را به متغیر محلی x اختصاص می‌دهد.

عملیات نمونه‌سازی ("فراخوانی" یک شیء کلاس) یک شیء خالی ایجاد می‌کند. بسیاری از کلاس‌ها دوست دارند اشیایی با نمونه‌های سفارشی‌سازی‌شده برای یک حالت اولیه خاص ایجاد کنند. بنابراین یک کلاس ممکن است یک متد ویژه به نام __init__() تعریف کند، مانند این:

>>> def __init__(self):
...     self.data = []
...

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

>>> x = MyClass()

البته، متد __init__() ممکن است برای انعطاف‌پذیری بیشتر، آرگومان‌هایی داشته باشد. در آن صورت، آرگومان‌های داده‌شده به عملگر نمونه‌سازی کلاس به __init__() منتقل می‌شوند. برای مثال:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)

۹.۳.۳. اشیاء نمونه (Instance Objects)

حالا با اشیاء نمونه چه کاری می‌توانیم انجام دهیم؟ تنها عملیاتی که توسط اشیاء نمونه درک می‌شوند، ارجاعات ویژگی هستند. دو نوع نام ویژگی معتبر وجود دارد: ویژگی‌های داده (Data Attributes) و متدها.

ویژگی‌های داده معادل "متغیرهای نمونه" در Smalltalk و "اعضای داده" در C++ هستند. ویژگی‌های داده نیازی به اعلان ندارند؛ مانند متغیرهای محلی، آن‌ها به محض اولین انتساب، به‌طور ناگهانی به وجود می‌آیند (Spring into existence). برای مثال، اگر x نمونه MyClass ایجادشده در بالا باشد، قطعه کد زیر مقدار 16 را چاپ می‌کند، بدون اینکه اثری از خود باقی بگذارد:

>>> x.counter = 1
>>> while x.counter < 10:
...     x.counter = x.counter * 2
...
>>> print(x.counter)
16
>>> del x.counter

نوع دیگر ارجاع ویژگی نمونه، یک متد (Method) است. یک متد، تابعی است که "متعلق به" یک شیء است.

نام‌های متد معتبر یک شیء نمونه به کلاس آن بستگی دارد. طبق تعریف، تمام ویژگی‌های یک کلاس که اشیاء تابع هستند، متدهای متناظری را برای نمونه‌های خود تعریف می‌کنند. بنابراین در مثال ما، x.f یک ارجاع متد معتبر است، زیرا MyClass.f یک تابع است، اما x.i معتبر نیست، زیرا MyClass.i یک تابع نیست. اما x.f همان چیزی نیست که MyClass.f است — این یک شیء متد (Method Object) است، نه یک شیء تابع.

۹.۳.۴. اشیاء متد (Method Objects)

معمولاً، یک متد بلافاصله پس از اتصال (Binding) فراخوانی می‌شود:

>>> x.f()
'hello world'

اگر x = MyClass() مانند بالا باشد، این کار رشته 'hello world' را برمی‌گرداند. با این حال، لازم نیست یک متد را بلافاصله فراخوانی کنید: x.f یک شیء متد است و می‌توان آن را ذخیره کرد و بعداً فراخوانی کرد. برای مثال:

>>> xf = x.f
>>> while True:
...     print(xf())
...     break  # Added break to prevent infinite loop
...
hello world

دقیقاً وقتی یک متد فراخوانی می‌شود، چه اتفاقی می‌افتد؟ ممکن است متوجه شده باشید که x.f() در بالا بدون آرگومان فراخوانی شد، حتی اگر تعریف تابع برای f() یک آرگومان را مشخص کرده بود. چه بر سر آن آرگومان آمد؟ مطمئناً پایتون وقتی یک تابع که نیاز به آرگومان دارد بدون هیچ آرگومانی فراخوانی شود، یک استثنا ایجاد می‌کند — حتی اگر آرگومان در واقع استفاده نشود…

در واقع، ممکن است پاسخ را حدس زده باشید: نکته ویژه در مورد متدها این است که شیء نمونه به‌عنوان اولین آرگومان تابع ارسال می‌شود. در مثال ما، فراخوانی x.f() دقیقاً معادل MyClass.f(x) است. به‌طور کلی، فراخوانی یک متد با لیستی از n آرگومان، معادل فراخوانی تابع متناظر با لیست آرگومانی است که با قرار دادن شیء نمونه متد قبل از اولین آرگومان ایجاد می‌شود.

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

۹.۳.۵. متغیرهای کلاس و نمونه (Class and Instance Variables)

به‌طور کلی، متغیرهای نمونه برای داده‌های منحصر به فرد هر نمونه هستند و متغیرهای کلاس برای ویژگی‌ها و متدهایی که توسط تمام نمونه‌های کلاس به اشتراک گذاشته می‌شوند:

>>> class Dog:
...     kind = 'canine'         # class variable shared by all instances
...     def __init__(self, name):
...         self.name = name    # instance variable unique to each instance
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

همان‌طور که در یک نکته درباره نام‌ها و اشیاء بحث شد، داده‌های مشترک (Shared Data) می‌توانند اثرات شگفت‌آوری شامل اشیاء تغییرپذیر (Mutable) مانند لیست‌ها و دیکشنری‌ها داشته باشند. برای مثال، لیست tricks در کد زیر نباید به‌عنوان یک متغیر کلاس استفاده شود زیرا فقط یک لیست بین تمام نمونه‌های Dog به اشتراک گذاشته می‌شود:

>>> class Dog:
...     tricks = []             # mistaken use of a class variable
...     def __init__(self, name):
...         self.name = name
...     def add_trick(self, trick):
...         self.tricks.append(trick)
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

طراحی صحیح کلاس باید به جای آن از یک متغیر نمونه استفاده کند:

>>> class Dog:
...     def __init__(self, name):
...         self.name = name
...         self.tricks = []    # creates a new empty list for each dog
...     def add_trick(self, trick):
...         self.tricks.append(trick)
...
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

۹.۴. نکات پراکنده (Random Remarks)

اگر همان نام ویژگی هم در یک نمونه و هم در یک کلاس وجود داشته باشد، جست‌وجوی ویژگی به نمونه اولویت می‌دهد:

>>> class Warehouse:
...     purpose = 'storage'
...     region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

ویژگی‌های داده ممکن است توسط متدها و همچنین توسط کاربران معمولی ("مشتریان" یا Clients) یک شیء ارجاع داده شوند. به عبارت دیگر، کلاس‌ها برای پیاده‌سازی انواع داده انتزاعی (Abstract Data Types) خالص قابل استفاده نیستند. در واقع، هیچ چیز در پایتون امکان اعمال پنهان‌سازی داده (Data Hiding) را فراهم نمی‌کند — همه چیز بر اساس قرارداد است. (از طرف دیگر، پیاده‌سازی پایتون که به زبان C نوشته شده است، می‌تواند جزئیات پیاده‌سازی را کاملاً پنهان کند و در صورت لزوم دسترسی به یک شیء را کنترل کند؛ این می‌تواند توسط افزونه‌های (Extensions) پایتون که به زبان C نوشته شده‌اند، استفاده شود.)

مشتریان باید با دقت از ویژگی‌های داده استفاده کنند — مشتریان ممکن است با دستکاری در ویژگی‌های داده خود، ناوردایی‌ها (Invariants) را که توسط متدها حفظ می‌شوند، به هم بزنند. توجه داشته باشید که مشتریان ممکن است ویژگی‌های داده خود را به یک شیء نمونه اضافه کنند بدون اینکه بر اعتبار متدها تأثیر بگذارند، تا زمانی که از تضاد نام‌ها (Name Conflicts) جلوگیری شود — باز هم، یک قرارداد نام‌گذاری می‌تواند در اینجا از بسیاری از سردردها جلوگیری کند.

هیچ میانبری برای ارجاع به ویژگی‌های داده (یا دیگر متدها!) از داخل متدها وجود ندارد. من معتقدم که این در واقع خوانایی متدها را افزایش می‌دهد: هنگام مرور یک متد، هیچ شانسی برای اشتباه گرفتن متغیرهای محلی و متغیرهای نمونه وجود ندارد.

اغلب، اولین آرگومان یک متد self نامیده می‌شود. این چیزی بیش از یک قرارداد نیست: نام self مطلقاً هیچ معنای خاصی برای پایتون ندارد. با این حال، توجه داشته باشید که با رعایت نکردن این قرارداد، ممکن است کد شما برای سایر برنامه‌نویسان پایتون کمتر خوانا شود، و همچنین قابل تصور است که برنامه‌ای به نام Class Browser نوشته شود که به چنین قراردادی متکی باشد.

هر شیء تابعی که یک ویژگی کلاس باشد، یک متد برای نمونه‌های آن کلاس تعریف می‌کند. لازم نیست که تعریف تابع به‌صورت متنی در تعریف کلاس محصور شده باشد: اختصاص یک شیء تابع به یک متغیر محلی در کلاس نیز خوب است. برای مثال:

>>> # Function defined outside the class
>>> def f1(self, x, y):
...     return min(x, x+y)
...
>>> class C:
...     f = f1
...     def g(self):
...         return 'hello world'
...     h = g
...

اکنون f، g و h همگی ویژگی‌های کلاس C هستند که به اشیاء تابع ارجاع می‌دهند و در نتیجه همه آن‌ها متدهای نمونه‌های C هستند — h دقیقاً معادل g است. توجه داشته باشید که این عمل معمولاً فقط باعث سردرگمی خواننده یک برنامه می‌شود.

متدها می‌توانند متدهای دیگر را با استفاده از ویژگی‌های متد آرگومان self فراخوانی کنند:

>>> class Bag:
...     def __init__(self):
...         self.data = []
...     def add(self, x):
...         self.data.append(x)
...     def addtwice(self, x):
...         self.add(x)
...         self.add(x)
...

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

هر مقداری یک شیء است و بنابراین دارای یک کلاس (که نوع آن نیز نامیده می‌شود) است. این به صورت object.__class__ ذخیره می‌شود.

۹.۵. وراثت (Inheritance)

البته، یک ویژگی زبانی بدون پشتیبانی از وراثت، شایسته نام "کلاس" نخواهد بود. نحو برای تعریف یک کلاس مشتق‌شده (Derived Class) به این صورت است:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

نام BaseClassName باید در یک فضای نام قابل دسترسی از حوزه حاوی تعریف کلاس مشتق‌شده تعریف شده باشد. به جای یک نام کلاس پایه، سایر عبارات دلخواه نیز مجاز هستند. این می‌تواند مفید باشد، برای مثال، زمانی که کلاس پایه در ماژول دیگری تعریف شده است:

class DerivedClassName(modname.BaseClassName):

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

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

کلاس‌های مشتق‌شده ممکن است متدهای کلاس‌های پایه خود را بازنویسی (Override) کنند. از آنجا که متدها هنگام فراخوانی متدهای دیگر همان شیء، امتیازات خاصی ندارند، یک متد از یک کلاس پایه که متد دیگری را که در همان کلاس پایه تعریف شده است فراخوانی می‌کند، ممکن است در نهایت متدی از یک کلاس مشتق‌شده را که آن را بازنویسی کرده است، فراخوانی کند. (برای برنامه‌نویسان C++: تمام متدها در پایتون به‌طور مؤثر virtual هستند.)

یک متد بازنویسی‌شده در یک کلاس مشتق‌شده ممکن است در واقع بخواهد به جای اینکه به‌سادگی متد کلاس پایه را با همان نام جایگزین کند، آن را گسترش دهد. یک راه ساده برای فراخوانی مستقیم متد کلاس پایه وجود دارد: فقط کافی است BaseClassName.methodname(self, arguments) را فراخوانی کنید. این گاهی اوقات برای مشتریان نیز مفید است. (توجه داشته باشید که این فقط زمانی کار می‌کند که کلاس پایه به‌عنوان BaseClassName در حوزه سراسری قابل دسترسی باشد.)

پایتون دو تابع توکار دارد که با وراثت کار می‌کنند:

  • از isinstance() برای بررسی نوع یک نمونه استفاده کنید: isinstance(obj, int) فقط در صورتی True خواهد بود که obj.__class__ از نوع int یا برخی کلاس‌های مشتق‌شده از int باشد.
  • از issubclass() برای بررسی وراثت کلاس استفاده کنید: issubclass(bool, int) True است زیرا bool زیرکلاسی از int است. با این حال، issubclass(float, int) False است زیرا float زیرکلاسی از int نیست.

۹.۵.۱. وراثت چندگانه (Multiple Inheritance)

پایتون از نوعی وراثت چندگانه نیز پشتیبانی می‌کند. یک تعریف کلاس با چندین کلاس پایه به این صورت است:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

برای بیشتر اهداف، در ساده‌ترین موارد، می‌توانید جست‌وجوی ویژگی‌های به ارث برده‌شده از یک کلاس والد را به‌صورت عمق-اول (Depth-first)، چپ-به-راست (Left-to-right) در نظر بگیرید، بدون اینکه دو بار در همان کلاس در جایی که همپوشانی (Overlap) در سلسله‌مراتب وجود دارد، جست‌وجو کند. بنابراین، اگر یک ویژگی در DerivedClassName یافت نشد، در Base1 و سپس (به‌صورت بازگشتی) در کلاس‌های پایه Base1 جست‌وجو می‌شود، و اگر در آنجا یافت نشد، در Base2 جست‌وجو می‌شود، و به همین ترتیب.

در واقع، این کمی پیچیده‌تر از آن است؛ ترتیب وضوح متد (Method Resolution Order) به‌صورت پویا تغییر می‌کند تا از فراخوانی‌های مشارکتی (Cooperative Calls) به super() پشتیبانی کند. این رویکرد در برخی دیگر از زبان‌های دارای وراثت چندگانه به‌عنوان call-next-method شناخته می‌شود و قدرتمندتر از فراخوانی super موجود در زبان‌های دارای وراثت تکی (Single-inheritance) است.

ترتیب پویا ضروری است زیرا تمام موارد وراثت چندگانه یک یا چند رابطه الماسی (Diamond Relationships) را نشان می‌دهند (جایی که حداقل یکی از کلاس‌های والد را می‌توان از طریق چندین مسیر از پایین‌ترین کلاس دسترسی داشت). برای مثال، همه کلاس‌ها از object به ارث می‌برند، بنابراین هر موردی از وراثت چندگانه بیش از یک مسیر برای رسیدن به object فراهم می‌کند. برای جلوگیری از دسترسی بیش از یک بار به کلاس‌های پایه، الگوریتم پویا، ترتیب جست‌وجو را به‌گونه‌ای خطی می‌کند (Linearize) که ترتیب چپ-به-راست مشخص‌شده در هر کلاس را حفظ کند، هر والد را فقط یک بار فراخوانی کند و یکنواخت (Monotonic) باشد (به این معنی که یک کلاس می‌تواند زیرکلاس (Subclass) شود بدون اینکه بر ترتیب اولویت والدین آن تأثیر بگذارد). این ویژگی‌ها در کنار هم، طراحی کلاس‌های قابل اعتماد و قابل توسعه با وراثت چندگانه را ممکن می‌سازند. برای جزئیات بیشتر، به The Python 2.3 Method Resolution Order مراجعه کنید.

۹.۶. متغیرهای خصوصی (Private Variables)

متغیرهای نمونه "خصوصی" که نمی‌توان از خارج از یک شیء به آن‌ها دسترسی داشت، در پایتون وجود ندارند. با این حال، یک قرارداد وجود دارد که توسط بیشتر کدهای پایتون دنبال می‌شود: نامی که با زیرخط (Underscore) پیشوند شده است (مانند _spam) باید به‌عنوان بخش غیرعمومی (Non-public) از API (چه یک تابع، چه یک متد یا یک عضو داده) در نظر گرفته شود. باید به‌عنوان جزئیات پیاده‌سازی (Implementation Detail) در نظر گرفته شود و بدون اطلاع قبلی قابل تغییر است.

از آنجا که یک مورد استفاده معتبر برای اعضای خصوصی کلاس (یعنی برای جلوگیری از تضاد نام‌ها با نام‌های تعریف‌شده توسط زیرکلاس‌ها) وجود دارد، پشتیبانی محدودی برای چنین مکانیزمی، به نام درهم‌ریختگی نام (Name Mangling)، وجود دارد. هر شناسه‌ای به شکل __spam (حداقل دو زیرخط ابتدایی، حداکثر یک زیرخط انتهایی) به‌صورت متنی با _classname__spam جایگزین می‌شود، جایی که classname نام کلاس جاری با حذف زیرخط‌ (های) ابتدایی است. این نام‌گذاری (Mangling) بدون توجه به موقعیت نحوی (Syntactic Position) شناسه، تا زمانی که در داخل تعریف یک کلاس رخ دهد، انجام می‌شود.

همچنین ببینید: مشخصات Private Name Mangling برای جزئیات و موارد خاص.

درهم‌ریختگی نام برای این مفید است که به زیرکلاس‌ها اجازه دهد متدها را بدون شکستن فراخوانی‌های متد درون‌کلاسی (Intraclass) بازنویسی کنند. برای مثال:

>>> class Mapping:
...     def __init__(self, iterable):
...         self.items_list = []
...         self.__update(iterable)
...     def update(self, iterable):
...         for item in iterable:
...             self.items_list.append(item)
...     __update = update   # private copy of original update() method
...
>>> class MappingSubclass(Mapping):
...     def update(self, keys, values):
...         # provides new signature for update()
...         # but does not break __init__()
...         for item in zip(keys, values):
...             self.items_list.append(item)
...

مثال بالا حتی اگر MappingSubclass یک شناسه __update معرفی کند، کار می‌کند زیرا در کلاس Mapping با _Mapping__update و در کلاس MappingSubclass با _MappingSubclass__update جایگزین می‌شود.

توجه داشته باشید که قوانین Mangling عمدتاً برای جلوگیری از حوادث طراحی شده‌اند؛ هنوز هم امکان دسترسی یا اصلاح یک متغیر که خصوصی در نظر گرفته می‌شود، وجود دارد. این حتی می‌تواند در شرایط خاص، مانند دیباگر (Debugger)، مفید باشد.

توجه داشته باشید که کدی که به exec() یا eval() ارسال می‌شود، نام کلاسِ کلاس فراخواننده را به‌عنوان کلاس جاری در نظر نمی‌گیرد؛ این مشابه تأثیر دستور global است که تأثیر آن نیز به کدی که با هم بایت-کامپایل (Byte-compiled) می‌شود، محدود است. همین محدودیت برای getattr()، setattr() و delattr() و همچنین هنگام ارجاع مستقیم به __dict__ اعمال می‌شود.

۹.۷. متفرقه (Odds and Ends)

گاهی اوقات داشتن یک نوع داده مشابه "رکورد" (Record) پاسکال یا "ساختار" (Struct) C، که چند آیتم داده نام‌گذاری‌شده را با هم جمع می‌کند، مفید است. رویکرد اصطلاحی (Idiomatic) استفاده از dataclasses برای این منظور است:

>>> from dataclasses import dataclass
>>>
>>> @dataclass
... class Employee:
...     name: str
...     dept: str
...     salary: int
...
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000

یک قطعه کد پایتون که یک نوع داده انتزاعی خاص را انتظار دارد، اغلب می‌تواند یک کلاس را دریافت کند که متدهای آن نوع داده را شبیه‌سازی (Emulate) کند. برای مثال، اگر تابعی دارید که داده‌ها را از یک شیء فایل قالب‌بندی می‌کند، می‌توانید کلاسی را با متدهای read() و readline() تعریف کنید که داده‌ها را از یک بافر رشته (String Buffer) به جای آن دریافت می‌کند و آن را به‌عنوان آرگومان ارسال می‌کنید.

اشیاء متد نمونه (Instance Method Objects) نیز ویژگی دارند: m.__self__ شیء نمونه با متد m() است، و m.__func__ شیء تابع متناظر با متد است.

۹.۸. تکرارگرها (Iterators)

تا کنون احتمالاً متوجه شده‌اید که بیشتر اشیاء ظرف (Container Objects) را می‌توان با استفاده از یک دستور for روی آن‌ها حلقه زد:

>>> for element in [1, 2, 3]:
...     print(element)
...
1
2
3
>>> for element in (1, 2, 3):
...     print(element)
...
1
2
3
>>> for key in {'one': 1, 'two': 2}:
...     print(key)
...
one
two
>>> for char in "123":
...     print(char)
...
1
2
3
>>> for line in open("myfile.txt"):
...     print(line, end='')
...

این سبک دسترسی واضح، مختصر و راحت است. استفاده از تکرارگرها (Iterators) در پایتون فراگیر است و آن را یکپارچه می‌کند. پشت صحنه، دستور for تابع iter() را روی شیء ظرف فراخوانی می‌کند. این تابع یک شیء تکرارگر را برمی‌گرداند که متد __next__() را تعریف می‌کند و عناصر را یکی‌یکی در ظرف دسترسی می‌دهد. وقتی عناصر بیشتری وجود نداشته باشد، __next__() یک استثنای StopIteration ایجاد می‌کند که به حلقه for می‌گوید که خاتمه یابد. می‌توانید متد __next__() را با استفاده از تابع توکار next() فراخوانی کنید؛ این مثال نشان می‌دهد که همه چیز چگونه کار می‌کند:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

با دیدن مکانیزم پشت پروتکل تکرارگر، اضافه کردن رفتار تکرارگر به کلاس‌های خود آسان است. یک متد __iter__() تعریف کنید که یک شیء با متد __next__() را برمی‌گرداند. اگر کلاس __next__() را تعریف کند، آنگاه __iter__() می‌تواند فقط self را برگرداند:

>>> class Reverse:
...     """Iterator for looping over a sequence backwards."""
...     def __init__(self, data):
...         self.data = data
...         self.index = len(data)
...     def __iter__(self):
...         return self
...     def __next__(self):
...         if self.index == 0:
...             raise StopIteration
...         self.index = self.index - 1
...         return self.data[self.index]
...
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

۹.۹. مولدها (Generators)

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

>>> def reverse(data):
...     for index in range(len(data)-1, -1, -1):
...         yield data[index]
...
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

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

یکی دیگر از ویژگی‌های کلیدی این است که متغیرهای محلی و وضعیت اجرا به‌طور خودکار بین فراخوانی‌ها ذخیره می‌شوند. این باعث شد تابع نوشتن آسان‌تر و بسیار واضح‌تر از رویکردی باشد که از متغیرهای نمونه مانند self.index و self.data استفاده می‌کند.

علاوه بر ایجاد خودکار متد و ذخیره وضعیت برنامه، زمانی که مولدها خاتمه می‌یابند، به‌طور خودکار StopIteration ایجاد می‌کنند. در ترکیب، این ویژگی‌ها ایجاد تکرارگرها را با تلاشی بیش از نوشتن یک تابع معمولی آسان می‌کند.

۹.۱۰. عبارت‌های مولد (Generator Expressions)

برخی از مولدهای ساده را می‌توان به‌صورت مختصر به‌عنوان عبارت با استفاده از نحوی مشابه درک لیست (List Comprehensions) اما با پرانتز به جای براکت‌های مربع، کدنویسی کرد. این عبارات برای موقعیت‌هایی طراحی شده‌اند که مولد بلافاصله توسط یک تابع محصورکننده استفاده می‌شود. عبارت‌های مولد فشرده‌تر اما کمتر از تعاریف کامل مولد همه‌کاره هستند و تمایل دارند نسبت به درک لیست معادل، حافظه-دوست‌تر (Memory Friendly) باشند.

مثال‌ها:

>>> sum(i*i for i in range(10))         # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x, y in zip(xvec, yvec)) # dot product
260

>>> unique_words = set(word for line in page for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

پانوشت‌ها

[1] به‌جز یک مورد. اشیاء ماژول یک ویژگی مخفی فقط‌خواندنی به نام __dict__ دارند که دیکشنری مورد استفاده برای پیاده‌سازی فضای نام ماژول را برمی‌گرداند؛ نام __dict__ یک ویژگی است اما یک نام سراسری نیست. بدیهی است که استفاده از این امر، انتزاع (Abstraction) پیاده‌سازی فضای نام را نقض می‌کند و باید به مواردی مانند دیباگرهای پس از مرگ (Post-mortem Debuggers) محدود شود.