راهنمای عملی برنامه‌نویسی تابعی

نویسنده:

A. M. Kuchling

انتشار:

0.32

در این سند، مروری بر ویژگی‌های پایتون مناسب برای پیاده‌سازی برنامه‌ها به سبک تابعی خواهیم داشت. پس از مقدمه‌ای بر مفاهیم برنامه‌نویسی تابعی، به ویژگی‌های زبانی مانند iterators و تولیدگرs و ماژول‌های کتابخانه‌ای مرتبط مانند itertools و functools می‌پردازیم.

مقدمه

این بخش مفهوم پایه‌ی برنامه‌نویسی تابعی را توضیح می‌دهد؛ اگر فقط به یادگیری ویژگی‌های زبان پایتون علاقه دارید، به بخش بعدی درباره‌ی پیمایش‌گرها بروید.

زبان‌های برنامه‌نویسی امکان تجزیه مسائل را به چندین روش مختلف فراهم می‌کنند:

  • بیشتر زبان‌های برنامه‌نویسی رویه‌ای هستند: برنامه‌ها فهرست‌هایی از دستورالعمل‌ها هستند که به رایانه می‌گویند با ورودی برنامه چه کاری انجام دهد. C، Pascal و حتی پوسته‌های یونیکس زبان‌های رویه‌ای هستند.

  • در زبان‌های اعلانی، شما مشخصاتی می‌نویسید که مسئله‌ی موردحل را توصیف می‌کند و پیاده‌سازی زبان تشخیص می‌دهد که چگونه محاسبات را به‌صورت کارآمد انجام دهد. SQL زبان اعلانی‌ای است که احتمالاً بیش از همه با آن آشنا هستید؛ یک پرس‌وجوی SQL مجموعه‌داده‌ای را که می‌خواهید بازیابی کنید توصیف می‌کند و موتور SQL تصمیم می‌گیرد که آیا جدول‌ها را پویش کند یا از اندیس‌ها استفاده کند، کدام زیربند باید ابتدا اجرا شود و غیره.

  • برنامه‌های شیء‌گرا مجموعه‌هایی از اشیاء را دستکاری می‌کنند. اشیاء دارای وضعیت داخلی هستند و از متدهایی پشتیبانی می‌کنند که این وضعیت داخلی را به شکلی پرس‌وجو یا تغییر می‌دهند. Smalltalk و Java زبان‌های شیء‌گرا هستند. C++ و Python زبان‌هایی هستند که از برنامه‌نویسی شیء‌گرا پشتیبانی می‌کنند، اما استفاده از قابلیت‌های شیء‌گرا را تحمیل نمی‌کنند.

  • برنامه‌نویسی تابعی یک مسئله را به مجموعه‌ای از توابع تجزیه می‌کند. در حالت ایده‌آل، توابع تنها ورودی‌ها را دریافت می‌کنند و خروجی‌ها را تولید می‌کنند و هیچ وضعیت داخلی‌ای ندارند که بر خروجی تولیدشده برای یک ورودی مشخص تأثیر بگذارد. زبان‌های تابعی شناخته‌شده شامل خانواده ML (شامل Standard ML، OCaml و سایر گونه‌ها) و Haskell هستند.

طراحان برخی زبان‌های رایانه‌ای انتخاب می‌کنند که بر یک رویکرد خاص به برنامه‌نویسی تأکید کنند. این موضوع اغلب نوشتن برنامه‌هایی را که از رویکرد متفاوتی استفاده می‌کنند دشوار می‌کند. سایر زبان‌ها، زبان‌های چندپارادایمی هستند که از چندین رویکرد متفاوت پشتیبانی می‌کنند. Lisp، C++ و Python چندپارادایمی هستند؛ شما می‌توانید در همه‌ی این زبان‌ها برنامه‌ها یا کتابخانه‌هایی بنویسید که عمدتاً رویه‌ای، شیءگرا یا تابعی هستند. در یک برنامه‌ی بزرگ، ممکن است بخش‌های مختلف با استفاده از رویکردهای متفاوتی نوشته شوند؛ برای مثال، ممکن است GUI شیءگرا باشد، در حالی که منطق پردازش رویه‌ای یا تابعی است.

در یک برنامه تابعی، ورودی از میان مجموعه‌ای از توابع عبور می‌کند. هر تابع روی ورودی خود عمل می‌کند و خروجی‌ای تولید می‌کند. سبک تابعی از توابع دارای عوارض جانبی‌ای که وضعیت داخلی را تغییر می‌دهند یا تغییرات دیگری ایجاد می‌کنند که در مقدار بازگشتی تابع قابل مشاهده نیستند، پرهیز می‌کند. به توابعی که هیچ‌گونه عارضه جانبی ندارند، کاملاً تابعی گفته می‌شود. اجتناب از عوارض جانبی به معنای عدم استفاده از ساختارهای داده‌ای است که در حین اجرای برنامه به‌روزرسانی می‌شوند؛ خروجی هر تابع باید تنها به ورودی آن وابسته باشد.

برخی زبان‌ها در مورد خلوص بسیار سخت‌گیر هستند و حتی دستور انتسابی مانند a=3 یا c = a + b ندارند، اما اجتناب از تمام عوارض جانبی، مانند چاپ روی صفحه‌نمایش یا نوشتن در یک پرونده روی دیسک، دشوار است. مثالی دیگر، فراخوانی تابع print() یا time.sleep() است که هیچ‌کدام مقدار مفیدی برنمی‌گرداند. هر دو فقط برای عوارض جانبی‌شان، یعنی ارسال مقداری متن به صفحه‌نمایش یا مکث در اجرا برای یک ثانیه، فراخوانی می‌شوند.

برنامه‌های پایتون نوشته‌شده به سبک تابعی معمولاً تا سرحد اجتناب کامل از تمام ورودی/خروجی‌ها یا تمام انتساب‌ها پیش نمی‌روند؛ در عوض، رابطی با ظاهر تابعی ارائه می‌دهند، اما به‌صورت درونی از قابلیت‌های غیرتابعی استفاده می‌کنند. برای مثال، پیاده‌سازی یک تابع همچنان از انتساب‌ها به متغیرهای محلی استفاده می‌کند، اما متغیرهای سراسری را تغییر نمی‌دهد و عوارض جانبی دیگری ندارد.

برنامه‌نویسی تابعی می‌تواند متضاد برنامه‌نویسی شیء‌گرا در نظر گرفته شود. شیء‌ها کپسول‌های کوچکی هستند که حاوی وضعیتی داخلی به همراه مجموعه‌ای از فراخوانی‌های متد هستند که به شما اجازه می‌دهند این وضعیت را تغییر دهید، و برنامه‌ها از ایجاد مجموعه‌ای درست از تغییرات وضعیت تشکیل شده‌اند. برنامه‌نویسی تابعی می‌خواهد تا حد امکان از تغییرات وضعیت اجتناب کند و با جریان داده بین توابع کار می‌کند. در پایتون ممکن است این دو رویکرد را با نوشتن توابعی ترکیب کنید که نمونه‌هایی نشان‌دهنده‌ی اشیایی در برنامه شما (پیام‌های ایمیل، تراکنش‌ها و غیره) را دریافت می‌کنند و برمی‌گردانند.

طراحی تابعی ممکن است محدودیتی عجیب برای کار کردن تحت آن به نظر برسد. چرا باید از اشیاء و عوارض جانبی اجتناب کنید؟ سبک تابعی مزایای نظری و عملی دارد:

  • اثبات‌پذیری صوری.

  • پیمانه‌ای بودن.

  • ترکیب‌پذیری.

  • سهولت اشکال‌زدایی و آزمون.

اثبات‌پذیری صوری

یک مزیت نظری این است که ساختن اثبات ریاضی برای صحیح بودن یک برنامه تابعی آسان‌تر است.

