functools --- توابع مرتبه بالاتر و عملیات روی اشیای فراخوانی‌پذیر

کد منبع: Lib/functools.py


ماژول functools برای توابع مرتبه بالاتر است: توابعی که روی توابع دیگر عمل می‌کنند یا آن‌ها را برمی‌گردانند. به‌طور کلی، هر شیء فراخوانی‌پذیر می‌تواند برای مقاصد این ماژول به‌عنوان یک تابع در نظر گرفته شود.

ماژول functools توابع زیر را تعریف می‌کند:

@functools.cache(user_function)

نهانگاه ساده، سبک و نامحدود توابع. گاهی به آن "memoize" گفته می‌شود.

مشابه lru_cache(maxsize=None) را برمی‌گرداند و پوششی نازک حول جست‌وجوی آرگومان‌های تابع در دیکشنری ایجاد می‌کند. از آنجا که هرگز نیازی به حذف مقادیر قدیمی ندارد، این از @lru_cache با محدودیت اندازه کوچک‌تر و سریع‌تر است.

برای مثال:

@cache
def factorial(n):
    return n * factorial(n-1) if n else 1

>>> factorial(10)   # no previously cached result, makes 11 recursive calls
3628800
>>> factorial(5)    # no new calls, just returns the cached result
120
>>> factorial(12)   # two new recursive calls, factorial(10) is cached
479001600

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

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

اضافه شده در نسخه‌ی 3.9.

@functools.cached_property(func)

یک متد از کلاس را به پراپرتی تبدیل می‌کند که مقدار آن یک بار محاسبه می‌شود و سپس برای طول عمر نمونه به‌عنوان یک ویژگی معمولی در نهانگاه ذخیره می‌شود. مشابه @property، با افزودن نهانگاه‌سازی. برای پراپرتی‌های محاسبه‌شده‌ی پرهزینه در نمونه‌هایی که در غیر این صورت عملاً تغییرناپذیر هستند، مفید است.

مثال:

class DataSet:

    def __init__(self, sequence_of_numbers):
        self._data = tuple(sequence_of_numbers)

    @cached_property
    def stdev(self):
        return statistics.stdev(self._data)

سازوکار @cached_property تا حدی با @property متفاوت است. یک پراپرتی معمولی، نوشتن روی ویژگی را مسدود می‌کند، مگر اینکه یک setter تعریف شده باشد. در مقابل، یک cached_property اجازه‌ی نوشتن می‌دهد.

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

مقدار نهانگاه‌شده را می‌توان با حذف ویژگی پاک کرد. این کار باعث می‌شود متد cached_property دوباره اجرا شود.

cached_property از ایجاد یک وضعیت رقابتی احتمالی در استفاده چندنخی جلوگیری نمی‌کند. تابع دریافت‌کننده ممکن است برای همان نمونه بیش از یک بار اجرا شود و آخرین اجرا مقدار نهان‌شده را تنظیم کند. اگر ویژگی نهان‌شده هم‌توان باشد یا اجرای بیش از یک‌باره‌ی آن برای یک نمونه به هر دلیل زیان‌آور نباشد، این موضوع مشکلی ایجاد نمی‌کند. اگر همگام‌سازی لازم است، قفل‌گذاری لازم را درون تابع دریافت‌کننده تزئین‌شده یا پیرامون دسترسی به ویژگی نهان‌شده پیاده‌سازی کنید.

توجه داشته باشید که این دکوراتور در عملکرد دیکشنری‌های کلید-مشترک PEP 412 اختلال ایجاد می‌کند. این بدان معناست که دیکشنری‌های نمونه می‌توانند فضای بیشتری نسبت به حالت عادی اشغال کنند.

همچنین، این دکوراتور نیاز دارد که ویژگی __dict__ هر نمونه یک نگاشت تغییرپذیر باشد. این بدان معناست که با برخی از انواع کار نمی‌کند، مانند فراکلاس‌ها (زیرا ویژگی‌های __dict__ در نمونه‌های type، پراکسی‌های فقط‌خواندنی برای فضای نام کلاس هستند) و انواعی که __slots__ را بدون گنجاندن __dict__ به‌عنوان یکی از جایگاه‌های تعریف‌شده تعیین می‌کنند (زیرا چنین کلاس‌هایی اصلاً ویژگی __dict__ را فراهم نمی‌کنند).

