ویژگی های جدید و جالب پایتون ۳.۸ — راهنمای کاربردی
جدیدترین نسخه پایتون یعنی پایتون 3.8 در 14 اکتبر 2019 (22 مهر 1398) منتشر شده است. اینک میتوانیم از امکانات جدید آن استفاده کرده و از مزیت جدیدترین بهبودها بهرهمند شویم. مستندات پایتون (+) مرور مناسبی در مورد ویژگیهای جدید این نسخه داشته است. با این حال در این مقاله قصد داریم بررسی عمیقتری در مورد برخی از ویژگی های جدید و جالب پایتون 3.8 داشته باشیم و شیوه بهرهگیری از مزیتهای نسخه 3.8 را توضیح دهیم.
در این مقاله با مطلب زیر آشنا خواهیم شد:
- استفاده از «عبارت انتسابی» برای سادهسازی برخی سازههای کد.
- الزام آرگومانهای «صرفاً موقعیتی» (positional-only) در تابعهای سفارشی.
- تعیین دقیقتر «سرنخ نوع» (type hint).
- استفاده از f-رشتهها برای دیباگ سادهتر.
به طور کلی به جز چند استثنا، نسخه 3.8 پایتون نسبت به نسخههای قبلی شامل بهبودهای کوچکی بوده است. در اواخر این مقاله بسیاری از این تغییرهای کوچک که توجه اندکی را برمیانگیزند، به همراه بحثهایی در مورد برخی بهینهسازیها که موجب شده نسخه 3.8 پایتون نسبت به نسخههای قبلی سریعتر باشد مشاهده خواهید کرد. در نهایت پیشنهادهایی در خصوص ارتقا به نسخ جدید پایتون ارائه شده است.
عبارت انتسابی
بزرگترین تغییر در نسخه 3.8 پایتون معرفی «عبارت انتسابی» (Assignment Expression) بوده است. این عبارتها با استفاده از نمادگذاری جدید =: نوشته میشوند. این عملگر غالباً به نام عملگر «گراز دریایی» (Walrus) خوانده میشود، چون شبیه چشمها و عاجهای این حیوان است.
عبارتهای انتسابی امکان انتساب و بازگشت یک مقدار را همزمان در یک عبارت فراهم ساختهاند. برای نمونه تا قبل از نسخه 3.8 اگر میخواستید یک مقدار به متغیری انتساب داده و آن را پرینت کنید، کدی مانند زیر مینوشتید:
1>>> walrus = False
2>>> print(walrus)
3False
در پایتون 3.8 امکان ترکیب این دو عبارت در یک عبارت واحد و استفاده از عملگر گراز دریایی فراهم شده است:
1>>> print(walrus := True)
2True
عبارت انتسابی امکان انتساب مقدار True به متغیر walrus را فراهم ساخته و بیدرنگ مقدار آن را پرینت میکند. اما به خاطر داشته باشید که عملگر گراز دریایی هیچ کاری که بدون استفاده از آن ممکن نباشد را انجام نمیدهد. این عملگر صرفاً موجب میشود که برخی سازهها سادهتر شوند. همچنین در برخی موارد میتواند موجب شود که منظور از کد روشنتر شود.
یک الگویی که برخی از نقاط قوت عملگر گراز دریایی را نشان میدهد، حلقههای While هستند که باید یک متغیر را مقداردهی کرده و بهروزرسانی کنند. برای نمونه کد زیر از کاربر میخواهد تا زمانی که عبارت quit را وارد نکرده است، ورودیهایی ارائه کند:
1inputs = list()
2current = input("Write something: ")
3while current != "quit":
4 inputs.append(current)
5 current = input("Write something: ")
این کد چندان بهینه نیست. گزاره ()input تکرار میشود و به هر حال باید پیش از تقاضا از کاربر برای وارد کردن مقدار current، آن را به list اضافه کنید. یک راهحل بهتر این است که یک حلقه نامتناهی While تنظیم کنید و از کاربر بخواهید که با استفاده از break حلقه را متوقف کند:
1inputs = list()
2while True:
3 current = input("Write something: ")
4 if current == "quit":
5 break
6 inputs.append(current)
این کد معادل کد قبلی است، اما از تکرار جلوگیری میکند و به نوعی با ترتیب منطقی نیز مطابقت بیشتری دارد. اگر از یک عبارت انتسابی استفاده کنید، میتوانید این حلقه را تا حدود زیادی سادهتر بنویسید:
1inputs = list()
2while (current := input("Write something: ")) != "quit":
3 inputs.append(current)
بدین ترتیب تست به خط while بازمیگردد و این همان جایی است که باید باشد. با این حال، چند اتفاق دیگر نیز در این خط رخ میدهند، از این رو برای خواندن آن با اندکی دشواری مواجه میشویم. تلاش کنید در مورد مواقعی که استفاده از عملگر گراز دریایی موجب بهبود خوانایی کدتان میشود تفکر کنید. در این صفحه (+) همه جزییات عبارت انتسابی شامل برخی از استدلالها برای معرفی آنها در این زبان و همچنین چندین نمونه از شیوه استفاده از این عملگر ارائه شده است.
آرگومانهای «صرفاً موقعیتی»
در پایتون میتوان از تابع داخلی ()float برای تبدیل رشتههای متنی و اعداد به اشیای float بهره گرفت. به مثال زیر توجه کنید:
1>>> float("3.8")
23.8
3
4>>> help(float)
5class float(object)
6 | float(x=0, /)
7 |
8 | Convert a string or number to a floating point number, if possible.
9
10[...]
به امضای متد ()float با دقت نگاه کنید. به کاراکتر / پس از پارامتر توجه کنید. معنای آن این است که وقتی یکی از پارامترهای ()float، به نام x باشد، مجاز به استفاده از نام آن نیستید:
1>>> float(x="3.8")
2Traceback (most recent call last):
3 File "<stdin>", line 1, in <module>
4TypeError: float() takes no keyword arguments
هنگامی که از ()float استفاده میکنید، تنها مجاز به تعیین آرگومانها بر اساس موقعیتشان هستید و نه برحسب کلیدواژه. تا پیش از پایتون 3.8، استفاده از چنین آرگومانهای «صرفاً موقعیتی» تنها در تابعهای داخلی ممکن بود. در آن زمان هیچ روش آسانی برای تعیین این که آرگومانها باید در تابعهای سفارشی نیز صرفاً موقعیتی باشند، وجود نداشت:
1>>> def incr(x):
2... return x + 1
3...
4>>> incr(3.8)
54.8
6
7>>> incr(x=3.8)
84.8
شبیهسازی آرگومانهای صرفاً موقعیتی با استفاده از args* ممکن است، اما انعطافپذیری کمی دارد، خوانایی آن پایین است و ما را ملزم به پیادهسازی یک تحلیل آرگومان سفارشی میکند. در پایتون 3.8 میتوان از کاراکتر ممیز (/) برای نشان دادن این که همه آرگومانهای پیش از آن باید برحسب موقعیت تعیین شوند، بهره گرفت. بدین ترتیب میتوانید ()incr را طوری بازنویسی کنید که آرگومانهای موقعیتی بپذیرد:
1>>> def incr(x, /):
2... return x + 1
3...
4>>> incr(3.8)
54.8
6
7>>> incr(x=3.8)
8Traceback (most recent call last):
9 File "<stdin>", line 1, in <module>
10TypeError: incr() got some positional-only arguments passed as
11 keyword arguments: 'x'
با افزودن / پس از x مشخص میسازیم که x یک آرگومان صرفاً موقعیتی است. امکان ترکیب آرگومانهای معمولی با آرگومانهای صرفاً موقعیتی، از طریق قرار دادن آرگومانهای معمولی پس از کاراکتر / وجود دارد:
1>>> def greet(name, /, greeting="Hello"):
2... return f"{greeting}, {name}"
3...
4>>> greet("Łukasz")
5'Hello, Łukasz'
6
7>>> greet("Łukasz", greeting="Awesome job")
8'Awesome job, Łukasz'
9
10>>> greet(name="Łukasz", greeting="Awesome job")
11Traceback (most recent call last):
12 File "<stdin>", line 1, in <module>
13TypeError: greet() got some positional-only arguments passed as
14 keyword arguments: 'name'
در کد فوق میبینیم که کاراکتر ممیز در ()greet بین name و greeting قرار گرفته است. معنی آن این است که name یک آرگومان صرفاً موقعیتی است در حالی که greeting یک آرگومان معمولی است که میتواند برحسب موقعیت یا کلیدواژه تحلیل شود. در نگاه نخست، آرگومانهای صرفاً موقعیتی تا حدودی محدودکننده به نظر میرسند و با شعار پایتون مبنی بر اهمیت خوانایی کد در تضاد هستند. احتمالاً متوجه خواهید شد که موارد چندانی وجود ندارند که استفاده از آرگومانهای صرفاً موقعیتی موجب بهبود کد شود.
با این حال آرگومانهای صرفاً موقعیتی در شرایط صحیح میتوانند نوعی انعطافپذیری در اختیار ما قرار دهند تا بتوانیم تابعهای خودمان را طراحی کنیم. آرگومانهای صرفاً موقعیتی در وهله نخست در مواردی کاربرد دارند که آرگومانهایی با ترتیب طبیعی داریم، اما تعیین نامهای خوب و گویا برای آنها دشوار است. مزیت احتمالی دیگر استفاده از آرگومانهای صرفاً موقعیتی این است که میتوان تابعهای سفارشی را آسانتر «بازسازی» (Refactor) کرد. به طور خاص میتوانید نام پارامترهای خود را بدون نگرانی از این که کدهای دیگر وابسته به آن نامها چه بر سرشان میآید، تغییر دهید.
آرگومانهای صرفاً موقعیتی به خوبی با آرگومانهای «صرفاً کلیدواژهای» (keyword-only) تکمیل میشوند. در پایتون نسخه 3 میتوانید آرگومانهای صرفاً کلیدواژهای را با استفاده از کاراکتر ستاره (*) تعیین کنید. هر آرگومانی پس از ستاره باید با استفاده از یک کلیدواژه تعیین شده باشد:
1>>> def to_fahrenheit(*, celsius):
2... return 32 + celsius * 9 / 5
3...
4>>> to_fahrenheit(40)
5Traceback (most recent call last):
6 File "<stdin>", line 1, in <module>
7TypeError: to_fahrenheit() takes 0 positional arguments but 1 was given
8
9>>> to_fahrenheit(celsius=40)
10104.0
Celsius یک آرگومان صرفاً کلیدواژهای است و از این رو در صورتی که تلاش کنید آن را برحسب موقعیت و بدون کلیدواژه تعیین کنید، پایتون خطایی صادر میکند. امکان ترکیب آرگومانهای صرفاً موقعیتی با آرگومانهای معمولی و صرفاً کلیدواژهای وجود دارد. به این منظور باید آنها را با ترتیبی که در جمله قبلی ذکر شد بیاورید و با کاراکترهای / و * از هم جدا کنید. در مثال زیر text یک آرگومان صرفاً موقعیتی است، border یک آرگومان معمولی با مقدار پیشفرض و width یک آرگومان صرفاً کلیدواژهای با یک مقدار پیشفرض است:
1>>> def headline(text, /, border="♦", *, width=50):
2... return f" {text} ".center(width, border)
3...
از آنجا که text، صرفاً موقعیتی است، نمیتوانید از کلیدواژه text استفاده کنید:
1>>> headline("Positional-only Arguments")
2'♦♦♦♦♦♦♦♦♦♦♦ Positional-only Arguments ♦♦♦♦♦♦♦♦♦♦♦♦'
3
4>>> headline(text="This doesn't work!")
5Traceback (most recent call last):
6 File "<stdin>", line 1, in <module>
7TypeError: headline() got some positional-only arguments passed as
8 keyword arguments: 'text'
از سوی دیگر border میتواند هم با کلیدواژه و هم بدون آن تعیین شود:
1>>> headline("Python 3.8", "=")
2'=================== Python 3.8 ==================='
3
4>>> headline("Real Python", border=":")
5':::::::::::::::::: Real Python :::::::::::::::::::'
در نهایت width باید صرفاً با استفاده از کلیدواژه تعیین شود:
1>>> headline("Python", "?", width=38)
2'??????????????? Python ???????????????'
3
4>>> headline("Python", "?", 38)
5Traceback (most recent call last):
6 File "<stdin>", line 1, in <module>
7TypeError: headline() takes from 1 to 2 positional arguments
8 but 3 were given
برای مطالعه بیشتر در خصوص آرگومانهای صرفاً موقعیتی به این صفحه (+) مراجعه کنید.
انواع دقیق دیگر
سیستم نوعبندی دادهها در پایتون هم اینک به بلوغ کامل رسیده است. با این حال در پایتون 3.8 ویژگیهای جدیدی به typing اضافه شده تا امکان نوعبندی دقیقتری فراهم شود:
- انواع Literal
- دیکشنریهای نوعدار
- شیءهای Final
- پروتکلها
پایتون از «سرنخ نوع» (type hints) به شیوه اختیاری پشتیبانی میکند که به طور معمول به صورت «حاشیهنویسی» (annotations) روی کد میآید:
1def double(number: float) -> float:
2 return 2 * number
در این مثال بیان میکنیم که number باید به صورت یک مقدار float باشد و تابع ()double باید یک مقدار float بازگشت دهد. با این حال پایتون این حاشیهنویسیها را به عنوان سرنخ تلقی میکند. آنها در زمان اجرا الزامی پیش نمیآورند:
1>>> double(3.14)
26.28
3
4>>> double("I'm not a float")
5"I'm not a floatI'm not a float"
بدین ترتیب ()double به خوبی I'm not a float را به عنوان یک آرگومان میپذیرد، هر چند میدانیم که float نیست. کتابخانههایی وجود دارند که میتوانند از نوعها در زمان اجرا استفاده کنند، اما کاربرد عمدهای در سیستم نوعبندی پایتون ندارند.
از سوی دیگر سرنخهای پایتون به ما امکان میدهند که «بررسیکننده نوع» (type checker) استاتیک داشته باشیم و نوعها را در کد پایتون بدون نیاز به اجرای عملی اسکریپتها مورد بررسی قرار دهیم. این امکان شبیه به دام انداختن خطاهای نوع از سوی کامپایلر در زبانهای دیگر مانند جاوا، Rust و Crystal است. به علاوه سرنخهای نوع به عنوان مستنداتی برای کد عمل میکنند که خواندن آن را سادهتر میسازند و به بهبود ویژگی تکمیل خودکار کد در IDE کمک میکنند. چندین نوع بررسیکننده نوع استاتیک از قبیل Pyright, Pytype و Pyre وجود دارند. ما در این مقاله از Mypy استفاده میکنیم. Mypy را میتوانید با استفاده از pip به صورت زیر نصب کنید:
$ python -m pip install mypy
در واقع به یک معنی Mypy پیادهسازی مرجع یک بررسیکننده نوع در پایتون محسوب میشود و از سوی Dropbox توسعه یافته است. خالق پایتون «گیدو فان روسوم» (Guido van Rossum) نیز عضوی از تیم Mypy است. برای کسب اطلاعات بیشتر در مورد سرنخ نوع در پایتون به این صفحه (+) مراجعه کنید.
چهار نوع جدید PEP در مورد بررسی نوع وجود دارد که پذیرش یافته و در پایتون 3.8 وارد شده است. در ادامه مثالهای کوچکی از هر کدام میبینید. PEP 586 نوع Literal را معرفی میکند. Literal کمی خاص است، زیرا یک یا چند مقدار خاص را نشان میدهد. یکی از کاربردهای Literal این است که میتوانیم در زمان استفاده از آرگومانهای رشتهای برای توصیف یک رفتار خاص، نوعها را به صورت دقیقی اضافه کنیم. به مثال زیر توجه کنید:
1# draw_line.py
2
3def draw_line(direction: str) -> None:
4 if direction == "horizontal":
5 ... # Draw horizontal line
6
7 elif direction == "vertical":
8 ... # Draw vertical line
9
10 else:
11 raise ValueError(f"invalid direction {direction!r}")
12
13draw_line("up")
این برنامه یک بررسیکننده نوع استاتیک ارسال خواهد کرد، هر چند up یک جهت نامعتبر محسوب میشود. بررسیکننده نوع تنها بررسی میکند که آیا up یک رشته است یا نه. در این حالت، اگر بخواهیم دقیقتر عمل کرده و اعلام کنیم که جهت باید یکی از رشتههای لفظی horizontal یا vertical باشد، میتوانیم از Literal به صورت زیر بهره بگیریم:
1# draw_line.py
2
3from typing import Literal
4
5def draw_line(direction: Literal["horizontal", "vertical"]) -> None:
6 if direction == "horizontal":
7 ... # Draw horizontal line
8
9 elif direction == "vertical":
10 ... # Draw vertical line
11
12 else:
13 raise ValueError(f"invalid direction {direction!r}")
14
15draw_line("up")
با اعلام مقادیر مجاز برای direction به بررسیکننده نوع، اینک میتوانیم در مورد بروز خطا هشداری صادر کنیم:
1$ mypy draw_line.py
2draw_line.py:15: error:
3 Argument 1 to "draw_line" has incompatible type "Literal['up']";
4 expected "Union[Literal['horizontal'], Literal['vertical']]"
5Found 1 error in 1 file (checked 1 source file)
این ساختار ابتدایی به صورت Literal[<literal>] است. برای نمونه Literal[38] نماینده مقدار لفظی 38 است. امکان افشای یکی از چندین مقدار لفظی با استفاده از Union وجود دارد:
Union[Literal["horizontal"], Literal["vertical"]]
از آنجا که این کاربرد نسبتاً رایجی است، میتوانید (و احتمالاً بهتر است) از نمادگذاری سادهتر Literal["horizontal", "vertical"] به جای آن بهره بگیرید. ما قبلاً از حالت دوم برای افزودن نوع به ()draw_line استفاده کردهایم. اگر به دقت به خروجی Mypy فوق نگاه کنید، میبینید که نمادگذاری سادهتر به صورت داخلی به نمادگذاری Union ترجمه شده است. مواردی وجود دارند که نوع مقدار بازگشتی یک تابع به آرگومانهای ورودی وابسته است. یک نمونه از آن ()open است که میتواند بسته به مقدار mode یک رشته متنی یا یک آرایه بایتی بازگشت دهد. انجام این کار از طریق Overloading (+) میسر است. در مثال زیر چارچوب یک ماشین حساب را میبینید که میتواند بسته به این که اعداد معمولی (38) و یا اعداد رومی (XXXVIII) وارد شده باشد، پاسخ متفاوتی بازگشت دهد:
1# calculator.py
2
3from typing import Union
4
5ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
6 (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
7 (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
8
9def _convert_to_roman_numeral(number: int) -> str:
10 """Convert number to a roman numeral string"""
11 result = list()
12 for arabic, roman in ARABIC_TO_ROMAN:
13 count, number = divmod(number, arabic)
14 result.append(roman * count)
15 return "".join(result)
16
17def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
18 """Add two numbers"""
19 result = num_1 + num_2
20
21 if to_roman:
22 return _convert_to_roman_numeral(result)
23 else:
24 return result
این کد سرنخهای نوع صحیحی دارد. نتیجه ()add به صورت str و یا int خواهد بود. با این حال، در اغلب موارد این کد با یک نوع لفظی True یا False به عنوان مقدار to_roman فراخوانی میشود که در این حالت علاقهمند هستیم بررسیکننده نوع استنباط کند مقدار بازگشتی str و یا int خواهد بود. این کار با استفاده همزمان از Literal و overload@ میسر است:
1# calculator.py
2
3from typing import Literal, overload, Union
4
5ARABIC_TO_ROMAN = [(1000, "M"), (900, "CM"), (500, "D"), (400, "CD"),
6 (100, "C"), (90, "XC"), (50, "L"), (40, "XL"),
7 (10, "X"), (9, "IX"), (5, "V"), (4, "IV"), (1, "I")]
8
9def _convert_to_roman_numeral(number: int) -> str:
10 """Convert number to a roman numeral string"""
11 result = list()
12 for arabic, roman in ARABIC_TO_ROMAN:
13 count, number = divmod(number, arabic)
14 result.append(roman * count)
15 return "".join(result)
16
17@overload
18def add(num_1: int, num_2: int, to_roman: Literal[True]) -> str: ...
19@overload
20def add(num_1: int, num_2: int, to_roman: Literal[False]) -> int: ...
21
22def add(num_1: int, num_2: int, to_roman: bool = True) -> Union[str, int]:
23 """Add two numbers"""
24 result = num_1 + num_2
25
26 if to_roman:
27 return _convert_to_roman_numeral(result)
28 else:
29 return result
در کد فوق امضای overload@ که اضافه شده است به بررسیکننده نوع کمک میکند که بسته به مقادیر لفظی to_roman نوع خروجی را به صورت int یا str استنباط کند. توجه داشته باشید که (...) ellipses بخشی لفظی از کد محسوب میشود. این بخش در امضای Overload شده به جای بدنه تابع آمده است.
PEP 591 نوع Final را به عنوان مکملی برای Literal معرفی کرده است. این qualifier تعیین میکند که یک متغیر یا خصوصی امکان انتساب مجدد، تغییر تعریف یا Override شدن را دارد یا نه. در ادامه یک خطای نوعبندی را مشاهده میکنید:
1from typing import Final
2
3ID: Final = 1
4
5...
6
7ID += 1
Mypy خط ID += 1 را هایلایت کرده و به خطای زیر اشاره میکند:
you Cannot assign to final name "ID"
بدین ترتیب میتوانیم مطمئن باشیم که مقدار ثابتها در کد هرگز تغییر نمییابد. به علاوه یک دکوراتور final@ نیز وجود دارد که میتواند روی کلاسها و متدها اعمال شود و کلاسهای دارای final@ نمیتوانند کلاسهای فرعی ایجاد کند. همچنین متدهای دارای final@ نمیتوانند از سوی کلاس فرعی Override شوند.
1from typing import final
2
3@final
4class Base:
5 ...
6
7class Sub(Base):
8 ...
Mypy این مثال را با پیام خطایی به صورت زیر علامتگذاری کرده است:
Cannot inherit from final class "Base".
برای یادگیری موارد بیشتر در خصوص دکوراتور final@ به این لینک (+) مراجعه کنید. سومین PEP که امکان تعیین سرنخهای دقیقتر نوع را فراهم ساخته PEP 589 است. این PEP TypedDict را معرفی میکند. TypedDict میتواند برای تعیین نوع برای کلیدها و مقادیر یک دیکشنری با استفاده از نمادگذاری مشابه NamedTuple مورد استفاده قرار گیرد.
به طور سنتی دیکشنریها با استفاده از dict حاشیهنویسی میشوند. مشکل این است که این dict تنها امکان داشتن یک نوع برای کلید و یک نوع را برای مقادیر فراهم میسازد. این وضعیت معمولاً منجر به برخی حاشیهنویسیها مانند Dict[str, Any] میشود. به عنوان مثال، یک دیکشنری را تصور بکنید که اطلاعاتی در مورد نسخههای پایتون ثبت میکند:
py38 = {"version": "3.8", "release_year": 2019}
مقدار متناظر با version یک رشته است، در حالی که مقدار release_year یک عدد صحیح است. این وضعیت با استفاده از دیکشنری به صورت دقیقی قابل بازنمایی نیست. با استفاده از TypedDict که در پایتون 3.8 معرفی شده است میتوان به صورت زیر عمل کرد:
1from typing import TypedDict
2
3class PythonVersion(TypedDict):
4 version: str
5 release_year: int
6
7py38 = PythonVersion(version="3.8", release_year=2019)
سپس بررسیکنندههای نوع میتوانند استنباط کنند که py38["version"] نوع str دارد، در حالی که py38["release_year"] یک int است. در زمان اجرا یک py38["release_year"] همان dict معمولی است و سرنخهای نوع به طور معمول نادیده گرفته میشوند. همچنین میتوانید از TypedDict صرفاً به عنوان یک حاشیهنویسی استفاده کنید:
py38: PythonVersion = {"version": "3.8", "release_year": 2019}
Mypy این امکان را فراهم میسازد که بدانیم کدام یک از مقادیر نوع نادرستی دارند. همچنین اگر از یک کلید استفاده کنید که اعلان نشده باشد، به شما هشداری داده میشود. برای مشاهده مثالهای بیشتر به صفحه PEP 589 (+) مراجعه کنید.
Mypy مدتی است که از «پروتکلها» (Protocols) پشتیبانی میکند. با این حال پذیرش رسمی آن در تاریخ می 2019 (اردیبهشت 1398) بوده است. پروتکلها روشی برای صورتبندی پشتیبانی پایتون از «نوعبندی اردکی» (duck typing) محسوب میشوند:
وقتی میبینیم که پرندهای شبیه اردک راه میرود، شبیه اردک شنا میکند و صدایی شبیه اردک دارد، این پرنده را اردک مینامیم. منبع (+)
برای نمونه نوعبندی اردکی به ما امکان میدهد که یک .name را روی هر شیئی که دارای خصوصیت .name است بخوانیم و عملاً اهمیتی به نوع شیء ندهیم. پشتیبانی از این وضعیت از سوی سیستم نوعبندی، غیرمنطقی به نظر میرسد. با این حال از طریق «نوعبندی فرعی ساختاری» (structural subtyping) امکان درک معنی نوعبندی اردکی وجود دارد. برای نمونه میتوان یک پروتکل به نام Named تعریف کرد که اشیای با خصوصیت name. را شناسایی کند:
1from typing import Protocol
2
3class Named(Protocol):
4 name: str
5
6def greet(obj: Named) -> None:
7 print(f"Hi {obj.name}")
در این کد، ()great تا زمانی که یک شیء خصوصیت .name را تعریف کرده باشد، آن را میپذیرد. برای کسب اطلاعات بیشتر در مورد PEP 544 به صفحه مستندات Mypy (+) مراجعه کنید.
دیباگ سادهتر با f-رشتهها
f-رشتهها در پایتون 3.6 معرفی شدند و محبوبیت زیادی کسب کردند. آنها احتمالاً رایجترین دلیل این مسئلهاند که اغلب کتابخانههای پایتون از نسخه 3.6 و بالاتر این زبان پشتیبانی میکنند. یک f-رشته در واقع یک لفظ رشتهای قالببندی شده است. آن را میتوان از روی حرف f در ابتدایش شناسایی کرد:
1>>> style = "formatted"
2>>> f"This is a {style} string"
3'This is a formatted string'
زمانی که از f-رشتهها استفاده میکنید، میتوانید متغیرها و حتی عبارتها را درون آکولاد داخل رشته قرار دهید. این موارد در ادامه در زمان اجرا مورد ارزیابی قرار میگیرند و در رشته گنجانده خواهند شد. میتوان چندین عبارت را در یک f-رشته داشت:
1>>> import math
2>>> r = 3.6
3
4>>> f"A circle with radius {r} has area {math.pi * r * r:.2f}"
5'A circle with radius 3.6 has area 40.72'
در عبارت آخر {math.pi * r * r:.2f} میتوان از یک «تعیینکننده قالب» (Format Specifier) نیز استفاده کرد. تعیینکنندههای قالب با یک کاراکتر دونقطه (:) از عبارت جدا میشوند:
برای نمونه 2f. به این معنی است که این ناحیه به صورت یک عدد اعشاری با 2 رقم اعشار قالببندی شده است. تعیینکنندههای قالب همانند ()format. عمل میکنند. برای کسب اطلاعات بیشتر در مورد لیست کامل این تعیینکنندههای قالب به این صفحه (+) مراجعه کنید. در پایتون 3.8 میتوانید از عبارت انتسابی درون f-رشتهها بهره بگیرید. کافی است مطمئن شوید که عبارت انتسابی درون پرانتز قرار گرفته است:
1>>> import math
2>>> r = 3.8
3
4>>> f"Diameter {(diam := 2 * r)} gives circumference {math.pi * diam:.2f}"
5'Diameter 7.6 gives circumference 23.88'
با این حال، خبر مهم مرتبط با f-رشتهها در پایتون 3.8 امکان دیباگ تعیینکنندهها است. اکنون میتوانید یک علامت مساوی (=) در ابتدای یک عبارت اضافه کنید و بدین ترتیب هم عبارت و مقدار آن را پرینت کنید:
1>>> python = 3.8
2>>> f"{python=}"
3'python=3.8'
این یک اختصار است و به صورت معمول در مواردی مفید است که به صورت تعاملی کار میکنیم و یا گزارههای پرینت را برای دیباگ اسکریپت اضافه میکنیم. در نسخههای قبلی پایتون، باید متغیر یا عبارت را دو بار مینوشتید، تا به این اطلاعات دسترسی یابید:
1>>> python = 3.7
2>>> f"python={python}"
3'python=3.7'
همچنین میتوانید کاراکترهای فاصله پیرامون علامت مساوی (=) اضافه کنید و از تعیینکنندههای قالب به صورت معمول بهره بگیرید:
1>>> name = "Eric"
2>>> f"{name = }"
3"name = 'Eric'"
4
5>>> f"{name = :>10}"
6'name = Eric'
در کد فوق، تعیینکننده قالب >10 بیان میکند که name باید در یک رشته با طول 10 کاراکتر در انتهای آن قرار گیرد. این وضعیت در مورد عبارتهای پیچیدهتر نیز کار میکند:
1>>> f"{name.upper()[::-1] = }"
2"name.upper()[::-1] = 'CIRE'"
شورای مدیریت پایتون
بحث مدیریت زبان پایتون از نظر فنی یک قابلیت مربوط به این زبان محسوب نمیشود. اما از نسخه 3.8 پایتون، این زبان دیگر تحت مدیریت منفرد شخص «گیدو فان روسوم» یعنی خالق خود قرار ندارد و از سوی یک شورای مدیریتی متشکل از پنج توسعهدهنده زیر توسعه مییابد:
- بِری وُرساو (Barry Warsaw)
- برِت کانون (Brett Cannon)
- کارول ویلینگ (Carol Willing)
- گیدو فان روسوم (Guido van Rossum)
- نیک کولِن (Nick Coghlan)
مسیر منتهی به این مدل مدیریت پایتون یک مطالعه جالب در خصوص خودسازماندهی محسوب میشود. گیدو فان روسوم پایتون را در اوایل دهه 1990 ساخت و خود را «دیکتاتور خیرخواه جاویدان» (Benevolent Dictator for Life) به اختصار BDFL نامید. در طی سالها به مرور تصمیمهای زیادی در مورد پایتون از طریق سیستم «پیشنهادهای بهبود پایتون» (Python Enhancement Proposals) به اختصار PEP اتخاذ شده است. اما گیدو هنوز به طور رسمی در مورد همه قابلیتهای جدید زبان پایتون حرف آخر را میزند.
گیدو پس از یک بحث طولانی و مطول در مورد عبارتهای انتسابی، در جولای 2018 (مرداد 1397) اعلام کرد که قصد دارد از نقش خود به عنوان BDFL بازنشسته شود. او عامدانه از تعیین جانشین خودداری کرد و از تیم اصلی توسعهدهندگان تقاضا نمود که تعیین کنند پایتون در ادامه چگونه باید مدیریت شود.
خوشبختانه فرایند PEP در این زمان به خوبی تثبیت شده بود و از این رو روند طبیعی این بود که از PEP-ها برای بحث و تصمیمگیری در مورد مدل حکمرانی جدید پایتون استفاده شود. در طی پاییز سال 2018 (1397) چندین مدل پیشنهاد شدند که شامل انتخاب یک BDFL جدید بود. عنوان آن به «مدیر تصمیمگیری در مورد تصمیمهای مؤثر بر امپراتوری» (Gracious Umpire Influencing Decisions Officer) به اختصار GUIDO تغییر مییافت. همچنین پیشنهاد حرکت به سمت مدل جامعهای بر مبنای اجماع و رأیدهی بدون مدیریت متمرکز مطرح شد. در دسامبر سال 2019 (آذر 1397) مدل «شورای مدیریتی» (Steering Council) پس از رأیگیری در میان توسعهدهندگان تیم مرکزی انتخاب شد.
شورای مدیریتی شامل پنج عضو از جامعه پایتون است که در تصویر فوق مشخص شدهاند. این اعضا پس از انتشار هر نسخه عمده پایتون (Major Release) انتخاب میشوند. به بیان دیگر پس از انتشار نسخه 3.8 پایتون یک انتخابات برگزار خواهد شد.
با این که این یک انتخابات باز است، اما انتظار میرود که اگر نه همه اعضای کنونی، دستکم اغلب اعضای آن مجدداً انتخاب شوند. شورای مدیریت دارای اختیارات در سطح هیئتمدیره برای اتخاذ تصمیم در مورد زبان پایتون است، اما باید تلاش کند از این اختیارات در حد امکان بهره نگیرد.
قابلیتهای جالب دیگر
تا به اینجا با عناوین قابلیتهای جدید پایتون 3.8 آشنا شدیم. با این حال تغییرهای زیاد دیگری وجود دارند که آنها نیز جالب توجه هستند. در این بخش برخی از آنها را با هم مرور میکنیم.
importlib.metadata
یک ماژول جدید در کتابخانه استاندارد پایتون 3.8 به نام importlib.metadata اضافه شده است. از طریق این ماژول میتوان به اطلاعاتی در مورد نسخه نصبی پایتون دسترسی یافت. این ماژول به همراه ماژول همراه خود importlib.resources کارکرد ماژول قدیمیتر pkg_resources را بهبود میبخشد. به عنوان مثال میتوانید در مورد pip اطلاعاتی به دست آورید:
1>>> from importlib import metadata
2>>> metadata.version("pip")
3'19.2.3'
4
5>>> pip_metadata = metadata.metadata("pip")
6>>> list(pip_metadata)
7['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author',
8 'Author-email', 'License', 'Keywords', 'Platform', 'Classifier',
9 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier',
10 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier',
11 'Classifier', 'Classifier', 'Requires-Python']
12
13>>> pip_metadata["Home-page"]
14'https://pip.pypa.io/'
15
16>>> pip_metadata["Requires-Python"]
17'>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*'
18
19>>> len(metadata.files("pip"))
20668
نسخه نصب شده کنونی pip به صورت 19.2.3 است. ماژول ()metadata امکان دسترسی به اغلب اطلاعتی که در PyPI میبینید را فراهم ساخته است. برای نمونه میتوان دید که این نسخه از pip نیازمند نسخه 2.7 پایتون و یا نسخه 3.5 و بالاتر از پایتون است. با استفاده از ()files میتوان به لیستی از همه فایلهایی که پکیجهای pip را تشکیل میدهند دسترسی داشت. در این مورد، تقریباً 700 فایل وجود دارد. ()files لیستی از اشیای Path بازگشت میدهد. این لیست یک روش آسان برای بررسی کد منبع پکیج با استفاده از ()read_text در اختیار ما قرار میدهد. مثال زیر init__.py__ را از پکیج realpython-reader نشان میدهد:
1>>> [p for p in metadata.files("realpython-reader") if p.suffix == ".py"]
2[PackagePath('reader/__init__.py'), PackagePath('reader/__main__.py'),
3 PackagePath('reader/feed.py'), PackagePath('reader/viewer.py')]
4
5>>> init_path = _[0] # Underscore access last returned value in the REPL
6>>> print(init_path.read_text())
7"""Real Python feed reader
8
9Import the `feed` module to work with the Real Python feed:
10
11 >>> from reader import feed
12 >>> feed.get_titles()
13 ['Logging in Python', 'The Best Python Books', ...]
14
15See https://github.com/realpython/reader/ for more information
16"""
17
18# Version of realpython-reader package
19__version__ = "1.0.0"
20
21...
همچنین میتوانید به وابستگیهای پکیج نیز دسترسی داشته باشید:
1>>> metadata.requires("realpython-reader")
2['feedparser', 'html2text', 'importlib-resources', 'typing']
متد ()requires وابستگیهای پکیج را لیست میکند. چنان که میبینید realpython-reader برای نمونه از feedparser در پسزمینه برای خواندن و تحلیل فید مقالات بهره میگیرد. یک در پشتی importlib.metadata نیز روی PyPI وجود دارد که روی نسخههای قبلی پایتون کار میکند. آن را میتوانید با استفاده از pip نصب کنید:
$ python -m pip install importlib-metadata
با استفاده از کد زیر میتوانید تعیین کنید که در صوت بروز مشکل از PyPI backport استفاده شود:
1try:
2 from importlib import metadata
3except ImportError:
4 import importlib_metadata as metadata
5
6...
برای کسب اطلاعت بیشتر به مستندات (+) importlib.metadata مراجعه کنید.
تابعهای جدید و بهبود یافته math و statistics
پایتون 3.8 بهبودهای زیادی در مورد پکیج کتابخانه استاندارد و ماژولها به همراه داشته است. math در کتابخانه استاندارد چند تابع جدید دارد. ()math.prod به طرزی مشابه ()sum داخلی کار میکند، اما برای multiplicative product مورد استفاده قرار میگیرد:
1>>> import math
2>>> math.prod((2, 8, 7, 7))
3784
4
5>>> 2 * 8 * 7 * 7
6784
این دو گزاره معادل هم هستند. استفاده از ()prod در مواردی که فاکتورها از قبل در یک «تکرارشونده» (iterable) ذخیره شده باشند، آسانتر خواهد بود. تابع جدید دیگر ()math.isqrt است. این تابع برای یافتن بخش صحیح ریشههای دوم (جذر) استفاده میشود:
1>>> import math
2>>> math.isqrt(9)
33
4
5>>> math.sqrt(9)
63.0
7
8>>> math.isqrt(15)
93
10
11>>> math.sqrt(15)
123.872983346207417
ریشه دوم 9 برابر با 3 است. چنان که میبینید ()isqrt بخش صحیح عدد را بازگشت میدهد، در حالی که ()math.sqrt همواره یک عدد float بازمیگرداند. ریشه دوم 15 برابر با 3.9 است. توجه کنید که ()isqrt پاسخ را به صورت عدد صحیح کوچکتر یعنی 3 درمیآورد.
در نهایت میتوانید در کتابخانه استاندارد به روش آسانتری با نقاط n-بعدی و بردارها کار کنید. بدین ترتیب میتوان مسافت بین دونقطه را با استفاده از ()math.dist یافت. طول بردار را نیز میتوان با ()math.hypot پیدا کرد:
1>>> import math
2>>> point_1 = (16, 25, 20)
3>>> point_2 = (8, 15, 14)
4
5>>> math.dist(point_1, point_2)
614.142135623730951
7
8>>> math.hypot(*point_1)
935.79106033634656
10
11>>> math.hypot(*point_2)
1222.02271554554524
بنابراین کار با نقطهها و بردارها با استفاده از کتابخانه استاندارد آسانتر شده است با این حال اگر محاسبات زیادی روی نقطهها و بردارها انجام میدهید، بهتر است از NumPy استفاده کنید.
ماژول statistics نیز چند تابع جدید دارد:
- ()statistics.fmean میانگین اعداد float را محاسبه میکند.
- ()statistics.geometric_mean میانگین هندسی اعداد float را محاسبه میکند.
- ()statistics.multimode فراوانی مقادیر را در یک دنباله محاسبه میکند.
- ()statistics.quantiles نقاط برش را در زمان تقسیم دادهها به n بازه پیوسته با احتمال یکسان محاسبه میکند.
در مثال زیر کاربرد عملی تابعها را مشاهده میکنید:
1>>> import statistics
2>>> data = [9, 3, 2, 1, 1, 2, 7, 9]
3>>> statistics.fmean(data)
44.25
5
6>>> statistics.geometric_mean(data)
73.013668912157617
8
9>>> statistics.multimode(data)
10[9, 2, 1]
11
12>>> statistics.quantiles(data, n=4)
13[1.25, 2.5, 8.5]
در پایتون 3.8 یک کلاس جدید به نام statistics.NormalDist وجود دارد که کار با توزیع نرمال گائوسی را آسانتر ساخته است. برای دیدن مثالی از کاربرد NormalDist میتوانید سرعت ()statistics.fmean و روش سنتی یعنی ()statistics.mean را مقایسه کنید:
1>>> import random
2>>> import statistics
3>>> from timeit import timeit
4
5>>> # Create 10,000 random numbers
6>>> data = [random.random() for _ in range(10_000)]
7
8>>> # Measure the time it takes to run mean() and fmean()
9>>> t_mean = [timeit("statistics.mean(data)", number=100, globals=globals())
10... for _ in range(30)]
11>>> t_fmean = [timeit("statistics.fmean(data)", number=100, globals=globals())
12... for _ in range(30)]
13
14>>> # Create NormalDist objects based on the sampled timings
15>>> n_mean = statistics.NormalDist.from_samples(t_mean)
16>>> n_fmean = statistics.NormalDist.from_samples(t_fmean)
17
18>>> # Look at sample mean and standard deviation
19>>> n_mean.mean, n_mean.stdev
20(0.825690647733245, 0.07788573997674526)
21
22>>> n_fmean.mean, n_fmean.stdev
23(0.010488564966666065, 0.0008572332785645231)
24
25>>> # Calculate the lower 1 percentile of mean
26>>> n_mean.quantiles(n=100)[0]
270.6445013221202459
در این مثال از timeit برای اندازهگیری زمان اجرای ()mean و ()fmean استفاده شده است. برای دریافت نتایج قابل اطمینان باید اجازه دهید timeit هر تابع را 100 بار اجرا کند و 30 نمونه زمانی این چنینی برای هر تابع به دست آورد. بر اساس این نمونهها میتوانید دو شیء NormalDist ایجاد کنید. توجه داشته باشید که اگر کد را خودتان اجرا کنید، ممکن است تا یک دقیقه زمان صرف گرداوری نمونههای زمانی مختلف شود.
NormalDist خصوصیتها و متدهای ساده زیادی دارد. برای دیدن لیست همه آنها به مستندات (+) مراجعه کنید. با بررسی .mean و .stdev میبینیم که ()statistics.mean در طی زمان 0.826 به علاوه منهای 0.078 ثانیه اجرا میشود، در حالی که متد جدید ()statistics.fmean برای اجرا به 0.0105 به علاوه منهای 0.0009 ثانیه زمان نیاز دارد. به بیان دیگر ()fmean روی این دادهها 80 بار سریعتر بوده است.
هشدار در مورد ساختار خطرناک
پایتون قابلیتی به صورت SyntaxWarning (+) دارد که در مورد ساختارهای مشکوک که عموماً یک SyntaxError نیستند، هشدار میدهد. پایتون 3.8 چند هشدار جدید اضافه کرده است که در زمان کدنویسی و دیباگ به شما کمک میکنند.
اختلاف بین is و == میتواند سردرگمکننده باشد. دومی برابر بودن مقدار را بررسی میکند، در حالی که is تنها زمانی True است که اشیا یکسان باشند. پایتون 3.8 تلاش میکند در خصوص مواردی که باید از == به جای is استفاده کنید، به شما هشدار بدهد:
1>>> # Python 3.7
2>>> version = "3.7"
3>>> version is "3.7"
4False
5
6>>> # Python 3.8
7>>> version = "3.8"
8>>> version is "3.8"
9<stdin>:1: SyntaxWarning: "is" with a literal. Did you mean "=="?
10False
11
12>>> version == "3.8"
13True
زمانی که مشغول نوشتن یک لیست بلند هستید، ممکن است به راحتی یکی از کاماها را فراموش کنید. این مورد به طور خاص در زمانی که قالببندی لیست به صورت عمودی است بیشتر مشاهده میشود. فراموش کردن یک کاما در لیستی از چندتاییها موجب صدور پیام خطای سردرگمکنندهای در مورد چندتایی میشود و آن را غیر قابل فراخوانی اعلام میکند. در پایتون 3.8 یک هشدار در مورد مشکل واقعی نیز ارائه میشود:
1>>> [
2... (1, 3)
3... (2, 4)
4... ]
5<stdin>:2: SyntaxWarning: 'tuple' object is not callable; perhaps
6 you missed a comma?
7Traceback (most recent call last):
8 File "<stdin>", line 2, in <module>
9TypeError: 'tuple' object is not callable
این هشدار در حال حاضر مشخص میکند که کامای فراموششده دلیل اصلی بروز خطا بوده است.
بهینهسازی
چندین بهینهسازی در پایتون 3.8 صورت گرفته است. برخی از آنها موجب اجرای سریعتر کد میشوند. برخی دیگر مصرف حافظه را کاهش میدهند. برای نمونه اکنون گشتن در یک namedtuple به میزان چشمگیری سریعتر از پایتون 3.7 شده است:
1>>> import collections
2>>> from timeit import timeit
3>>> Person = collections.namedtuple("Person", "name twitter")
4>>> raymond = Person("Raymond", "@raymondh")
5
6>>> # Python 3.7
7>>> timeit("raymond.twitter", globals=globals())
80.05876131607996285
9
10>>> # Python 3.8
11>>> timeit("raymond.twitter", globals=globals())
120.0377705999400132
چنان که میبینید گشتن در .twitter به دنبال namedtuple اکنون در پایتون 3.8 به میزان 30 تا 40% سریعتر شده است. لیستها زمانی که از تکرارشوندههایی با طول مشخص مقداردهی شوند، موجب صرفهجویی در مصرف حافظه هم میشوند. به مثال زیر توجه کنید:
1>>> import sys
2
3>>> # Python 3.7
4>>> sys.getsizeof(list(range(20191014)))
5181719232
6
7>>> # Python 3.8
8>>> sys.getsizeof(list(range(20191014)))
9161528168
در این حالت، لیست از 11% حافظه کمتری در پایتون 3.8 در مقایسه با نسخه 3.7 استفاده میکند. بهینهسازیهای دیگری نیز در نسخه 3.8 به صورت بهبود عملکرد subprocess، کپی کردن سریعتر فایل با shutil، بهبود عملکرد پیشفرض در pickle و عملیات سریعتر عملگر itemgetter. مشاهده میشود.
چرا باید پایتون را به نسخه 3.8 ارتقا دهیم؟
ابتدا پاسخ سادهتر را به این سؤال ارائه میکنیم. اگر میخواهید قابلیتهای جدیدی که در این مقاله مورد بررسی قرار دادیم را در عمل مشاهده کنید، باید از پایتون 3.8 استفاده کنید. ابزارهایی مانند pyenv و Anaconda امکان نصب چندین نسخه از پایتون را در کنار هم به سادگی فراهم ساختهاند. به طور جایگزین میتوانید کانتینر داکر رسمی پایتون 3.8 (+) را اجرا کنید. استفاده از پایتون 3.8 هیچ مشکلی برای شما ایجاد نخواهد کرد.
اما سؤالهای پیچیدهتر این است که آیا باید محیط پروداکشن را نیز به پایتون 3.8 ارتقا دهیم؟ آیا باید پروژه خود را به نسخه 3.8 وابسته کنیم تا از مزیت قابلیتهای جدید بهرهمند شویم؟
اجرای کد پایتون 3.7 در نسخه 3.8 نباید مشکل چندانی ایجاد کند. از این رو ارتقای محیط برای اجرای پایتون 3.8 کاملاً امن است و میتوانید از مزیت بهینهسازیهای صورت گرفته در نسخه جدید بهرهمند شوید. نسخههای بتای مختلف پایتون 3.8 ماهها است که عرضه شدهاند و به همین جهت به احتمال زیاد اغلب باگها هم اینک رفع شدهاند. با این حال اگر میخواهید محافظهکارانه عمل کنید، بهتر است تا زمان ارائه نسخه maintenance یعنی پایتون 3.8.1 صبر کنید.
زمانی که پایتون را به نسخه 3.8 ارتقا دهید، میتوانید شروع به بررسی قابلیتهایی که صرفاً در این نسخه وجود دارد مانند عبارتهای انتسابی و آرگومانهای صرفاً موقعیتی بکنید. با این حال در صورتی که افراد دیگری نیز روی کد شما کار میکنند، باید در این مورد محتاطانه عمل کنید، زیرا در این حالت آنها نیز مجبور خواهند شد محیطشان را ارتقا دهند. اغلب کتابخانههای محبوب تا مدت مدیدی همچنان از دستکم پایتون 3.6 پشتیبانی خواهند کرد.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی پایتون Python
- مجموعه آموزشهای برنامهنویسی
- گنجینه آموزشهای برنامهنویسی پایتون (Python)
- زبان برنامه نویسی پایتون (Python) — از صفر تا صد
- تحلیل شبکههای اجتماعی در پایتون — راهنمای کاربردی
==
میشه امکان ارسال به ایمیل ر توش فعال کنین؟