برای مدت طولانی، پژوهشگران به یافتن راه‌هایی برای اثبات ریاضیاتی صحت برنامه‌ها علاقه‌مند بوده‌اند. این امر با آزمایش یک برنامه روی ورودی‌های متعدد و نتیجه‌گیری اینکه خروجی آن معمولاً صحیح است، یا خواندن کد منبع یک برنامه و نتیجه‌گیری اینکه کد درست به نظر می‌رسد، متفاوت است؛ در عوض، هدف اثبات دقیقی است که نشان می‌دهد یک برنامه نتیجه درست را برای تمام ورودی‌های ممکن تولید می‌کند.

روشی که برای اثبات صحت برنامه‌ها به کار می‌رود، نوشتن ناورداها است؛ یعنی ویژگی‌هایی از داده‌های ورودی و متغیرهای برنامه که همیشه برقرار هستند. سپس برای هر خط کد، نشان می‌دهید که اگر ناورداهای X و Y پیش از اجرای آن خط برقرار باشند، ناورداهای اندکی متفاوت X' و Y' پس از اجرای آن خط برقرار خواهند بود. این کار تا رسیدن شما به پایان برنامه ادامه می‌یابد؛ در آن نقطه، ناورداها باید با شرایط مطلوب برای خروجی برنامه مطابقت کنند.

پرهیز برنامه‌نویسی تابعی از انتساب‌ها به این دلیل به‌وجود آمد که مدیریت انتساب‌ها با این روش دشوار است؛ انتساب‌ها می‌توانند ناورداهایی را که پیش از انتساب برقرار بودند نقض کنند، بدون آن‌که ناوردای جدیدی ایجاد کنند که بتوان آن را به مراحل بعدی منتقل کرد.

متأسفانه، اثبات صحت برنامه‌ها تا حد زیادی غیرعملی است و ارتباطی به نرم‌افزار پایتون ندارد. حتی برنامه‌های بدیهی نیز به اثبات‌هایی چندین‌صفحه‌ای نیاز دارند؛ اثبات صحت یک برنامه‌ی نسبتاً پیچیده بسیار بزرگ خواهد بود، و بسیار کم یا هیچ‌کدام از برنامه‌هایی که روزانه استفاده می‌کنید (مفسر پایتون، پارسرٔ XML شما، مرورگر وب شما) را نمی‌توان اثبات صحت کرد. حتی اگر اثباتی را یادداشت یا تولید کنید، آنگاه مسئله‌ی تأیید آن اثبات مطرح می‌شود؛ شاید خطایی در آن وجود داشته باشد و شما به‌اشتباه باور داشته باشید که صحت برنامه را اثبات کرده‌اید.

ماژولار بودن

یک مزیت عملی‌تر برنامه‌نویسی تابعی این است که شما را وادار می‌کند مسئله‌ی خود را به بخش‌های کوچک تقسیم کنید. در نتیجه، برنامه‌ها ماژولارتر می‌شوند. مشخص کردن و نوشتن یک تابع کوچک که یک کار انجام می‌دهد، آسان‌تر از مشخص کردن و نوشتن یک تابع بزرگ است که یک تبدیل پیچیده را انجام می‌دهد. توابع کوچک نیز برای خواندن و بررسی خطاها آسان‌تر هستند.

سهولت اشکال‌زدایی و آزمون

آزمون و اشکال‌زدایی یک برنامه به سبک تابعی آسان‌تر است.

اشکال‌زدایی ساده‌تر می‌شود، زیرا توابع عموماً کوچک و به‌وضوح مشخص‌شده‌اند. هنگامی که برنامه‌ای کار نمی‌کند، هر تابع یک نقطه‌ی رابط است که می‌توانید در آن درست بودن داده‌ها را بررسی کنید. شما می‌توانید با بررسی ورودی‌ها و خروجی‌های میانی، تابعی را که مسئول یک اشکال است، به‌سرعت جدا کنید.

آزمون آسان‌تر است، زیرا هر تابع موضوع بالقوه‌ای برای یک آزمون واحد است. توابع به وضعیت سیستمی که باید پیش از اجرای یک آزمون بازتولید شود وابسته نیستند؛ در عوض فقط باید ورودی درست را بسازید و سپس بررسی کنید که خروجی با انتظارات مطابقت دارد.

ترکیب‌پذیری

هنگامی که روی برنامه‌ای به سبک تابعی کار می‌کنید، تعدادی تابع با ورودی‌ها و خروجی‌های متفاوت خواهید نوشت. برخی از این توابع ناگزیر برای یک کاربرد خاص تخصصی خواهند بود، اما برخی دیگر در طیف وسیعی از برنامه‌ها مفید خواهند بود. برای مثال، تابعی که مسیر یک پوشه را دریافت می‌کند و همه پرونده‌های XML موجود در آن پوشه را برمی‌گرداند، یا تابعی که نام یک پرونده را دریافت می‌کند و محتویات آن را برمی‌گرداند، می‌تواند در موقعیت‌های مختلف بسیاری کاربرد داشته باشد.

به‌مرور زمان، شما کتابخانه‌ای شخصی از ابزارهای کاربردی تشکیل خواهید داد. اغلب، برنامه‌های جدید را با چیدمان توابع موجود در یک پیکربندی جدید و نوشتن چند تابع مخصوص وظیفه‌ی جاری خواهید ساخت.

پیمایش‌گرها

با بررسی ویژگی‌ای از زبان پایتون آغاز می‌کنم که پایه‌ای مهم برای نوشتن برنامه‌ها به‌سبک تابعی است: پیمایش‌گرها .

پیمایش‌گر شیئی است که جریانی از داده را نشان می‌دهد؛ این شیء داده‌ها را هر بار یک عنصر برمی‌گرداند. یک پیمایش‌گر پایتون باید از متدی به نام __next__() پشتیبانی کند که هیچ آرگومانی نمی‌گیرد و همیشه عنصر بعدی جریان را برمی‌گرداند. اگر دیگر عنصری در جریان وجود نداشته باشد، __next__() باید استثنای StopIteration را پرتاب کند. البته پیمایش‌گرها لازم نیست متناهی باشند؛ نوشتن پیمایش‌گری که جریان نامتناهی از داده تولید می‌کند، کاملاً معقول است.

تابع توکار iter() یک شیء دلخواه را می‌گیرد و تلاش می‌کند پیمایش‌گری برگرداند که محتویات یا عناصر شیء را برمی‌گرداند، و اگر شیء از پیمایش پشتیبانی نکند، TypeError را پرتاب می‌کند. چندین نوع داده‌ی توکار پایتون از پیمایش پشتیبانی می‌کنند، که رایج‌ترین آن‌ها فهرست‌ها و دیکشنری‌ها هستند. یک شیء پیمایش‌پذیر (پیمایش‌پذیر) نامیده می‌شود اگر بتوانید پیمایش‌گری برای آن دریافت کنید.

می‌توانید رابط پیمایش را به‌صورت دستی آزمایش کنید:

>>> L = [1, 2, 3]
>>> it = iter(L)
>>> it
<...iterator object at ...>
>>> it.__next__()  # same as next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

پایتون در چندین زمینه مختلف، اشیاء پیمایش‌پذیر را انتظار دارد، مهم‌ترین آن‌ها دستور for است. در دستور for X in Y، Y باید یک پیمایش‌گر باشد یا شیئی که iter() بتواند برای آن یک پیمایش‌گر ایجاد کند. این دو دستور معادل هستند:

for i in iter(obj):
    print(i)

for i in obj:
    print(i)

پیمایش‌گرها را می‌توان با استفاده از توابع سازنده‌ی list() یا tuple() به‌صورت فهرست یا تاپل درآورد:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> t = tuple(iterator)
>>> t
(1, 2, 3)

واگشایی دنباله همچنین از پیمایش‌گرها پشتیبانی می‌کند: اگر می‌دانید یک پیمایش‌گر N عنصر برمی‌گرداند، می‌توانید آن‌ها را در یک N-تایی واگشایی کنید:

>>> L = [1, 2, 3]
>>> iterator = iter(L)
>>> a, b, c = iterator
>>> a, b, c
(1, 2, 3)

