| jupytext | kernelspec | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
(functions)=
<div id="qe-notebook-header" align="right" style="text-align:right;">
<a href="https://quantecon.org/" title="quantecon.org">
<img style="width:250px;display:inline;" width="250px" src="https://assets.quantecon.org/img/qe-menubar-logo.svg" alt="QuantEcon">
</a>
</div>
توابع (Functions) یکی از ساختارهای بسیار کاربردی هستند که تقریباً در تمام زبانهای برنامهنویسی وجود دارند.
ما تاکنون با چندین تابع آشنا شدهایم، مانند
- تابع
()sqrtاز کتابخانه NumPy و - تابع داخلی
()print
در این درس ما:
- توابع را به صورت سیستماتیک بررسی میکنیم و نحوه نوشتن و موارد استفاده از آنها را پوشش میدهیم، و
- یاد میگیریم که چگونه توابع سفارشی خودمان را بسازیم.
ما از import های زیر استفاده خواهیم کرد.
import numpy as np
import matplotlib.pyplot as plt
تابع یک بخش نامگذاری شده از یک برنامه است که یک وظیفه خاص را اجرا میکند.
توابع زیادی از قبل وجود دارند و ما میتوانیم از آنها به همان شکل استفاده کنیم.
ابتدا این توابع را بررسی میکنیم و سپس بحث میکنیم که چگونه میتوانیم توابع خودمان را بسازیم.
پایتون تعدادی تابع داخلی دارد که بدون نیاز به import در دسترس هستند.
ما قبلاً با برخی از آنها آشنا شدهایم
max(19, 20)
print('foobar')
str(22)
type(22)
لیست کامل توابع داخلی پایتون در اینجا موجود است.
اگر توابع داخلی نیاز ما را پوشش ندهند، یا باید توابع را import کنیم یا توابع خودمان را بسازیم.
نمونههایی از import کردن و استفاده از توابع در {doc}درس قبلی <python_by_example> آورده شده است.
در اینجا نمونه دیگری داریم که بررسی میکند آیا یک سال خاص، سال کبیسه است یا خیر:
import calendar
calendar.isleap(2024)
در بسیاری از موارد، باید بتوانیم توابع مدنظر خودمان را تعریف کنیم.
بیایید با بحث در مورد نحوه انجام آن شروع کنیم.
در اینجا یک تابع بسیار ساده پایتون داریم که تابع ریاضی
def f(x):
return 2 * x + 1
حالا که این تابع را تعریف کردیم، بیایید آن را فراخوانی کنیم و بررسی کنیم که آیا کاری که انتظار داریم را انجام میدهد:
f(1)
f(10)
در اینجا یک تابع طولانیتر داریم که قدر مطلق یک عدد داده شده را محاسبه میکند.
(چنین تابعی از قبل به عنوان یک تابع داخلی وجود دارد، اما بیایید برای تمرین، تابع خودمان را بنویسیم.)
def new_abs_function(x):
if x < 0:
abs_value = -x
else:
abs_value = x
return abs_value
بیایید نحو را در اینجا بررسی کنیم.
defیک کلمه کلیدی پایتون است که برای شروع تعریف توابع استفاده میشود.:def new_abs_function(x)نشان میدهد که نام تابعnew_abs_functionاست و یک آرگومان واحدxدارد.- کد تورفتگیدار یک بلوک کد است که بدنه تابع نامیده میشود.
- کلمه کلیدی
returnنشان میدهد کهabs_valueشیئی است که باید به کد فراخوانیکننده برگردانده شود.
تمام این تعریف تابع توسط مفسر پایتون خوانده میشود و در حافظه ذخیره میشود.
بیایید آن را فراخوانی کنیم تا بررسی کنیم که کار میکند:
print(new_abs_function(3))
print(new_abs_function(-3))
توجه کنید که یک تابع میتواند تعداد دلخواهی دستور return داشته باشد (از جمله صفر).
اجرای تابع زمانی که به اولین return برسد، خاتمه مییابد و این امکان را میدهد که کدهایی مانند مثال زیر بنویسیم:
def f(x):
if x < 0:
return 'negative'
return 'nonnegative'
(نوشتن توابع با چندین دستور return معمولاً توصیه نمیشود، زیرا میتواند دنبال کردن منطق را سخت کند.)
توابعی که دستور return ندارند، به طور خودکار شیء خاص پایتون به نام None را برمیگردانند.
(pos_args)=
در {ref}درس قبلی <python_by_example>، با عبارت زیر مواجه شدید
:class: no-execute
plt.plot(x, 'b-', label="white noise")
در فراخوانی تابع plot از کتابخانه Matplotlib، باید توجه داشته باشید که آخرین آرگومان با نحو name=argument ارسال میشود.
این را یک آرگومان کلیدواژهای(keyword argument) مینامند، که label همان کلیدواژه است.
آرگومانهای غیر کلیدواژهای را آرگومانهای ترتیبی(positional argument) مینامند، زیرا معنای آنها با ترتیب مشخص میشود.
plot(x, 'b-')باplot('b-', x)متفاوت است
آرگومانهای کلیدواژهای زمانی کاربردی هستند که یک تابع آرگومانهای زیادی دارد، در این صورت به خاطر سپردن ترتیب صحیح سخت است.
شما میتوانید آرگومانهای کلیدواژهای را در توابع تعریف شده توسط کاربر بدون مشکل به کار ببرید.
به مثال بعدی توجه کنید:
def f(x, a=1, b=1):
return a + b * x
مقادیر آرگومان کلیدواژهای که در تعریف تابع f مشخص شدهاند، به عنوان مقادیر پیشفرض درنظر گرفته میشوند
f(2)
آنها را میتوان به شکل زیر تغییر داد:
f(2, a=4, b=5)
همانطور که در {ref}درس قبلی <python_by_example> بحث کردیم، توابع پایتون بسیار انعطافپذیر هستند.
به طور خاص
- هر تعداد تابع میتواند در یک فایل مشخص تعریف شود.
- توابع میتوانند (و اغلب هم می شوند) در داخل توابع دیگر تعریف شوند.
- هر شیء میتواند به عنوان آرگومان به یک تابع داده شود، از جمله توابع دیگر.
- یک تابع میتواند هر نوع شیء را برگرداند، از جمله توابع.
در بخشهای بعدی، با مثالهایی نشان خواهیم داد که انتقال یک تابع به یک تابع دیگر تا چه حد ساده است.
کلمه کلیدی lambda برای ایجاد توابع ساده در یک خط استفاده میشود.
برای نمونه، مثال های زیر کاملا معادل یکدیگر هستند:
def f(x):
return x**3
f = lambda x: x**3
برای اینکه ببینیم چرا lambda کاربردی است، فرض کنید میخواهیم
کتابخانه SciPy تابعی به نام quad دارد که این محاسبه را برای ما انجام میدهد.
نحو تابع quad به صورت quad(f, a, b) است که f یک تابع و a و b اعداد هستند.
برای ایجاد تابع lambda به شکل زیر استفاده کنیم:
from scipy.integrate import quad
quad(lambda x: x**3, 0, 2)
در اینجا تابع ایجاد شده توسط lambda ناشناس(anonymous) نامیده میشود زیرا هرگز نامی به آن داده نشده است.
توابع تعریف شده توسط کاربر برای بهبود شفافیت و خوانایی کد شما اهمیت دارند، زیرا:
- جنبههای مختلف منطق برنامه را از هم جدا میکند
- امکان استفاده مجدد کد را فراهم میکنند
(نوشتن یک چیز یکسان برای دو بار تقریباً همیشه ایده بدی است)
ما بعداً بیشتر در این مورد صحبت خواهیم کرد.
دوباره به این کد از {doc}درس قبلی <python_by_example> نگاه کنید
ts_length = 100
ϵ_values = [] # empty list
for i in range(ts_length):
e = np.random.randn()
ϵ_values.append(e)
plt.plot(ϵ_values)
plt.show()
ما این برنامه را به دو بخش تقسیم خواهیم کرد:
- یک تابع تعریف شده توسط کاربر که لیستی از متغیرهای تصادفی تولید میکند.
- بخش اصلی برنامه که
- این تابع را برای دریافت داده فراخوانی میکند
- دادهها را رسم میکند
این کار در برنامه بعدی انجام میشود
(funcloopprog)=
def generate_data(n):
ϵ_values = []
for i in range(n):
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100)
plt.plot(data)
plt.show()
وقتی مفسر به عبارت generate_data(100) میرسد، بدنه تابع را با n برابر با 100 اجرا میکند.
نتیجه نهایی این است که نام data به لیست ϵ_values برگردانده شده توسط تابع متصل میشود.
تابع ()generate_data ما نسبتاً محدود است.
بیایید آن را با دادن قابلیت برگرداندن یا متغیرهای تصادفی نرمال استاندارد یا متغیرهای تصادفی یکنواخت در
این کار در قطعه کد بعدی انجام میشود.
(funcloopprog2)=
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
if generator_type == 'U':
e = np.random.uniform(0, 1)
else:
e = np.random.randn()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, 'U')
plt.plot(data)
plt.show()
امیدواریم نحو عبارت if/else خودش گویا باشد؛ در اینجا نیز همانند قبل، تورفتگی محدوده بلوکهای کد را مشخص میکند.
نکات:
- ما آرگومان
Uرا به عنوان یک رشته بکار میبریم، به همین دلیل آن را به صورت'U'مینویسیم. - توجه کنید که برابری با نحو
==آزمایش میشود، نه=.- به عنوان مثال، دستور
a = 10نامaرا به مقدار10اختصاص میدهد. - عبارت
a == 10با توجه به مقدارaمیتواندTrueیاFalseارزیابی شود .
- به عنوان مثال، دستور
حالا، چندین راه وجود دارد که میتوانیم کد بالا را ساده کنیم.
به عنوان مثال، میتوانیم شرطها را کاملاً حذف کنیم و فقط نوع خروجی مورد نظر را به عنوان یک تابع به برنامه بدهیم.
برای درک این موضوع، نسخه زیر را در نظر بگیرید.
(test_program_6)=
def generate_data(n, generator_type):
ϵ_values = []
for i in range(n):
e = generator_type()
ϵ_values.append(e)
return ϵ_values
data = generate_data(100, np.random.uniform)
plt.plot(data)
plt.show()
حالا، وقتی تابع ()generate_data را فراخوانی میکنیم، np.random.uniform را به عنوان آرگومان دوم ارسال میکنیم.
این شیء یک تابع است.
وقتی فراخوانی تابع generate_data(100, np.random.uniform) اجرا میشود، پایتون بلوک کد تابع را با n برابر با 100 و نام generator_type "متصل" به تابع np.random.uniform اجرا میکند.
- در حالی که این خطوط اجرا میشوند، نامهای
generator_typeوnp.random.uniform"مترادف" هستند و میتوانند به روشهای یکسان استفاده شوند.
این اصل به طور کلی هم کار میکند؛ به عنوان مثال، قطعه کد زیر را در نظر بگیرید:
max(7, 2, 4) # max() is a built-in Python function
m = max
m(7, 2, 4)
در اینجا ما نام دیگری برای تابع داخلی ()max ایجاد کردیم که سپس میتوانست به روشهای یکسان استفاده شود.
در چارچوب برنامه ما، توانایی اتصال نامهای جدید به توابع به این معنی است که هیچ مشکلی در ارسال یک تابع به عنوان آرگومان به تابع دیگر وجود ندارد؛همانطور که در بالا انجام دادیم.
(recursive_functions)=
این یک مبحث پیشرفته است که میتوانید از مطالعه آن صرفنظر کنید.
با این حال، ایدهی جالبی است که در مراحل بعدی برنامهنویسی خود با آن آشنا شوید.
اساساً، یک تابع بازگشتی(Recursive Function) تابعی است که خودش را فراخوانی میکند.
به عنوان مثال، مسئله محاسبه
:label: xseqdoub
x_{t+1} = 2 x_t, \quad x_0 = 1
واضح است که جواب
ما میتوانیم این را به راحتی با یک حلقه محاسبه کنیم
def x_loop(t):
x = 1
for i in range(t):
x = 2 * x
return x
همچنین میتوانیم از یک راهحل بازگشتی همانند زیر استفاده کنیم:
def x(t):
if t == 0:
return 1
else:
return 2 * x(t-1)
آنچه در اینجا اتفاق میافتد این است که هر فراخوانی متوالی، فریم(frame) مخصوص خودش را در پشته(stack) استفاده میکند.
- فریم جایی است که متغیرهای محلی یک فراخوانی تابع مشخص نگهداری میشود
- پشته حافظهای است که برای پردازش فراخوانیهای تابع استفاده میشود
- و مانند یک صف ورود اول، خروج آخر یا First In Last Out (FILO) عمل می کند.
این مثال تا حدودی ساختگی است، زیرا معمولا راهحل تکراری(iterative) معمولاً به جای راهحل بازگشتی(recursive) ترجیح داده میشود.
ما بعداً با کاربردهای کمتر ساختگی بازگشت آشنا خواهیم شد.
(factorial_exercise)=
:label: func_ex1
به یاد داشته باشید که
ما فقط
توابعی برای محاسبه این در ماژولهای مختلف وجود دارد، اما بیایید به عنوان تمرین نسخه خودمان را بنویسیم.
به طور خاص، تابعی به نام factorial بنویسید به طوری که factorial(n) برای هر عدد صحیح مثبت
:class: dropdown
یک راه حل این است:
def factorial(n):
k = 1
for i in range(n):
k = k * (i + 1)
return k
factorial(4)
:label: func_ex2
متغیر تصادفی دوجملهای
بدون هیچ import به جز from numpy.random import uniform، تابعی به نام binomial_rv بنویسید به طوری که binomial_rv(n, p) یک نمونه از
:class: dropdown
اگر $U$ یکنواخت در $(0, 1)$ و $p \in (0,1)$ باشد، آنگاه عبارت `U < p` با احتمال $p$ به `True` ارزیابی میشود.
:class: dropdown
یک راه حل این است:
from numpy.random import uniform
def binomial_rv(n, p):
count = 0
for i in range(n):
U = uniform()
if U < p:
count = count + 1 # Or count += 1
return count
binomial_rv(10, 0.5)
:label: func_ex3
ابتدا، تابعی بنویسید که یک نمونه از دستگاه تصادفی زیر را برگرداند:
- یک سکه را عادلانه 10 بار پرتاب کنید.
- اگر شیر
kبار یا بیشتر به طور متوالی در این دنباله حداقل یک بار ظاهر شود، یک دلار پرداخت کنید. - در غیر این صورت، چیزی پرداخت نکنید.
سپس، تابع دیگری بنویسید که همان کار را انجام دهد به جز اینکه قانون دوم دستگاه تصادفی بالا به این شکل تبدیل :شود
- اگر شیر
kبار یا بیشتر در این دنباله ظاهر شود، یک دلار پرداخت کنید.
از هیچ import به جز from numpy.random import uniform استفاده نکنید.
:class: dropdown
در اینجا تابعی برای دستگاه تصادفی اول آورده شده است:
from numpy.random import uniform
def draw(k): # pays if k consecutive successes in a sequence
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + 1 if U < 0.5 else 0
print(count) # print counts for clarity
if count == k:
payoff = 1
return payoff
draw(3)
در اینجا تابع دیگری برای دستگاه تصادفی دوم وجود دارد:
def draw_new(k): # pays if k successes in a sequence
payoff = 0
count = 0
for i in range(10):
U = uniform()
count = count + ( 1 if U < 0.5 else 0 )
print(count)
if count == k:
payoff = 1
return payoff
draw_new(3)
در تمرینات زیر، ما با هم توابع بازگشتی را خواهیم نوشت.
:label: func_ex4
اعداد فیبوناچی به این صورت تعریف میشوند
:label: fib
x_{t+1} = x_t + x_{t-1}, \quad x_0 = 0, \; x_1 = 1
چند عدد اول در این دنباله
تابعی برای محاسبه بازگشتی
:class: dropdown
در اینجا راهحل استاندارد این مسئله را می توانید ببینید:
def x(t):
if t == 0:
return 0
if t == 1:
return 1
else:
return x(t-1) + x(t-2)
بیایید آن را آزمایش کنیم
print([x(i) for i in range(10)])
:label: func_ex5
تابع ()factorial از تمرین 1 را با استفاده از بازگشت بازنویسی کنید.
:class: dropdown
در اینجا راهحل استاندارد را میتوانید ببینید:
def recursion_factorial(n):
if n == 1:
return n
else:
return n * recursion_factorial(n-1)
بیایید آن را آزمایش کنیم
print([recursion_factorial(i) for i in range(1, 10)])