9. کلاسها¶
کلاسها وسیلهای را برای بستهبندی کردن داده و عملکرد با هم فراهم میکنند. ایجاد یک کلاس جدید، نوع جدیدی ایجاد میکند که اجازه میدهد موارد جدیدی از آن نوع ساخته شود. هر نمونه از کلاس میتواند صفاتی داشته باشد که به آن متصل شدهاند تا وضعیت آن را حفظ کنند. نمونههای کلاس میتوانند همچنین متدهایی (که توسط کلاس تعریف شدهاند) داشته باشند تا وضعیت آن را تغییر دهند.
در مقایسه با دیگر زبانهای برنامهنویسی، مکانیزم کلاس در پایتون کلاسها را با حداقل قواعد و معناشناسی جدید اضافه میکند. این مکانیزم ترکیبی از مکانیزمهای کلاسی است که در C++ و Modula-3 یافت میشود. کلاسهای پایتون تمام ویژگیهای استاندارد برنامهنویسی شیءگرا را فراهم میکنند: مکانیزم ارث بری کلاسها اجازه میدهد کلاسهای پایهی متعددی داشته باشید، یک کلاس مشتق میتواند هر یک از متدهای کلاسهای پایهی خود را بازنویسی کند و یک متد میتواند متدی از کلاس پایه با همان نام را فراخوانی کند. اشیاء میتوانند حاوی مقادیر و انواع دادهای دلخواه باشند. همانطور که برای ماژولها صادق است، کلاسها نیز از طبیعت پویا در پایتون برخوردارند: آنها در زمان اجرا ایجاد میشوند و میتوانند پس از ایجاد بیشتر تغییر داده شوند.
در اصطلاحات C++، معمولاً اعضای کلاس (از جمله اعضای داده) public هستند (به جز موردی که در پایین متغیرهای خصوصی ذکر شده است)، و تمامی توابع عضو مجازی هستند. همانطور که در Modula-3، هیچ میانبرهایی برای ارجاع به اعضای شیء از متدهای آن وجود ندارد: تابع متد با یک آرگومان اول صریح که نمایانگر شیء است، اعلام میشود که این آرگومان به صورت ضمنی توسط فراخوانی فراهم میشود. همانطور که در Smalltalk، خود کلاسها اشیاء هستند. این ویژگی معناشناسی برای وارد کردن و تغییر نام را فراهم میآورد. برخلاف C++ و Modula-3، نوعهای داخلی میتوانند بهعنوان کلاسهای پایه برای توسعه توسط کاربر استفاده شوند. همچنین، مانند C++، بیشتر عملگرهای داخلی با نحوی خاص (عملگرهای ریاضی، زیرنویسی و غیره) میتوانند برای نمونههای کلاس بازتعریف شوند.
(با نبود اصطلاحات پذیرفته شده جهانی برای صحبت دربارهی کلاسها، گاهی از اصطلاحات Smalltalk و C++ استفاده خواهم کرد. من از اصطلاحات Modula-3 استفاده میکردم، چون معنای شیءگرای آن به پایتون نزدیکتر از C++ است، اما انتظار دارم که تعداد کمی از خوانندگان با آن آشنا باشند.)
9.1. سخنی دربارهی نامها و اشیاء¶
اشیاء هویت فردی دارند و چندین نام (در چندین محدوده) میتوانند به یک شیء مشخص متصل شوند. این در زبانهای دیگر با نام «اسمهای مستعار» شناخته میشود. این ویژگی معمولاً در نگاه اول به پایتون چندان جلب توجه نمیکند و هنگام کار با انواع پایهی تغییرناپذیر (اعداد، رشتهها، تاپلها) میتوان آن را نادیده گرفت. با این حال، نامهای مستعار میتوانند تأثیری شگفتآور بر معناشناسی کد پایتونی داشته باشند، بهویژه هنگامی که با اشیاء تغییرپذیر مانند فهرستها، دیکشنریها و بیشتر انواع دیگر سروکار داریم. این ویژگی معمولاً به سود برنامه تمام میشود، چون نامهای مستعار از برخی جهات مانند اشارهگرها (pointer) رفتار میکنند. برای مثال، ارسال یک شیء بهعنوان آرگومان ارزان است، چون پیادهسازی فقط یک اشارهگر را ارسال میکند؛ و اگر تابعی شیءای را که بهعنوان آرگومان دریافت کرده تغییر دهد، فراخواننده تغییر را مشاهده خواهد کرد --- این کار نیاز به دو مکانیزم متفاوت برای ارسال آرگومان (مانند آنچه در پاسکال وجود دارد) را از بین میبرد.
9.2. محدودهها و فضای نامها در پایتون¶
قبل از معرفی کلاسها، ابتدا باید چیزی دربارهی قوانین محدوده در پایتون به شما بگویم. تعریف کلاسها چند ترفند جالب با فضاهای نام انجام میدهد و برای اینکه بهطور کامل متوجه شوید چه میگذرد، باید بدانید که محدودهها و فضاهای نام چگونه کار میکنند. ضمناً، آگاهی از این موضوع برای هر برنامهنویس پیشرفتهی پایتون مفید است.
بیایید با چند تعریف شروع کنیم.
«فضای نام» namespace نگاشتی است از نامها به اشیا. بیشتر فضای نامها در حال حاضر بهصورت دیکشنریهای پایتون پیادهسازی شدهاند، اما معمولاً این موضوع (به جز در بازدهی قابل مشاهده نیست و ممکن است در آینده تغییر کند. مثالهایی از فضای نام عبارتاند از: مجموعه نامهای داخلی (که شامل توابعی مثل abs() و نامهای استثنائات داخلی است)؛ نامهای سراسری در یک ماژول؛ و نامهای محلی در یک فراخوانی تابع. به نوعی، مجموعه صفات «attributes» یک شیء نیز یک فضای نام را تشکیل میدهد. نکتهی مهم دربارهی فضای نامها این است که هیچ ارتباطی بین نامها در فضای نامهای مختلف وجود ندارد؛ برای مثال، دو ماژول مختلف میتوانند هر دو تابعی با نام maximize تعریف کنند بدون اینکه ابهامی پیش بیاید — استفادهکنندگان از ماژولها باید نام ماژول را بهعنوان پیشوند بیاورند.
بههرحال، من واژهی ویژگی «attribute» را برای هر نامی که بعد از یک نقطه بیاید به کار میبرم — برای مثال، در عبارت z.real, real یک ویژگی شیء z است. از نظر دقیقتر، ارجاع به نامها در ماژولها نیز نوعی ارجاع به ویژگی محسوب میشود: در عبارت modname.funcname, modname یک شیء ماژول است و funcname یکی از ویژگیهای آن است. در این حالت، نگاشتی ساده و مستقیم بین ویژگیهای ماژول و نامهای سراسری تعریفشده در آن وجود دارد: هر دو یک فضای نام (namespace) را به اشتراک میگذارند! [1]
ویژگیها ممکن است فقط خواندنی یا قابل نوشتن باشند. در حالت دوم، اختصاص دادن به ویژگیها ممکن است. ویژگیهای ماژول قابل نوشتن هستند: میتوانید modname.the_answer = 42 را بنویسید. ویژگیهای قابل نوشتن را میتوان با دستور del نیز حذف کرد. بهعنوان مثال، del modname.the_answer ویژگی the_answer را از شیئی که توسط modname نامگذاری شده است حذف خواهد کرد.
فضاهای نام در لحظات مختلف ایجاد میشوند و دارای طول عمرهای متفاوتی هستند. فضای نام که شامل نامهای داخلی است، زمانی که مفسر پایتون شروع به کار میکند ایجاد میشود و هرگز حذف نمیشود. فضای نام عمومی برای یک ماژول زمانی که تعریف ماژول خوانده میشود ایجاد میگردد؛ بهطور معمول، فضاهای نام ماژول نیز تا زمانی که مفسر پایان یابد باقی میمانند. دستوراتی که توسط اجرای سطح بالای مفسر، یا از فایل اسکریپت خوانده شده یا بهطور تعاملی اجرا میشوند، بهعنوان بخشی از ماژولی به نام __main__ در نظر گرفته میشوند، بنابراین آنها فضای نام عمومی خود را دارند. (نامهای درونساخت نیز در واقع در یک ماژول قرار دارند؛ این ماژول builtins نامیده میشود.)
فضای نام محلی برای یک تابع زمانی ایجاد میشود که تابع فراخوانی شود و زمانی که تابع بازمیگردد یا استثناای پرتاب میکند که در درون تابع مدیریت نمیشود، حذف میگردد. (در واقع، «فراموش شدن» توصیف بهتری برای چیزی است که واقعاً اتفاق میافتد.) البته، هر فراخوانی بازگشتی، فضای نام محلی خودش را دارد.
یک دامنه «scope» یک ناحیهی متنی از یک برنامهی پایتون است که در آن یک فضای نام «namespace» بهطور مستقیم قابل دستیابی است. "بهطور مستقیم قابل دستیابی" در اینجا به این معنی است که یک ارجاع بدون صلاحیت به یک نام تلاش میکند تا نام «name» را در فضای نام «namespace» پیدا کند.
اگرچه محدودهها بهصورت ایستا تعیین میشوند، اما بهصورت پویا مورد استفاده قرار میگیرند. در هر زمان حین اجرا، ۳ یا ۴ محدودهی تو در تو وجود دارند که فضاهای نام آنها مستقیماً قابل دسترسی هستند:
درونیترین محدوده، که ابتدا جستوجو میشود، شامل نامهای محلی است
محدودههای هر تابع محاطشونده، که جستوجو از نزدیکترین محدودهی محاطشونده آغاز میشود، شامل نامهای غیرمحلی و همچنین غیرسراسری هستند
محدودهی قبل از آخر شامل نامهای عمومی ماژول جاری است
بیرونیترین محدوده (که آخر از همه جستوجو میشود) فضای نامی است که شامل نامهای توکار است
اگر یک نام بهعنوان سراسری اعلام شود، تمام ارجاعها و تخصیصها مستقیماً به محدودهی یکیماندهبهآخر که شامل نامهای سراسری ماژول است، میروند. برای اتصال مجدد متغیرهایی که در خارج از درونیترین محدوده یافت میشوند، میتوان از دستور nonlocal استفاده کرد؛ اگر بهعنوان nonlocal اعلام نشوند، آن متغیرها فقطخواندنی هستند (تلاش برای نوشتن در چنین متغیری، صرفاً یک متغیر محلی جدید در درونیترین محدوده ایجاد میکند و متغیر بیرونی با همان نام را بدون تغییر باقی میگذارد).
معمولاً، محدودهی محلی به نامهای محلی تابع جاری (از نظر متنی) ارجاع میدهد. در خارج از توابع، محدودهی محلی به همان فضای نامی ارجاع میدهد که محدودهی سراسری به آن ارجاع میدهد: فضای نام ماژول. تعاریف کلاسها، فضای نام دیگری را در محدودهی محلی قرار میدهند.
درک این نکته مهم است که محدودهها بهصورت متنی (بر اساس جایگاه در کد) تعیین میشوند: محدودهی سراسری تابعی که در یک ماژول تعریف شده است، همان فضای نام آن ماژول است، فرقی نمیکند که تابع از کجا یا با چه نام مستعاری فراخوانی شود. از سوی دیگر، جستوجوی واقعی برای نامها بهصورت پویا (داینامیک) و در زمان اجرا انجام میشود --- با این حال، تعریف زبان به سمت و سوی حلِ ایستا (استاتیک) نامها در زمان «کامپایل» پیش میرود، پس به حلِ پویای نامها اعتماد نکنید! (در واقع، متغیرهای محلی از قبل بهصورت ایستا تعیین میشوند.)
یک ویژگی خاص پایتون این است که --- اگر دستور global یا nonlocal فعال نباشد --- تخصیصهای (مقداردهیهای) نامها همیشه به درونیترین محدوده (محلیترین) میروند. تخصیصها دادهها را کپی نمیکنند --- بلکه فقط نامها را به اشیاء متصل میکنند. همین موضوع در مورد حذفها نیز صادق است: دستور del x اتصال x را از فضای نامی که توسط محدودهی محلی ارجاع داده شده است، حذف میکند. در واقع، تمام عملیاتی که نامهای جدیدی معرفی میکنند از محدودهی محلی استفاده میکنند: بهویژه، دستورات import و تعاریف توابع، نام ماژول یا تابع را در محدودهی محلی متصل میکنند.
از دستور global میتوان برای نشان دادن این استفاده کرد که متغیرهای خاصی در محدودهی سراسری قرار دارند و باید در همان محدوده دوباره متصل شوند؛ دستور nonlocal نشان میدهد که متغیرهای خاصی در یک محدودهی محاطشونده قرار دارند و باید در آن محدوده دوباره متصل شوند.
9.2.1. مثال محدودهها و فضای نامها¶
این مثالی است که نحوهی ارجاع به محدودهها و فضای نامهای مختلف، و تأثیر دستورات 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()
print("In global scope:", spam)
خروجی کد مثال:
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
توجه کنید که تخصیص محلی (که بهصورت پیشفرض است)، اتصال spam را در scope_test تغییر نداد. تخصیص nonlocal، اتصال spam را در scope_test تغییر داد، و تخصیص global، اتصال را در سطح ماژول تغییر داد.
همچنین میتوانید ببینید که قبل از تخصیص global، هیچ اتصال قبلی برای spam وجود نداشت.
9.3. در نگاه اول به کلاسها¶
کلاسها کمی نحو جدید، سه نوع شیء جدید، و مفاهیم معنایی جدیدی را معرفی میکنند.
9.3.1. قواعد تعریف کلاس¶
سادهترین شکل تعریف کلاس به این صورت است:
class ClassName:
<statement-1>
.
.
.
<statement-N>
تعاریف کلاسها، مانند تعاریف توابع (دستورات def)، باید قبل از اینکه تأثیری داشته باشند، اجرا شوند. (بهطور تئوری میتوانید یک تعریف کلاس را در یک شاخه از دستور if یا داخل یک تابع قرار دهید.)
در عمل، دستورات داخل یک تعریف کلاس معمولاً تعاریف توابع هستند، اما دستورات دیگری نیز مجاز هستند و گاهی مفیدند --- بعداً به این موضوع بازخواهیم گشت. تعاریف توابع داخل یک کلاس معمولاً دارای شکل خاصی از فهرست آرگومانها هستند که توسط قراردادهای فراخوانی برای متدها تعیین میشود --- باز هم، این موضوع بعداً توضیح داده میشود.
هنگامی که یک تعریف کلاس وارد میشود، یک فضای نام جدید ایجاد شده و بهعنوان محدودهی محلی استفاده میشود --- بنابراین، تمام تخصیصها به متغیرهای محلی به این فضای نام جدید میروند. بهویژه، تعاریف توابع، نام تابع جدید را در اینجا متصل میکنند.
هنگامی که یک تعریف کلاس بهطور عادی (با رسیدن به انتها) ترک میشود، یک شیء کلاس ایجاد میشود. این در اصل یک پوسته (wrapper) پیرامون محتویات فضای نامی است که توسط تعریف کلاس ایجاد شده است؛ در بخش بعدی بیشتر دربارهی اشیاء کلاس خواهیم آموخت. محدودهی محلی اصلی (همان محدودهای که درست قبل از ورود به تعریف کلاس فعال بود) بازیابی میشود، و شیء کلاس در اینجا به نام کلاسی که در سرآیند تعریف کلاس داده شده است، متصل میشود (ClassName در مثال).
9.3.2. اشیاء کلاس¶
اشیاء کلاس از دو نوع عملیات پشتیبانی میکنند: ارجاع به ویژگیها و نمونهسازی.
ارجاع به ویژگیها از همان دستور زبان استاندارد که برای همهی ارجاعها به ویژگیها در پایتون استفاده میشود، پیروی میکند: 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".
نمونهسازی کلاس از نماد توابع استفاده میکند. فقط تصور کنید که شیء کلاس، یک تابع بدون پارامتر است که یک نمونهی جدید از کلاس را برمیگرداند. برای مثال (با فرض کلاس بالا):
x = MyClass()
یک نمونه جدید از کلاس ایجاد میکند و این شیء را به متغیر محلی 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)
9.3.3. اشیاء نمونه¶
حالا با اشیاء نمونه چه کاری میتوانیم انجام دهیم؟ تنها عملیاتی که اشیاء نمونه میفهمند، ارجاع به ویژگیها (attribute references) است. دو نوع نام ویژگی معتبر وجود دارد: ویژگیهای داده (data attributes) و متدها (methods).
ویژگیهای داده معادل «متغیرهای نمونه» در زبان اسمالتاک و «عضوهای داده» در ++C هستند. ویژگیهای داده نیازی به اعلام ندارند؛ مانند متغیرهای محلی، هنگامی که برای اولین بار به آنها مقدار داده میشود، بهوجود میآیند. برای مثال، اگر x نمونهای از MyClass باشد که در بالا ایجاد شده است، قطعه کد زیر مقدار 16 را چاپ میکند، بدون اینکه هیچ اثری از خود بر جای بگذارد:
x.counter = 1
while x.counter < 10:
x.counter = x.counter * 2
print(x.counter)
del x.counter
نوع دیگر ارجاع به ویژگی نمونه، متد است. متد، تابعی است که «به» یک شیء تعلق دارد.
نامهای متد معتبر یک شیء نمونه به کلاس آن بستگی دارد. طبق تعریف، تمام ویژگیهای یک کلاس که شیء تابع هستند، متدهای متناظر با نمونههای آن را تعریف میکنند. بنابراین در مثال ما، x.f یک ارجاع متد معتبر است، زیرا MyClass.f یک تابع است، اما x.i معتبر نیست، زیرا MyClass.i تابع نیست. اما x.f با MyClass.f یکسان نیست --- بلکه یک شیء متد است، نه یک شیء تابع.
9.3.4. متد اشیاء¶
معمولاً، یک متد بلافاصله پس از اتصال فراخوانی میشود:
x.f()
اگر x = MyClass()، همانطور که در بالا ذکر شد، این (فراخوانی) رشتهی 'hello world' را برمیگرداند. با این حال، لزومی ندارد که یک متد بلافاصله فراخوانی شود: x.f یک شیء متد است و میتوان آن را ذخیره کرده و در زمان دیگری فراخوانی کرد. برای مثال:
xf = x.f
while True:
print(xf())
تا پایان زمان به چاپ hello world ادامه میدهد.
دقیقاً وقتی یک متد فراخوانی میشود، چه اتفاقی میافتد؟ احتمالاً متوجه شدهاید که x.f() در بالا بدون هیچ آرگومانی فراخوانی شد، حتی اگر تعریف تابع f() یک آرگومان را مشخص کرده بود. چه بر سر آن آرگومان آمد؟ مسلماً پایتون وقتی یک تابع که به آرگومان نیاز دارد، بدون هیچ آرگومانی فراخوانی میشود، یک استثنا ایجاد میکند --- حتی اگر آن آرگومان واقعاً استفاده نشود...
در واقع، شاید پاسخ را حدس زده باشید: نکتهی ویژه در مورد متدها این است که شیء نمونه بهعنوان اولین آرگومان تابع ارسال میشود. در مثال ما، فراخوانی x.f() دقیقاً معادل MyClass.f(x) است. به طور کلی، فراخوانی یک متد با فهرستی از n آرگومان معادل فراخوانی تابع متناظر با فهرست آرگومانی است که با قرار دادن شیء نمونهی متد قبل از اولین آرگومان ایجاد میشود.
به طور کلی، متدها به صورت زیر کار میکنند. هنگامی که به یک ویژگی غیردادهای از یک نمونه ارجاع داده میشود، کلاس آن نمونه جستوجو میشود. اگر آن نام نشاندهندهی یک ویژگی کلاس معتبر باشد که یک شیء تابع است، ارجاعات به هر دو شیء نمونه و شیء تابع در یک شیء متد بستهبندی میشوند. هنگامی که شیء متد با یک فهرست آرگومان فراخوانی میشود، یک فهرست آرگومان جدید از شیء نمونه و فهرست آرگومان ساخته میشود و شیء تابع با این فهرست آرگومان جدید فراخوانی میشود.
9.3.5. کلاس و متغیرهای نمونه¶
بهطور کلی، متغیرهای نمونه برای دادههایی هستند که مختص به هر نمونه و متغیرهای کلاس برای ویژگیها و متدهایی هستند که بین همهی نمونهها از آن کلاس مشترک هستند:
class Dog:
kind = 'canine' # متغیر کلاسی که بین همهٔ نمونهها مشترک است
def __init__(self, name):
self.name = name # متغیر نمونه که برای هر نمونه یکتا است
>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind # بین همهٔ سگها مشترک است
'canine'
>>> e.kind # بین همهٔ سگها مشترک است
'canine'
>>> d.name # مخصوص d است
'Fido'
>>> e.name # مخصوص e است
'Buddy'
همانطور که در سخنی دربارهی نامها و اشیاء بحث شد، دادههای مشترک میتوانند تأثیرات شگفتآوری بر روی اشیاء mutable مانند فهرستها و دیکشنریها داشته باشند. برای مثال، فهرست tricks در کد زیر نباید بهعنوان یک متغیر کلاس استفاده شود، زیرا فقط یک فهرست واحد بین همهی نمونههای Dog مشترک خواهد بود:
class Dog:
tricks = [] # استفادهٔ اشتباه از یک متغیر کلاسی
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 # بهطور غیرمنتظره بین همهٔ سگها مشترک است
['roll over', 'play dead']
طراحی صحیح کلاس باید به جای آن از یک متغیر نمونه استفاده کند:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # برای هر سگ، یک فهرست خالی جدید ایجاد میکند
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']
9.4. نکات پراکنده¶
اگر نام ویژگی مشابهی هم در یک نمونه و هم در یک کلاس وجود داشته باشد، آنگاه جستوجوی ویژگی، نمونه را اولویتبندی میکند:
>>> 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") یک شیء ارجاع داده شوند. به عبارت دیگر، کلاسها برای پیادهسازی انواع دادهی انتزاعی خالص قابل استفاده نیستند. در واقع، هیچ چیزی در پایتون امکان اعمال پنهانسازی داده را فراهم نمیکند --- همهچیز بر اساس قرارداد است. (از سوی دیگر، پیادهسازی پایتون که به زبان C نوشته شده است، میتواند جزئیات پیادهسازی را کاملاً پنهان کند و در صورت نیاز دسترسی به یک شیء را کنترل کند؛ این قابلیت میتواند توسط افزونههای پایتون که به زبان C نوشته میشوند، استفاده شود.)
کاربران (clients) باید با احتیاط از ویژگیهای داده استفاده کنند --- کاربران ممکن است ناورداهای حفظشده توسط متدها را با دستکاری در ویژگیهای دادهشان به هم بریزند. توجه داشته باشید که کاربران ممکن است ویژگیهای دادهی خود را به یک شیء نمونه اضافه کنند بدون اینکه بر صحت متدها تأثیر بگذارند، تا زمانی که تداخل نامها اجتناب شود --- باز هم، یک قرارداد نامگذاری میتواند از بسیاری از سردرگمیها در اینجا جلوگیری کند.
هیچ گونه میانبری برای ارجاع به ویژگیهای داده (یا سایر متدها!) از درون متدها وجود ندارد. به نظر من، این موضوع در واقع خوانایی متدها را افزایش میدهد: هیچ شانسی برای اشتباه گرفتن متغیرهای محلی و متغیرهای نمونه هنگام مرور سریع یک متد وجود ندارد.
معمولاً، اولین آرگومان یک متد self نامیده میشود. این چیزی بیش از یک قرارداد نیست: نام self هیچ معنی خاصی برای پایتون ندارد. با این حال، توجه داشته باشید که با رعایت نکردن این قرارداد، کد شما ممکن است برای سایر برنامهنویسان پایتون کمتر خوانا باشد، و همچنین قابلتصور است که یک برنامه مرورگر کلاس (class browser) نوشته شود که به چنین قراردادی وابسته باشد.
هر شیء تابعی که یک ویژگی کلاس باشد، یک متد برای نمونههای آن کلاس تعریف میکند. لازم نیست که تعریف تابع لزوماً درون تعریف کلاس قرار داشته باشد: اختصاص یک شیء تابع به یک متغیر محلی در کلاس نیز مجاز است. برای مثال:
# تابع بیرون از کلاس تعریف شده است
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__ ذخیره میشود.
9.5. ارثبری¶
مسلماً، یک ویژگی زبانی بدون پشتیبانی از ارثبری، شایستهی نام «کلاس» نخواهد بود. نحو تعریف یک کلاس مشتقشده (derived class) به این شکل است:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
نام BaseClassName باید در فضای نامی تعریف شده باشد که از محدودهای که شامل تعریف کلاس مشتقشده است، قابل دسترسی باشد. به جای نام کلاس پایه، سایر عبارتهای دلخواه نیز مجاز هستند. این میتواند مفید باشد، برای مثال، زمانی که کلاس پایه در ماژول دیگری تعریف شده است:
class DerivedClassName(modname.BaseClassName):
اجرای تعریف یک کلاس مشتقشده به همان شیوهای است که برای یک کلاس پایه انجام میشود. هنگامی که شیء کلاس ساخته میشود، کلاس پایه به خاطر سپرده میشود. این (اطلاعات) برای حل کردن ارجاعات به ویژگیها استفاده میشود: اگر ویژگی درخواستی در کلاس یافت نشود، جستوجو در کلاس پایه ادامه مییابد. این قاعده بهصورت بازگشتی اعمال میشود اگر خود کلاس پایه از کلاس دیگری مشتق شده باشد.
هیچ نکتهی خاصی دربارهی نمونهسازی کلاسهای مشتقشده وجود ندارد: DerivedClassName() یک نمونهی جدید از کلاس ایجاد میکند. ارجاعهای متد بهصورت زیر حل میشوند: ویژگی کلاس متناظر جستوجو میشود، در صورت لزوم در زنجیرهی کلاسهای پایه به پایین میرود، و ارجاع متد در صورتی معتبر است که این جستوجو به یک شیء تابع منجر شود.
کلاسهای مشتقشده ممکن است متدهای کلاسهای پایهی خود را بازنویسی کنند. از آنجایی که متدها هنگام فراخوانی سایر متدهای همان شیء، امتیاز ویژهای ندارند، یک متد از یک کلاس پایه که متد دیگری را که در همان کلاس پایه تعریف شده فراخوانی میکند، ممکن است در نهایت متدی از یک کلاس مشتقشده را فراخوانی کند که آن را بازنویسی کرده است. (برای برنامهنویسان C++: تمام متدها در پایتون بهطور مؤثر virtual هستند.)
یک متد بازنویسیشده در یک کلاس مشتقشده ممکن است در واقع بخواهد به جای اینکه صرفاً متد کلاس پایه را با همین نام جایگزین کند، آن را گسترش دهد. یک راه ساده برای فراخوانی مستقیم متد کلاس پایه وجود دارد: فقط BaseClassName.methodname(self, arguments) را فراخوانی کنید. این روش گاهی برای کاربران (clients) نیز مفید است. (توجه داشته باشید که این روش تنها زمانی کار میکند که کلاس پایه بهعنوان BaseClassName در محدودهی سراسری (global scope) قابل دسترسی باشد.)
پایتون دو تابع داخلی دارد که با ارثبری کار میکنند:
برای بررسی نوع یک نمونه (instance) از
isinstance()استفاده کنید:isinstance(obj, int)تنها زمانیTrueخواهد بود کهobj.__class__از نوعintباشد یا کلاسی که ازintمشتق شده باشد.برای بررسی ارثبری کلاسها از
issubclass()استفاده کنید:issubclass(bool, int)مقدارTrueاست چونboolزیرکلاسی ازintاست. با این حال،issubclass(float, int)مقدارFalseاست چونfloatزیرکلاسی ازintنیست.
9.5.1. ارثبری چندگانه¶
پایتون همچنین از نوعی ارثبری چندگانه پشتیبانی میکند. تعریف یک کلاس با چندین کلاس پایه به این شکل است:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
برای بیشتر کاربردها، در سادهترین موارد، میتوانید جستوجو برای ویژگیهای بهارثبردهشده از یک کلاس والد را بهصورت عمقاول، چپبهراست، بدون جستوجوی دوباره در همان کلاس در جایی که همپوشانی در سلسلهمراتب وجود دارد، در نظر بگیرید. بنابراین، اگر ویژگیای در DerivedClassName یافت نشد، در Base1 جستوجو میشود، سپس (بهصورت بازگشتی) در کلاسهای پایهی Base1، و اگر در آنجا یافت نشد، در Base2 جستوجو میشود، و به همین ترتیب.
در واقع، این موضوع کمی پیچیدهتر از این است؛ ترتیب حل متد بهصورت پویا تغییر میکند تا از فراخوانیهای مشارکتی به super() پشتیبانی کند. این رویکرد در برخی دیگر از زبانهای دارای ارثبری چندگانه با نام call-next-method شناخته میشود و از فراخوانی super موجود در زبانهای دارای ارثبری تکی قدرتمندتر است.
ترتیبدهی پویا ضروری است زیرا همهی موارد ارثبری چندگانه دارای یک یا چند رابطهی الماسی هستند (جایی که حداقل یکی از کلاسهای والد از طریق چندین مسیر از پایینترین کلاس قابل دسترسی است). برای مثال، همهی کلاسها از object ارثبری میکنند، بنابراین هر موردی از ارثبری چندگانه بیش از یک مسیر برای رسیدن به object فراهم میکند. برای جلوگیری از دسترسی بیش از یک بار به کلاسهای پایه، الگوریتم پویا ترتیب جستوجو را بهگونهای خطیسازی میکند که ترتیب چپبهراست مشخصشده در هر کلاس را حفظ کند، که هر والد را فقط یک بار فراخوانی میکند، و یکنواخت است (به این معنی که یک کلاس میتواند زیرکلاس داشته باشد بدون اینکه بر ترتیب اولویت والدین آن تأثیر بگذارد). این ویژگیها در کنار هم، طراحی کلاسهای قابل اعتماد و قابل توسعه با ارثبری چندگانه را ممکن میسازند. برای جزئیات بیشتر، به The Python 2.3 Method Resolution Order مراجعه کنید.
9.6. متغیرهای خصوصی¶
متغیرهای نمونهای «خصوصی» که جز از درون یک شیء قابل دسترسی نیستند، در پایتون وجود ندارند. با این حال، یک قرارداد وجود دارد که توسط بیشتر کدهای پایتون دنبال میشود: یک نام که با زیرخط آغاز میشود (مثلاً _spam) باید بهعنوان بخشی غیرعمومی از API در نظر گرفته شود (خواه یک تابع، یک متد یا یک عضو داده باشد). باید بهعنوان یک جزئیات پیادهسازی در نظر گرفته شود و ممکن است بدون اطلاعرسانی تغییر کند.
از آنجا که یک مورد استفادهی معتبر برای اعضای خصوصی کلاس وجود دارد (بهویژه برای جلوگیری از برخورد نامها با نامهای تعریفشده توسط زیرکلاسها)، پشتیبانی محدودی برای چنین مکانیزمی، به نام name mangling (دستکاری نام) وجود دارد. هر شناسهای به شکل __spam (حداقل دو زیرخط در ابتدا، حداکثر یک زیرخط در انتها) بهصورت متنی با _classname__spam جایگزین میشود، که در آن classname نام کلاس جاری است با زیرخطهای ابتدایی حذفشده. این دستکاری بدون در نظر گرفتن جایگاه نحوی شناسه انجام میشود، تا زمانی که درون تعریف یک کلاس رخ دهد.
همچنین ملاحظه نمائید
برای جزئیات و موارد خاص، به مشخصات دستکاری نامهای خصوصی مراجعه کنید.
دستکاری نام برای این مفید است که به زیرکلاسها اجازه دهد متدها را بدون شکستن فراخوانیهای متد درونکلاسی بازنویسی کنند. برای مثال:
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 # نسخهٔ خصوصی از متد اصلی update()
class MappingSubclass(Mapping):
def update(self, keys, values):
# امضای جدیدی برای update() فراهم میکند
# اما __init__ را خراب نمیکند
for item in zip(keys, values):
self.items_list.append(item)
مثال بالا حتی اگر MappingSubclass یک شناسهی __update معرفی کند، کار خواهد کرد زیرا این شناسه به ترتیب در کلاس Mapping با _Mapping__update و در کلاس MappingSubclass با _MappingSubclass__update جایگزین میشود.
توجه داشته باشید که قوانین دستکاری نام بیشتر برای جلوگیری از اشتباهات طراحی شدهاند؛ همچنان دسترسی یا تغییر یک متغیر که خصوصی در نظر گرفته میشود، ممکن است. این حتی میتواند در شرایط خاص، مانند اشکالزدایی، مفید باشد.
توجه داشته باشید که کد ارسالشده به exec() یا eval() نام کلاس کلاس فراخوانکننده را بهعنوان کلاس جاری در نظر نمیگیرد؛ این مشابه تأثیر دستور global است که تأثیر آن نیز به همین ترتیب به کدی که با هم بایتکامپایل میشوند، محدود است. همین محدودیت برای getattr()، setattr() و delattr() نیز اعمال میشود، همچنین زمانی که به طور مستقیم به __dict__ ارجاع داده میشود.
9.7. نکات متفرقه¶
گاهی اوقات داشتن یک نوع داده مشابه «رکورد» در پاسکال یا «ساختار» در C مفید است، که چندین آیتم دادهی نامدار را کنار هم بستهبندی میکند. روش استاندارد برای این کار استفاده از 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
یک قطعه کد پایتون که انتظار یک نوع دادهٔ انتزاعی خاص را دارد، اغلب میتواند به جای آن، یک کلاس دریافت کند که متدهای آن نوع داده را شبیهسازی میکند. برای مثال، اگر تابعی دارید که دادههایی را از یک شیء فایل قالببندی میکند، میتوانید کلاسی تعریف کنید با متدهای read() و readline() که دادهها را از یک بافر رشتهای دریافت میکنند، و آن را بهعنوان آرگومان به تابع ارسال کنید.
اشیاء متد نمونه نیز ویژگیهایی دارند: m.__self__ شیء نمونهای است که متد m() به آن تعلق دارد، و m.__func__ همان شیء تابع متناظر با آن متد است.
9.8. تکرارگرها¶
تا اینجا احتمالاً متوجه شدهاید که میتوان روی بیشتر اشیاء ظرف با استفاده از دستور for حلقه زد:
for element in [1, 2, 3]:
print(element)
for element in (1, 2, 3):
print(element)
for key in {'one':1, 'two':2}:
print(key)
for char in "123":
print(char)
for line in open("myfile.txt"):
print(line, end='')
این سبک دسترسی واضح، مختصر و راحت است. استفاده از تکرارگرها در همهجا نفوذ کرده و پایتون را یکپارچه میکنند. در پشت صحنه، دستور 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
9.9. تولیدگر¶
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 را ایجاد میکنند. این ویژگیها به صورت ترکیبی، ساختن تکرارگرها را به همان سادگی نوشتن یک تابع عادی امکان پذیر میسازند.
9.10. عبارات تولیدگر¶
برخی از تولیدگرهای ساده را میتوان بهصورت مختصر بهعنوان عبارت کدنویسی کرد، با استفاده از یک نحو مشابه درکفهرستها اما با پرانتز به جای کروشهی باز. این عبارتها برای شرایطی طراحی شدهاند که تولیدگر بلافاصله توسط یک تابع محاطشونده استفاده میشود. عبارتهای تولیدگر فشردهتر هستند اما نسبت به تعاریف کامل تولیدگر، انعطافپذیری کمتری دارند و معمولاً نسبت به درکفهرستهای معادل، با حافظه سازگارتر هستند.
مثالها:
>>> sum(i*i for i in range(10)) # مجموع مربعها
285
>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec)) # حاصلضرب داخلی
260
پاورقیها