منابع ترجمه · The Python Tutorial · فصل ۹
کلاسها (آموزش رسمی پایتون، فصل ۹)
منبع اصلی: The Python Tutorial, Chapter 9: Classes — Python Software Foundation
💡 این صفحه مثل یک نوتبوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلولهای بعدی همین صفحه در دسترس است. سلولها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلولهای قبلیِ اجرانشده را یکجا اجرا کنید.
کلاسها ابزاری برای بستهبندی داده و کارکرد با یکدیگر فراهم میکنند. ساختن یک کلاس جدید، یک نوع (type) جدید از شیء میسازد و اجازه میدهد نمونههای (instances) جدیدی از آن نوع ساخته شود. هر نمونهٔ کلاس میتواند ویژگیهایی (attributes) داشته باشد که برای نگهداری وضعیتش به آن پیوست شدهاند. نمونههای کلاس همچنین میتوانند متدهایی (که توسط کلاسشان تعریف شدهاند) برای تغییر وضعیتشان داشته باشند.
در مقایسه با زبانهای برنامهنویسی دیگر، سازوکار کلاس در پایتون کلاسها را با حداقل نحو و معناشناسی جدید اضافه میکند. ترکیبی از سازوکارهای کلاس موجود در ++C و Modula-3 است. کلاسهای پایتون همهٔ ویژگیهای استاندارد برنامهنویسی شیءگرا را فراهم میکنند: سازوکار وراثت کلاس چند کلاس پایه را مجاز میکند، یک کلاس مشتق میتواند هر متدی از کلاس یا کلاسهای پایهٔ خود را بازنویسی (override) کند و یک متد میتواند متد همنام یک کلاس پایه را فراخوانی کند. اشیا میتوانند مقدار و نوع دلخواهی از داده را در بر گیرند. همانطور که برای ماژولها صادق است، کلاسها از ماهیت پویای پایتون بهره میبرند: آنها در زمان اجرا ساخته میشوند و پس از ساخته شدن هم میتوان آنها را بیشتر تغییر داد.
در اصطلاحات ++C، معمولاً اعضای کلاس (از جمله اعضای داده) عمومی (public) هستند (بهجز آنچه در متغیرهای خصوصی در ادامه میآید) و همهٔ توابع عضو مجازی (virtual) هستند. همانطور که در Modula-3، هیچ میانبری برای ارجاع به اعضای شیء از درون متدهایش وجود ندارد: تابع متد با یک آرگومان اول صریح که نمایندهٔ شیء است اعلان میشود، که بهطور ضمنی توسط فراخوانی فراهم میشود. همانطور که در Smalltalk، خود کلاسها اشیا هستند. این معناشناسی برای import کردن و تغییر نام فراهم میکند. بر خلاف ++C و Modula-3، انواع درونساخت را میتوان بهعنوان کلاس پایه برای توسعه توسط کاربر به کار برد. همچنین، مانند ++C، بیشتر عملگرهای درونساخت با نحو ویژه (عملگرهای حسابی، اندیسگذاری و غیره) را میتوان برای نمونههای کلاس بازتعریف کرد.
(از آنجا که اصطلاحات پذیرفتهشدهٔ جهانی برای صحبت دربارهٔ کلاسها وجود ندارد، گاهگاه از اصطلاحات Smalltalk و ++C استفاده میکنم. از اصطلاحات Modula-3 استفاده میکردم، چون معناشناسی شیءگرای آن به پایتون نزدیکتر از ++C است، اما انتظار دارم کمتر خوانندهای نام آن را شنیده باشد.)
۹٫۱. دربارهٔ نامها و اشیا
اشیا فردیت دارند و چند نام (در حوزههای مختلف) میتوانند به یک شیء متصل شوند. این در زبانهای دیگر با نام همنامسازی (aliasing) شناخته میشود. این معمولاً در نگاه اول به پایتون قدر دانسته نمیشود و هنگام کار با انواع پایهٔ تغییرناپذیر (اعداد، رشتهها، تاپلها) میتوان با خیال راحت نادیدهاش گرفت. با این حال، aliasing اثری احتمالاً شگفتانگیز بر معناشناسی کد پایتونی که اشیای تغییرپذیر مانند فهرستها، دیکشنریها و بیشتر انواع دیگر را در بر میگیرد دارد. این معمولاً به نفع برنامه به کار میرود، چون aliasها از برخی جهات مانند اشارهگر رفتار میکنند. برای مثال، ارسال یک شیء ارزان است چون پیادهسازی فقط یک اشارهگر را ارسال میکند؛ و اگر تابعی شیئی را که بهعنوان آرگومان ارسال شده تغییر دهد، فراخواننده تغییر را خواهد دید — این نیاز به دو سازوکار مختلف ارسال آرگومان مانند Pascal را از بین میبرد.
۹٫۲. حوزهها و فضاهای نام در پایتون
پیش از معرفی کلاسها، ابتدا باید چیزی دربارهٔ قواعد حوزه (scope) در پایتون به شما بگویم. تعریفهای کلاس با فضاهای نام ترفندهای جالبی میزنند و باید بدانید حوزهها و فضاهای نام چگونه کار میکنند تا آنچه را در جریان است کاملاً بفهمید. ضمناً دانستن این موضوع برای هر برنامهنویس پیشرفتهٔ پایتون مفید است.
بیایید با چند تعریف شروع کنیم.
یک فضای نام (namespace) نگاشتی از نامها به اشیاست. بیشتر فضاهای نام در حال حاضر بهصورت دیکشنریهای پایتون پیادهسازی شدهاند، اما این معمولاً به هیچ شکلی قابل توجه نیست (بهجز عملکرد) و ممکن است در آینده تغییر کند. نمونههایی از فضاهای نام: مجموعهٔ نامهای درونساخت (شامل توابعی مانند abs() و نامهای استثنای درونساخت)؛ نامهای سراسری در یک ماژول؛ و نامهای محلی در یک فراخوانی تابع. به یک معنا، مجموعهٔ ویژگیهای یک شیء هم یک فضای نام را تشکیل میدهند. نکتهٔ مهم دربارهٔ فضاهای نام که باید دانست این است که مطلقاً هیچ رابطهای بین نامها در فضاهای نام مختلف وجود ندارد؛ برای مثال، دو ماژول مختلف میتوانند هر دو تابعی به نام maximize تعریف کنند بدون سردرگمی — کاربران ماژولها باید آن را با نام ماژول پیشوند بزنند.
ضمناً، من کلمهٔ ویژگی (attribute) را برای هر نامی که بعد از نقطه میآید به کار میبرم — برای مثال، در عبارت z.real، real ویژگی شیء z است. بهطور دقیق، ارجاع به نامها در ماژولها ارجاع به ویژگی هستند: در عبارت modname.funcname، modname یک شیء ماژول است و funcname ویژگی آن است. در این مورد اتفاقاً نگاشتی سرراست بین ویژگیهای ماژول و نامهای سراسری تعریفشده در ماژول وجود دارد: آنها یک فضای نام را به اشتراک میگذارند! [۱]
ویژگیها ممکن است فقطخواندنی یا قابلنوشتن باشند. در حالت دوم، انتساب به ویژگیها ممکن است. ویژگیهای ماژول قابلنوشتن هستند: میتوانید modname.the_answer = 42 بنویسید. ویژگیهای قابلنوشتن را میتوان با دستور del هم حذف کرد. برای مثال، del modname.the_answer ویژگی the_answer را از شیئی که با modname نامیده شده حذف میکند.
فضاهای نام در لحظههای مختلف ساخته میشوند و عمر متفاوتی دارند. فضای نامی که شامل نامهای درونساخت است وقتی مفسر پایتون راهاندازی میشود ساخته میشود و هرگز حذف نمیشود. فضای نام سراسری یک ماژول وقتی تعریف ماژول خوانده میشود ساخته میشود؛ معمولاً فضاهای نام ماژول هم تا زمانی که مفسر خارج شود باقی میمانند. دستورهایی که توسط فراخوانی سطح بالای مفسر اجرا میشوند، چه از یک فایل اسکریپت خوانده شده باشند و چه بهصورت تعاملی، بخشی از ماژولی به نام __main__ در نظر گرفته میشوند، بنابراین فضای نام سراسری خودشان را دارند. (نامهای درونساخت در واقع هم در یک ماژول زندگی میکنند؛ این ماژول builtins نام دارد.)
فضای نام محلی یک تابع وقتی تابع فراخوانی میشود ساخته میشود و وقتی تابع برمیگردد یا استثنایی پرتاب میکند که درون تابع مدیریت نشده حذف میشود. (در واقع، «فراموش شدن» توصیف بهتری برای آنچه واقعاً اتفاق میافتد است.) البته، هر فراخوانی بازگشتی فضای نام محلی خودش را دارد.
یک حوزه (scope) ناحیهای متنی از یک برنامهٔ پایتون است که در آن یک فضای نام مستقیماً در دسترس است. «مستقیماً در دسترس» در اینجا یعنی یک ارجاع بدون پیشوند به یک نام تلاش میکند نام را در فضای نام پیدا کند.
اگرچه حوزهها بهطور ایستا تعیین میشوند، بهطور پویا به کار میروند. در هر زمان هنگام اجرا، ۳ یا ۴ حوزهٔ تودرتو وجود دارند که فضاهای نامشان مستقیماً در دسترس است:
- درونیترین حوزه، که اول جستوجو میشود، نامهای محلی را دارد
- حوزههای هر تابع دربرگیرنده، که با نزدیکترین حوزهٔ دربرگیرنده شروع به جستوجو میشوند، نامهای غیرمحلی (nonlocal) اما غیرسراسری هم دارند
- یکی مانده به آخرین حوزه نامهای سراسری ماژول جاری را دارد
- بیرونیترین حوزه (که آخر جستوجو میشود) فضای نامی است که نامهای درونساخت را دارد
اگر نامی global اعلان شود، آنگاه همهٔ ارجاعها و انتسابها مستقیماً به یکی مانده به آخرین حوزه که نامهای سراسری ماژول را دارد میروند. برای متصل کردن مجدد (rebind) متغیرهای یافتشده بیرون از درونیترین حوزه، میتوان از دستور nonlocal استفاده کرد؛ اگر nonlocal اعلان نشوند، آن متغیرها فقطخواندنیاند (تلاش برای نوشتن در چنین متغیری صرفاً یک متغیر محلی جدید در درونیترین حوزه میسازد و متغیر بیرونی همنام را بدون تغییر میگذارد).
معمولاً حوزهٔ محلی به نامهای محلی تابع (از نظر متنی) جاری ارجاع میدهد. بیرون از توابع، حوزهٔ محلی به همان فضای نام حوزهٔ سراسری ارجاع میدهد: فضای نام ماژول. تعریفهای کلاس فضای نام دیگری را هم در حوزهٔ محلی قرار میدهند.
مهم است که بدانید حوزهها بهصورت متنی تعیین میشوند: حوزهٔ سراسری تابعی که در یک ماژول تعریف شده فضای نام آن ماژول است، مهم نیست از کجا یا با چه نام مستعاری تابع فراخوانی شود. از سوی دیگر، جستوجوی واقعی نامها بهطور پویا، در زمان اجرا انجام میشود — با این حال، تعریف زبان به سمت تفکیک نام ایستا، در زمان «کامپایل»، در حال تکامل است، پس به تفکیک نام پویا تکیه نکنید! (در واقع، متغیرهای محلی هماکنون بهطور ایستا تعیین میشوند.)
یک ویژگی عجیب پایتون این است که — اگر هیچ دستور global یا nonlocal در جریان نباشد — انتساب به نامها همیشه به درونیترین حوزه میرود. انتسابها داده را کپی نمیکنند — فقط نامها را به اشیا متصل میکنند. همین برای حذفها هم صادق است: دستور del x اتصال x را از فضای نامی که حوزهٔ محلی به آن ارجاع میدهد حذف میکند. در واقع، همهٔ عملیاتی که نامهای جدید معرفی میکنند از حوزهٔ محلی استفاده میکنند: بهویژه دستورهای import و تعریفهای تابع نام ماژول یا تابع را در حوزهٔ محلی متصل میکنند.
دستور global را میتوان برای نشان دادن اینکه متغیرهای خاصی در حوزهٔ سراسری زندگی میکنند و باید در آنجا دوباره متصل شوند به کار برد؛ دستور nonlocal نشان میدهد که متغیرهای خاصی در یک حوزهٔ دربرگیرنده زندگی میکنند و باید در آنجا دوباره متصل شوند.
۹٫۲٫۱. مثال حوزهها و فضاهای نام
این مثالی است که نشان میدهد چگونه به حوزهها و فضاهای نام مختلف ارجاع دهیم، و چگونه 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 وجود نداشت.
۹٫۳. نگاهی اول به کلاسها
کلاسها کمی نحو جدید، سه نوع شیء جدید و کمی معناشناسی جدید معرفی میکنند.
۹٫۳٫۱. نحو تعریف کلاس
سادهترین شکل تعریف کلاس اینگونه است:
class ClassName:
<statement-1>
.
.
.
<statement-N>
تعریفهای کلاس، مانند تعریفهای تابع (دستورهای def)، باید پیش از اینکه اثری داشته باشند اجرا شوند. (میتوانید تصور کنید که یک تعریف کلاس را در شاخهٔ یک دستور if یا درون یک تابع قرار دهید.)
در عمل، دستورهای درون یک تعریف کلاس معمولاً تعریفهای تابع خواهند بود، اما دستورهای دیگر هم مجازند و گاهی مفیدند — بعداً به این برمیگردیم. تعریفهای تابع درون یک کلاس معمولاً شکل عجیبی از فهرست آرگومان دارند، که قراردادهای فراخوانی برای متدها آن را دیکته میکنند — باز هم، این بعداً توضیح داده میشود.
وقتی وارد یک تعریف کلاس میشویم، یک فضای نام جدید ساخته میشود و بهعنوان حوزهٔ محلی به کار میرود — بنابراین، همهٔ انتسابها به متغیرهای محلی به این فضای نام جدید میروند. بهویژه، تعریفهای تابع نام تابع جدید را اینجا متصل میکنند.
وقتی یک تعریف کلاس بهطور عادی (از طریق انتها) ترک میشود، یک شیء کلاس (class object) ساخته میشود. این اساساً پوششی دور محتوای فضای نامی است که تعریف کلاس ساخته؛ در بخش بعد دربارهٔ اشیای کلاس بیشتر یاد خواهیم گرفت. حوزهٔ محلی اصلی (آنچه درست پیش از ورود به تعریف کلاس در جریان بود) دوباره برقرار میشود و شیء کلاس اینجا به نام کلاس دادهشده در سرایند تعریف کلاس (ClassName در مثال) متصل میشود.
۹٫۳٫۲. اشیای کلاس
اشیای کلاس دو نوع عملیات را پشتیبانی میکنند: ارجاع به ویژگی و نمونهسازی.
ارجاع به ویژگی از نحو استاندارد استفادهشده برای همهٔ ارجاعهای ویژگی در پایتون استفاده میکند: obj.name. نامهای ویژگی معتبر همهٔ نامهایی هستند که هنگام ساخته شدن شیء کلاس در فضای نام کلاس بودند. بنابراین، اگر تعریف کلاس اینگونه بود:
class MyClass:
"""A simple example class"""
i = 12345
def f(self):
return 'hello world'
آنگاه MyClass.i و MyClass.f ارجاعهای ویژگی معتبرند و به ترتیب یک عدد صحیح و یک شیء تابع برمیگردانند. به ویژگیهای کلاس هم میتوان مقدار نسبت داد، بنابراین میتوانید مقدار MyClass.i را با انتساب تغییر دهید. __doc__ هم ویژگی معتبری است که رشتهٔ مستندسازی متعلق به کلاس را برمیگرداند: "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)
۹٫۳٫۳. اشیای نمونه
حالا با اشیای نمونه چه کاری میتوانیم بکنیم؟ تنها عملیاتی که اشیای نمونه میفهمند ارجاع به ویژگی است. دو نوع نام ویژگی معتبر وجود دارد: ویژگیهای داده و متدها.
ویژگیهای داده متناظر «متغیرهای نمونه» در Smalltalk و «اعضای داده» در ++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 نیست — یک شیء متد است، نه یک شیء تابع.
۹٫۳٫۴. اشیای متد
معمولاً یک متد درست بعد از متصل شدن فراخوانی میشود:
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 آرگومان همارز فراخوانی تابع متناظر با فهرست آرگومانی است که با درج شیء نمونهٔ متد پیش از اولین آرگومان ساخته میشود.
بهطور کلی، متدها اینگونه کار میکنند. وقتی به یک ویژگی غیرداده از یک نمونه ارجاع داده میشود، کلاس نمونه جستوجو میشود. اگر نام یک ویژگی معتبر کلاس را که یک شیء تابع است نشان دهد، ارجاعها به هر دو شیء نمونه و شیء تابع در یک شیء متد بستهبندی میشوند. وقتی شیء متد با یک فهرست آرگومان فراخوانی میشود، فهرست آرگومان جدیدی از شیء نمونه و فهرست آرگومان ساخته میشود و شیء تابع با این فهرست آرگومان جدید فراخوانی میشود.
۹٫۳٫۵. متغیرهای کلاس و نمونه
بهطور کلی، متغیرهای نمونه برای دادهٔ منحصربهفرد هر نمونه هستند و متغیرهای کلاس برای ویژگیها و متدهایی هستند که بین همهٔ نمونههای کلاس مشترکند:
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'
همانطور که در دربارهٔ نامها و اشیا بحث شد، دادهٔ مشترک میتواند اثرات احتمالاً شگفتانگیزی دربارهٔ اشیای تغییرپذیر مانند فهرستها و دیکشنریها داشته باشد. برای مثال، فهرست 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']
۹٫۴. نکتههای پراکنده
اگر یک نام ویژگی یکسان هم در یک نمونه و هم در یک کلاس رخ دهد، جستوجوی ویژگی نمونه را در اولویت قرار میدهد:
>>> 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
ویژگیهای داده را هم متدها و هم کاربران عادی («مشتریها») یک شیء میتوانند ارجاع دهند. به عبارت دیگر، کلاسها برای پیادهسازی انواع دادهای انتزاعیِ خالص قابل استفاده نیستند. در واقع، هیچ چیزی در پایتون امکان اجبار پنهانسازی داده را فراهم نمیکند — همهچیز بر پایهٔ قرارداد است. (از سوی دیگر، پیادهسازی پایتون که با C نوشته شده میتواند جزئیات پیادهسازی را کاملاً پنهان کند و در صورت لزوم دسترسی به یک شیء را کنترل کند؛ این را میتوان توسط افزونههای پایتون که با C نوشته شدهاند به کار برد.)
مشتریها باید از ویژگیهای داده با دقت استفاده کنند — مشتریها ممکن است با پا گذاشتن روی ویژگیهای داده، ناورداییهایی را که توسط متدها نگه داشته میشود بههم بریزند. توجه کنید که مشتریها میتوانند ویژگیهای دادهای مختص خودشان را به یک شیء نمونه اضافه کنند بدون اینکه اعتبار متدها را مختل کنند، تا زمانی که از تعارض نام پرهیز شود — باز هم، یک قرارداد نامگذاری میتواند در اینجا خیلی از دردسرها را کم کند.
هیچ میانبری برای ارجاع به ویژگیهای داده (یا متدهای دیگر!) از درون متدها وجود ندارد. من متوجه شدهام که این در واقع خوانایی متدها را افزایش میدهد: هنگام نگاه سریع به یک متد هیچ احتمالی برای اشتباه گرفتن متغیرهای محلی و متغیرهای نمونه نیست.
اغلب، اولین آرگومان یک متد 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)
متدها میتوانند به نامهای سراسری به همان شکل توابع معمولی ارجاع دهند. حوزهٔ سراسری مرتبط با یک متد، ماژولی است که تعریف آن را در بر دارد. (کلاس هرگز بهعنوان حوزهٔ سراسری به کار نمیرود.) درحالی که به ندرت دلیل خوبی برای استفاده از دادهٔ سراسری در یک متد پیدا میشود، کاربردهای مشروع زیادی از حوزهٔ سراسری وجود دارد: برای یک چیز، توابع و ماژولهایی که در حوزهٔ سراسری import شدهاند را میتوان توسط متدها به کار برد، همینطور توابع و کلاسهایی که در آن تعریف شدهاند. معمولاً کلاسی که متد را در بر دارد خودش در این حوزهٔ سراسری تعریف شده است و در بخش بعد چند دلیل خوب پیدا خواهیم کرد که چرا یک متد ممکن است بخواهد به کلاس خودش ارجاع دهد.
هر مقدار یک شیء است و بنابراین یک کلاس دارد (که نوع آن هم نامیده میشود). این بهصورت object.__class__ ذخیره میشود.
۹٫۵. وراثت
البته، یک ویژگی زبان بدون پشتیبانی از وراثت شایستهٔ نام «کلاس» نخواهد بود. نحو تعریف کلاس مشتق اینگونه است:
class DerivedClassName(BaseClassName):
<statement-1>
.
.
.
<statement-N>
نام BaseClassName باید در یک فضای نام قابلدسترس از حوزهای که تعریف کلاس مشتق را در بر دارد تعریف شده باشد. بهجای نام کلاس پایه، عبارتهای دلخواه دیگری هم مجازند. این میتواند مفید باشد، برای مثال، وقتی کلاس پایه در ماژول دیگری تعریف شده است:
class DerivedClassName(modname.BaseClassName):
اجرای تعریف کلاس مشتق مانند کلاس پایه پیش میرود. وقتی شیء کلاس ساخته میشود، کلاس پایه به خاطر سپرده میشود. این برای تفکیک ارجاعهای ویژگی به کار میرود: اگر ویژگی درخواستشده در کلاس پیدا نشود، جستوجو به نگاه کردن در کلاس پایه ادامه مییابد. اگر خود کلاس پایه از کلاس دیگری مشتق شده باشد، این قاعده بهصورت بازگشتی اعمال میشود.
هیچ چیز ویژهای دربارهٔ نمونهسازی کلاسهای مشتق نیست: 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نیست.
۹٫۵٫۱. وراثت چندگانه
پایتون شکلی از وراثت چندگانه را هم پشتیبانی میکند. تعریف کلاس با چند کلاس پایه اینگونه است:
class DerivedClassName(Base1, Base2, Base3):
<statement-1>
.
.
.
<statement-N>
برای بیشتر مقاصد، در سادهترین موارد، میتوانید جستوجو برای ویژگیهای موروثی از یک کلاس والد را بهصورت عمقاول (depth-first) و چپبهراست در نظر بگیرید، بدون جستوجوی دوباره در همان کلاس جایی که در سلسلهمراتب همپوشانی هست. بنابراین، اگر ویژگی در DerivedClassName پیدا نشود، در Base1 جستوجو میشود، سپس (بهصورت بازگشتی) در کلاسهای پایهٔ Base1، و اگر آنجا هم پیدا نشد، در Base2 جستوجو میشد و به همین ترتیب.
در واقع، کمی پیچیدهتر از این است؛ ترتیب تفکیک متد (method resolution order) بهطور پویا تغییر میکند تا از فراخوانیهای همکارانه به super() پشتیبانی کند. این رویکرد در برخی زبانهای وراثت چندگانهٔ دیگر بهنام call-next-method شناخته میشود و از فراخوانی super موجود در زبانهای وراثت تکگانه قدرتمندتر است.
ترتیب پویا ضروری است چون همهٔ موارد وراثت چندگانه یک یا چند رابطهٔ الماسی (diamond) را نشان میدهند (که در آن دستکم یکی از کلاسهای والد از طریق چند مسیر از پایینترین کلاس قابل دسترسی است). برای مثال، همهٔ کلاسها از object ارث میبرند، بنابراین هر مورد وراثت چندگانه بیش از یک مسیر برای رسیدن به object فراهم میکند. برای جلوگیری از اینکه کلاسهای پایه بیش از یک بار در دسترس قرار گیرند، الگوریتم پویا ترتیب جستوجو را به شکلی خطی میکند که ترتیب چپبهراست مشخصشده در هر کلاس را حفظ کند، هر والد را فقط یک بار فراخوانی کند، و یکنوا (monotonic) باشد (یعنی یک کلاس را بتوان بدون اثر بر ترتیب تقدم والدینش زیرکلاس کرد). این ویژگیها در کنار هم طراحی کلاسهای قابلاعتماد و توسعهپذیر با وراثت چندگانه را ممکن میکنند. برای جزئیات بیشتر، ترتیب تفکیک متد پایتون ۲٫۳ را ببینید.
در برخی موارد وراثت چندگانه مجاز نیست؛ برای جزئیات وراثت چندگانه را ببینید.
۹٫۶. متغیرهای خصوصی
متغیرهای نمونهٔ «خصوصی» که نتوان به آنها جز از درون یک شیء دسترسی داشت در پایتون وجود ندارند. با این حال، قراردادی هست که بیشتر کدهای پایتون از آن پیروی میکنند: نامی که با زیرخط پیشوند شده باشد (مثلاً _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 # 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 جایگزین میشود.
توجه کنید که قواعد تحریف عمدتاً برای جلوگیری از حوادث طراحی شدهاند؛ هنوز میتوان به متغیری که خصوصی در نظر گرفته میشود دسترسی پیدا کرد یا آن را تغییر داد. این حتی در شرایط خاصی مانند اشکالزدا میتواند مفید باشد.
توجه کنید که کدی که به exec() یا eval() ارسال میشود نام کلاس فراخواننده را نام کلاس جاری در نظر نمیگیرد؛ این شبیه اثر دستور global است که اثر آن هم به کدی که با هم به بایتکد کامپایل شده محدود است. همین محدودیت برای getattr()، setattr() و delattr() و همچنین هنگام ارجاع مستقیم به __dict__ اعمال میشود.
۹٫۷. خردهریزها
گاهی داشتن نوع دادهٔ مشابه «رکورد» Pascal یا «struct» در 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__ شیء تابع متناظر با متد است.
۹٫۸. تکرارکنندهها (Iterators)
تا الان احتمالاً متوجه شدهاید که روی بیشتر اشیای ظرف (container) میتوان با دستور 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
۹٫۹. مولدها (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)
برخی مولدهای ساده را میتوان بهطور مختصر بهصورت عبارتهایی با نحوی مشابه لیست کامپرهنشنها، اما با پرانتز بهجای کروشه، کدنویسی کرد. این عبارتها برای موقعیتهایی طراحی شدهاند که مولد بیدرنگ توسط یک تابع دربرگیرنده به کار میرود. عبارتهای مولد فشردهتر اما کمانعطافتر از تعریفهای کامل مولد هستند و تمایل دارند از لیست کامپرهنشنهای همارز از نظر حافظه دوستانهتر باشند.
مثالها:
>>> 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']
>>> x = [[1,2,3], [], [4, 5]]
>>> g = (*i for i in x)
>>> list(g)
[1, 2, 3, 4, 5]
در بیشتر موارد، عبارتهای مولد باید داخل پرانتز قرار گیرند. با این حال، بهعنوان یک مورد خاص، وقتی بهعنوان تنها آرگومان یک تابع داده شوند (مانند مثالهای شامل sum، set، max و list در بالا)، عبارت مولد نیازی به قرار گرفتن در مجموعهٔ اضافی پرانتز ندارد. یعنی، دو قطعه کد زیر از نظر معنایی همارزند:
>>> f(x for x in y)
>>> f((x for x in y))
مانند موارد زیر:
>>> f(*x for x in y)
>>> f((*x for x in y))
پاورقیها
[۱] بهجز یک مورد. اشیای ماژول یک ویژگی فقطخواندنی مخفی به نام __dict__ دارند که دیکشنری مورد استفاده برای پیادهسازی فضای نام ماژول را برمیگرداند؛ نام __dict__ یک ویژگی است اما نام سراسری نیست. بدیهی است که استفاده از این، انتزاع پیادهسازی فضای نام را نقض میکند و باید به مواردی مانند اشکالزداهای پس از مرگ (post-mortem) محدود شود.