اگر نگاشت تغییرپذیر در دسترس نباشد یا اشتراک‌گذاری کلیدها به‌صورت بهینه از نظر فضا مطلوب باشد، می‌توان اثری مشابه @cached_property را نیز با قرار دادن @property بر روی @lru_cache به‌دست آورد. برای جزئیات بیشتر درباره‌ی تفاوت این روش با @cached_property، چگونه فراخوانی‌های متد را در نهانگاه ذخیره کنم؟ را ببینید.

اضافه شده در نسخه‌ی 3.8.

تغییر یافته در نسخه‌ی 3.12: پیش از پایتون 3.12، @cached_property شامل یک قفل مستندنشده بود تا تضمین کند که در استفاده‌ی چندنخی، تابع getter فقط یک‌بار به‌ازای هر نمونه اجرا می‌شود. با این حال، این قفل به‌ازای هر پراپرتی بود، نه به‌ازای هر نمونه، که می‌توانست منجر به رقابت قفل بسیار بالا و غیرقابل‌قبول شود. در پایتون 3.12 و بالاتر این قفل‌سازی حذف شده است.

functools.cmp_to_key(func)

یک تابع مقایسه به سبک قدیمی را به یک key function تبدیل می‌کند. با ابزارهایی که توابع کلید را می‌پذیرند استفاده می‌شود (مانند sorted()، min()، max()، heapq.nlargest()، heapq.nsmallest()، itertools.groupby()). این تابع عمدتاً به‌عنوان ابزاری برای گذار برنامه‌هایی استفاده می‌شود که در حال تبدیل از Python 2 (که از استفاده از توابع مقایسه پشتیبانی می‌کرد) هستند.

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

مثال:

sorted(iterable, key=cmp_to_key(locale.strcoll))  # ترتیب مرتب‌سازی آگاه از locale

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

اضافه شده در نسخه‌ی 3.2.

@functools.lru_cache(user_function)
@functools.lru_cache(maxsize=128, typed=False)

دکوراتوری برای دربرگرفتن یک تابع با یک فراخوانی‌پذیر memoizing که حداکثر maxsize فراخوانی اخیر را ذخیره می‌کند. این دکوراتور می‌تواند هنگامی که یک تابع پرهزینه یا وابسته به I/O به‌صورت دوره‌ای با آرگومان‌های یکسان فراخوانی می‌شود، باعث صرفه‌جویی در زمان شود.

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

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

از آن‌جا که از یک دیکشنری برای نهانگاه‌سازی نتایج استفاده می‌شود، آرگومان‌های جایگاهی و کلیدواژه‌ای تابع باید hashable باشند.

الگوهای متمایز آرگومان‌ها ممکن است به‌عنوان فراخوانی‌های متمایز با ورودی‌های جداگانه در نهانگاه در نظر گرفته شوند. برای مثال، f(a=1, b=2) و f(b=2, a=1) در ترتیب آرگومان‌های کلیدواژه‌ای خود تفاوت دارند و ممکن است دو ورودی جداگانه در نهانگاه داشته باشند.

اگر user_function مشخص شده باشد، باید فراخوانی‌پذیر باشد. این امکان را فراهم می‌کند که دکوراتور lru_cache مستقیماً روی یک تابع کاربر اعمال شود، در حالی که maxsize روی مقدار پیش‌فرض خود یعنی ۱۲۸ باقی می‌ماند:

@lru_cache
def count_vowels(sentence):
    return sum(sentence.count(vowel) for vowel in 'AEIOUaeiou')

اگر maxsize برابر None قرار داده شود، قابلیت LRU غیرفعال می‌شود و نهانگاه می‌تواند بدون محدودیت رشد کند.

اگر typed روی درست تنظیم شود، آرگومان‌های تابع از انواع مختلف به‌طور جداگانه در نهانگاه ذخیره می‌شوند. اگر typed نادرست باشد، پیاده‌سازی معمولاً آن‌ها را به‌عنوان فراخوانی‌های هم‌ارز در نظر می‌گیرد و فقط یک نتیجه را در نهانگاه ذخیره می‌کند. (برخی انواع مانند str و int ممکن است حتی وقتی typed نادرست است، به‌طور جداگانه در نهانگاه ذخیره شوند.)