توابع توکار مانند max() و min() می‌توانند یک آرگومان پیمایش‌گر بگیرند و بزرگ‌ترین یا کوچک‌ترین عنصر را برمی‌گردانند. عملگرهای "in" و "not in" نیز از پیمایش‌گرها پشتیبانی می‌کنند: X in iterator زمانی درست است که X در جریان برگردانده‌شده توسط پیمایش‌گر پیدا شود. اگر پیمایش‌گر بی‌پایان باشد، با مشکلات آشکاری مواجه خواهید شد؛ max() و min() هرگز برنمی‌گردند، و اگر عنصر X هرگز در جریان ظاهر نشود، عملگرهای "in" و "not in" نیز برنمی‌گردند.

توجه داشته باشید که در یک پیمایش‌گر فقط می‌توانید به جلو بروید؛ هیچ راهی برای دریافت عنصر قبلی، بازنشانی پیمایش‌گر، یا ایجاد یک کپی از آن وجود ندارد. اشیای پیمایش‌گر می‌توانند به‌صورت اختیاری این قابلیت‌های اضافی را فراهم کنند، اما پروتکل پیمایش‌گر فقط متد __next__() را مشخص می‌کند. بنابراین توابع ممکن است تمام خروجی پیمایش‌گر را مصرف کنند، و اگر نیاز دارید کار متفاوتی با همان جریان انجام دهید، باید یک پیمایش‌گر جدید ایجاد کنید.

انواع داده‌ای که از پیمایش‌گرها پشتیبانی می‌کنند

پیش‌تر دیده‌ایم که فهرست‌ها و تاپل‌ها از پیمایش‌گرها پشتیبانی می‌کنند. در واقع، هر نوع دنباله‌ای در پایتون، مانند رشته‌ها، به‌طور خودکار از ایجاد یک پیمایش‌گر پشتیبانی می‌کند.

فراخوانی iter() روی یک دیکشنری، پیمایش‌گری برمی‌گرداند که روی کلیدهای دیکشنری حلقه می‌زند:

>>> m = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
...      'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
>>> for key in m:
...     print(key, m[key])
Jan 1
Feb 2
Mar 3
Apr 4
May 5
Jun 6
Jul 7
Aug 8
Sep 9
Oct 10
Nov 11
Dec 12

توجه داشته باشید که از Python 3.7 به بعد، تضمین می‌شود که ترتیب پیمایش دیکشنری همان ترتیب درج باشد. در نسخه‌های پیشین، این رفتار مشخص نشده بود و می‌توانست بین پیاده‌سازی‌ها متفاوت باشد.

اعمال iter() روی یک دیکشنری همیشه روی کلیدها حلقه می‌زند، اما دیکشنری‌ها متدهایی دارند که پیمایش‌گرهای دیگری برمی‌گردانند. اگر بخواهید روی مقادیر یا جفت‌های کلید/مقدار پیمایش کنید، می‌توانید به‌صراحت متدهای values() یا items() را فراخوانی کنید تا پیمایش‌گر مناسبی به دست آورید.

سازنده‌ی dict() می‌تواند پیمایش‌گری را بپذیرد که جریان متناهی از تاپل‌های (key, value) را برمی‌گرداند:

>>> L = [('Italy', 'Rome'), ('France', 'Paris'), ('US', 'Washington DC')]
>>> dict(iter(L))
{'Italy': 'Rome', 'France': 'Paris', 'US': 'Washington DC'}

پرونده‌ها همچنین از پیمایش از طریق فراخوانی متد readline() تا زمانی که دیگر سطری در پرونده وجود نداشته باشد، پشتیبانی می‌کنند. این بدان معناست که می‌توانید هر خط از یک پرونده را به این صورت بخوانید:

for line in file:
    # do something for each line
    ...

مجموعه‌ها می‌توانند محتوای خود را از یک پیمایش‌پذیر بگیرند و به شما اجازه دهند عناصر مجموعه را پیمایش کنید:

>>> S = {2, 3, 5, 7, 11, 13}
>>> for i in S:
...     print(i)
2
3
5
7
11
13

عبارت‌های تولیدگر و درک‌های فهرستی

دو عملیات رایج روی خروجی یک پیمایش‌گر عبارت‌اند از ۱) انجام عملیاتی برای هر عنصر، ۲) انتخاب زیرمجموعه‌ای از عناصر که شرطی را برآورده می‌کنند. برای مثال، با داشتن فهرستی از رشته‌ها، ممکن است بخواهید فضای سفید انتهایی هر خط را حذف کنید یا تمام رشته‌هایی که حاوی یک زیررشته‌ی مشخص هستند را استخراج کنید.

