پرسشهای متداول دربارهی برنامهنویسی¶
پرسشهای عمومی¶
آیا یک اشکالزدای سطح کد منبع با نقاط توقف و اجرای گامبهگام وجود دارد؟¶
بله.
چندین اشکالزدا برای پایتون در زیر توضیح داده شدهاند، و تابع توکار breakpoint() به شما امکان میدهد وارد هر یک از آنها شوید.
ماژول pdb یک اشکالزدای ساده اما کافی در حالت کنسول برای پایتون است. این ماژول بخشی از کتابخانه استاندارد پایتون است و مستند شده در راهنمای مرجع کتابخانه است. همچنین میتوانید با استفاده از کد pdb بهعنوان مثال، اشکالزدای خودتان را بنویسید.
محیط توسعه تعاملی IDLE، که بخشی از توزیع استاندارد پایتون است (معمولاً بهصورت idlelib در دسترس است)، شامل یک اشکالزدا گرافیکی است.
PythonWin یک محیط توسعه یکپارچه (IDE) برای پایتون است که شامل یک اشکالزدای مبتنی بر pdb با رابط کاربری گرافیکی (GUI) است. اشکالزدای PythonWin نقاط شکست را رنگی نمایش میدهد و قابلیتهای جالب توجه متعددی مانند اشکالزدایی برنامههایی غیر از PythonWin دارد. PythonWin بهعنوان بخشی از پروژهی pywin32 و بهعنوان بخشی از توزیع ActivePython در دسترس است.
Eric یک IDE است که بر پایه PyQt و کامپوننت ویرایش Scintilla ساخته شده است.
trepan3k یک اشکالزدای شبیه به gdb است.
Visual Studio Code یک محیط توسعه یکپارچه (IDE) با ابزارهای اشکالزدایی است که با نرمافزار کنترل نسخه یکپارچه میشود.
تعدادی IDE تجاری پایتون وجود دارند که شامل اشکالزداهای گرافیکی هستند. این موارد شامل:
آیا ابزارهایی برای کمک به یافتن خطاها یا انجام تحلیل ایستا وجود دارد؟¶
بله.
Ruff، Pylint و Pyflakes بررسیهای پایهای انجام میدهند که به شما کمک میکند اشکالها را زودتر پیدا کنید.
بررسیکنندههای نوع ایستا مانند mypy، ty، Pyrefly و pytype میتوانند راهنماییهای نوع را در کد منبع پایتون بررسی کنند.
چگونه میتوانم از یک اسکریپت پایتون یک دودویی مستقل بسازم؟¶
اگر تمام خواسته شما برنامهای مستقل است که کاربران بتوانند بدون نیاز به نصب توزیع پایتون در ابتدا، آن را دانلود و اجرا کنند، نیازی به قابلیت کامپایل پایتون به کد C ندارید. ابزارهای متعددی وجود دارند که مجموعه ماژولهای مورد نیاز یک برنامه را تعیین میکنند و این ماژولها را به همراه یک دودویی پایتون به هم مقید میکنند تا یک پرونده اجرایی واحد تولید شود.
یکی استفاده از ابزار freeze است که در درخت منبع پایتون بهصورت Tools/freeze گنجانده شده است. این ابزار بایتکد پایتون را به آرایههای C تبدیل میکند؛ با یک کامپایلر C میتوانید همه ماژولهای خود را در یک برنامه جدید تعبیه کنید، که سپس با ماژولهای استاندارد پایتون پیوند داده میشود.
این ابزار با اسکن بازگشتی کد منبع شما بهدنبال دستورات ایمپورت (در هر دو شکل) و جستوجوی ماژولها در مسیر استاندارد پایتون و همچنین در پوشه منبع (برای ماژولهای توکار) کار میکند. سپس بایتکد ماژولهای نوشتهشده با پایتون را به کد C تبدیل میکند (مقداردهیهای اولیه آرایه که میتوان آنها را با استفاده از ماژول marshal به اشیای کد تبدیل کرد) و یک پرونده پیکربندی سفارشی ایجاد میکند که فقط شامل آن ماژولهای توکاری است که واقعاً در برنامه بهکار رفتهاند. سپس کد C تولیدشده را کامپایل میکند و آن را با بقیه مفسر پایتون پیوند میدهد تا یک دودویی خودکفا تشکیل شود که دقیقاً مانند اسکریپت شما عمل میکند.
بستههای زیر میتوانند به ایجاد پروندههای اجرایی برای کنسول و رابط کاربری گرافیکی (GUI) کمک کنند:
Nuitka (چندسکویی)
PyInstaller (چندسکویی)
PyOxidizer (چندسکویی)
cx_Freeze (چندسکویی)
py2app (فقط macOS)
py2exe (فقط ویندوز)
آیا برای برنامههای پایتون، استانداردهای کدنویسی یا راهنمای سبکی وجود دارد؟¶
بله. سبک کدنویسی مورد نیاز برای ماژولهای کتابخانه استاندارد، در PEP 8 مستند شده است.
زبان اصلی¶
چرا وقتی متغیر دارای مقدار است، خطای UnboundLocalError دریافت میکنم؟¶
ممکن است در کدی که پیشتر کار میکرد، پس از اصلاح آن با افزودن یک دستور انتساب در جایی از بدنه یک تابع، دریافت UnboundLocalError غافلگیرکننده باشد.
این کد:
>>> x = 10
>>> def bar():
... print(x)
...
>>> bar()
10
کار میکند، اما این کد:
>>> x = 10
>>> def foo():
... print(x)
... x += 1
منجر به UnboundLocalError میشود:
>>> foo()
Traceback (most recent call last):
...
UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
این به این دلیل است که وقتی در یک محدوده به یک متغیر انتساب میدهید، آن متغیر برای آن محدوده محلی میشود و هر متغیر همنام در محدوده بیرونی را میپوشاند. از آنجا که آخرین دستور در foo مقدار جدیدی به x اختصاص میدهد، کامپایلر آن را بهعنوان یک متغیر محلی میشناسد. در نتیجه، وقتی print(x) قبلی تلاش میکند متغیر محلی مقداردهینشده را چاپ کند، خطایی رخ میدهد.
در مثال بالا میتوانید با سراسری اعلام کردن متغیر محدوده بیرونی، به آن دسترسی داشته باشید:
>>> x = 10
>>> def foobar():
... global x
... print(x)
... x += 1
...
>>> foobar()
10
این اعلام صریح لازم است تا به شما یادآوری کند که (برخلاف موقعیت بهظاهر مشابه با متغیرهای کلاس و نمونه) شما در واقع مقدار متغیر را در محدوده بیرونی تغییر میدهید:
>>> print(x)
11
شما میتوانید کار مشابهی را در یک محدودهی تودرتو با استفاده از کلیدواژه nonlocal انجام دهید:
>>> def foo():
... x = 10
... def bar():
... nonlocal x
... print(x)
... x += 1
... bar()
... print(x)
...
>>> foo()
10
11
قوانین متغیرهای محلی و سراسری در پایتون چیست؟¶
در پایتون، متغیرهایی که فقط درون یک تابع به آنها ارجاع داده میشوند، بهطور ضمنی سراسری هستند. اگر به متغیری در هر جایی از بدنهی تابع مقداری انتساب داده شود، آن متغیر محلی فرض میشود، مگر اینکه بهصراحت بهعنوان سراسری اعلام شود.
هرچند این موضوع در ابتدا کمی تعجبآور است، اما با کمی تأمل قابل توضیح است. از یک سو، الزام به استفاده از global برای متغیرهایی که به آنها انتساب داده میشود، مانعی در برابر عوارض جانبی ناخواسته ایجاد میکند. از سوی دیگر، اگر global برای همه ارجاعات سراسری الزامی بود، همواره از global استفاده میکردید. مجبور بودید هر ارجاع به یک تابع توکار یا به یک کامپوننت از یک ماژول ایمپورتشده را بهعنوان سراسری اعلام کنید. این شلوغی، سودمندی اعلام global را برای شناسایی عوارض جانبی از بین میبرد.
چرا لامبداهای تعریفشده در یک حلقه با مقادیر مختلف، همگی نتیجهی یکسانی را برمیگردانند؟¶
فرض کنید با استفاده از یک حلقه for، چند لامبدای متفاوت (یا حتی توابع ساده) تعریف میکنید، برای مثال:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda: x**2)
این به شما فهرستی شامل ۵ لامبدا میدهد که x**2 را محاسبه میکنند. ممکن است انتظار داشته باشید که آنها هنگام فراخوانی، بهترتیب 0، 1، 4، 9 و 16 را برگردانند. با این حال، وقتی واقعاً آن را امتحان کنید، خواهید دید که همهی آنها 16 را برمیگردانند:
>>> squares[2]()
16
>>> squares[4]()
16
این اتفاق به این دلیل رخ میدهد که x برای لامبداها محلی نیست، بلکه در محدوده بیرونی تعریف شده است، و دسترسی به آن هنگام فراخوانی لامبدا انجام میشود — نه هنگام تعریف آن. در پایان حلقه، مقدار x برابر 4 است، بنابراین همه توابع اکنون 4**2 را برمیگردانند، یعنی 16. همچنین میتوانید این موضوع را با تغییر مقدار x تأیید کنید و ببینید نتایج لامبداها چگونه تغییر میکنند:
>>> x = 8
>>> squares[2]()
64
برای اجتناب از این موضوع، باید مقادیر را در متغیرهای محلیِ lambdaها ذخیره کنید، تا آنها به مقدار x سراسری وابسته نباشند:
>>> squares = []
>>> for x in range(5):
... squares.append(lambda n=x: n**2)
در اینجا، n=x یک متغیر جدید n ایجاد میکند که محلی برای لامبدا است و هنگام تعریف لامبدا محاسبه میشود، بهطوری که همان مقداری را دارد که x در آن نقطه از حلقه داشت. این بدان معناست که مقدار n در اولین لامبدا 0، در دومین لامبدا 1، در سومین لامبدا 2 و بههمین ترتیب خواهد بود. بنابراین هر لامبدا اکنون نتیجهی صحیح را برمیگرداند:
>>> squares[2]()
4
>>> squares[4]()
16
توجه داشته باشید که این رفتار مختص به لامبداها نیست، بلکه در مورد توابع معمولی نیز صدق میکند.
«بهترین روشها» برای استفاده از ایمپورت در یک ماژول چیست؟¶
بهطور کلی، از from modulename import * استفاده نکنید. این کار باعث شلوغ شدن فضای نام ایمپورتکننده میشود و تشخیص نامهای تعریفنشده را برای لینترها (linters) بسیار دشوارتر میکند.
ماژولها را در ابتدای پرونده ایمپورت کنید. این کار مشخص میکند که کد شما به چه ماژولهای دیگری نیاز دارد و از تردید در اینکه نام ماژول در محدوده قرار دارد جلوگیری میکند. استفاده از یک ایمپورت در هر خط، افزودن و حذف ایمپورت ماژولها را آسان میکند، اما استفاده از چند ایمپورت در هر خط فضای کمتری از صفحه را اشغال میکند.
بهتر است ماژولها را به ترتیب زیر ایمپورت کنید:
ماژولهای کتابخانهی استاندارد -- مانند
sys،os،argparse،reماژولهای کتابخانهای شخص ثالث (هر چیزی که در پوشه site-packages پایتون نصب شده است) — مانند dateutil، requests، tzdata
ماژولهای توسعهیافته بهصورت محلی
گاهی لازم است ایمپورتها را به یک تابع یا کلاس منتقل کنید تا از مشکلات ناشی از ایمپورتهای چرخهای اجتناب کنید. گوردون مکمیلان میگوید:
ایمپورتهای حلقهای در صورتی مشکلی ندارند که هر دو ماژول از شکل «import <module>» برای ایمپورت استفاده کنند. این ایمپورتها زمانی شکست میخورند که ماژول دوم بخواهد نامی را از ماژول اول بگیرد («from module import name») و ایمپورت در سطح بالا باشد. دلیل این امر آن است که نامهای ماژول اول هنوز در دسترس نیستند، زیرا ماژول اول مشغول ایمپورت کردن ماژول دوم است.
در این حالت، اگر ماژول دوم فقط در یک تابع استفاده شود، میتوان ایمپورت را بهراحتی به آن تابع منتقل کرد. تا زمانی که ایمپورت فراخوانی شود، ماژول اول مقداردهی اولیهی خود را به پایان رسانده است و ماژول دوم میتواند ایمپورت خود را انجام دهد.
ممکن است همچنین لازم باشد ایمپورتها را به بیرون از سطح بالایی کد منتقل کنید، اگر برخی از ماژولها مختص پلتفرم باشند. در آن صورت، حتی ممکن است نتوان تمام ماژولها را در ابتدای پرونده ایمپورت کرد. در این حالت، ایمپورت کردن ماژولهای صحیح در کد مختص پلتفرم متناظر، گزینه خوبی است.
فقط در صورتی ایمپورتها را به یک محدوده محلی منتقل کنید، مانند درون تعریف یک تابع، که این کار برای حل مشکلی مانند اجتناب از یک ایمپورت چرخهای (circular import) ضروری باشد یا بخواهید زمان مقداردهی اولیه یک ماژول را کاهش دهید. این تکنیک بهویژه زمانی مفید است که بسیاری از ایمپورتها، بسته به نحوه اجرای برنامه، غیرضروری باشند. همچنین ممکن است بخواهید ایمپورتها را به یک تابع منتقل کنید، اگر ماژولها تنها در همان تابع استفاده شوند. توجه داشته باشید که بارگذاری یک ماژول برای اولین بار ممکن است به دلیل مقداردهی اولیه یکباره ماژول پرهزینه باشد، اما بارگذاری چندباره یک ماژول عملاً رایگان است و تنها هزینه چند جستجوی دیکشنری را دارد. حتی اگر نام ماژول از محدوده خارج شده باشد، آن ماژول احتمالاً در sys.modules در دسترس است.
چگونه میتوانم پارامترهای اختیاری یا کلیدواژهای را از یک تابع به تابع دیگر منتقل کنم؟¶
آرگومانها را با استفاده از مشخصکنندههای * و ** در فهرست پارامترهای تابع جمعآوری کنید؛ این کار آرگومانهای جایگاهی را بهصورت یک تاپل و آرگومانهای کلیدواژهای را بهصورت یک دیکشنری به شما میدهد. سپس میتوانید این آرگومانها را هنگام فراخوانی تابعی دیگر با استفاده از * و ** منتقل کنید:
def f(x, *args, **kwargs):
...
kwargs['width'] = '14.3c'
...
g(x, *args, **kwargs)
تفاوت آرگومانها و پارامترها چیست؟¶
پارامترها با نامهایی که در تعریف تابع ظاهر میشوند تعریف میشوند، در حالی که آرگومانها مقادیری هستند که هنگام فراخوانی تابع واقعاً به تابع ارسال میشوند. پارامترها مشخص میکنند که تابع چه نوع آرگومانهایی میتواند بپذیرد. برای مثال، با توجه به تعریف تابع:
def func(foo, bar=None, **kwargs):
pass
foo، bar و kwargs پارامترهای func هستند. با این حال، هنگام فراخوانی func، برای مثال:
func(42, bar=314, extra=somevar)
مقادیر 42، 314 و somevar آرگومان هستند.
چرا تغییر فهرست 'y' باعث تغییر فهرست 'x' نیز شد؟¶
اگر کدی مانند زیر نوشته باشید:
>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>> x
[10]
شاید برایتان پرسش شده باشد که چرا افزودن یک عنصر به y، x را نیز تغییر داد.
دو عامل وجود دارند که این نتیجه را ایجاد میکنند:
متغیرها صرفاً نامهایی هستند که به شیءها ارجاع میدهند. با انجام
y = xرونوشتی از فهرست ایجاد نمیشود، بلکه یک متغیر جدیدyایجاد میشود که به همان شیئی ارجاع میدهد کهxبه آن ارجاع میدهد. این بدان معناست که تنها یک شیء (فهرست) وجود دارد و هر دوxوyبه آن ارجاع میدهند.فهرستها تغییرپذیر هستند، به این معنا که میتوانید محتوای آنها را تغییر دهید.
پس از فراخوانی append()، محتوای شیء تغییرپذیر از [] به [10] تغییر کرده است. از آنجا که هر دو متغیر به یک شیء ارجاع دارند، با استفاده از هر یک از نامها میتوان به مقدار تغییرکرده [10] دسترسی پیدا کرد.
اگر بهجای آن یک شیء تغییرناپذیر را به x انتساب دهیم:
>>> x = 5 # ints are immutable
>>> y = x
>>> x = x + 1 # 5 can't be mutated, we are creating a new object here
>>> x
6
>>> y
5
میتوانیم ببینیم که در این حالت x و y دیگر برابر نیستند. این به این دلیل است که اعداد صحیح تغییرناپذیر هستند، و هنگامی که x = x + 1 را اجرا میکنیم، با افزایش مقدار آن، عدد صحیح 5 را تغییر نمیدهیم؛ در عوض، یک شیء جدید (عدد صحیح 6) ایجاد میکنیم و آن را به x اختصاص میدهیم (یعنی اینکه x به کدام شیء ارجاع دارد را تغییر میدهیم). پس از این انتساب، دو شیء (اعداد صحیح 6 و 5) و دو متغیر داریم که به آنها ارجاع دارند (x اکنون به 6 ارجاع دارد اما y هنوز به 5 ارجاع دارد).
برخی عملیات (برای مثال y.append(10) و y.sort()) شیء را تغییر میدهند، در حالی که عملیات ظاهراً مشابه (برای مثال y = y + [10] و sorted(y)) شیء جدیدی ایجاد میکنند. بهطور کلی در پایتون (و در همه موارد در کتابخانه استاندارد)، متدی که یک شیء را تغییر میدهد مقدار None را برمیگرداند تا کمک کند این دو نوع عملیات با هم اشتباه گرفته نشوند. بنابراین اگر بهاشتباه y.sort() را بنویسید و فکر کنید که نسخهای مرتبشده از y به شما میدهد، در عوض مقدار None دریافت خواهید کرد که احتمالاً باعث میشود برنامه شما خطایی تولید کند که بهراحتی قابل تشخیص است.
با این حال، دستهای از عملیات وجود دارد که در آنها یک عملیات واحد گاهی با انواع مختلف رفتارهای متفاوتی دارد: عملگرهای انتساب افزوده. برای مثال، += فهرستها را تغییر میدهد، اما تاپلها یا اعداد صحیح را تغییر نمیدهد (a_list += [1, 2, 3] معادل a_list.extend([1, 2, 3]) است و a_list را تغییر میدهد، در حالی که some_tuple += (1, 2, 3) و some_int += 1 اشیاء جدیدی ایجاد میکنند).
به عبارت دیگر:
اگر یک شیء تغییرپذیر (مانند
list،dict،set) داشته باشیم، میتوانیم از برخی عملیات خاص برای تغییر آن استفاده کنیم و همه متغیرهایی که به آن ارجاع دارند، تغییر را خواهند دید.اگر یک شیء تغییرناپذیر داشته باشیم (مانند
str،int،tuple)، همه متغیرهایی که به آن ارجاع میکنند همیشه مقدار یکسانی را مشاهده خواهند کرد، اما عملیاتی که آن مقدار را به مقدار جدیدی تبدیل میکنند همیشه شیء جدیدی را برمیگردانند.
اگر میخواهید بدانید که آیا دو متغیر به یک شیء ارجاع میدهند یا خیر، میتوانید از عملگر is یا تابع توکار id() استفاده کنید.
چگونه تابعی با پارامترهای خروجی (فراخوانی با ارجاع) بنویسم؟¶
به یاد داشته باشید که آرگومانها در پایتون با انتساب ارسال میشوند. از آنجا که انتساب صرفاً ارجاعهایی به شیءها ایجاد میکند، هیچ نام مستعاری بین نام یک آرگومان در فراخواننده و فراخوانده وجود ندارد و در نتیجه هیچ فراخوانی با ارجاعی وجود ندارد. شما میتوانید به روشهای گوناگونی به اثر مطلوب دست یابید.
با برگرداندن یک تاپل از نتایج:
>>> def func1(a, b): ... a = 'new-value' # a and b are local names ... b = b + 1 # assigned to new objects ... return a, b # return new values ... >>> x, y = 'old-value', 99 >>> func1(x, y) ('new-value', 100)
این تقریباً همیشه واضحترین راهحل است.
با استفاده از متغیرهای سراسری. این روش ایمن نسبت به نخ (thread-safe) نیست و توصیه نمیشود.
با ارسال یک شیء تغییرپذیر (قابل تغییر بهصورت درجا):
>>> def func2(a): ... a[0] = 'new-value' # 'a' references a mutable list ... a[1] = a[1] + 1 # changes a shared object ... >>> args = ['old-value', 99] >>> func2(args) >>> args ['new-value', 100]
با پاس دادن یک دیکشنری که تغییر مییابد:
>>> def func3(args): ... args['a'] = 'new-value' # args is a mutable dictionary ... args['b'] = args['b'] + 1 # change it in-place ... >>> args = {'a': 'old-value', 'b': 99} >>> func3(args) >>> args {'a': 'new-value', 'b': 100}
یا مقادیر را در یک نمونه کلاس دستهبندی کنید:
>>> class Namespace: ... def __init__(self, /, **args): ... for key, value in args.items(): ... setattr(self, key, value) ... >>> def func4(args): ... args.a = 'new-value' # args is a mutable Namespace ... args.b = args.b + 1 # change object in-place ... >>> args = Namespace(a='old-value', b=99) >>> func4(args) >>> vars(args) {'a': 'new-value', 'b': 100}
تقریباً هرگز دلیل خوبی برای اینقدر پیچیده شدن وجود ندارد.
بهترین انتخاب شما برگرداندن یک تاپل حاوی نتایج متعدد است.
چگونه در پایتون یک تابع مرتبه بالاتر میسازید؟¶
شما دو گزینه دارید: میتوانید از محدودههای تودرتو یا از اشیای فراخوانیپذیر استفاده کنید. برای مثال، فرض کنید میخواهید linear(a,b) را تعریف کنید که تابع f(x) را برمیگرداند؛ تابعی که مقدار a*x+b را محاسبه میکند. با استفاده از محدودههای تودرتو:
def linear(a, b):
def result(x):
return a * x + b
return result
یا با استفاده از یک شیء فراخوانیپذیر:
class linear:
def __init__(self, a, b):
self.a, self.b = a, b
def __call__(self, x):
return self.a * x + self.b
در هر دو مورد،
taxes = linear(0.3, 2)
یک شیء فراخوانیپذیر میدهد که در آن taxes(10e6) == 0.3 * 10e6 + 2.
رویکرد شیء فراخوانیپذیر این عیب را دارد که کمی کندتر است و منجر به کد کمی طولانیتر میشود. با این حال، توجه داشته باشید که مجموعهای از اشیاء فراخوانیپذیر میتوانند امضای خود را از طریق ارثبری به اشتراک بگذارند:
class exponential(linear):
# __init__ inherited
def __call__(self, x):
return self.a * (x ** self.b)
شیء میتواند وضعیت را برای چندین متد کپسوله کند:
class counter:
value = 0
def set(self, x):
self.value = x
def up(self):
self.value = self.value + 1
def down(self):
self.value = self.value - 1
count = counter()
inc, dec, reset = count.up, count.down, count.set
در اینجا inc()، dec() و reset() مانند توابعی عمل میکنند که متغیر شمارندهی یکسانی را به اشتراک میگذارند.
چگونه میتوانم یک شیء را در پایتون کپی کنم؟¶
بهطور کلی، برای حالت عمومی، copy.copy() یا copy.deepcopy() را امتحان کنید. همهی اشیاء قابل کپی نیستند، اما بیشتر آنها قابل کپی هستند.
برخی از اشیاء را میتوان بهسادگی بیشتری کپی کرد. دیکشنریها دارای متد copy() هستند:
newdict = olddict.copy()
دنبالهها را میتوان با اسلایس کردن کپی کرد:
new_l = l[:]
چگونه میتوانم متدها یا ویژگیهای یک شیء را پیدا کنم؟¶
برای یک نمونه x از یک کلاس تعریفشده توسط کاربر، dir(x) فهرستی مرتبشده به ترتیب الفبا از نامها را برمیگرداند که شامل ویژگیهای نمونه، متدها و ویژگیهای تعریفشده توسط کلاس آن است.
کد من چگونه میتواند نام یک شیء را کشف کند؟¶
بهطور کلی، امکانپذیر نیست، زیرا اشیاء واقعاً نام ندارند. در اصل، انتساب همیشه یک نام را به یک مقدار مقید میکند؛ همین موضوع برای دستورهای def و class نیز صادق است، اما در آن حالت مقدار یک فراخوانیپذیر است. کد زیر را در نظر بگیرید:
>>> class A:
... pass
...
>>> B = A
>>> a = B()
>>> b = a
>>> print(b)
<__main__.A object at 0x16D07CC>
>>> print(a)
<__main__.A object at 0x16D07CC>
میتوان گفت که کلاس دارای نام است: با وجود اینکه به دو نام مقید است و از طریق نام B فراخوانی میشود، نمونهی ایجادشده همچنان بهعنوان نمونهای از کلاس A گزارش میشود. با این حال، نمیتوان گفت که نام نمونه a است یا b، زیرا هر دو نام به یک مقدار مقید هستند.
بهطور کلی، نباید ضرورتی داشته باشد که کد شما «نامهای مقادیر خاص را بداند». مگر اینکه عمداً در حال نوشتن برنامههای دروننگر (introspective) باشید، این معمولاً نشانهای است که تغییر رویکرد ممکن است مفید باشد.
در comp.lang.python، Fredrik Lundh یکبار تمثیلی عالی در پاسخ به این پرسش ارائه کرد:
به همان شیوهای که نام آن گربهای را که در ایوان خود پیدا کردهاید میفهمید: خود گربه (شیء) نمیتواند نامش را به شما بگوید، و واقعاً برایش اهمیتی ندارد — بنابراین تنها راه برای فهمیدن نام آن این است که از همه همسایههایتان (فضای نامها) بپرسید که آیا این گربه، گربهی آنها (شیء) است یا خیر...
....و تعجب نکنید اگر متوجه شدید که با نامهای بسیاری شناخته میشود، یا اصلاً نامی ندارد!
وضعیت اولویت عملگر ویرگول چیست؟¶
کاما در پایتون عملگر نیست. این نشست را در نظر بگیرید:
>>> "a" in "b", "a"
(False, 'a')
از آنجا که کاما یک عملگر نیست، بلکه جداکنندهای بین عبارتها است، مورد بالا طوری ارزیابی میشود که گویی شما وارد کردهاید:
("a" in "b"), "a"
not:
"a" in ("b", "a")
همین موضوع در مورد انواع عملگرهای انتساب (=، += و غیره) نیز صادق است. آنها واقعاً عملگر نیستند، بلکه جداکنندههای نحوی در دستورات انتساب هستند.
آیا معادلی برای عملگر سهگانهی "?:" در C وجود دارد؟¶
بله، وجود دارد. سینتکس به صورت زیر است:
[on_true] if [expression] else [on_false]
x, y = 50, 25
small = x if x < y else y
پیش از معرفی این سینتکس در Python 2.5، الگوی رایج این بود که از عملگرهای منطقی استفاده شود:
[expression] and [on_true] or [on_false]
با این حال، این شیوه ناامن است، زیرا هنگامی که on_true مقدار بولی نادرست داشته باشد، میتواند نتایج اشتباهی بدهد. بنابراین، همیشه بهتر است از قالب ... if ... else ... استفاده کنید.
آیا امکان نوشتن تکسطریهای مبهم در پایتون وجود دارد؟¶
بله. معمولاً این کار با تودرتو کردن lambda درون lambda انجام میشود. ۳ مثال زیر را ببینید، که اندکی از Ulf Bartelt اقتباس شدهاند:
from functools import reduce
# Primes < 1000
print(list(filter(None,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))
# First 10 Fibonacci numbers
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), range(10))))
# Mandelbrot set
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+'\n'+y,map(lambda y,
Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM,
Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro,
i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k<=0)or (x*x+y*y
>=4.0) or 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr(
64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy),range(Sy
))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24))
# \___ ___/ \___ ___/ | | |__ lines on screen
# V V | |______ columns on screen
# | | |__________ maximum of "iterations"
# | |_________________ range on y axis
# |____________________________ range on x axis
بچهها، این کار را در خانه امتحان نکنید!
اسلش (/) در فهرست پارامترهای یک تابع چه معنایی دارد؟¶
اسلش در فهرست آرگومانهای یک تابع نشان میدهد که پارامترهای پیش از آن فقط جایگاهی هستند. پارامترهای فقط جایگاهی، پارامترهایی بدون نام قابل استفاده از بیرون هستند. هنگام فراخوانی تابعی که پارامترهای فقط جایگاهی میپذیرد، آرگومانها صرفاً بر اساس جایگاهشان به پارامترها نگاشت میشوند. برای مثال، divmod() تابعی است که پارامترهای فقط جایگاهی میپذیرد. مستندات آن به این شکل است:
>>> help(divmod)
Help on built-in function divmod in module builtins:
divmod(x, y, /)
Return the tuple (x//y, x%y). Invariant: div*y + mod == x.
اسلش در انتهای فهرست پارامترها به این معناست که هر دو پارامتر فقط جایگاهی هستند. بنابراین، فراخوانی divmod() با آرگومانهای کلیدواژهای منجر به خطا خواهد شد:
>>> divmod(x=3, y=4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: divmod() takes no keyword arguments
اعداد و رشتهها¶
چگونه میتوانم اعداد صحیح مبنای شانزده و مبنای هشت را مشخص کنم؟¶
برای مشخص کردن یک رقم مبنای هشت، پیش از مقدار مبنای هشت یک صفر و سپس حرف "o" کوچک یا بزرگ قرار دهید. برای مثال، برای تنظیم متغیر "a" به مقدار مبنای هشت "10" (۸ در مبنای ده)، تایپ کنید:
>>> a = 0o10
>>> a
8
مبنای شانزده هم به همین سادگی است. کافی است پیش از عدد مبنای شانزده، یک صفر و سپس یک «x» کوچک یا بزرگ قرار دهید. ارقام مبنای شانزده را میتوان با حروف کوچک یا بزرگ مشخص کرد. برای مثال، در مفسر پایتون:
>>> a = 0xa5
>>> a
165
>>> b = 0XB2
>>> b
178
چرا -22 // 10 مقدار -3 را برمیگرداند؟¶
این موضوع عمدتاً ناشی از این خواسته است که i % j همان علامت j را داشته باشد. اگر این را میخواهید، و همچنین میخواهید:
i == (i // j) * j + (i % j)
در این صورت تقسیم عدد صحیح باید مقدار کف (floor) را برگرداند. زبان C نیز برقرار بودن این اتحاد را الزامی میداند، و در این صورت کامپایلرهایی که i // j را قطع میکنند باید i % j را همعلامت با i کنند.
وقتی j منفی است، موارد استفاده واقعی کمی برای i % j وجود دارد. وقتی j مثبت است، موارد زیادی وجود دارد، و تقریباً در همهی آنها، مفیدتر است که i % j >= 0 باشد. اگر ساعت اکنون ۱۰ باشد، ۲۰۰ ساعت پیش چند بوده است؟ -190 % 12 == 2 مفید است؛ -190 % 12 == -10 باگی است که در کمین نشسته تا مشکلساز شود.
چگونه میتوانم بهجای SyntaxError، ویژگی لفظی int را دریافت کنم؟¶
تلاش برای جستوجوی ویژگی یک لفظی int به روش معمول باعث SyntaxError میشود، زیرا نقطه بهعنوان نقطهی اعشار در نظر گرفته میشود:
>>> 1.__class__
File "<stdin>", line 1
1.__class__
^
SyntaxError: invalid decimal literal
راهحل این است که لفظی را با یک فاصله یا پرانتز از نقطه جدا کنید.
>>> 1 .__class__
<class 'int'>
>>> (1).__class__
<class 'int'>
چگونه یک رشته را به عدد تبدیل کنم؟¶
برای اعداد صحیح، از سازندهی نوع توکار int() استفاده کنید، برای مثال، int('144') == 144. بهطور مشابه، float() به یک عدد ممیز شناور تبدیل میکند، برای مثال، float('144') == 144.0.
بهطور پیشفرض، اینها عدد را بهصورت مبنای ده تفسیر میکنند، بهطوری که int('0144') == 144 برقرار است و int('0x144') استثنای ValueError را پرتاب میکند. int(string, base) مبنای تبدیل را بهعنوان آرگومان اختیاری دوم دریافت میکند، بنابراین int( '0x144', 16) == 324. اگر مبنا برابر ۰ مشخص شود، عدد با استفاده از قوانین پایتون تفسیر میشود: پیشوند '0o' نشاندهندهی عدد مبنای هشت است و '0x' نشاندهندهی یک عدد مبنای شانزده است.
اگر تنها نیاز شما تبدیل رشتهها به اعداد است، از تابع توکار eval() استفاده نکنید. eval() بهطور قابلتوجهی کندتر خواهد بود و خطر امنیتی ایجاد میکند: ممکن است شخصی یک عبارت پایتون را به شما بدهد که عوارض جانبی ناخواستهای داشته باشد. برای مثال، ممکن است شخصی __import__('os').system("rm -rf $HOME") را ارسال کند که پوشه خانه شما را پاک کند.
eval() همچنین اعداد را بهعنوان عبارتهای پایتون تفسیر میکند، بهطوری که برای مثال، eval('09') یک خطای سینتکس میدهد، زیرا پایتون اجازه نمیدهد '0' در ابتدای یک عدد مبنای ده قرار گیرد (بهجز '0').
چگونه یک عدد را به رشته تبدیل کنم؟¶
برای مثال، برای تبدیل عدد 144 به رشتهی '144'، از سازندهی نوع توکار str() استفاده کنید. اگر نمایش مبنای شانزده یا مبنای هشت را میخواهید، از توابع توکار hex() یا oct() استفاده کنید. برای قالببندی پیشرفته، بخشهای افاسترینگها و سینتکس رشته قالب را ببینید. برای مثال، "{:04d}".format(144) مقدار '0144' را برمیگرداند و "{:.3f}".format(1.0/3.0) مقدار '0.333' را برمیگرداند.
چگونه میتوانم یک رشته را بهصورت درجا تغییر دهم؟¶
نمیتوانید، زیرا رشتهها تغییرناپذیرند. در بیشتر مواقع، باید بهسادگی رشتهی جدیدی را از اجزای مختلفی که میخواهید از آنها تشکیل شود، بسازید. با این حال، اگر به شیئی با قابلیت اصلاح دادههای یونیکد بهصورت درجا نیاز دارید، سعی کنید از یک شیء io.StringIO یا ماژول array استفاده کنید:
>>> import io
>>> s = "Hello, world"
>>> sio = io.StringIO(s)
>>> sio.getvalue()
'Hello, world'
>>> sio.seek(7)
7
>>> sio.write("there!")
6
>>> sio.getvalue()
'Hello, there!'
>>> import array
>>> a = array.array('w', s)
>>> print(a)
array('w', 'Hello, world')
>>> a[0] = 'y'
>>> print(a)
array('w', 'yello, world')
>>> a.tounicode()
'yello, world'
چگونه میتوانم از رشتهها برای فراخوانی توابع/متدها استفاده کنم؟¶
روشهای مختلفی وجود دارد.
بهترین روش استفاده از یک دیکشنری است که رشتهها را به توابع نگاشت میکند. مزیت اصلی این روش آن است که رشتهها نیازی نیست با نام توابع مطابقت داشته باشند. این روش همچنین روش اصلی مورد استفاده برای شبیهسازی یک ساختار case است:
def a(): pass def b(): pass dispatch = {'go': a, 'stop': b} # Note lack of parens for funcs dispatch[get_input()]() # Note trailing parens to call function
از تابع توکار
getattr()استفاده کنید:import foo getattr(foo, 'bar')()
توجه داشته باشید که
getattr()روی هر شیئی کار میکند، از جمله کلاسها، نمونههای کلاس، ماژولها و غیره.این در چندین جای کتابخانهی استاندارد استفاده میشود، مانند این:
class Foo: def do_foo(self): ... def do_bar(self): ... f = getattr(foo_instance, 'do_' + opname) f()
برای تعیین نام تابع از
locals()استفاده کنید:def myFunc(): print("hello") fname = "myFunc" f = locals()[fname] f()
آیا معادلی برای chomp() در Perl برای حذف سطرهای جدید انتهایی از رشتهها وجود دارد؟¶
شما میتوانید از S.rstrip("\r\n") برای حذف همهی موارد هر پایانگر خط از انتهای رشتهی S بدون حذف دیگر فضاهای سفید انتهایی استفاده کنید. اگر رشتهی S نمایانگر بیش از ۱ خط باشد، با چند خط خالی در انتها، پایانگرهای خط برای همهی سطرهای خالی حذف خواهند شد:
>>> lines = ("line 1 \r\n"
... "\r\n"
... "\r\n")
>>> lines.rstrip("\n\r")
'line 1 '
از آنجا که این کار معمولاً تنها زمانی مطلوب است که متن را هر بار یک خط میخوانید، استفاده از S.rstrip() به این شکل بهخوبی کار میکند.
آیا معادلی برای scanf() یا sscanf() وجود دارد؟¶
نه بههمین صورت.
برای تجزیه ساده ورودی، معمولاً آسانترین روش این است که خط را با استفاده از متد split() در اشیای رشتهای به کلمات جداشده با فضای خالی تقسیم کنید و سپس رشتههای دهدهی را با استفاده از int() یا float() به مقادیر عددی تبدیل کنید. split() از یک پارامتر اختیاری "sep" پشتیبانی میکند که زمانی مفید است که خط از چیزی غیر از فضای خالی بهعنوان جداکننده استفاده کند.
برای تجزیه ورودی پیچیدهتر، عبارتهای باقاعده قدرتمندتر از sscanf در C و برای این کار مناسبتر هستند.
منظور از خطای UnicodeDecodeError یا UnicodeEncodeError چیست؟¶
راهنمای عملی یونیکد را ببینید.
آیا میتوانم یک رشته خام را با تعداد فردی از بکاسلشها به پایان برسانم؟¶
رشته خامی که با تعداد فردی از بکاسلشها تمام میشود، علامت نقلقول رشته را خنثی میکند:
>>> r'C:\this\will\not\work\'
File "<stdin>", line 1
r'C:\this\will\not\work\'
^
SyntaxError: unterminated string literal (detected at line 1)
برای این موضوع چند راهحل جایگزین وجود دارد. یکی از آنها استفاده از رشتههای معمولی و دو برابر کردن بکاسلشها است:
>>> 'C:\\this\\will\\work\\'
'C:\\this\\will\\work\\'
راه دیگر، الحاق یک رشته معمولی حاوی بکاسلش خنثیشده به رشته خام است:
>>> r'C:\this\will\work' '\\'
'C:\\this\\will\\work\\'
همچنین میتوانید از os.path.join() برای افزودن یک بکاسلش در ویندوز استفاده کنید:
>>> os.path.join(r'C:\this\will\work', '')
'C:\\this\\will\\work\\'
توجه داشته باشید که اگرچه یک بکاسلش علامت نقلقول را برای تعیین محل پایان رشته خام «خنثی» میکند، هنگام تفسیر مقدار رشته خام هیچ خنثیسازی رخ نمیدهد. یعنی، بکاسلش در مقدار رشته خام باقی میماند:
>>> r'backslash\'preserved'
"backslash\\'preserved"
همچنین مشخصات موجود در مرجع زبان را ببینید.
کارایی¶
برنامهی من بسیار کند است. چگونه آن را سریعتر کنم؟¶
بهطور کلی، این پرسش دشواری است. نخست، در اینجا فهرستی از مواردی که باید پیش از ادامهی عمیقتر به خاطر بسپارید آمده است:
ویژگیهای عملکردی در پیادهسازیهای مختلف پایتون متفاوت است. این پرسشهای متداول بر CPython تمرکز دارد.
رفتار ممکن است در سیستمعاملهای مختلف متفاوت باشد، بهویژه هنگامی که صحبت از ورودی/خروجی یا چندنخی میشود.
شما باید همیشه نقاط داغ برنامهی خود را پیش از تلاش برای بهینهسازی هر کدی پیدا کنید (ماژول
profileرا ببینید).نوشتن اسکریپتهای محک به شما امکان میدهد هنگام جستوجو برای بهبودها، بهسرعت تکرار کنید (ماژول
timeitرا ببینید).اکیداً توصیه میشود پیش از آنکه بهطور بالقوه رگرسیونهایی پنهان در بهینهسازیهای پیچیده ایجاد کنید، پوشش کد خوبی (از طریق آزمون واحد یا هر تکنیک دیگری) داشته باشید.
با این حال، ترفندهای زیادی برای سرعت بخشیدن به کد پایتون وجود دارد. در اینجا برخی اصول کلی آورده شده است که کمک شایانی به رسیدن به سطوح عملکرد قابلقبول میکنند:
سریعتر کردن الگوریتمهایتان (یا تغییر به الگوریتمهای سریعتر) میتواند مزایای بسیار بزرگتری نسبت به تلاش برای پخش کردن ترفندهای ریزبهینهسازی (micro-optimization) در سراسر کدتان به همراه داشته باشد.
از ساختارهای داده مناسب استفاده کنید. مستندات مربوط به Built-in Types و ماژول
collectionsرا مطالعه کنید.هنگامی که کتابخانه استاندارد ساختار اولیهای برای انجام کاری فراهم میکند، احتمالاً (هرچند تضمینی نیست) سریعتر از هر جایگزینی است که ممکن است شما ارائه دهید. این موضوع برای ساختارهای اولیه نوشتهشده به زبان C، مانند توکارها و برخی از انواع توسعهای، دوچندان صادق است. برای مثال، حتماً برای مرتبسازی از متد توکار
list.sort()یا تابع مرتبطsorted()استفاده کنید (و برای نمونههایی از کاربرد نسبتاً پیشرفته، روشهای مرتبسازی را ببینید).انتزاعها معمولاً باعث ایجاد ارجاعهای غیرمستقیم میشوند و مفسر را مجبور به کار بیشتر میکنند. اگر سطوح ارجاع غیرمستقیم بر مقدار کار مفید انجامشده غلبه کند، برنامه شما کندتر خواهد شد. شما باید از انتزاع بیشازحد، بهویژه در قالب توابع یا متدهای بسیار کوچک (که اغلب به خوانایی نیز آسیب میزنند) اجتناب کنید.
اگر به محدودیت آنچه پایتون خالص اجازه میدهد رسیدهاید، ابزارهایی وجود دارند که شما را فراتر میبرند. برای مثال، Cython میتواند نسخهای اندکی تغییریافته از کد پایتون را به یک افزونه C کامپایل کند و میتواند روی سکوهای مختلفی استفاده شود. Cython میتواند با بهرهگیری از کامپایل (و حاشیهنویسیهای نوع اختیاری)، کد شما را بهطور قابلتوجهی سریعتر از حالت تفسیرشده کند. اگر به مهارتهای برنامهنویسی C خود اطمینان دارید، همچنین میتوانید خودتان یک ماژول توسعهای C بنویسید.
همچنین ملاحظه نمائید
صفحهی ویکی اختصاصیافته به نکات کارایی.
کارآمدترین روش برای الحاق رشتههای بسیاری به یکدیگر چیست؟¶
str and bytes objects are immutable, therefore concatenating
many strings together is inefficient as each concatenation creates a new
object. In the general case, the total runtime cost is quadratic in the
total string length. See Time complexity of operations on built-in types for more information.
برای انباشتن تعداد زیادی شیء str، روش توصیهشده این است که آنها را در یک فهرست قرار دهید و در پایان str.join() را فراخوانی کنید:
chunks = []
for s in my_strings:
chunks.append(s)
result = ''.join(chunks)
(یک روش نسبتاً کارآمد دیگر استفاده از io.StringIO است.)
برای انباشتن بسیاری از اشیای bytes، روش توصیهشده این است که یک شیء bytearray را با استفاده از الحاق درجا (عملگر +=) گسترش دهید:
result = bytearray()
for b in my_bytes_objects:
result += b
دنبالهها (تاپلها/فهرستها)¶
چگونه تاپلها و فهرستها را به یکدیگر تبدیل کنم؟¶
سازندهی نوع tuple(seq) هر دنبالهای (در واقع، هر پیمایشپذیری) را به یک تاپل با همان آیتمها و همان ترتیب تبدیل میکند.
برای مثال، tuple([1, 2, 3]) مقدار (1, 2, 3) را حاصل میدهد و tuple('abc') مقدار ('a', 'b', 'c') را حاصل میدهد. اگر آرگومان یک تاپل باشد، نسخهای نمیسازد بلکه همان شیء را برمیگرداند، بنابراین وقتی مطمئن نیستید که یک شیء از قبل یک تاپل است، فراخوانی tuple() کمهزینه است.
سازندهی نوع list(seq) هر دنباله یا پیمایشپذیری را به یک فهرست با همان آیتمها به همان ترتیب تبدیل میکند. برای مثال، list((1, 2, 3)) مقدار [1, 2, 3] را برمیگرداند و list('abc') مقدار ['a', 'b', 'c'] را برمیگرداند. اگر آرگومان یک فهرست باشد، دقیقاً مانند seq[:] یک کپی میسازد.
اندیس منفی چیست؟¶
دنبالههای پایتون با اعداد مثبت و منفی اندیسدهی میشوند. برای اعداد مثبت، ۰ اولین اندیس، ۱ دومین اندیس و به همین ترتیب است. برای اندیسهای منفی، منفی ۱ آخرین اندیس، منفی ۲ اندیس ماقبل آخر (یکی مانده به آخر) و به همین ترتیب است. seq[-n] را معادل seq[len(seq)-n] در نظر بگیرید.
استفاده از اندیسهای منفی میتواند بسیار مفید باشد. برای مثال S[:-1] تمام رشته بهجز آخرین نویسه آن است، که برای حذف خط جدید پایانی از یک رشته مفید است.
چگونه میتوانم یک دنباله را در ترتیب معکوس پیمایش کنم؟¶
از تابع توکار reversed() استفاده کنید:
for x in reversed(sequence):
... # do something with x ...
این کار دنباله اصلی شما را تغییر نمیدهد، بلکه یک رونوشت جدید با ترتیب معکوس برای پیمایش میسازد.
چگونه موارد تکراری را از یک فهرست حذف میکنید؟¶
برای بحث مفصل دربارهی راههای بسیاری برای انجام این کار، Python Cookbook را ببینید:
اگر با تغییر ترتیب فهرست مشکلی ندارید، آن را مرتب کنید و سپس از انتهای فهرست پیمایش کنید و در حین پیشروی، موارد تکراری را حذف کنید:
if mylist:
mylist.sort()
last = mylist[-1]
for i in range(len(mylist)-2, -1, -1):
if last == mylist[i]:
del mylist[i]
else:
last = mylist[i]
اگر بتوان از همهی عناصر فهرست بهعنوان کلیدهای مجموعه استفاده کرد (یعنی همهی آنها hashable باشند)، این کار معمولاً سریعتر است:
mylist = list(set(mylist))
این کار فهرست را به یک مجموعه تبدیل میکند، در نتیجه موارد تکراری حذف میشوند، و سپس آن را دوباره به یک فهرست برمیگرداند.
چگونه چندین آیتم را از یک فهرست حذف میکنید؟¶
مانند حذف تکراریها، پیمایش معکوس بهصورت صریح همراه با یک شرط حذف یکی از امکانها است. با این حال، استفاده از جایگزینی اسلایس با پیمایش رو به جلو بهصورت ضمنی یا صریح، سادهتر و سریعتر است. در اینجا سه حالت مختلف آمده است:
mylist[:] = filter(keep_function, mylist)
mylist[:] = (x for x in mylist if keep_condition)
mylist[:] = [x for x in mylist if keep_condition]
درک فهرستی ممکن است سریعترین باشد.
چگونه در پایتون یک آرایه میسازید؟¶
از یک فهرست استفاده کنید:
["this", 1, "is", "an", "array"]
فهرستها از نظر پیچیدگی زمانی معادل آرایههای C یا Pascal هستند؛ تفاوت اصلی این است که یک فهرست پایتون میتواند شامل اشیایی از انواع گوناگون باشد.
ماژول array همچنین متدهایی برای ایجاد آرایههایی از انواع ثابت با بازنماییهای فشرده فراهم میکند، اما اندیسدهی به آنها کندتر از فهرستها است. همچنین توجه داشته باشید که NumPy و سایر بستههای شخص ثالث نیز ساختارهای آرایهمانندی با ویژگیهای مختلف تعریف میکنند.
برای داشتن فهرستهای پیوندی به سبک Lisp، میتوانید cons cells را با استفاده از تاپلها شبیهسازی کنید:
lisp_list = ("like", ("this", ("example", None) ) )
اگر تغییرپذیری مورد نظر باشد، میتوانید بهجای تاپلها از فهرستها استفاده کنید. در اینجا معادل car در Lisp برابر lisp_list[0] و معادل cdr برابر lisp_list[1] است. این کار را تنها در صورتی انجام دهید که مطمئن هستید واقعاً به آن نیاز دارید، زیرا معمولاً بسیار کندتر از استفاده از فهرستهای پایتون است.
چگونه یک فهرست چندبعدی ایجاد کنم؟¶
احتمالاً سعی کردهاید یک آرایه چندبعدی مانند این بسازید:
>>> A = [[None] * 2] * 3
اگر آن را چاپ کنید، درست به نظر میرسد:
>>> A
[[None, None], [None, None], [None, None]]
اما وقتی مقداری را انتساب میدهید، در چندین جا ظاهر میشود:
>>> A[0][0] = 5
>>> A
[[5, None], [5, None], [5, None]]
دلیل این است که تکثیر یک فهرست با * نسخههایی ایجاد نمیکند، بلکه فقط ارجاعهایی به اشیاء موجود ایجاد میکند. *3 فهرستی شامل ۳ ارجاع به همان فهرست با طول دو ایجاد میکند. تغییرات در یک ردیف در تمام ردیفها نمایان خواهد شد، که تقریباً بهطور قطع آنچه شما میخواهید نیست.
رویکرد پیشنهادی این است که ابتدا فهرستی به طول دلخواه ایجاد کنید و سپس هر عنصر را با فهرستی که بهتازگی ایجاد شده است پر کنید:
A = [None] * 3
for i in range(3):
A[i] = [None] * 2
این یک فهرست شامل ۳ فهرست متفاوت به طول دو تولید میکند. شما همچنین میتوانید از یک درک فهرستی استفاده کنید:
w, h = 2, 3
A = [[None] * w for i in range(h)]
یا میتوانید از افزونهای استفاده کنید که نوع داده ماتریسی را فراهم میکند؛ NumPy شناختهشدهترین است.
چگونه یک متد یا تابع را بر دنبالهای از شیءها اعمال کنم؟¶
برای فراخوانی یک متد یا تابع و جمعآوری مقادیر بازگشتی در یک فهرست، درک فهرستی راهحلی ظریف است:
result = [obj.method() for obj in mylist]
result = [function(obj) for obj in mylist]
برای اینکه فقط متد یا تابع را بدون ذخیرهی مقادیر بازگشتی اجرا کنید، یک حلقهی for ساده کافی است:
for obj in mylist:
obj.method()
for obj in mylist:
function(obj)
چرا a_tuple[i] += ['item'] در حالی که عمل جمع کار میکند، استثنا پرتاب میکند؟¶
این بهدلیل ترکیبی از این واقعیت است که عملگرهای انتساب افزوده، عملگرهای انتساب هستند و تفاوت میان اشیاء تغییرپذیر و تغییرناپذیر در پایتون.
این بحث بهطور کلی زمانی صدق میکند که عملگرهای انتساب افزوده بر عناصری از یک تاپل که به اشیاء تغییرپذیر اشاره میکنند، اعمال شوند، اما از یک list و += بهعنوان نمونهی خود استفاده میکنیم.
اگر نوشتید:
>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
دلیل این استثنا باید بلافاصله روشن باشد: با اضافه شدن 1 به شیئی که a_tuple[0] به آن اشاره میکند (1)، شیء نتیجه، 2، تولید میشود، اما وقتی تلاش میکنیم نتیجهی محاسبه، 2، را به عنصر 0 تاپل انتساب دهیم، با خطا مواجه میشویم، زیرا نمیتوانیم آنچه را که یک عنصر از تاپل به آن اشاره میکند تغییر دهیم.
آنچه این دستور انتساب افزوده در پشت صحنه انجام میدهد، تقریباً این است:
>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
این بخش انتساب عملیات است که خطا را ایجاد میکند، زیرا تاپل تغییرناپذیر است.
هنگامی که چیزی مانند زیر مینویسید:
>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
این استثنا کمی تعجبآورتر است، و حتی تعجبآورتر این واقعیت است که با وجود خطا، append کار کرد:
>>> a_tuple[0]
['foo', 'item']
برای اینکه ببینید چرا این اتفاق میافتد، باید بدانید که (الف) اگر یک شیء متد جادویی __iadd__() را پیادهسازی کرده باشد، هنگامی که انتساب تقویتشدهی += اجرا شود، آن متد فراخوانی میشود و مقدار بازگشتی آن چیزی است که در دستور انتساب به کار میرود؛ و (ب) برای فهرستها، __iadd__() معادل فراخوانی extend() روی فهرست و برگرداندن فهرست است. به همین دلیل است که میگوییم برای فهرستها، += «میانبر» برای list.extend() است:
>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]
این معادل است با:
>>> result = a_list.__iadd__([1])
>>> a_list = result
شیئی که a_list به آن اشاره دارد تغییریافته است و اشارهگر به شیء تغییریافته دوباره به a_list منتسب میشود. نتیجه نهایی این انتساب، یک عملیات بیاثر (no-op) است، زیرا این یک اشارهگر به همان شیئی است که a_list پیشتر به آن اشاره میکرد، اما انتساب همچنان انجام میشود.
بنابراین، در مثال تاپل ما، آنچه رخ میدهد معادل است با:
>>> result = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
__iadd__() با موفقیت اجرا میشود، و بنابراین فهرست گسترش مییابد، اما با وجود اینکه result به همان شیئی اشاره میکند که a_tuple[0] از قبل به آن اشاره میکند، آن انتساب نهایی همچنان منجر به خطا میشود، زیرا تاپلها تغییرناپذیر هستند.
میخواهم یک مرتبسازی پیچیده انجام دهم: آیا میتوانید تبدیل شوارتزی (Schwartzian Transform) را در پایتون انجام دهید؟¶
این تکنیک، منسوب به رندال شوارتز از کامیونیتی Perl، عناصر یک فهرست را بر اساس معیاری مرتب میکند که هر عنصر را به «مقدار مرتبسازی» آن نگاشت میکند. در پایتون، از آرگومان key برای متد list.sort() استفاده کنید:
Isorted = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))
چگونه میتوانم یک فهرست را بر اساس مقادیر فهرست دیگر مرتبسازی کنم؟¶
آنها را در یک پیمایشگر از تاپلها ادغام کنید، فهرست حاصل را مرتب کنید و سپس عنصری را که میخواهید انتخاب کنید.
>>> list1 = ["what", "I'm", "sorting", "by"]
>>> list2 = ["something", "else", "to", "sort"]
>>> pairs = zip(list1, list2)
>>> pairs = sorted(pairs)
>>> pairs
[("I'm", 'else'), ('by', 'sort'), ('sorting', 'to'), ('what', 'something')]
>>> result = [x[1] for x in pairs]
>>> result
['else', 'sort', 'to', 'something']
اشیاء¶
کلاس چیست؟¶
کلاس، نوع شیء خاصی است که با اجرای یک دستور کلاس ایجاد میشود. اشیای کلاس بهعنوان قالبهایی برای ایجاد اشیای نمونه استفاده میشوند که هم دادهها (ویژگیها) و هم کد (متدها) خاص یک نوع داده را در بر میگیرند.
یک کلاس میتواند بر پایهی یک یا چند کلاس دیگر باشد که به آنها کلاسهای پایهی آن گفته میشود. سپس ویژگیها و متدهای کلاسهای پایهی خود را به ارث میبرد. این امکان را فراهم میکند که یک مدل شیء بهتدریج از طریق وراثت پالایش شود. ممکن است شما یک کلاس Mailbox عام داشته باشید که متدهای دسترسی پایه برای یک صندوق پستی را ارائه میکند، و زیرکلاسهایی مانند MboxMailbox، MaildirMailbox و OutlookMailbox که قالبهای مشخص مختلفی از صندوق پستی را مدیریت میکنند.
متد چیست؟¶
متد، تابعی بر روی یک شیء x است که معمولاً آن را بهصورت x.name(arguments...) فراخوانی میکنید. متدها بهعنوان توابعی درون تعریف کلاس تعریف میشوند:
class C:
def meth(self, arg):
return arg * 2 + self.attribute
self چیست؟¶
Self صرفاً نامی مرسوم برای اولین آرگومان یک متد است. متدی که بهصورت meth(self, a, b, c) تعریف شده است، باید برای یک نمونه x از کلاسی که تعریف در آن رخ میدهد، بهصورت x.meth(a, b, c) فراخوانی شود؛ متد فراخوانیشده فرض خواهد کرد که بهصورت meth(x, a, b, c) فراخوانی شده است.
همچنین چرا باید از 'self' بهصراحت در تعاریف و فراخوانیهای متد استفاده شود؟ را ببینید.
چگونه میتوانم بررسی کنم که آیا یک شیء نمونهای از یک کلاس مشخص یا زیرکلاسی از آن است؟¶
از تابع توکار isinstance(obj, cls) استفاده کنید. شما میتوانید با ارائه یک تاپل بهجای یک کلاس واحد، بررسی کنید که آیا یک شیء نمونهای از هر یک از چند کلاس است یا خیر، برای مثال، isinstance(obj, (class1, class2, ...))، و همچنین میتوانید بررسی کنید که آیا یک شیء یکی از انواع توکار پایتون است یا خیر، برای مثال، isinstance(obj, str) یا isinstance(obj, (int, float, complex)).
توجه داشته باشید که isinstance() همچنین وراثت مجازی از یک abstract base class را بررسی میکند. بنابراین، این آزمون برای یک کلاس ثبتشده True را برمیگرداند، حتی اگر بهطور مستقیم یا غیرمستقیم از آن ارث نبرده باشد. برای آزمون «وراثت واقعی»، method resolution order (MRO) کلاس را پیمایش کنید:
from collections.abc import Mapping
class P:
pass
class C(P):
pass
Mapping.register(P)
>>> c = C()
>>> isinstance(c, C) # direct
True
>>> isinstance(c, P) # indirect
True
>>> isinstance(c, Mapping) # virtual
True
# Actual inheritance chain
>>> type(c).__mro__
(<class 'C'>, <class 'P'>, <class 'object'>)
# Test for "true inheritance"
>>> Mapping in type(c).__mro__
False
توجه داشته باشید که بیشتر برنامهها استفادهی زیادی از isinstance() روی کلاسهای تعریفشده توسط کاربر نمیکنند. اگر خودتان کلاسها را توسعه میدهید، سبک شیءگرای مناسبتر این است که متدهایی در کلاسها تعریف کنید که رفتار خاصی را کپسوله میکنند، بهجای اینکه کلاس شیء را بررسی کنید و بر اساس اینکه شیء از چه کلاسی است، کار متفاوتی انجام دهید. برای مثال، اگر تابعی دارید که کاری انجام میدهد:
def search(obj):
if isinstance(obj, Mailbox):
... # code to search a mailbox
elif isinstance(obj, Document):
... # code to search a document
elif ...
رویکرد بهتر این است که یک متد search() در همه کلاسها تعریف کنید و فقط آن را فراخوانی کنید:
class Mailbox:
def search(self):
... # code to search a mailbox
class Document:
def search(self):
... # code to search a document
obj.search()
واگذاری (delegation) چیست؟¶
واگذاری (delegation) یک تکنیک شیءگرا است (که به آن الگوی طراحی نیز گفته میشود). فرض کنید شما یک شیء x دارید و میخواهید رفتار فقط یکی از متدهای آن را تغییر دهید. میتوانید یک کلاس جدید بسازید که پیادهسازی جدیدی از متدی که قصد تغییر آن را دارید ارائه میدهد و تمام متدهای دیگر را به متد متناظر از x واگذار میکند.
برنامهنویسان پایتون میتوانند بهراحتی واگذاری (delegation) را پیادهسازی کنند. برای مثال، کلاس زیر کلاسی را پیادهسازی میکند که مانند یک پرونده رفتار میکند، اما تمام دادههای نوشتهشده را به حروف بزرگ تبدیل میکند:
class UpperOut:
def __init__(self, outfile):
self._outfile = outfile
def write(self, s):
self._outfile.write(s.upper())
def __getattr__(self, name):
return getattr(self._outfile, name)
در اینجا کلاس UpperOut متد write() را بازتعریف میکند تا رشتهی آرگومان را پیش از فراخوانی متد زیرین self._outfile.write() به حروف بزرگ تبدیل کند. تمام متدهای دیگر به شیء زیرین self._outfile واگذار میشوند. این واگذاری از طریق متد __getattr__() انجام میشود؛ برای اطلاعات بیشتر دربارهی کنترل دسترسی به ویژگی، به مرجع زبان مراجعه کنید.
توجه داشته باشید که در موارد کلیتر، واگذاری میتواند پیچیدهتر شود. هنگامی که ویژگیها باید علاوه بر بازیابی، مقداردهی شوند، کلاس باید یک متد __setattr__() نیز تعریف کند، و باید این کار را با دقت انجام دهد. پیادهسازی پایهی __setattr__() تقریباً معادل عبارت زیر است:
class X:
...
def __setattr__(self, name, value):
self.__dict__[name] = value
...
بسیاری از پیادهسازیهای __setattr__()، برای تنظیم یک ویژگی روی self بدون ایجاد بازگشت بیپایان، object.__setattr__() را فراخوانی میکنند:
class X:
def __setattr__(self, name, value):
# Custom logic here...
object.__setattr__(self, name, value)
بهعنوان جایگزین، میتوان ویژگیها را با درج مستقیم ورودیها در self.__dict__ تنظیم کرد.
چگونه یک متد تعریفشده در کلاس پایه را از یک کلاس مشتق که آن را گسترش میدهد فراخوانی کنم؟¶
از تابع توکار super() استفاده کنید:
class Derived(Base):
def meth(self):
super().meth() # calls Base.meth
در این مثال، super() بهطور خودکار نمونهای را که از آن فراخوانی شده است (مقدار self) تعیین میکند، method resolution order (MRO) را با type(self).__mro__ جستجو میکند، و مورد بعدی در ترتیب پس از Derived در MRO را برمیگرداند: Base.
چگونه میتوانم کد خود را سازماندهی کنم تا تغییر کلاس پایه آسانتر شود؟¶
میتوانید کلاس پایه را به یک نام مستعار اختصاص دهید و از آن نام مستعار مشتق شوید. سپس تنها چیزی که باید تغییر دهید، مقدار اختصاصدادهشده به نام مستعار است. ضمناً، این ترفند زمانی هم که بخواهید بهصورت پویا تصمیم بگیرید (مثلاً بسته به دسترسپذیری منابع) از کدام کلاس پایه استفاده کنید، مفید است. مثال:
class Base:
...
BaseAlias = Base
class Derived(BaseAlias):
...
چگونه دادههای ایستای کلاس و متدهای ایستای کلاس را ایجاد کنم؟¶
در پایتون، هم از دادههای ایستا و هم از متدهای ایستا (به معنای C++ یا Java) پشتیبانی میشود.
برای دادههای ایستا، کافی است یک ویژگی کلاس تعریف کنید. برای انتساب مقدار جدید به ویژگی، باید در انتساب بهصراحت از نام کلاس استفاده کنید:
class C:
count = 0 # number of times C.__init__ called
def __init__(self):
C.count = C.count + 1
def getcount(self):
return C.count # or return self.count
c.count همچنین برای هر c که isinstance(c, C) برقرار باشد، به C.count ارجاع میدهد، مگر اینکه خود c یا کلاسی در مسیر جستجوی کلاس پایه از c.__class__ تا C آن را بازنویسی کرده باشد.
هشدار: در داخل یک متد از C، انتسابی مانند self.count = 42 یک نمونه جدید و نامرتبط به نام "count" در دیکشنری خود self ایجاد میکند. برای انتساب مجدد یک نام دادهی ایستای کلاس، همیشه باید کلاس مشخص شود، چه داخل یک متد و چه خارج از آن:
C.count = 314
متدهای ایستا امکانپذیر هستند:
class C:
@staticmethod
def static(arg1, arg2, arg3):
# No 'self' parameter!
...
با این حال، راه بسیار سادهتر برای بهدست آوردن اثر یک متد ایستا، از طریق یک تابع ساده در سطح ماژول است:
def getcount():
return C.count
اگر کد شما بهگونهای ساختار یافته باشد که در هر ماژول یک کلاس (یا سلسلهمراتبی از کلاسهای کاملاً مرتبط) تعریف شود، این امر کپسولهسازی (encapsulation) مطلوب را فراهم میکند.
چگونه میتوانم سازندهها (یا متدها) را در پایتون سربارگذاری (overload) کنم؟¶
این پاسخ در واقع در مورد همه متدها صدق میکند، اما این پرسش معمولاً ابتدا در زمینهی سازندهها مطرح میشود.
در C++ شما مینویسید:
class C {
C() { cout << "No arguments\n"; }
C(int i) { cout << "Argument is " << i << "\n"; }
}
در پایتون باید یک سازنده واحد بنویسید که با استفاده از آرگومانهای پیشفرض، همه حالتها را پوشش میدهد. برای مثال:
class C:
def __init__(self, i=None):
if i is None:
print("No arguments")
else:
print("Argument is", i)
این بهطور کامل معادل نیست، اما در عمل به اندازه کافی نزدیک است.
همچنین میتوانید یک فهرست آرگومان با طول متغیر را امتحان کنید، برای مثال:
def __init__(self, *args):
...
همین رویکرد برای تمام تعریفهای متد کار میکند.
من سعی میکنم از __spam استفاده کنم و با خطایی درباره _SomeClassName__spam مواجه میشوم.¶
نام متغیرهایی که دو زیرخط آغازین دارند، تغییر نام داده میشوند (mangled) تا راهی ساده اما مؤثر برای تعریف متغیرهای خصوصی کلاس فراهم شود. هر شناسهای بهشکل __spam (حداقل دو زیرخط آغازین و حداکثر یک زیرخط پایانی) بهصورت متنی با _classname__spam جایگزین میشود، که در آن classname نام کلاس جاری است که هرگونه زیرخط آغازین آن حذف شده است.
میتوان از شناسه بدون تغییر درون کلاس استفاده کرد، اما برای دسترسی به آن بیرون از کلاس، باید از نام تغییرشکلیافته (mangled name) استفاده کرد:
class A:
def __one(self):
return 1
def two(self):
return 2 * self.__one()
class B(A):
def three(self):
return 3 * self._A__one()
four = 4 * A()._A__one()
بهویژه، این کار خصوصی بودن را تضمین نمیکند، زیرا یک کاربر بیرونی همچنان میتواند عمداً به ویژگی خصوصی دسترسی پیدا کند؛ بسیاری از برنامهنویسان پایتون اصلاً زحمت استفاده از نامهای متغیر خصوصی را به خود نمیدهند.
همچنین ملاحظه نمائید
برای جزئیات و موارد خاص، مشخصات تغییر نام خصوصی (private name mangling).
کلاس من __del__ را تعریف میکند، اما هنگامی که شیء را حذف میکنم فراخوانی نمیشود.¶
دلایل متعددی ممکن است برای این موضوع وجود داشته باشد.
دستور del لزوماً __del__() را فراخوانی نمیکند -- صرفاً شمار ارجاعهای شیء را کاهش میدهد، و اگر این شمار به صفر برسد، __del__() فراخوانی میشود.
اگر ساختارهای دادهی شما حاوی پیوندهای حلقهای باشند (برای مثال، درختی که در آن هر فرزند یک ارجاع به والد دارد و هر والد فهرستی از فرزندان دارد)، شمار ارجاعها هرگز به صفر بازنمیگردد. هر از گاهی پایتون الگوریتمی را برای تشخیص چنین چرخههایی اجرا میکند، اما ممکن است زبالهروب مدتی پس از از بین رفتن آخرین ارجاع به ساختار دادهی شما اجرا شود، بنابراین ممکن است متد __del__() شما در زمانی نامناسب و تصادفی فراخوانی شود. اگر در تلاش برای بازتولید یک مشکل باشید، این موضوع نامناسب است. بدتر آنکه ترتیبی که متدهای __del__() اشیاء اجرا میشوند، دلخواه است. برای اجبار به زبالهروبی، میتوانید gc.collect() را اجرا کنید، اما موارد مرضی وجود دارند که اشیاء هرگز جمعآوری نمیشوند.
با وجود جمعکننده چرخه، هنوز هم بهتر است یک متد صریح close() برای شیءها تعریف کنید تا هرگاه کار شما با آنها تمام شد، فراخوانی شود. سپس متد close() میتواند ویژگیهایی را که به زیرشیءها ارجاع میدهند حذف کند. __del__() را مستقیماً فراخوانی نکنید -- __del__() باید close() را فراخوانی کند و close() باید اطمینان حاصل کند که میتوان آن را بیش از یک بار برای یک شیء فراخوانی کرد.
راه دیگر برای اجتناب از ارجاعهای چرخهای استفاده از ماژول weakref است، که به شما امکان میدهد به اشیاء بدون افزایش شمار ارجاعهایشان اشاره کنید. برای مثال، ساختارهای دادهای درختی باید برای ارجاعهای والد و گرههای همسطح خود (در صورت نیاز!) از ارجاعهای ضعیف استفاده کنند.
در نهایت، اگر متد __del__() شما استثنایی را پرتاب کند، یک پیام هشدار در sys.stderr چاپ میشود.
چگونه میتوانم فهرستی از همه نمونههای یک کلاس مشخص را به دست آورم؟¶
پایتون تمام نمونههای یک کلاس (یا یک نوع توکار) را پیگیری نمیکند. شما میتوانید سازندهی کلاس را بهگونهای بنویسید که با نگهداری فهرستی از ارجاعهای ضعیف به هر نمونه، تمام نمونهها را پیگیری کند.
چرا به نظر میرسد نتیجه id() یکتا نیست؟¶
تابع توکار id() یک عدد صحیح برمیگرداند که یکتا بودن آن در طول عمر شیء تضمین شده است. از آنجا که در CPython، این مقدار نشانی حافظهی شیء است، اغلب اتفاق میافتد که پس از حذف یک شیء از حافظه، شیء تازهساختهی بعدی در همان موقعیت حافظه تخصیص داده شود. این موضوع در این مثال نشان داده شده است:
>>> id(1000)
13901272
>>> id(2000)
13901272
دو شناسه به اشیای عدد صحیح متفاوتی تعلق دارند که پیش از اجرای فراخوانی id() ایجاد شدهاند و بلافاصله پس از اجرای آن حذف میشوند. برای اطمینان از زنده بودن اشیایی که میخواهید شناسهی آنها را بررسی کنید، یک ارجاع دیگر به شیء ایجاد کنید:
>>> a = 1000; b = 2000
>>> id(a)
13901272
>>> id(b)
13891296
چه زمانی میتوانم به آزمونهای هویت با عملگر is اتکا کنم؟¶
عملگر is هویت شیء را میآزماید. آزمون a is b معادل id(a) == id(b) است.
مهمترین ویژگی یک آزمون هویت این است که یک شیء همیشه با خودش یکسان است، a is a همیشه True را برمیگرداند. آزمونهای هویت معمولاً سریعتر از آزمونهای برابری هستند. و برخلاف آزمونهای برابری، تضمین شده است که آزمونهای هویت یک مقدار بولی True یا False برمیگردانند.
با این حال، آزمونهای هویت فقط زمانی میتوانند جایگزین آزمونهای برابری شوند که هویت شیء تضمین شده باشد. بهطور کلی، ۳ حالت وجود دارد که در آنها هویت تضمین میشود:
انتسابها نامهای جدیدی ایجاد میکنند، اما هویت شیء را تغییر نمیدهند. پس از انتساب
new = old، تضمین میشود کهnew is old.قرار دادن یک شیء در ظرفی که ارجاعهای شیء را ذخیره میکند، هویت شیء را تغییر نمیدهد. پس از انتساب
s[0] = xدر فهرست، تضمین میشود کهs[0] is x.اگر شیئی تکنمونه باشد، به این معناست که تنها یک نمونه از آن شیء میتواند وجود داشته باشد. پس از انتسابهای
a = Noneوb = None، تضمین میشود کهa is bزیراNoneیک تکنمونه است.
در بیشتر شرایط دیگر، آزمونهای هویت توصیه نمیشوند و آزمونهای برابری ترجیح داده میشوند. بهویژه، نباید از آزمونهای هویت برای بررسی ثابتهایی مانند int و str استفاده کرد، زیرا تضمین نمیشود که نمونههای یکتا باشند:
>>> a = 10_000_000
>>> b = 5_000_000
>>> c = b + 5_000_000
>>> a is c
False
>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a is c
False
به همین ترتیب، نمونههای جدید از ظرفهای تغییرپذیر هرگز یکسان نیستند:
>>> a = []
>>> b = []
>>> a is b
False
در کد کتابخانهی استاندارد، چندین الگوی رایج را برای استفادهی صحیح از آزمونهای هویت خواهید دید:
به توصیهی PEP 8، آزمون هویت روش ارجح برای بررسی
Noneاست. این روش در کد مانند انگلیسی ساده خوانده میشود و از اشتباه گرفتن آن با اشیاء دیگری که ممکن است مقادیر بولی آنها به false ارزیابی شوند، جلوگیری میکند.تشخیص آرگومانهای اختیاری میتواند زمانی که
Noneیک مقدار ورودی معتبر است، دشوار باشد. در این شرایط، میتوانید یک شیء نشانگر تکنمونه (singleton sentinel object) ایجاد کنید که متمایز بودن آن از سایر اشیاء تضمین شده است. برای مثال، در اینجا نحوهی پیادهسازی متدی که مانندdict.pop()رفتار میکند آمده است:_sentinel = object() def pop(self, key, default=_sentinel): if key in self: value = self[key] del self[key] return value if default is _sentinel: raise KeyError(key) return default
پیادهسازیهای ظرف گاهی نیاز دارند آزمونهای برابری را با آزمونهای هویت تکمیل کنند. این کار از سردرگم شدن کد در اثر اشیایی مانند
float('NaN')که با خودشان برابر نیستند جلوگیری میکند.
برای مثال، در اینجا پیادهسازی collections.abc.Sequence.__contains__() آمده است:
def __contains__(self, value):
for v in self:
if v is value or v == value:
return True
return False
چگونه یک زیرکلاس میتواند کنترل کند که چه دادههایی در یک نمونهی تغییرناپذیر ذخیره میشوند؟¶
هنگام زیرکلاسسازی یک نوع تغییرناپذیر، بهجای متد __init__()، متد __new__() را بازنویسی کنید. متد دوم فقط پس از ایجاد یک نمونه اجرا میشود و در آن زمان برای تغییر دادههای یک نمونهی تغییرناپذیر دیگر دیر است.
همهی این کلاسهای تغییرناپذیر، امضایی متفاوت از کلاس والد خود دارند:
import datetime as dt
class FirstOfMonthDate(dt.date):
"Always choose the first day of the month"
def __new__(cls, year, month, day):
return super().__new__(cls, year, month, 1)
class NamedInt(int):
"Allow text names for some numbers"
xlat = {'zero': 0, 'one': 1, 'ten': 10}
def __new__(cls, value):
value = cls.xlat.get(value, value)
return super().__new__(cls, value)
class TitleStr(str):
"Convert str to name suitable for a URL path"
def __new__(cls, s):
s = s.lower().replace(' ', '-')
s = ''.join([c for c in s if c.isalnum() or c == '-'])
return super().__new__(cls, s)
میتوان از این کلاسها به این صورت استفاده کرد:
>>> FirstOfMonthDate(2012, 2, 14)
FirstOfMonthDate(2012, 2, 1)
>>> NamedInt('ten')
10
>>> NamedInt(20)
20
>>> TitleStr('Blog: Why Python Rocks')
'blog-why-python-rocks'
چگونه فراخوانیهای متد را در نهانگاه ذخیره کنم؟¶
دو ابزار اصلی برای نهانسازی متدها، @functools.cached_property و @functools.lru_cache هستند. اولی نتایج را در سطح نمونه ذخیره میکند و دومی آنها را در سطح کلاس.
روش cached_property فقط با متدهایی کار میکند که هیچ آرگومانی دریافت نمیکنند. این روش ارجاعی به نمونه ایجاد نمیکند. نتیجهی نهانشدهی متد فقط تا زمانی که نمونه زنده است نگه داشته میشود.
مزیت این است که هنگامی که دیگر از یک نمونه استفاده نمیشود، نتیجهی نهانشدهی متد بلافاصله آزاد میشود. عیب این است که اگر نمونهها انباشته شوند، نتایج متدها نیز انباشته خواهند شد. آنها میتوانند بدون محدودیت رشد کنند.
رویکرد lru_cache با متدهایی کار میکند که آرگومانهای hashable دارند. این روش یک ارجاع به نمونه ایجاد میکند، مگر اینکه تلاشهای ویژهای برای ارسال ارجاعهای ضعیف انجام شود.
مزیت الگوریتم کمترین استفادهی اخیر این است که نهانگاه به maxsize مشخصشده محدود میشود. عیب آن این است که نمونهها زنده نگه داشته میشوند تا زمانی که بهدلیل قدیمی شدن از نهانگاه خارج شوند یا نهانگاه پاک شود.
این مثال تکنیکهای مختلف را نشان میدهد:
class Weather:
"Lookup weather information on a government website"
def __init__(self, station_id):
self._station_id = station_id
# The _station_id is private and immutable
def current_temperature(self):
"Latest hourly observation"
# Do not cache this because old results
# can be out of date.
@cached_property
def location(self):
"Return the longitude/latitude coordinates of the station"
# Result only depends on the station_id
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='mm'):
"Rainfall on a given date"
# Depends on the station_id, date, and units.
مثال بالا فرض میکند که station_id هرگز تغییر نمیکند. اگر ویژگیهای مربوط به نمونه تغییرپذیر باشند، نمیتوان از رویکرد cached_property استفاده کرد، زیرا نمیتواند تغییرات ویژگیها را تشخیص دهد.
برای اینکه رویکرد lru_cache زمانی که station_id تغییرپذیر است کار کند، کلاس باید متدهای __eq__() و __hash__() را تعریف کند تا نهانگاه بتواند بهروزرسانیهای مرتبط با ویژگی را تشخیص دهد:
class Weather:
"Example with a mutable station identifier"
def __init__(self, station_id):
self.station_id = station_id
def change_station(self, station_id):
self.station_id = station_id
def __eq__(self, other):
return self.station_id == other.station_id
def __hash__(self):
return hash(self.station_id)
@lru_cache(maxsize=20)
def historic_rainfall(self, date, units='cm'):
'Rainfall on a given date'
# Depends on the station_id, date, and units.
ماژولها¶
چگونه یک پرونده .pyc ایجاد کنم؟¶
هنگامی که یک ماژول برای نخستین بار ایمپورت میشود (یا وقتی پرونده منبع پس از ایجاد پرونده کامپایلشدهی فعلی تغییر کرده باشد)، یک پرونده .pyc شامل کد کامپایلشده باید در زیرپوشهی __pycache__ از پوشهی حاوی پرونده .py ایجاد شود. نام پرونده .pyc با همان نام پرونده .py آغاز میشود و با .pyc پایان مییابد، و دارای یک کامپوننت میانی است که به دودویی python خاصی که آن را ایجاد کرده است بستگی دارد. (برای جزئیات، PEP 3147 را ببینید.)
یکی از دلایلی که ممکن است یک پرونده .pyc ایجاد نشود، مشکل مجوزها در پوشهی حاوی پرونده منبع است، به این معنا که زیرپوشهی __pycache__ نمیتواند ایجاد شود. این موضوع میتواند، برای مثال، زمانی رخ دهد که شما بهعنوان یک کاربر توسعه میدهید اما بهعنوان کاربر دیگری اجرا میکنید، مانند زمانی که با یک وبسرور در حال آزمایش هستید.
مگر اینکه متغیر محیطی PYTHONDONTWRITEBYTECODE تنظیمشده باشد، ایجاد یک پرونده .pyc خودکار است، مشروط بر اینکه شما ماژولی را ایمپورت کنید و پایتون توانایی لازم (دسترسیها، فضای آزاد و غیره) را برای ایجاد یک زیرپوشهی __pycache__ و نوشتن ماژول کامپایلشده در آن زیرپوشه داشته باشد.
اجرای پایتون بر روی یک اسکریپت سطح بالا، بهعنوان یک ایمپورت در نظر گرفته نمیشود و هیچ پرونده .pyc ایجاد نخواهد شد. برای مثال، اگر یک ماژول سطح بالا به نام foo.py دارید که ماژول دیگری به نام xyz.py را ایمپورت میکند، وقتی foo را اجرا میکنید (با وارد کردن python foo.py بهعنوان یک دستور پوسته)، یک پرونده .pyc برای xyz ایجاد میشود، زیرا xyz ایمپورت شده است، اما هیچ پرونده .pyc برای foo ایجاد نخواهد شد، زیرا foo.py ایمپورت نمیشود.
اگر نیاز دارید یک پرونده .pyc برای foo ایجاد کنید -- یعنی برای ماژولی که ایمپورت نشده است یک پرونده .pyc ایجاد کنید -- میتوانید این کار را با استفاده از ماژولهای py_compile و compileall انجام دهید.
ماژول py_compile میتواند هر ماژولی را بهصورت دستی کامپایل کند. یکی از راهها استفاده از تابع compile() در آن ماژول بهصورت تعاملی است:
>>> import py_compile
>>> py_compile.compile('foo.py')
این کار .pyc را در یک زیرپوشهی __pycache__ در همان محل foo.py مینویسد (یا میتوانید آن را با پارامتر اختیاری cfile تغییر دهید).
همچنین میتوانید با استفاده از ماژول compileall، همهی پروندههای موجود در یک پوشه یا چند پوشه را بهصورت خودکار کامپایل کنید. میتوانید این کار را از خط فرمان پوسته با اجرای compileall.py و ارائهی مسیر پوشهای حاوی پروندههای پایتون برای کامپایل انجام دهید:
python -m compileall .
چگونه نام ماژول جاری را پیدا کنم؟¶
یک ماژول میتواند با بررسی متغیر سراسری از پیش تعریفشده __name__، نام ماژول خود را پیدا کند. اگر این متغیر مقدار '__main__' داشته باشد، برنامه بهعنوان یک اسکریپت در حال اجرا است. بسیاری از ماژولها که معمولاً با ایمپورت کردن آنها استفاده میشوند، یک رابط خط فرمان یا خودآزمایی نیز فراهم میکنند و این کد را تنها پس از بررسی __name__ اجرا میکنند:
def main():
print('Running test...')
...
if __name__ == '__main__':
main()
چگونه میتوانم ماژولهایی داشته باشم که یکدیگر را بهطور متقابل ایمپورت کنند؟¶
فرض کنید ماژولهای زیر را دارید:
foo.py:
from bar import bar_var
foo_var = 1
bar.py:
from foo import foo_var
bar_var = 2
مشکل این است که مفسر مراحل زیر را انجام خواهد داد:
main،
fooرا ایمپورت میکندفضای نام سراسری خالی برای
fooایجاد میشودfooکامپایل میشود و اجرای آن آغاز میشودfoo،barرا ایمپورت میکندمتغیرهای سراسری خالی برای
barایجاد میشوندbarکامپایل میشود و شروع به اجرا میکندbarماژولfooرا ایمپورت میکند (که یک عملیات بیاثر (no-op) است، زیرا از قبل ماژولی به نامfooوجود دارد)سازوکار ایمپورت تلاش میکند
foo_varرا از متغیرهای سراسریfooبخواند، تاbar.foo_var = foo.foo_varرا تنظیم کند
آخرین مرحله شکست میخورد، زیرا پایتون هنوز تفسیر foo را به پایان نرسانده است و دیکشنری نمادهای سراسری برای foo هنوز خالی است.
همین اتفاق زمانی رخ میدهد که از import foo استفاده کنید و سپس سعی کنید در کد سراسری به foo.foo_var دسترسی پیدا کنید.
برای این مشکل (حداقل) ۳ راهحل جایگزین ممکن وجود دارد.
گیدو ون روسوم توصیه میکند که از هرگونه استفاده از from <module> import ... خودداری شود و تمام کد درون توابع قرار گیرد. برای مقداردهی اولیهی متغیرهای سراسری و متغیرهای کلاس باید فقط از ثابتها یا توابع توکار استفاده شود. این بدان معناست که همهچیز از یک ماژول ایمپورتشده بهصورت <module>.<name> ارجاع داده میشود.
Jim Roskind پیشنهاد میکند که مراحل در هر ماژول بهترتیب زیر انجام شوند:
موارد اکسپورتشده (متغیرهای سراسری، توابع و کلاسهایی که نیازی به کلاسهای پایهی ایمپورتشده ندارند)
دستورهای
importکد فعال (شامل متغیرهای سراسری که از مقادیر ایمپورتشده مقداردهی اولیه شدهاند).
ون روسوم این رویکرد را چندان نمیپسندد، زیرا ایمپورتها در جای عجیبی ظاهر میشوند، اما این رویکرد کار میکند.
ماتیاس اورلیکس توصیه میکند ساختار کد خود را بهگونهای تغییر دهید که اصلاً نیازی به ایمپورت بازگشتی نباشد.
این راهحلها مانعالجمع نیستند.
__import__('x.y.z') <module 'x'> را برمیگرداند؛ چگونه میتوانم z را دریافت کنم؟¶
بهتر است بهجای آن از تابع کمکی import_module() در importlib استفاده کنید:
z = importlib.import_module('x.y.z')
هنگامی که یک ماژول ایمپورتشده را ویرایش میکنم و دوباره آن را ایمپورت میکنم، تغییرات مشاهده نمیشوند. چرا این اتفاق میافتد؟¶
به دلایل کارایی و همچنین سازگاری، پایتون پرونده ماژول را فقط در اولین باری که یک ماژول ایمپورت میشود میخواند. اگر اینطور نبود، در برنامهای متشکل از ماژولهای زیاد که هر کدام همان ماژول پایه را ایمپورت میکنند، ماژول پایه بارها تجزیه و دوباره تجزیه میشد. برای وادار کردن به بازخوانی یک ماژول تغییریافته، این کار را انجام دهید:
import importlib
import modname
importlib.reload(modname)
هشدار: این روش ۱۰۰٪ مصون از خطا نیست. بهویژه، ماژولهای شامل دستورهایی مانند:
from modname import some_objects
با نسخهی قدیمی اشیای ایمپورتشده به کار خود ادامه خواهند داد. اگر ماژول حاوی تعریفهای کلاس باشد، نمونههای موجود کلاس برای استفاده از تعریف جدید کلاس بهروزرسانی نخواهند شد. این میتواند منجر به رفتار متناقض زیر شود:
>>> import importlib
>>> import cls
>>> c = cls.C() # Create an instance of C
>>> importlib.reload(cls)
<module 'cls' from 'cls.py'>
>>> isinstance(c, cls.C) # isinstance is false?!?
False
اگر «هویت» اشیای کلاس را چاپ کنید، ماهیت مشکل روشن میشود:
>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'