فصل ۱۹
چیزای خوب
یکی از اهداف من برای نوشتن این کتاب این بود تا آنجایی که درتوانم هست به شما اندکی پایتون یاد بدهم. وقتی در سر دوراهی انجام کاری بودم یکی را انتخاب می کردم و به راه دیگر اشاره ای نمی کردم. یا این که راه دوم را در یک تمرین می گذاشتم.
حالا من قصد دارم به یک سری خرده ریز خوب که پشت سر گذاشته ام برگردم. پایتون تعدادی قابلیت دارد که در واقع ضروری نیستند – و شما می توانید بدون دانستن آنها کدهای خوبی بنویسید- اما با این ویژگی ها می توانید کدهایی بنویسید که مختصر ، خوانا ویا موثرتر باشد و در اغلب موارد هر سه این موارد
۱-۱۹عبارت های شرطی
ما در توضیحات بخش ۵.۴ دیدیم که عبارات شرطی اغلب برای انتخاب بین یک یا دو مقدار استفاده می شود.به عنوان مثال:
if x > 0:
y = math.log(x)
else:
y = float('nan')
این عبارت کنترل می کد که x مثبت است یا خیر. اگر باشد math.log.را محاسبه می کند. اگر نباشد math.log مقدار خطا را برمی گرداند. برای اجتناب از توقف برنامه ما “NaN” را ساختیم که یک مقدار خاص شناور-ممیز دار است تا نشان دهد این یک عدد نیست.
ما می توانیم این قسمت را با عبارت شرطی بسیار موجزتر بنویسیم:
y = math.log(x) if x > 0 else float('nan')
شما می توانید این خط را مثل یک جمله انگلیسی هم بنویسید: y لوگ x را می گیرد اگر که x بزرگتر از صفر باشد در غیر اینصورت NaN را می گیرد.
توابع بازگشتی در اکثر موارد می توانند به صورت عبارات شرطی نوشته شوند. برای مثال تابع بازگشتی فاکتوریل را اینجا می آوریم:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)
ما می توانیم آنرا به شکل زیر بازنویسی کنیم
def factorial(n):
return 1 if n == 0 else n * factorial(n-1)
یکی دیگر از کاربردهای عبارات شرطی مدیریت متغیرهای اختیاری است. برای مثال اینجا یک روش شروع از است. (تمرین ۱۷.۲ را ببینید)
def __init__(self, name, contents=None):
self.name = name
if contents == None:
contents = [ ]
self.pouch_contents = contents
ما می توانیم این یکی را هم به شکل زیر بازنویسی کنیم:
def __init__(self, name, contents=None):
self.name = name
self.pouch_contents = [] if contents == None else contents
در حالت کلی، شما می توانید یک قسمت از برنامه شرطی را به شکل عبارات شرطی بنویسید اگر هر دو شاخه شامل عبارات ساده ای باشد که یا به یک متغیر مشابه اخصاص یافته و یا مقدار را برگرداند.
۲-۱۹ درک لیست
در بخش ۷-۱۰ الگوهای فیلتر کردن و مپ را دیدیم. برای مثال این تابع یک لیست رشته ای از ما می گیرد، با روش capitalize رشته را به عناصر مپ کرده و یه لیست جدید رشته ای را بر می گرداند:
def capitalize_all(t):
res = []
for s in t:
res.append(s.capitalize())
return res
ما می توانیم آن رابا درک لیست ها خیلی خلاصه تر بنویسیم:
def capitalize_all(t):
return [s.capitalize() for s in t]
عملگر براکت اشاره به این دارد که ما در حال ساختن یک لیست جدید هستیم. عبارت داخل براکت عناصر لیست را مشخص می کند، و قسمت for اشاره می کند که کدام sequence را باید پیمایش کنیم.
کدهای مربوط به درک لیست یک مقدار وحشتناک به نظر می رسد به خاطر متغیرهای حلقه for ، s در این مثال در عبارت ظاهر شده است قبل از اینکه ما آنرا تعریف کنیم.
درک لیست همچنین می تواند برای فربال و فلیتر کردن هم استفاده شود. برای مقال این تابع عناصری را انتخاب می کند که فقط t در آن ها با حروف بزرگ باشد و سپس لیست جدید را بر می گرداند:
def only_upper(t):
res = []
for s in t:
if s.isupper():
res.append(s)
return res
ما میتوانیم آنرا با استفاده از درک لیست به شکل زیر دوباره نویسی کنیم:
def only_upper(t):
return [s for s in t if s.isupper()]
درک لیست بسیار مختصر و ساده خوانده می شود به همچنین برای عبارات ساده. و آن ها معمولا سریعتر از معادل های حلقه ای شان هستند و دراغلب موارد خیلی خیلی سریع تر هستند. بنابر این اگر بنده را دیوانه بخوانید که چرا این ها را زودتر نگفتم شما را درک می کنم.
اما در دفاع از خودم باید بگویم درک لیست ها برای دیباگ کردن سخت تر هستند به علت اینکه شما نمی توانید یک دستور پرینت در یه تکه از برنامه ی داخل حلقه بگنجانید. من پیشنهاد می کنم شما آن را زمانی استفاده کنید که محاسبات ساده ای دارید تا آنرا برای اولین بار به طور صحیح درک کنید. و این برای برای تازه کارها یعنی هرگز استفاده نکنید.
۳-۱۹ عبارات مولد
عبارات مولد (تولید کننده) شبیه عبارات درک لیست هستند اما همراه با پرانتز بر خلاف براکت ها:
>>> g = (x**2 for x in range(5))
>>> g
<generator object <genexpr> at 0x7f4c45a786c0>
نتیجه یک شیئ مولد است که می داند چگونه در میان کدام sequence از متغیرها تکرار شود. اما بر خلاف درک لیست نمی تواند تمام مقادیر را در لحظه محاسبه کند. مولد صبر می کند تا از او پرسیده شود.
تابع توکار next مقداربعدی را از مولد می گیرد:
>>> next(g)
0
>>> next(g)
1
وقتی شما به پایان sequence می رسید next دستور رد شدن با توقف تکرار را بالا می برد. شما همچنین می توانید از حلقه ی for برای تکرار میان مقادیر استفاده کنید.
>>> for val in g:
... print(val)
4
9
16
شیئ مولد ردپای اینکه کجای sequence است را نگه داشته و بنابراین حلقه for از آنجاییکه next رفته است ادامه می دهد. هر گاه مولد خسته شود StopException را بالا می آورد:
>>> next(g)
StopIteration
عبارت های مولد اغلب با توابعی مثل sum,max و min استفاده می شوند:
>>> sum(x**2 for x in range(5))
30
۴-۱۹ any و all
پایتون یک تابع تو کار any را فراهم می کند برای اینکه sequence مقدار بولین بگیرد . True برگرداند هر وقت تمام مقادیر True باشند. این هم بر روی لیست ها کار می کند:
>>> any([False, False, True])
True
اما اغلب با عبارت مولد کار می کند:
>>> any(letter == 't' for letter in 'monty')
True
این مثال خیلی مفید نیست چون شبیه عملگر in است. اما می توانیم any را برای دوباره نویسی برخی از توابع جستجو که در فصل ۹.۳ نوشتیم بکار ببریم.برای مثال می توانیم avoid را شبیه ذیل بنویسیم:
def avoids(word, forbidden):
return not any(letter in forbidden for letter in word)
اگر بخواهیم مثل یک جمله ی معمولی انرا بخوانیم اینطور می شود: word از forbidden اجتناب می کند اگر کلمه ی forbidden در wordوجود نداشته باشد.
استفاده کردن از any همراه عبارات مولد کافیست زیرا می تواند به ناگاه بایستد اگر مقدار True را بیابد پس مجبور نیست تمام sequence را ارزیابی کند.
پایتون یک تابع توکار دیگری فراهم می آورد، all مقدار True برمی گرداند اگر هر کدام از عناصر sequence, True باشند. مانند مثال از all استفاده کنید تا uses-all از بخش ۹.۳ را دوباره بنویسید.
۵- ۱۹ مجموعه ها
در بخش ۱۳.۶ از فرهنگ لغت استفاده کردیم تا کلماتی را که در یک سند بود پیدا کند اما در در یک لیست کلمات. تابعی که من نوشته ام d1 شامل تمام کلمات سند است به عنوان کلید ها و d2 شامل تمام لیست لغات است. این برنامه فرهنگ لغتی را بر می گرداند که شامل کلیدها از d1 باشد و نه در d2
def subtract(d1, d2):
res = dict()
for key in d1:
if key not in d2:
res[key] = None
return res
در تمام این فرهنگ لغات مقدار None می باشد چرا که ما از آن ها استفاده نکرده ایم. در نتیجه ما مقداری از فضای انباره را اشغال کرده ایم.
پایتون یک نوع دیگر از توکارها را که set یا مجموعه نامیده می شود فراهم کرده است که مانند یک کلکسیون از کلیدهای فرهنگ لغت بدون مقدار رفتار می کنند. اضافه کردن عناصر به مجموعه سریع است و همچنین عضویت در آن. و set ها روش ها و عملگرهایی برای محاسبه عملیات های معمول مجموعه فراهم کرده اند.
برای مثال کم کردن از مجموعه در روشی و یا در عملگری به نام difference موجود است پس ما می توانیم subtract (کم کردن) را به شکل ذیل بازنویسی کنیم:
def subtract(d1, d2):
return set(d1) - set(d2)
نتیجه یک مجموعه است بر خلاف فرهنگ لغت اما در این عملیات مانند تکرار رفتار ها مشابه است.
خیلی از تمرینات این کتاب می تواند با مجموعه ها مختصر و مفید انجام شود. برای مثال اینجا راه حلی برای has_duplicates از تمرین ۱۰.۷ که از فرهنگ لغات استفاده می کند آورده ام:
def has_duplicates(t):
d = {}
for x in t:
if x in d:
return True
d[x] = True
return False
وقتی یک عنصر برای اولین بار ظاهر می شود به فرهنگ لغات اضافه می شود اگر عنصر مشابه دوباره ظاهر شود تابع مقدار True را بر می گرداند.
با ساتفاده از مجموعه ها می توانیم یک تابع مشابه به صورت ذیل بنویسیم:
def has_duplicates(t):
return len(set(t)) < len(t)
یک عنصر در یک مجموعه فقط یکبار ظاهر می شود پس اگر یک عنصر بیشتر از یکبار ظاهر شود مجموعه کوچکتر از t خواهد بود.اگر هیچ تکراری وجود نداشته باشد مجموعه و t هم اندازه هستند.
ما همچنین مجموعه ها را برای استفاده در تمرین های بخش ۹ کتاب استفاده می کنیم. به عنوان مثال یک نسخه از uses_only را باحلقه در اینجا داریم:
def uses_only(word, available):
for letter in word:
if letter not in available:
return False
return True
uses_only کنترل می کند که تمام لغات در word در available باشند. حالا آنرا به این شکل بازنویسی می کنیم:
def uses_only(word, available):
return set(word) <= set(available)
عملگر <= کنترل می کند که یک مجموعه زیر مجموعه دیگری است یا خیر به همچنین احتمال مساوی بودن دارند یا نه که اگر درستباشد تمامی حروف در word در available وجود دارند.
مانند تمرینات قبل avoids را با استفاده از مجموعه ها بنویسید.
۱۹-۶ شمارنده ها
یک شمارنده شبیه یک مجموعه است با این تفاوت که اگر عنصری بیش از یک بار ظاهر شود شمارنده تعداد ظاهر شدنش را نگه می دارد.اگر شما با مقوله ی multiset در ریاضیات آشنا باشید شمارنده یه راه طبیعی برای نشان دادن multiset است
شمارنده یک ماژول استاندارد به نام collections تعریف کرده و شما می توانید آنرا به درون بیاورید Import it. شما می توانید یک شمارنده را با یک لیست رشته ای یا با هر چیزی که تکرار را پشتیبانی می کند بهینه کنید:
>>> from collections import Counter
>>> count = Counter('parrot')
>>> count
Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})
شمارنده ها مانند فرهنگ لغات در بسیاری از موارد عمل می کنند. آنها هر کلید را به تعداد ظاهر شدن map می کنند. مانند فرهنگ لغات کلیدها باید قابل hashشدن باشند
برخلاف فرهنگ لغات شمارنده ها هنگام دسترسی به یک عنصری که ظاهر نشده است یک استثناء بالا نمی آورند در عوض مقدار صفر را بر می گردانند:
>>> count['d']
0
شمارنده را برای بازنویسی is_anagram از تمرین ۱۰.۶ بکار می بریم:
def is_anagram(word1, word2):
return Counter(word1) == Counter(word2)
اگر دو کلمه هم خانواده باشند آن ها از حروف مشابه و تعداد مشابه حروف برخوردارند پس شمارنده ها نیز مساوی هستند.
شمارنده ها روش ها و عملیاتی را برای اجرای عملگر شبه مجموعه set-like فراهم کرده اند، شامل اضافه کردن، کم کردن، اجتماع و رسیدن.
و روشی که معمولا مفید واقع می شود most_common را نیز فراهم کرده اند تا یک لیست از جفت های دارای ارزش تکرار به صورت مرتب شده از متدوال ترین تا آخر برگرداند
>>> count = Counter('parrot')
>>> for val, freq in count.most_common(3):
... print(val, freq)
r 2
p 1
a 1
۷-۱۹ defaultdict (فرهنگ لغت پیشفرض)
ماژول Collections ماژول defaultdict را هم فراهم می آورد که شبیه فرهنگ لغت dictionaryاست جز در این مورد که اگر شما به کلیدی رسیدید که وجود نداشت آن می تواند بvای شما یک مقدار جدید در حین عملکردش بسازد.
وقتی شما defaultdict را ساختید یک تابع که برای ساختن مقدار جدید استفاده می شود را فراهم کرده اید. یک تابع که برای ساخت اشیایی بکار می رود که معمولا factory نامیده می شوند. یک تابع توکار که لیست ها،مجموعه ها و سایر چیزهایی که ممکن است توسط factory استفاده شود را می سازد:
>>> from collections import defaultdict
>>> d = defaultdict(list)
خاطر نشان می کنم که نشانوند ما list است که یک class object است نه list() که یک لیست جدید است. تابعی که شما فراهم آورده اید تاهنگامی که به کلیدی بر نخورد که وجود نداشته باشد فراخوانده نمی شود.
>>> t = d['new key']
>>> t
[]
لیست جدید که ما آنرا t می خوانیم نیز به dictionary اضافه شد. پس اگر ما t را تغییر دهیم تغییرات در d ظاهر می شود:
>>> t.append('new value')
>>> d
defaultdict(<class 'list'>, {'new key': ['new value']})
اگر شما یک dictionary از لیست ها ساخته اید اغلب باید بتوانید کد ساده تری با استفاده از defaultdict بنویسید. در راه حل های من در تمرین ۱۲.۲ که شما از http://thinkpython2.com/code/anagram_sets.py دریافت کردید من یک dictionary ساختم که مپ شده بود از رشته های مرتب شده بر اساس حروف لغات که با آن حروف می توان کلمات را هجی کرد. برای مثال 'opst' به لیست ['opts','post', 'pots', 'spot', 'stop', 'tops']مپ شده است.
این هم از کد اصلی:
def all_anagrams(filename):
d = {}
for line in open(filename):
word = line.strip().lower()
t = signature(word)
if t not in d:
d[t] = [word]
else:
d[t].append(word)
return d
این می تواند با استفاده از setdefault که ممکن است شما در تمرین ۱۱.۲ استفاده کرده باشید، ساده شود:
def all_anagrams(filename):
d = {}
for line in open(filename):
word = line.strip().lower()
t = signature(word)
d.setdefault(t, []).append(word)
return d
این راه حل یک اشکال دارد، اینکه هر بار یک لیست می سازد بدون در نظر گرفتن اینکه چه چیزی مورد نیاز است. برای لیست ها خیلی مساله ی خاصی نیست اما اگر تابع عمکردی پیچیده داشته باشیم ممکن است مشکل پیش بیاید.
می توانیم ازین معضل با ساده سازی کد بااستفاده از defaultdict اجتناب کنیم.
def all_anagrams(filename):
d = defaultdict(list)
for line in open(filename):
word = line.strip().lower()
t = signature(word)
d[t].append(word)
return d
راه حل من در تمرین ۱۸.۳ که شما از http://thinkpython2.com/code/PokerHandSoln.py دریافت کردید استفاده از setdefault در تابع has_straightflush است. این راه حل این مشکل را در ساختن شیئ hand هر بار که در حلقه می افتد دارد خواه نیاز باشد یا نباشد.مانند تمرین آنرا با defaultdict دوباره نویسی کنید.
۸-۱۹Named tuples
اساسا خیلی از اشیاء مجموعه ای از ارزش های بهم مرتبط هستند. بعنوان مثال شیء Point که در فصل ۱۵ تعریف شد شامل دو عدد x و y است. وقتی شما یک کلاس را مانند این تعریف می کنید شما بطور معمول با حالت init و str شروع کرده اید:
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
مقادیر زیادی از کدها وجود دارد تا مقدار کمتری از اطلاعات را انتقال دهد. پایتون یک راه بسیار مختصر را برای بیان یک حرف مشترک فراهم می آورد:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
اولین نشانوندی که بریا ساختن یک کلاس انتخاب می کنید نام کلاس است. دومین آن که باید داشته باشید یک فهرست از خصوصیات شیء Point است مانند رشته ها. ارزشی که از namedtuple برگردانده می شود یک شیء کلاس (class object) است:
>>> Point
<class '__main__.Point'>
Point بصورت اتوماتیک روش هایی شبیه –init-- و –str— را فراهم می کند پس شما نیاز ندارید تا آن ها را بنویسید.
برای ساختن شیء Point شما از کلاس Point بعنوان یک تابع استفاده می کنید:
>>> p = Point(1, 2)
>>> p
Point(x=1, y=2)
روش آغازین (init) نشانوندها را به خصوصیاتی که شما نامگذاری کرده اید پیوند می دهد. روش srt شیء Point و خصوصیات آن را نمایش می دهد.
شما می توانید به عناصر named tuple بااسم دسترسی پیدا کنید:
>>> p.x, p.y
(1, 2)
اما شما می توانید با named tuple مانند یک tuple (چند تایی) رفتار کنید:
>>> p[0], p[1]
(1, 2)
>>> x, y = p
>>> x, y
(1, 2)
Named tuples یک راه سریع برای تعریف کلاس های ساده است. یک اشکال آن اینست که کلاس های ساده همیشه ساده نمی مانند. ممکن است شما در آینده تصمیم بگیرید که روش های دیگری را به named tuple اضافه کنید. در آنصورت باید یک کلاس جدید تعریف کنید که از named tuple ارثبری کند:
class Pointier(Point):
# add more methods here
یا اینکه به روش سنتی تعریف کلاس برگردید.
۹-۱۹ گردآوری لغات کلیدی آرگومان ها
در قسمت ۱۲.۴ دیدیم که چگونه یک تابع نوشت تا آرگومان ها (نشانوند ها) را درون یک tuple جمع آوری کند.
def printall(*args):
print(args)
شما می توانید این تابع را با هر عدد از آرگومان های مکانی فراخوانی کنید ( که آرگومان هایی هستند که لغات کلیدی ندارند):
>>> printall(1, 2.0, '3')
(1, 2.0, '3')
اما عملگر * آرگومان های کلمات کلیدی را گردآوری نمی کند:
>>> printall(1, 2.0, third='3')
TypeError: printall() got an unexpected keyword argument 'third'
برای گردآوری آرگومان های کلمات کلیدی می توانید از عملگر ** استفاده کنید:
def printall(*args, **kwargs):
print(args, kwargs)
شما می توانید پارامتر گردآوری لغات را به هر گونه که بخواهید فراخوانی کنید، اما kwargs یک انتخاب معمولی است. نتیجه، یک فرهنگ لغات است که لغات کلیدی را به ارزش ها مپ کرده است:
>>> printall(1, 2.0, third='3')
(1, 2.0) {'third': '3'}
اگر شما یک فرهنگ لغت از کلمات کلیدی و ارزشها داشته باشید می توانید از عملگر جداکننده ** برای فراخوانی تابع استفاده کنید:
>>> d = dict(x=1, y=2)
>>> Point(**d)
Point(x=1, y=2)
بدون عملگر جداکننده تابع d مانند آرگومان مکانی تنها عمل می کند پس d را به x اختصاص می دهد و شکایت می کند چرا که هیچ چیزی برای اختصاص دادن به y ندارد:
>>> d = dict(x=1, y=2)
>>> Point(d)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: __new__() missing 1 required positional argument: 'y'
وقتی شما مشغول کارکردن با توابع هستید که تعداد زیادی از پارامترها را دارد خیلی مفید است فرهنگ لغت ها را بسازید و توزیع کنید تا گزینه های زیاد استفاده شده را مشخص کند.
۱۰-۱۹ واژه نامه
عبارت شرطی : عبارتی که بسته به شرط یکی از دو ارزش را داشته باشد.
درک لیست: یک عبارنت با حلقه ی for در براکت های دوگانه که لیست جدید بازده ی آن است.
عبارت مولد: عبارتی است با حلقه ی for در پرانتزها که یک مولد شیء را به بار می آورد.
چندمجموعه: یک موجودیت ریاضی که مپ شدن بین عناصر یک مجموعه و تعدد ظهور آنان را نشان می دهد.
کارخانه: یک تابع که معمولا مانند پارامتر برای ساخت اشیاء استفاده می شود.
۱۱-۱۹ تمرین ها
تمرین ۱-۱۹
در ذیل یک تابع آورده شده که ضریب دوجمله ای را بصورت بازگشتی محاسبه می کند.
def binomial_coeff(n, k):
"""Compute the binomial coefficient "n choose k".
n: number of trials
k: number of successes
returns: int
"""
if k == 0:
return 1
if n == 0:
return 0
res = binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1)
return res
بدنه تابع را با عبارات شرطی تودرتو بازنویسی کنید.
یک یادداشت: این تابع خیلی کارا نیست چون تا محاسبه ی ارزش های برابر ادامه می دهد. شما می توانید این را با memorizing بسیار کاراتر کنید. (بخش ۱۱.۶ را ببینید) اما شما خواهید فهمید که memorize کردن بسیار سخت تر است اگر از عبارات شرطی در نوشتن برنامه استفاده کنید.