درک‌های فهرستی و عبارات تولیدگر (شکل کوتاه: «listcomps» و «genexps») نمادگذاری مختصری برای چنین عملیاتی هستند که از زبان برنامه‌نویسی تابعی Haskell (https://www.haskell.org/) وام گرفته شده‌اند. می‌توانید با کد زیر تمام فضای سفید را از یک جریان از رشته‌ها حذف کنید:

>>> line_list = ['  line 1\n', 'line 2  \n', ' \n', '']

>>> # Generator expression -- returns iterator
>>> stripped_iter = (line.strip() for line in line_list)

>>> # List comprehension -- returns list
>>> stripped_list = [line.strip() for line in line_list]

شما می‌توانید با افزودن یک شرط "if"، فقط عناصر خاصی را انتخاب کنید:

>>> stripped_list = [line.strip() for line in line_list
...                  if line != ""]

با یک درک فهرستی، شما یک فهرست پایتون دریافت می‌کنید؛ stripped_list فهرستی شامل ردیف‌های حاصل است، نه یک پیمایش‌گر. عبارت‌های تولیدگر یک پیمایش‌گر برمی‌گردانند که مقادیر را در حد نیاز محاسبه می‌کند و نیازی به ایجاد همه مقادیر به‌صورت یک‌جا ندارد. این بدان معناست که اگر با پیمایش‌گرهایی کار می‌کنید که یک جریان بی‌نهایت یا مقدار بسیار زیادی از داده را برمی‌گردانند، درک‌های فهرستی مفید نیستند. در این موقعیت‌ها، عبارت‌های تولیدگر ترجیح داده می‌شوند.

عبارت‌های تولیدگر در میان پرانتز ("()") و درک‌های فهرستی در میان کروشه ("[]") قرار دارند. عبارت‌های تولیدگر به این شکل هستند:

( expression for expr in sequence1
             if condition1
             for expr2 in sequence2
             if condition2
             for expr3 in sequence3
             ...
             if condition3
             for exprN in sequenceN
             if conditionN )

باز هم، برای درک فهرستی تنها کروشه‌های بیرونی متفاوت هستند (کروشه‌های مربع به‌جای پرانتزها).

عناصر خروجی تولیدشده، مقادیر متوالی expression خواهند بود. بندهای if همگی اختیاری هستند؛ در صورت وجود، expression تنها زمانی ارزیابی و به نتیجه اضافه می‌شود که condition درست باشد.

عبارت‌های تولیدگر همیشه باید داخل پرانتز نوشته شوند، اما پرانتزهای نشان‌دهنده‌ی فراخوانی تابع نیز به‌حساب می‌آیند. اگر بخواهید پیمایش‌گری بسازید که بلافاصله به یک تابع پاس داده شود، می‌توانید بنویسید:

obj_total = sum(obj.count for obj in list_all_objects())

بندهای for...in شامل دنباله‌هایی هستند که باید پیمایش شوند. دنباله‌ها لازم نیست هم‌طول باشند، زیرا از چپ به راست پیمایش می‌شوند، نه به‌صورت موازی. برای هر عنصر در sequence1، روی sequence2 از ابتدا حلقه زده می‌شود. سپس sequence3 برای هر جفت عنصر حاصل از sequence1 و sequence2 حلقه زده می‌شود.

به‌بیان دیگر، یک درک فهرستی یا عبارت تولیدگر معادل کد پایتون زیر است:

for expr1 in sequence1:
    if not (condition1):
        continue   # Skip this element
    for expr2 in sequence2:
        if not (condition2):
            continue   # Skip this element
        ...
        for exprN in sequenceN:
            if not (conditionN):
                continue   # Skip this element

            # Output the value of
            # the expression.

این بدان معناست که وقتی چندین بند for...in وجود داشته باشد اما هیچ بند if وجود نداشته باشد، طول خروجی حاصل برابر با حاصل‌ضرب طول همه دنباله‌ها خواهد بود. اگر دو فهرست به طول ۳ داشته باشید، فهرست خروجی ۹ عنصر طول دارد:

>>> seq1 = 'abc'
>>> seq2 = (1, 2, 3)
>>> [(x, y) for x in seq1 for y in seq2]
[('a', 1), ('a', 2), ('a', 3),
 ('b', 1), ('b', 2), ('b', 3),
 ('c', 1), ('c', 2), ('c', 3)]

برای جلوگیری از ایجاد ابهام در دستور زبان پایتون، اگر expression یک تاپل ایجاد کند، باید درون پرانتز قرار گیرد. نخستین درک فهرستی زیر یک خطای سینتکسی است، در حالی که دومی صحیح است:

# Syntax error
[x, y for x in seq1 for y in seq2]
# Correct
[(x, y) for x in seq1 for y in seq2]

تولیدگرها

تولیدگرها کلاسی خاص از توابع هستند که کار نوشتن پیمایش‌گرها را ساده می‌کنند. توابع معمولی یک مقدار را محاسبه می‌کنند و آن را برمی‌گردانند، اما تولیدگرها پیمایش‌گری را برمی‌گردانند که جریانی از مقادیر را برمی‌گرداند.

شما بدون شک با چگونگی کار فراخوانی‌های معمول تابع در پایتون یا C آشنا هستید. هنگامی که تابعی را فراخوانی می‌کنید، یک فضای نام خصوصی برای آن ایجاد می‌شود که متغیرهای محلی‌اش در آن ساخته می‌شوند. وقتی تابع به یک دستور return می‌رسد، متغیرهای محلی از بین می‌روند و مقدار به فراخواننده بازگردانده می‌شود. فراخوانی بعدی همان تابع، یک فضای نام خصوصی جدید و مجموعه‌ای تازه از متغیرهای محلی ایجاد می‌کند. اما چه می‌شد اگر متغیرهای محلی هنگام خروج از یک تابع دور انداخته نمی‌شدند؟ چه می‌شد اگر می‌توانستید بعداً تابع را از همان‌جایی که متوقف شده بود از سر بگیرید؟ این همان چیزی است که تولیدگرها فراهم می‌کنند؛ می‌توان آن‌ها را توابعی قابل از سرگیری دانست.

در اینجا ساده‌ترین مثال از یک تابع تولیدگر آمده است:

>>> def generate_ints(N):
...    for i in range(N):
...        yield i

هر تابعی که شامل کلیدواژه‌ی yield باشد، یک تابع تولیدگر است؛ این موضوع را کامپایلر bytecode پایتون تشخیص می‌دهد و در نتیجه، تابع را به‌صورت ویژه کامپایل می‌کند.

وقتی یک تابع تولیدگر را فراخوانی می‌کنید، مقدار واحدی را برنمی‌گرداند؛ بلکه یک شیء تولیدگر برمی‌گرداند که از پروتکل پیمایش‌گر پشتیبانی می‌کند. با اجرای عبارت yield، تولیدگر مقدار i را خروجی می‌دهد، مشابه یک دستور return. تفاوت بزرگ میان yield و یک دستور return این است که با رسیدن به yield، وضعیت اجرای تولیدگر به حالت تعلیق درمی‌آید و متغیرهای محلی حفظ می‌شوند. در فراخوانی بعدی متد __next__() تولیدگر، تابع اجرای خود را از سر می‌گیرد.

در اینجا یک نمونه استفاده از تولیدگر generate_ints() آمده است:

>>> gen = generate_ints(3)
>>> gen
<generator object generate_ints at ...>
>>> next(gen)
0
>>> next(gen)
1
>>> next(gen)
2
>>> next(gen)
Traceback (most recent call last):
  File "stdin", line 1, in <module>
  File "stdin", line 2, in generate_ints
StopIteration

همچنین می‌توانید for i in generate_ints(5) یا a, b, c = generate_ints(3) را بنویسید.

درون یک تابع تولیدگر، return value باعث پرتاب StopIteration(value) از متد __next__() می‌شود. پس از وقوع این حالت، یا پس از رسیدن به انتهای تابع، دنباله‌ی مقادیر پایان می‌یابد و تولیدگر نمی‌تواند مقادیر بیشتری تولید کند.

می‌توانید اثر تولیدگرها را به‌صورت دستی با نوشتن کلاس خودتان و ذخیره کردن تمام متغیرهای محلی تولیدگر به‌عنوان متغیرهای نمونه به دست آورید. برای مثال، برگرداندن فهرستی از اعداد صحیح می‌تواند با تنظیم self.count روی ۰ و افزایش self.count در متد __next__() و برگرداندن آن انجام شود. با این حال، برای یک تولیدگر نسبتاً پیچیده، نوشتن کلاس متناظر می‌تواند بسیار آشفته‌تر باشد.

بدنه‌ی آزمون موجود در کتابخانه‌ی پایتون، Lib/test/test_generators.py، شامل چندین مثال جالب‌تر است. در اینجا یک تولیدگر آمده است که پیمایش میان‌ترتیب یک درخت را با استفاده‌ی بازگشتی از تولیدگرها پیاده‌سازی می‌کند.

# A recursive generator that generates Tree leaves in in-order.
def inorder(t):
    if t:
        for x in inorder(t.left):
            yield x

        yield t.label

        for x in inorder(t.right):
            yield x

دو مثال دیگر در test_generators.py راه‌حل‌هایی برای مسئله‌ی N وزیر (N-Queens، قرار دادن N وزیر روی یک صفحه‌ی شطرنج NxN به‌گونه‌ای که هیچ وزیری وزیر دیگری را تهدید نکند) و گشت اسب (Knight's Tour، یافتن مسیری که یک اسب را به همه‌ی خانه‌های یک صفحه‌ی شطرنج NxN ببرد، بدون آنکه از هیچ خانه‌ای دو بار بازدید شود) تولید می‌کنند.

ارسال مقادیر به یک تولیدگر

در پایتون 2.4 و نسخه‌های پیش از آن، تولیدگرها تنها خروجی تولید می‌کردند. پس از فراخوانی کد یک تولیدگر برای ایجاد یک پیمایش‌گر، هیچ راهی برای ارسال اطلاعات جدید به تابع در زمان از سرگیری اجرای آن وجود نداشت. شما می‌توانستید این قابلیت را به‌صورت دست‌وپاشکسته با وادار کردن تولیدگر به بررسی یک متغیر سراسری یا با ارسال یک شیء تغییرپذیر که فراخوان‌کنندگان سپس آن را تغییر می‌دهند، پیاده‌سازی کنید، اما این روش‌ها آشفته هستند.

در پایتون 2.5 راه ساده‌ای برای ارسال مقادیر به یک تولیدگر وجود دارد. yield به یک عبارت تبدیل شد و مقداری را برمی‌گرداند که می‌توان آن را به یک متغیر انتساب داد یا عملیات دیگری روی آن انجام داد:

val = (yield i)

توصیه می‌کنم که همیشه اطراف عبارت yield پرانتز قرار دهید، هنگامی که با مقدار برگردانده‌شده کاری انجام می‌دهید، همان‌گونه که در مثال بالا آمده است. پرانتزها همیشه ضروری نیستند، اما افزودن همیشگی آن‌ها آسان‌تر است تا اینکه به یاد داشته باشید چه زمانی لازم هستند.

(PEP 342 قوانین دقیق را توضیح می‌دهد، که بر اساس آن‌ها عبارت yield باید همیشه داخل پرانتز قرار گیرد، مگر وقتی که به‌عنوان عبارت سطح بالا در سمت راست یک انتساب ظاهر شود. این بدان معناست که می‌توانید val = yield i بنویسید، اما وقتی عملیاتی وجود دارد باید از پرانتز استفاده کنید، مانند val = (yield i) + 12.)

مقادیر با فراخوانی متد send(value) یک تولیدگر، به آن ارسال می‌شوند. این متد کد تولیدگر را از سر می‌گیرد و عبارت yield مقدار مشخص‌شده را بازمی‌گرداند. اگر متد معمولی __next__() فراخوانی شود، yield مقدار None را بازمی‌گرداند.

در اینجا یک شمارنده ساده آمده است که به اندازه ۱ افزایش می‌یابد و امکان تغییر مقدار شمارنده داخلی را فراهم می‌کند.

def counter(maximum):
    i = 0
    while i < maximum:
        val = (yield i)
        # If value provided, change counter
        if val is not None:
            i = val
        else:
            i += 1

و در اینجا مثالی از تغییر شمارنده آمده است:

>>> it = counter(10)
>>> next(it)
0
>>> next(it)
1
>>> it.send(8)
8
>>> next(it)
9
>>> next(it)
Traceback (most recent call last):
  File "t.py", line 15, in <module>
    it.next()
StopIteration

از آنجا که yield اغلب مقدار None را برمی‌گرداند، باید همیشه این حالت را بررسی کنید. مقدار آن را به‌سادگی در عبارت‌ها به کار نبرید، مگر اینکه مطمئن باشید متد send() تنها متدی است که برای از سرگیری تابع تولیدگر شما استفاده می‌شود.

علاوه بر send()، تولیدگرها دو متد دیگر نیز دارند:

  • throw(value) برای پرتاب استثنا درون تولیدگر استفاده می‌شود؛ این استثنا توسط عبارت yield در نقطه‌ای که اجرای تولیدگر مکث کرده است پرتاب می‌شود.

  • close() استثنای GeneratorExit را به تولیدگر می‌فرستد تا تکرار را پایان دهد. هنگام دریافت این استثنا، کد تولیدگر باید یا GeneratorExit یا StopIteration را پرتاب کند؛ گرفتن این استثنا و انجام هر کار دیگری غیرمجاز است و باعث پرتاب RuntimeError می‌شود. همچنین وقتی تولیدگر زباله‌روبی می‌شود، close() توسط زباله‌روب پایتون فراخوانی می‌شود.

    اگر نیاز دارید هنگام رخ دادن GeneratorExit کد پاک‌سازی را اجرا کنید، پیشنهاد می‌کنم به‌جای گرفتن GeneratorExit از یک بلوک try: ... finally: استفاده کنید.

اثر تجمعی این تغییرات این است که تولیدگرها را از تولیدکنندگان یک‌طرفه‌ی اطلاعات به هر دو تولیدکننده و مصرف‌کننده تبدیل می‌کند.

تولیدگرها همچنین به هم‌روال‌ها تبدیل می‌شوند، شکل تعمیم‌یافته‌تری از زیربرنامه‌ها. زیربرنامه‌ها در یک نقطه وارد می‌شوند و از نقطه‌ای دیگر خارج می‌شوند (ابتدای تابع و یک دستور return)، اما هم‌روال‌ها می‌توانند در نقاط مختلفی وارد شوند، خارج شوند و از سر گرفته شوند (دستورهای yield).

توابع توکار

بگذارید توابع توکاری را که اغلب با پیمایش‌گرها استفاده می‌شوند، با جزئیات بیشتری بررسی کنیم.

دو مورد از توابع توکار پایتون، map() و filter()، قابلیت‌های عبارات تولیدگر را تکرار می‌کنند:

map(f, iterA, iterB, ...) یک پیمایش‌گر بر روی دنباله برمی‌گرداند

f(iterA[0], iterB[0]), f(iterA[1], iterB[1]), f(iterA[2], iterB[2]), ....

>>> def upper(s):
...     return s.upper()
>>> list(map(upper, ['sentence', 'fragment']))
['SENTENCE', 'FRAGMENT']
>>> [upper(s) for s in ['sentence', 'fragment']]
['SENTENCE', 'FRAGMENT']

البته می‌توانید با استفاده از یک درک فهرستی نیز همان اثر را ایجاد کنید.

filter(predicate, iter) یک پیمایش‌گر بر روی تمام عناصر دنباله‌ای که شرط خاصی را برآورده می‌کنند، برمی‌گرداند و به‌طور مشابه می‌توان آن را با درک فهرستی پیاده‌سازی کرد. یک تابع محمول (predicate) تابعی است که مقدار درستی یک شرط را برمی‌گرداند؛ برای استفاده با filter()، تابع محمول باید یک مقدار واحد دریافت کند.

>>> def is_even(x):
...     return (x % 2) == 0
>>> list(filter(is_even, range(10)))
[0, 2, 4, 6, 8]

این را می‌توان به‌صورت یک درک فهرستی نیز نوشت:

>>> list(x for x in range(10) if is_even(x))
[0, 2, 4, 6, 8]

enumerate(iter, start=0) عناصر موجود در پیمایش‌پذیر را می‌شمارد و تاپل‌های دوتایی (2-tuples) شامل شماره (از start) و هر عنصر را برمی‌گرداند.

>>> for item in enumerate(['subject', 'verb', 'object']):
...     print(item)
(0, 'subject')
(1, 'verb')
(2, 'object')

enumerate() اغلب هنگام پیمایش یک فهرست با حلقه و ثبت اندیس‌هایی که شرایط خاصی در آن‌ها برقرار است استفاده می‌شود:

f = open('data.txt', 'r')
for i, line in enumerate(f):
    if line.strip() == '':
        print('Blank line at line #%i' % i)

sorted(iterable, key=None, reverse=False) تمام عناصر یک شیء پیمایش‌پذیر را در یک فهرست گردآوری می‌کند، فهرست را مرتب می‌کند و نتیجه‌ی مرتب‌شده را برمی‌گرداند. آرگومان‌های key و reverse به متد sort() فهرست ساخته‌شده منتقل می‌شوند.

>>> import random
>>> # Generate 8 random numbers between [0, 10000)
>>> rand_list = random.sample(range(10000), 8)
>>> rand_list
[769, 7953, 9828, 6431, 8442, 9878, 6213, 2207]
>>> sorted(rand_list)
[769, 2207, 6213, 6431, 7953, 8442, 9828, 9878]
>>> sorted(rand_list, reverse=True)
[9878, 9828, 8442, 7953, 6431, 6213, 2207, 769]

(برای بحثی با جزئیات بیشتر درباره مرتب‌سازی، روش‌های مرتب‌سازی را ببینید.)

توابع توکار any(iter) و all(iter) مقادیر درستی محتوای یک پیمایش‌پذیر را بررسی می‌کنند. any() اگر یکی از المان‌های پیمایش‌پذیر مقدار درست باشد، True را برمی‌گرداند، و all() اگر تمام المان‌ها مقادیر درست باشند، True را برمی‌گرداند:

>>> any([0, 1, 0])
True
>>> any([0, 0, 0])
False
>>> any([1, 1, 1])
True
>>> all([0, 1, 0])
False
>>> all([0, 0, 0])
False
>>> all([1, 1, 1])
True

zip(iterA, iterB, ...) یک عنصر از هر پیمایش‌پذیر می‌گیرد و آن‌ها را در قالب یک تاپل برمی‌گرداند:

zip(['a', 'b', 'c'], (1, 2, 3)) =>
  ('a', 1), ('b', 2), ('c', 3)

It doesn't construct an in-memory list and exhaust all the input iterators before returning; instead tuples are constructed and returned only if they're requested. (The technical term for this behaviour is lazy evaluation.)

این پیمایش‌گر برای استفاده با پیمایش‌پذیرهایی در نظر گرفته شده است که همگی طول یکسانی دارند. اگر پیمایش‌پذیرها طول‌های متفاوتی داشته باشند، جریان حاصل هم‌طول با کوتاه‌ترین پیمایش‌پذیر خواهد بود.

zip(['a', 'b'], (1, 2, 3)) =>
  ('a', 1), ('b', 2)

با این حال، باید از انجام این کار خودداری کنید، زیرا ممکن است عنصری از پیمایش‌گرهای طولانی‌تر گرفته و دور انداخته شود. این بدان معناست که نمی‌توانید به استفاده بیشتر از پیمایش‌گرها ادامه دهید، زیرا ممکن است یک عنصر دور انداخته‌شده را از قلم بیندازید.

ماژول itertools

ماژول itertools شامل تعدادی پیمایش‌گر پرکاربرد و همچنین توابعی برای ترکیب چندین پیمایش‌گر است. این بخش با نمایش مثال‌های کوچک، محتوای این ماژول را معرفی می‌کند.

توابع این ماژول به چند کلاس کلی تقسیم می‌شوند:

  • توابعی که یک پیمایش‌گر جدید را بر اساس یک پیمایش‌گر موجود ایجاد می‌کنند.

  • توابعی برای در نظر گرفتن عناصر یک پیمایش‌گر به‌عنوان آرگومان‌های تابع.

  • توابعی برای انتخاب بخش‌هایی از خروجی یک پیمایش‌گر .

  • تابعی برای گروه‌بندی خروجی یک پیمایش‌گر.

ایجاد پیمایش‌گرهای جدید

itertools.count(start, step) یک جریان بی‌پایان از مقادیر با فاصله‌های مساوی بازمی‌گرداند. به‌صورت اختیاری می‌توانید عدد شروع را که پیش‌فرض آن ۰ است و فاصله بین اعداد را که پیش‌فرض آن ۱ است، مشخص کنید:

itertools.count() =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...
itertools.count(10) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...
itertools.count(10, 5) =>
  10, 15, 20, 25, 30, 35, 40, 45, 50, 55, ...

itertools.cycle(iter) یک کپی از محتوای پیمایش‌پذیر ارائه‌شده ذخیره می‌کند و پیمایش‌گر جدیدی بازمی‌گرداند که عناصر آن را از اولین تا آخرین بازمی‌گرداند. این پیمایش‌گر جدید، این عناصر را به‌طور بی‌نهایت تکرار خواهد کرد.

itertools.cycle([1, 2, 3, 4, 5]) =>
  1, 2, 3, 4, 5, 1, 2, 3, 4, 5, ...

itertools.repeat(elem, [n]) المان ارائه‌شده را n بار برمی‌گرداند، یا اگر n ارائه نشده باشد، المان را بی‌پایان برمی‌گرداند.

itertools.repeat('abc') =>
  abc, abc, abc, abc, abc, abc, abc, abc, abc, abc, ...
itertools.repeat('abc', 5) =>
  abc, abc, abc, abc, abc

itertools.chain(iterA, iterB, ...) takes an arbitrary number of iterables as input, and returns all the elements of the first iterator, then all the elements of the second, and so on, until all of the iterables have been exhausted.

itertools.chain(['a', 'b', 'c'], (1, 2, 3)) =>
  a, b, c, 1, 2, 3

itertools.islice(iter, [start], stop, [step]) جریانی را برمی‌گرداند که اسلایسی از پیمایش‌گر است. با تنها یک آرگومان stop، نخستین stop عنصر را برمی‌گرداند. اگر یک اندیس آغاز ارائه دهید، stop-start عنصر دریافت خواهید کرد، و اگر مقداری برای step ارائه دهید، مطابق آن از روی عناصر پرش خواهد شد. برخلاف اسلایس رشته و فهرست در پایتون، نمی‌توانید از مقادیر منفی برای start، stop یا step استفاده کنید.

itertools.islice(range(10), 8) =>
  0, 1, 2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8) =>
  2, 3, 4, 5, 6, 7
itertools.islice(range(10), 2, 8, 2) =>
  2, 4, 6

itertools.tee(iter, [n]) یک پیمایش‌گر را تکثیر می‌کند؛ این تابع n پیمایش‌گر مستقل برمی‌گرداند که همگی محتوای پیمایش‌گر منبع را برمی‌گردانند. اگر مقداری برای n ارائه ندهید، مقدار پیش‌فرض ۲ است. تکثیر پیمایش‌گرها نیازمند ذخیره کردن بخشی از محتوای پیمایش‌گر منبع است، بنابراین اگر پیمایش‌گر بزرگ باشد و یکی از پیمایش‌گرهای جدید بیشتر از بقیه مصرف شود، می‌تواند حافظه قابل‌توجهی مصرف کند.

itertools.tee( itertools.count() ) =>
   iterA, iterB

که در آن iterA ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

و   iterB ->
   0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ...

فراخوانی توابع بر روی المان‌ها

ماژول operator شامل مجموعه‌ای از توابع متناظر با عملگرهای پایتون است. برخی نمونه‌ها عبارت‌اند از operator.add(a, b) (دو مقدار را جمع می‌کند)، operator.ne(a, b) (معادل a != b است)، و operator.attrgetter('id') (یک فراخوانی‌پذیر برمی‌گرداند که ویژگی .id را دریافت می‌کند).

itertools.starmap(func, iter) فرض می‌کند که پیمایش‌پذیر جریانی از تاپل‌ها را برمی‌گرداند، و func را با استفاده از این تاپل‌ها به‌عنوان آرگومان‌ها فراخوانی می‌کند:

itertools.starmap(os.path.join,
                  [('/bin', 'python'), ('/usr', 'bin', 'java'),
                   ('/usr', 'bin', 'perl'), ('/usr', 'bin', 'ruby')])
=>
  /bin/python, /usr/bin/java, /usr/bin/perl, /usr/bin/ruby

انتخاب عناصر

گروه دیگری از توابع، زیرمجموعه‌ای از عناصر یک پیمایش‌گر را بر اساس یک محک (predicate) انتخاب می‌کنند.

itertools.filterfalse(predicate, iter) نقطه مقابل filter() است و همه عناصری را برمی‌گرداند که محمول برای آن‌ها false برمی‌گرداند:

itertools.filterfalse(is_even, itertools.count()) =>
  1, 3, 5, 7, 9, 11, 13, 15, ...

itertools.takewhile(predicate, iter) عناصر را تا زمانی که شرط مقدار درست را برگرداند، برمی‌گرداند. هنگامی که محمول مقدار نادرست را برگرداند، پیمایش‌گر پایان نتایج خود را اعلام می‌کند.

def less_than_10(x):
    return x < 10

itertools.takewhile(less_than_10, itertools.count()) =>
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9

itertools.takewhile(is_even, itertools.count()) =>
  0

itertools.dropwhile(predicate, iter) عناصر را تا زمانی که محمول مقدار درست را برگرداند نادیده می‌گیرد، و سپس باقی‌مانده‌ی نتایج تکرارپذیر را برمی‌گرداند.

itertools.dropwhile(less_than_10, itertools.count()) =>
  10, 11, 12, 13, 14, 15, 16, 17, 18, 19, ...

itertools.dropwhile(is_even, itertools.count()) =>
  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...

itertools.compress(data, selectors) takes two iterators and returns only those elements of data for which the corresponding element of selectors is true, stopping whenever either one is exhausted:

itertools.compress([1, 2, 3, 4, 5], [True, True, False, False, True]) =>
   1, 2, 5

توابع ترکیبیاتی

itertools.combinations(iterable, r) یک پیمایش‌گر برمی‌گرداند که همه‌ی ترکیب‌های ممکن به‌صورت تاپل‌های r عضوی از عناصر موجود در iterable را می‌دهد.

itertools.combinations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 3), (2, 4), (2, 5),
  (3, 4), (3, 5),
  (4, 5)