توجه داشته باشید که ویژگی خاص‌بودن نوع (type specificity) فقط به آرگومان‌های مستقیم تابع اعمال می‌شود، نه به محتوای آن‌ها. آرگومان‌های اسکالر (scalar)، Decimal(42) و Fraction(42)، به‌عنوان فراخوانی‌های مجزا با نتایج مجزا در نظر گرفته می‌شوند. در مقابل، آرگومان‌های تاپل، ('answer', Decimal(42)) و ('answer', Fraction(42))، معادل یکدیگر در نظر گرفته می‌شوند.

تابع دربرگرفته‌شده به یک تابع cache_parameters() مجهز شده است که یک dict جدید برمی‌گرداند و مقادیر maxsize و typed را نشان می‌دهد. این صرفاً برای اطلاع‌رسانی است. تغییر مقادیر هیچ تأثیری ندارد.

برای کمک به اندازه‌گیری اثربخشی نهانگاه و تنظیم پارامتر maxsize، تابع دربرگرفته‌شده به یک تابع cache_info() مجهز شده است که یک named tuple برمی‌گرداند و hits، misses، maxsize و currsize را نشان می‌دهد.

این دکوراتور همچنین یک تابع cache_clear() برای پاک‌سازی یا بی‌اعتبار کردن نهانگاه فراهم می‌کند.

تابع زیربنایی اصلی از طریق ویژگی __wrapped__ در دسترس است. این برای درون‌نگری، دور زدن نهانگاه، یا پیچیدن دوباره تابع با نهانگاهی متفاوت مفید است.

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

اگر یک متد نهانگاه‌شده باشد، آرگومان نمونه self در نهانگاه لحاظ می‌شود. چگونه فراخوانی‌های متد را در نهانگاه ذخیره کنم؟ را ببینید

نهانگاه LRU (کمترین استفاده‌ی اخیر) زمانی بهترین عملکرد را دارد که آخرین فراخوانی‌ها بهترین پیش‌بینی‌کننده‌های فراخوانی‌های آینده باشند (برای مثال، محبوب‌ترین مقاله‌ها در یک سرور خبری معمولاً هر روز تغییر می‌کنند). محدودیت اندازه‌ی نهانگاه تضمین می‌کند که نهانگاه در فرآیندهای طولانی‌مدت مانند سرورهای وب بدون محدودیت رشد نمی‌کند.

به‌طور کلی، نهانگاه LRU فقط زمانی باید استفاده شود که بخواهید مقادیر از پیش محاسبه‌شده را دوباره به کار ببرید. بر همین اساس، منطقی نیست که توابع دارای اثرات جانبی، توابعی که نیاز دارند در هر فراخوانی اشیاء تغییرپذیر متمایزی ایجاد کنند (مانند تولیدگرها و توابع ناهمگام)، یا توابع غیرخالص مانند time() یا random() را نهان‌سازی کنید.

نمونه‌ای از یک نهانگاه LRU برای محتوای وب ثابت:

@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = f'https://peps.python.org/pep-{num:04d}'
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'

>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
...     pep = get_pep(n)
...     print(n, len(pep))

>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

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

@lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

>>> [fib(n) for n in range(16)]
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

>>> fib.cache_info()
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

اضافه شده در نسخه‌ی 3.2.

تغییر یافته در نسخه‌ی 3.3: گزینه‌ی typed اضافه شد.

تغییر یافته در نسخه‌ی 3.8: گزینه‌ی user_function افزوده شد.

تغییر یافته در نسخه‌ی 3.9: تابع cache_parameters() افزوده شد

@functools.total_ordering

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

کلاس باید یکی از __lt__()، __le__()، __gt__() یا __ge__() را تعریف کند. علاوه بر این، بهتر است کلاس یک متد __eq__() نیز فراهم کند.

برای مثال:

@total_ordering
class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

توجه

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

توجه

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

اضافه شده در نسخه‌ی 3.2.

تغییر یافته در نسخه‌ی 3.4: بازگرداندن NotImplemented از تابع مقایسه‌ی زیربنایی برای انواع ناشناخته اکنون پشتیبانی می‌شود.

functools.Placeholder

شیء یگانه‌ای که به‌عنوان نشانگر (sentinel) برای رزرو جایگاهی برای آرگومان‌های جایگاهی هنگام فراخوانی partial() و partialmethod() استفاده می‌شود.

اضافه شده در نسخه‌ی 3.14.

functools.partial(func, /, *args, **keywords)