itertools.combinations([1, 2, 3, 4, 5], 3) =>
  (1, 2, 3), (1, 2, 4), (1, 2, 5), (1, 3, 4), (1, 3, 5), (1, 4, 5),
  (2, 3, 4), (2, 3, 5), (2, 4, 5),
  (3, 4, 5)

عناصر هر تاپل به همان ترتیبی که iterable آن‌ها را برگردانده است باقی می‌مانند. برای مثال، عدد ۱ همیشه قبل از ۲، ۳، ۴ یا ۵ در مثال‌های بالا قرار دارد. تابعی مشابه، itertools.permutations(iterable, r=None)، این محدودیت روی ترتیب را برمی‌دارد و همه‌ی آرایش‌های ممکن به طول r را برمی‌گرداند:

itertools.permutations([1, 2, 3, 4, 5], 2) =>
  (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 1), (2, 3), (2, 4), (2, 5),
  (3, 1), (3, 2), (3, 4), (3, 5),
  (4, 1), (4, 2), (4, 3), (4, 5),
  (5, 1), (5, 2), (5, 3), (5, 4)

itertools.permutations([1, 2, 3, 4, 5]) =>
  (1, 2, 3, 4, 5), (1, 2, 3, 5, 4), (1, 2, 4, 3, 5),
  ...
  (5, 4, 3, 2, 1)

اگر مقداری برای r ارائه ندهید، از طول پیمایش‌پذیر استفاده می‌شود، به این معنا که همه عناصر جایگشت داده می‌شوند.

توجه داشته باشید که این توابع تمام ترکیب‌های ممکن را بر اساس جایگاه تولید می‌کنند و نیازی نیست که محتویات iterable یکتا باشند:

itertools.permutations('aba', 3) =>
  ('a', 'b', 'a'), ('a', 'a', 'b'), ('b', 'a', 'a'),
  ('b', 'a', 'a'), ('a', 'a', 'b'), ('a', 'b', 'a')

تاپل یکسان ('a', 'a', 'b') دو بار ظاهر می‌شود، اما دو رشته‌ی 'a' از موقعیت‌های متفاوتی آمده‌اند.

تابع itertools.combinations_with_replacement(iterable, r) محدودیت دیگری را برمی‌دارد: عناصر می‌توانند در یک تاپل تکرار شوند. از نظر مفهومی، یک عنصر برای جایگاه نخست هر تاپل انتخاب می‌شود و سپس پیش از انتخاب عنصر دوم، بازگردانده می‌شود.

itertools.combinations_with_replacement([1, 2, 3, 4, 5], 2) =>
  (1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
  (2, 2), (2, 3), (2, 4), (2, 5),
  (3, 3), (3, 4), (3, 5),
  (4, 4), (4, 5),
  (5, 5)

گروه‌بندی عناصر

آخرین تابعی که بررسی خواهم کرد، itertools.groupby(iter, key_func=None)، پیچیده‌ترین است. key_func(elem) تابعی است که می‌تواند یک مقدار کلید برای هر المان بازگردانده‌شده از پیمایش‌پذیر محاسبه کند. اگر تابع کلید را ارائه نکنید، کلید صرفاً خود هر المان است.

groupby() تمام عناصر متوالیِ پیمایش‌پذیر زیرین را که مقدار کلید یکسانی دارند، جمع‌آوری می‌کند و جریانی از تاپل‌های دوتایی شامل یک مقدار کلید و یک پیمایش‌گر برای عناصر با آن کلید را برمی‌گرداند.

city_list = [('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL'),
             ('Anchorage', 'AK'), ('Nome', 'AK'),
             ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ'),
             ...
            ]

def get_state(city_state):
    return city_state[1]

itertools.groupby(city_list, get_state) =>
  ('AL', iterator-1),
  ('AK', iterator-2),
  ('AZ', iterator-3), ...

که در آن
iterator-1 =>
  ('Decatur', 'AL'), ('Huntsville', 'AL'), ('Selma', 'AL')
iterator-2 =>
  ('Anchorage', 'AK'), ('Nome', 'AK')
iterator-3 =>
  ('Flagstaff', 'AZ'), ('Phoenix', 'AZ'), ('Tucson', 'AZ')

groupby() فرض می‌کند که محتوای پیمایش‌پذیر زیربنایی از قبل بر اساس کلید مرتب شده باشد. توجه داشته باشید که پیمایش‌گرهای برگردانده‌شده نیز از پیمایش‌پذیر زیربنایی استفاده می‌کنند، بنابراین باید نتایج پیمایش‌گر ۱ را پیش از درخواست پیمایش‌گر ۲ و کلید متناظر آن مصرف کنید.

ماژول functools

ماژول functools شامل برخی توابع مرتبه بالاتر است. یک تابع مرتبه بالاتر یک یا چند تابع را به‌عنوان ورودی می‌گیرد و یک تابع جدید را برمی‌گرداند. مفیدترین ابزار در این ماژول، تابع functools.partial() است.

برای برنامه‌هایی که به سبک تابعی نوشته شده‌اند، گاهی ممکن است بخواهید نسخه‌هایی از توابع موجود بسازید که برخی از پارامترهای آن‌ها از قبل پر شده باشند. یک تابع پایتون f(a, b, c) را در نظر بگیرید؛ ممکن است بخواهید یک تابع جدید g(b, c) بسازید که معادل f(1, b, c) باشد؛ شما مقداری را برای یکی از پارامترهای f() جایگذاری می‌کنید. به این کار «کاربرد جزئی تابع» (partial function application) گفته می‌شود.

سازنده‌ی partial() آرگومان‌های (function, arg1, arg2, ..., kwarg1=value1, kwarg2=value2) را دریافت می‌کند. شیء حاصل فراخوانی‌پذیر است، بنابراین کافی است آن را فراخوانی کنید تا function با آرگومان‌های مقداردهی‌شده فراخوانی شود.

در اینجا مثالی کوچک اما واقع‌بینانه آمده است:

import functools

def log(message, subsystem):
    """Write the contents of 'message' to the specified subsystem."""
    print('%s: %s' % (subsystem, message))
    ...

server_log = functools.partial(log, subsystem='server')
server_log('Unable to open socket')

functools.reduce(func, iter, [initial_value]) cumulatively performs an operation on all the iterable's elements and, therefore, can't be applied to infinite iterables. func must be a function that takes two elements and returns a single value. functools.reduce() takes the first two elements A and B returned by the iterator and calculates func(A, B). It then requests the third element, C, calculates func(func(A, B), C), combines this result with the fourth element returned, and continues until the iterable is exhausted. If the iterable returns no values at all, a TypeError exception is raised. If the initial value is supplied, it's used as a starting point and func(initial_value, A) is the first calculation.

>>> import operator, functools
>>> functools.reduce(operator.concat, ['A', 'BB', 'C'])
'ABBC'
>>> functools.reduce(operator.concat, [])
Traceback (most recent call last):
  ...
TypeError: reduce() of empty iterable with no initial value
>>> functools.reduce(operator.mul, [1, 2, 3], 1)
6
>>> functools.reduce(operator.mul, [], 1)
1

اگر از operator.add() همراه با functools.reduce() استفاده کنید، تمام عناصر پیمایش‌پذیر را با هم جمع می‌کنید. این حالت آن‌قدر رایج است که یک تابع توکار ویژه به نام sum() برای محاسبه‌ی آن وجود دارد:

>>> import functools, operator
>>> functools.reduce(operator.add, [1, 2, 3, 4], 0)
10
>>> sum([1, 2, 3, 4])
10
>>> sum([])
0

با این حال، برای بسیاری از کاربردهای functools.reduce()، نوشتن همان حلقه for آشکار می‌تواند خواناتر باشد:

import functools
# Instead of:
product = functools.reduce(operator.mul, [1, 2, 3], 1)

# You can write:
product = 1
for i in [1, 2, 3]:
    product *= i

یک تابع مرتبط، itertools.accumulate(iterable, func=operator.add) است. این تابع همان محاسبه را انجام می‌دهد، اما به‌جای برگرداندن تنها نتیجه نهایی، accumulate() پیمایش‌گری برمی‌گرداند که هر نتیجه جزئی را نیز تولید می‌کند:

itertools.accumulate([1, 2, 3, 4, 5]) =>
  1, 3, 6, 10, 15

itertools.accumulate([1, 2, 3, 4, 5], operator.mul) =>
  1, 2, 6, 24, 120

ماژول operator

ماژول operator پیش‌تر ذکر شد. این ماژول شامل مجموعه‌ای از توابع است که متناظر با عملگرهای پایتون هستند. این توابع اغلب در کد با سبک تابعی مفید هستند، زیرا شما را از نوشتن توابع ساده‌ای که تنها یک عمل واحد را انجام می‌دهند بی‌نیاز می‌کنند.

برخی از توابع این ماژول عبارتند از:

  • عملیات ریاضی: add()، sub()، mul()، floordiv()، abs()، ...

  • عملیات منطقی: not_()، truth().

  • عملیات‌های بیتی: and_()، or_()، invert().

  • مقایسه‌ها: eq()، ne()، lt()، le()، gt() و ge().

  • هویت شیء: is_()، is_not().

برای فهرستی کامل، به مستندات ماژول operator مراجعه کنید.

توابع کوچک و عبارت لامبدا

هنگام نوشتن برنامه‌هایی با سبک تابعی، اغلب به توابع کوچکی نیاز خواهید داشت که به‌عنوان محمول (predicate) عمل کنند یا عناصر را به شکلی با هم ترکیب کنند.

اگر یک تابع توکار پایتون یا یک تابع ماژول مناسب وجود داشته باشد، اصلاً نیازی نیست که تابع جدیدی تعریف کنید:

stripped_lines = [line.strip() for line in lines]
existing_files = filter(os.path.exists, file_list)

اگر تابع مورد نیاز شما وجود ندارد، باید آن را بنویسید. یکی از راه‌های نوشتن توابع کوچک، استفاده از عبارت lambda است. lambda تعدادی پارامتر و یک عبارت که این پارامترها را ترکیب می‌کند می‌گیرد و یک تابع ناشناس می‌سازد که مقدار عبارت را برمی‌گرداند:

adder = lambda x, y: x+y

print_assign = lambda name, value: name + '=' + str(value)

یک جایگزین این است که صرفاً از دستور def استفاده کنید و تابعی را به روش معمول تعریف کنید:

def adder(x, y):
    return x + y

def print_assign(name, value):
    return name + '=' + str(value)

کدام جایگزین ارجح است؟ این پرسشی درباره‌ی سبک است؛ روش همیشگی من پرهیز از استفاده از lambda است.

یکی از دلایل ترجیح من این است که lambda در توابعی که می‌تواند تعریف کند، بسیار محدود است. نتیجه باید به‌صورت یک عبارت قابل محاسبه باشد، به این معنا که نمی‌توانید مقایسه‌های چندشاخه‌ای if... elif... else یا دستورهای try... except داشته باشید. اگر سعی کنید کار زیادی در یک دستور lambda انجام دهید، در نهایت عبارتی بیش از حد پیچیده خواهید داشت که خواندن آن دشوار است. سریع، کد زیر چه کاری انجام می‌دهد؟

import functools
total = functools.reduce(lambda a, b: (0, a[1] + b[1]), items)[1]

می‌توانید آن را بفهمید، اما گره‌گشایی از عبارت برای دریافتن اینکه چه اتفاقی می‌افتد زمان‌بر است. استفاده از دستورهای def تودرتوی کوتاه، اوضاع را کمی بهتر می‌کند:

import functools
def combine(a, b):
    return 0, a[1] + b[1]

total = functools.reduce(combine, items)[1]

اما بهترین حالت این بود که به‌سادگی از یک حلقه for استفاده کرده بودم:

total = 0
for a, b in items:
    total += b

یا تابع توکار sum() و یک عبارت تولیدگر:

total = sum(b for a, b in items)

بسیاری از کاربردهای functools.reduce() هنگامی که به‌صورت حلقه‌های for نوشته شوند، واضح‌تر هستند.

فریدریک لوند زمانی مجموعه‌ای از قواعد زیر را برای بازسازی موارد استفاده از lambda پیشنهاد کرد:

  1. یک تابع lambda بنویسید.

  2. کامنتی بنویسید که توضیح دهد آن lambda دقیقاً چه کاری انجام می‌دهد.

  3. مدتی کامنت را بررسی کنید و به نامی بیندیشید که بیانگر ماهیت آن باشد.

  4. لامبدا را با استفاده از آن نام، به یک دستور def تبدیل کنید.

  5. کامنت را حذف کنید.

من این قوانین را بسیار می‌پسندم، اما شما آزادید درباره‌ی اینکه آیا این سبک بدون lambda بهتر است یا خیر، نظر متفاوتی داشته باشید.

تاریخچه‌ی بازبینی و سپاسگزاری‌ها

نویسنده مایل است از افراد زیر برای ارائه پیشنهادها، اصلاحات و کمک در پیش‌نویس‌های مختلف این مقاله قدردانی کند: Ian Bicking، Nick Coghlan، Nick Efford، Raymond Hettinger، Jim Jewett، Mike Krell، Leandro Lameiro، Jussi Salmela، Collin Winter، Blake Winton.

نسخه‌ی 0.1: منتشرشده در ۳۰ ژوئن ۲۰۰۶.

نسخه 0.11: منتشرشده در ۱ ژوئیه ۲۰۰۶. اصلاح اشتباهات تایپی.

نسخه 0.2: منتشرشده در ۱۰ ژوئیه ۲۰۰۶. بخش‌های genexp و listcomp در یک بخش ادغام شدند. اصلاح غلط‌های تایپی.

نسخه 0.21: مراجع بیشتری که در فهرست پستی tutor پیشنهاد شده بودند، افزوده شدند.

نسخه‌ی 0.30: بخشی درباره‌ی ماژول functional نوشته‌شده توسط Collin Winter اضافه می‌کند؛ بخش کوتاهی درباره‌ی ماژول operator اضافه می‌کند؛ چند ویرایش دیگر.

ارجاع‌ها

عمومی

ساختار و تفسیر برنامه‌های کامپیوتری، اثر هارولد ابلسون و جرالد جی ساسمن با همکاری جولی ساسمن. این کتاب را می‌توانید در https://mitpress.mit.edu/sicp بیابید. در این کتاب درسی کلاسیک علوم کامپیوتر، فصل‌های ۲ و ۳ به بررسی استفاده از دنباله‌ها و جریان‌ها برای سازمان‌دهی جریان داده درون یک برنامه می‌پردازند. این کتاب در مثال‌های خود از Scheme استفاده می‌کند، اما بسیاری از رویکردهای طراحی توصیف‌شده در این فصل‌ها بر کد پایتون به‌سبک تابعی قابل‌اعمال هستند.

https://defmacro.org/2006/06/19/fp.html: مقدمه‌ای عمومی بر برنامه‌نویسی تابعی که از مثال‌های جاوا استفاده می‌کند و دارای مقدمه‌ای تاریخی و مفصل است.

https://fa.wikipedia.org/wiki/برنامه‌نویسی_تابعی: مدخل عمومی ویکی‌پدیا که برنامه‌نویسی تابعی را توصیف می‌کند.

https://en.wikipedia.org/wiki/Coroutine: مدخل هم‌روال‌ها.

https://en.wikipedia.org/wiki/Partial_application: مدخلی برای مفهوم اعمال جزئی تابع (partial function application).

https://en.wikipedia.org/wiki/Currying: مدخلی برای مفهوم کری‌کردن (currying).

مختص پایتون

https://gnosis.cx/TPiP/: فصل نخست کتاب دیوید مرتز Text Processing in Python در بخشی با عنوان «به‌کارگیری توابع مرتبه بالاتر در پردازش متن» به برنامه‌نویسی تابعی برای پردازش متن می‌پردازد.

مرتز همچنین مجموعه‌ای ۳ قسمتی از مقالات درباره‌ی برنامه‌نویسی تابعی برای وب‌سایت DeveloperWorks شرکت IBM نوشت؛ بخش ۱، بخش ۲، و بخش ۳ را ببینید،

مستندات پایتون

مستندات ماژول itertools.

مستندات ماژول functools.

مستندات ماژول operator.

PEP 289: «عبارت‌های تولیدگر»

PEP 342: «Coroutines via Enhanced Generators» قابلیت‌های جدید تولیدگر در پایتون 2.5 را توصیف می‌کند.