یک شیء partial جدید برمی‌گرداند که هنگام فراخوانی، مانند func فراخوانی‌شده با آرگومان‌های جایگاهی args و آرگومان‌های کلیدواژه‌ای keywords رفتار می‌کند. اگر آرگومان‌های بیشتری در فراخوانی ارائه شوند، به args افزوده می‌شوند. اگر آرگومان‌های کلیدواژه‌ای اضافی ارائه شوند، keywords را گسترش می‌دهند و بازنویسی می‌کنند. تقریباً معادل است با:

def partial(func, /, *args, **keywords):
    def newfunc(*more_args, **more_keywords):
        return func(*args, *more_args, **(keywords | more_keywords))
    newfunc.func = func
    newfunc.args = args
    newfunc.keywords = keywords
    return newfunc

تابع partial() برای اعمال جزئی تابع استفاده می‌شود که در آن بخشی از آرگومان‌ها و/یا کلیدواژه‌های یک تابع «فریز» می‌شود و یک شیء جدید با امضای ساده‌تر به دست می‌آید. برای مثال، می‌توان از partial() برای ایجاد یک شیء فراخوانی‌پذیر استفاده کرد که مانند تابع int() رفتار می‌کند، به‌طوری که مقدار پیش‌فرض آرگومان base آن 2 است:

>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

اگر نشانگرهای Placeholder (sentinels) در args وجود داشته باشند، هنگام فراخوانی partial() ابتدا آن‌ها پر می‌شوند. این کار از پیش پر کردن هر آرگومان جایگاهی را با فراخوانی partial() ممکن می‌سازد؛ بدون Placeholder، تنها می‌توان تعداد انتخاب‌شده‌ای از آرگومان‌های جایگاهی ابتدایی را از پیش پر کرد.

اگر هر نشانگر (sentinel) از Placeholder وجود داشته باشد، همه باید در زمان فراخوانی پر شوند:

>>> say_to_world = partial(print, Placeholder, Placeholder, "world!")
>>> say_to_world('Hello', 'dear')
Hello dear world!

فراخوانی say_to_world('Hello') باعث پرتاب یک TypeError می‌شود، زیرا تنها یک آرگومان جایگاهی ارائه شده است، اما دو جای‌نگهدار وجود دارد که باید پر شوند.

اگر partial() بر یک شیء partial موجود اعمال شود، نشانگرهای Placeholder شیء ورودی با آرگومان‌های جایگاهی جدید پر می‌شوند. می‌توان یک جای‌نگهدار را با درج یک نشانگر Placeholder جدید در جایگاهی که یک Placeholder پیشین آن را نگه‌داشته است، حفظ کرد:

>>> from functools import partial, Placeholder as _
>>> remove = partial(str.replace, _, _, '')
>>> message = 'Hello, dear dear world!'
>>> remove(message, ' dear')
'Hello, world!'
>>> remove_dear = partial(remove, _, ' dear')
>>> remove_dear(message)
'Hello, world!'
>>> remove_first_dear = partial(remove_dear, _, 1)
>>> remove_first_dear(message)
'Hello, dear world!'

Placeholder نمی‌تواند به‌عنوان آرگومان کلیدواژه‌ای به partial() ارسال شود.

تغییر یافته در نسخه‌ی 3.14: پشتیبانی از Placeholder در آرگومان‌های جایگاهی اضافه شد.

class functools.partialmethod(func, /, *args, **keywords)

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

func باید یک توصیف‌گر یا فراخوانی‌پذیر باشد (اشیایی که هر دو هستند، مانند توابع معمولی، به‌عنوان توصیف‌گر در نظر گرفته می‌شوند).

هنگامی که func یک توصیف‌گر باشد (مانند یک تابع معمولی پایتون، classmethod()، staticmethod()، abstractmethod() یا نمونه‌ای دیگر از partialmethod)، فراخوانی‌های متد __get__ به توصیف‌گر زیرین محول می‌شوند و یک شیء partial مناسب به‌عنوان نتیجه برگردانده می‌شود.

هنگامی که func یک شیء فراخوانی‌پذیر غیرتوصیف‌گر باشد، یک متد مقید مناسب به‌صورت پویا ایجاد می‌شود. این متد هنگامی که به‌عنوان یک متد استفاده شود، مانند یک تابع معمولی پایتون رفتار می‌کند: آرگومان self به‌عنوان اولین آرگومان جایگاهی درج می‌شود، حتی پیش از args و keywords ارائه‌شده به سازنده‌ی partialmethod.

مثال:

>>> class Cell:
...     def __init__(self):
...         self._alive = False
...     @property
...     def alive(self):
...         return self._alive
...     def set_state(self, state):
...         self._alive = bool(state)
...     set_alive = partialmethod(set_state, True)
...     set_dead = partialmethod(set_state, False)
...
>>> c = Cell()
>>> c.alive
False
>>> c.set_alive()
>>> c.alive
True

اضافه شده در نسخه‌ی 3.4.

functools.reduce(function, iterable, /[, initial])

function با ۲ آرگومان را به‌صورت تجمیعی روی آیتم‌های iterable، از چپ به راست، اعمال می‌کند تا پیمایش‌پذیر را به یک مقدار واحد کاهش دهد. برای مثال، reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) ((((1+2)+3)+4)+5) را محاسبه می‌کند. آرگومان سمت چپ، x، مقدار انباشته‌شده است و آرگومان سمت راست، y، مقدار به‌روزرسانی از iterable است. اگر initial اختیاری وجود داشته باشد، در محاسبه پیش از آیتم‌های پیمایش‌پذیر قرار می‌گیرد و به‌عنوان مقدار پیش‌فرض زمانی که پیمایش‌پذیر خالی است عمل می‌کند. اگر initial داده نشود و iterable فقط یک آیتم داشته باشد، اولین آیتم برگردانده می‌شود.

تقریباً معادل با:

initial_missing = object()

def reduce(function, iterable, /, initial=initial_missing):
    it = iter(iterable)
    if initial is initial_missing:
        value = next(it)
    else:
        value = initial
    for element in it:
        value = function(value, element)
    return value

برای پیمایش‌گری که تمام مقادیر میانی را تولید می‌کند، itertools.accumulate() را ببینید.

تغییر یافته در نسخه‌ی 3.14: اکنون initial به‌عنوان یک آرگومان کلیدواژه‌ای پشتیبانی می‌شود.

@functools.singledispatch

یک تابع را به یک تابع عام ارسال تک‌گانه تبدیل می‌کند.

برای تعریف یک تابع عام، آن را با دکوراتور @singledispatch بیارایید. هنگام تعریف یک تابع با استفاده از @singledispatch، توجه داشته باشید که اعزام (dispatch) بر اساس نوع اولین آرگومان انجام می‌شود:

>>> from functools import singledispatch
>>> @singledispatch
... def fun(arg, verbose=False):
...     if verbose:
...         print("Let me just say,", end=" ")
...     print(arg)

برای افزودن پیاده‌سازی‌های سربارگذاری‌شده به تابع، از ویژگی register() تابع عام استفاده کنید، که می‌توان از آن به‌عنوان دکوراتور استفاده کرد. برای توابعی که با نوع‌ها حاشیه‌نویسی شده‌اند، دکوراتور نوع اولین آرگومان را به‌طور خودکار استنتاج می‌کند:

>>> @fun.register
... def _(arg: int, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> @fun.register
... def _(arg: list, verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

همچنین می‌توان از typing.Union استفاده کرد:

>>> @fun.register
... def _(arg: int | float, verbose=False):
...     if verbose:
...         print("Strength in numbers, eh?", end=" ")
...     print(arg)
...
>>> from typing import Union
>>> @fun.register
... def _(arg: Union[list, set], verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)
...

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

>>> @fun.register(complex)
... def _(arg, verbose=False):
...     if verbose:
...         print("Better than complicated.", end=" ")
...     print(arg.real, arg.imag)
...

برای کدی که بر اساس نوع یک مجموعه اعزام می‌کند (مثلاً list)، اما می‌خواهد راهنمای نوع آیتم‌های مجموعه را مشخص کند (مثلاً list[int])، نوع اعزام باید به‌صراحت به خود دکوراتور ارسال شود و راهنمای نوع در تعریف تابع قرار بگیرد:

>>> @fun.register(list)
... def _(arg: list[int], verbose=False):
...     if verbose:
...         print("Enumerate this:")
...     for i, elem in enumerate(arg):
...         print(i, elem)

توجه

در ران‌تایم، تابع بر اساس یک نمونه از فهرست اعزام می‌شود، صرف‌نظر از نوع موجود در فهرست؛ یعنی [1,2,3] همانند ["foo", "bar", "baz"] اعزام می‌شود. حاشیه‌نویسی ارائه‌شده در این مثال فقط برای بررسی‌کننده‌های نوع ایستا است و هیچ تأثیری در ران‌تایم ندارد.

برای فراهم کردن امکان ثبت لامبداها و توابع از پیش موجود، همچنین می‌توان از ویژگی register() به‌شکل تابعی استفاده کرد:

>>> def nothing(arg, verbose=False):
...     print("Nothing.")
...
>>> fun.register(type(None), nothing)

ویژگی register() تابع بدون دکوراتور را برمی‌گرداند. این امر امکان چیدن دکوراتورها روی هم، pickling و ایجاد آزمون‌های واحد برای هر گونه به‌طور مستقل را فراهم می‌کند:

>>> @fun.register(float)
... @fun.register(Decimal)
... def fun_num(arg, verbose=False):
...     if verbose:
...         print("Half of your number:", end=" ")
...     print(arg / 2)
...
>>> fun_num is fun
False

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

>>> fun("Hello, world.")
Hello, world.
>>> fun("test.", verbose=True)
Let me just say, test.
>>> fun(42, verbose=True)
Strength in numbers, eh? 42
>>> fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)
Enumerate this:
0 spam
1 spam
2 eggs
3 spam
>>> fun(None)
Nothing.
>>> fun(1.23)
0.615

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

اگر یک پیاده‌سازی برای یک abstract base class ثبت شده باشد، زیرکلاس‌های مجازیِ کلاس پایه به آن پیاده‌سازی ارسال می‌شوند:

>>> from collections.abc import Mapping
>>> @fun.register
... def _(arg: Mapping, verbose=False):
...     if verbose:
...         print("Keys & Values")
...     for key, value in arg.items():
...         print(key, "=>", value)
...
>>> fun({"a": "b"})
a => b

برای بررسی این‌که تابع عام کدام پیاده‌سازی را برای یک نوع مشخص انتخاب می‌کند، از ویژگی dispatch() استفاده کنید:

>>> fun.dispatch(float)
<function fun_num at 0x1035a2840>
>>> fun.dispatch(dict)    # note: default implementation
<function fun at 0x103fe0000>

برای دسترسی به همه پیاده‌سازی‌های ثبت‌شده، از ویژگی registry فقط‌خواندنی استفاده کنید:

>>> fun.registry.keys()
dict_keys([<class 'NoneType'>, <class 'int'>, <class 'object'>,
          <class 'decimal.Decimal'>, <class 'list'>,
          <class 'float'>])
>>> fun.registry[float]
<function fun_num at 0x1035a2840>
>>> fun.registry[object]
<function fun at 0x103fe0000>

اضافه شده در نسخه‌ی 3.4.

تغییر یافته در نسخه‌ی 3.7: ویژگی register() اکنون از حاشیه‌نویسی‌های نوع (type annotations) پشتیبانی می‌کند.

تغییر یافته در نسخه‌ی 3.11: ویژگی register() اکنون از typing.Union به‌عنوان یک حاشیه‌نویسی نوع پشتیبانی می‌کند.

class functools.singledispatchmethod(func)

یک متد را به یک تابع عام ارسال تک‌گانه (single-dispatch) تبدیل می‌کند.

برای تعریف یک متد عام، از دکوراتور @singledispatchmethod استفاده کنید. هنگام تعریف یک متد با استفاده از @singledispatchmethod، توجه داشته باشید که اعزام بر اساس نوع اولین آرگومان به‌جز self یا cls صورت می‌گیرد:

class Negator:
    @singledispatchmethod
    def neg(self, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    def _(self, arg: int):
        return -arg

    @neg.register
    def _(self, arg: bool):
        return not arg

@singledispatchmethod از تودرتو شدن با سایر دکوراتورها مانند @classmethod پشتیبانی می‌کند. توجه داشته باشید که برای امکان استفاده از dispatcher.register، singledispatchmethod باید بیرونی‌ترین دکوراتور باشد. در اینجا کلاس Negator با متدهای neg آمده است که به‌جای نمونه‌ای از کلاس، به خود کلاس متصل شده‌اند:

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

می‌توان از همین الگو برای دکوراتورهای مشابه دیگر نیز استفاده کرد: @staticmethod، @~abc.abstractmethod و دیگر موارد.

اضافه شده در نسخه‌ی 3.8.

functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

یک تابع پوششی را به‌روزرسانی کنید تا شبیه تابع پوشیده‌شده به نظر برسد. آرگومان‌های اختیاری، تاپل‌هایی هستند که مشخص می‌کنند کدام ویژگی‌های تابع اصلی مستقیماً به ویژگی‌های متناظر در تابع پوششی انتساب داده می‌شوند و کدام ویژگی‌های تابع پوششی با ویژگی‌های متناظر از تابع اصلی به‌روزرسانی می‌شوند. مقادیر پیش‌فرض این آرگومان‌ها، ثابت‌های سطح ماژول WRAPPER_ASSIGNMENTS (که به ویژگی‌های تابع پوششی شامل __module__، __name__، __qualname__، __annotations__، __type_params__ و __doc__، یعنی رشته مستندسازی، مقدار انتساب می‌دهد) و WRAPPER_UPDATES (که __dict__ تابع پوششی، یعنی دیکشنری نمونه، را به‌روزرسانی می‌کند) هستند.

برای امکان دسترسی به تابع اصلی جهت درون‌نگری و سایر اهداف (مثلاً دور زدن یک دکوراتور نهان‌سازی مانند @lru_cache)، این تابع به‌طور خودکار یک ویژگی __wrapped__ را که به تابع پوشیده‌شده اشاره می‌کند، به تابع پوششی اضافه می‌کند.

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

update_wrapper() می‌تواند با فراخوانی‌پذیر‌هایی غیر از توابع نیز استفاده شود. هر یک از ویژگی‌هایی که در assigned یا updated نام برده شده‌اند و در شیءای که پوشش داده می‌شود وجود نداشته باشند، نادیده گرفته می‌شوند (یعنی این تابع تلاش نخواهد کرد آن‌ها را روی تابع پوششی تنظیم کند). اگر خود تابع پوششی فاقد هر یک از ویژگی‌هایی باشد که در updated نام برده شده‌اند، همچنان AttributeError پرتاب می‌شود.

تغییر یافته در نسخه‌ی 3.2: ویژگی __wrapped__ اکنون به‌صورت خودکار اضافه می‌شود. ویژگی __annotations__ اکنون به‌طور پیش‌فرض کپی می‌شود. ویژگی‌های ناموجود دیگر باعث AttributeError نمی‌شوند.

تغییر یافته در نسخه‌ی 3.4: ویژگی __wrapped__ اکنون همیشه به تابع پوشش‌داده‌شده ارجاع می‌دهد، حتی اگر آن تابع ویژگی __wrapped__ را تعریف کرده باشد. (ببینید bpo-17482)

تغییر یافته در نسخه‌ی 3.12: ویژگی __type_params__ اکنون به‌طور پیش‌فرض کپی می‌شود.

@functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)

این یک تابع کمکی برای فراخوانی update_wrapper() به‌عنوان دکوراتور تابع هنگام تعریف یک تابع پوششی است. این معادل partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated) است. برای مثال:

>>> from functools import wraps
>>> def my_decorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwds):
...         print('Calling decorated function')
...         return f(*args, **kwds)
...     return wrapper
...
>>> @my_decorator
... def example():
...     """Docstring"""
...     print('Called example function')
...
>>> example()
Calling decorated function
Called example function
>>> example.__name__
'example'
>>> example.__doc__
'Docstring'

بدون استفاده از این کارخانه‌ی دکوراتور، نام تابع نمونه 'wrapper' می‌شد و رشته‌ی مستندسازی تابع اصلی example() از بین می‌رفت.

اشیای partial

اشیای partial، اشیای فراخوانی‌پذیری هستند که توسط partial() ایجاد شده‌اند. آن‌ها ۳ ویژگی فقط‌خواندنی دارند:

partial.func

یک شیء یا تابع فراخوانی‌پذیر. فراخوانی‌های شیء partial به func ارجاع داده می‌شوند، همراه با آرگومان‌ها و کلیدواژه‌های جدید.

partial.args

چپ‌ترین آرگومان‌های جایگاهی که به ابتدای آرگومان‌های جایگاهی ارائه‌شده به فراخوانی شیء partial اضافه می‌شوند.

partial.keywords

آرگومان‌های کلیدواژه‌ای که هنگام فراخوانی شیء partial تأمین خواهند شد.

اشیاء partial از این جهت شبیه اشیاء تابعی هستند که فراخوانی‌پذیر و قابل ارجاع ضعیف (weak referenceable) هستند و می‌توانند ویژگی داشته باشند. برخی تفاوت‌های مهم وجود دارد. برای مثال، ویژگی‌های __name__ و __doc__ به‌صورت خودکار ایجاد نمی‌شوند.