منابع ترجمه · Think Python · فصل ۴
توابع و رابطها (فصل ۴ Think Python)
منبع اصلی: Think Python, Chapter 4: Functions and Interfaces — Allen B. Downey
💡 این صفحه مثل یک نوتبوک Jupyter است: متغیر و تابعی که در یک سلول تعریف شود، در سلولهای بعدی همین صفحه در دسترس است. سلولها را به ترتیب اجرا کنید، یا با «⏮ تا اینجا» همهٔ سلولهای قبلیِ اجرانشده را یکجا اجرا کنید.
خروجیهای این فصل شکلهایی هستند که لاکپشت میکشد؛ چون در فایل متنی قابل نمایش نیستند، زیر هر کد توضیح کوتاهی دربارهی شکل حاصل آمده است.
این فصل ماژولی به نام jupyturtle را معرفی میکند که به شما امکان میدهد با دادن دستور به یک لاکپشت خیالی نقاشیهای ساده بکشید. از این ماژول برای نوشتن توابعی استفاده میکنیم که مربع، چندضلعی و دایره میکشند – و برای نشان دادن طراحی رابط (interface design)، که روشی برای طراحی توابعی است که با هم کار میکنند.
۴.۱. ماژول jupyturtle
برای استفاده از ماژول jupyturtle میتوانیم آن را اینطور import کنیم.
import jupyturtle
حالا میتوانیم از توابعی که در ماژول تعریف شدهاند، مثل make_turtle و forward، استفاده کنیم.
jupyturtle.make_turtle()
jupyturtle.forward(100)
(خروجی: یک خط افقی به طول ۱۰۰ واحد و لاکپشتی در انتهای آن که رو به راست است.)
make_turtle یک بوم (canvas) میسازد؛ فضایی روی صفحه که میتوانیم در آن نقاشی کنیم، و یک لاکپشت که با یک لاکِ دایرهای و یک سرِ مثلثی نمایش داده میشود. دایره موقعیت لاکپشت را نشان میدهد و مثلث جهتی را که رو به آن است.
forward لاکپشت را به اندازهی مسافت دادهشده در جهتی که رو به آن است جلو میبرد و در راه یک پارهخط میکشد. مسافت بر حسب واحدهای دلخواه است – اندازهی واقعی به صفحهی نمایش کامپیوتر شما بستگی دارد.
بارها از توابع تعریفشده در ماژول jupyturtle استفاده میکنیم، پس خوب است اگر لازم نباشد هر بار نام ماژول را بنویسیم. اگر ماژول را اینطور import کنیم این امکان وجود دارد.
from jupyturtle import make_turtle, forward
این نسخه از دستور import، make_turtle و forward را از ماژول jupyturtle وارد میکند تا بتوانیم آنها را اینطور فراخوانی کنیم.
make_turtle()
forward(100)
jupyturtle دو تابع دیگر هم دارد که به کار میبریم، به نامهای left و right. آنها را اینطور import میکنیم.
from jupyturtle import left, right
left باعث میشود لاکپشت به چپ بچرخد. یک آرگومان میگیرد که زاویهی چرخش بر حسب درجه است. برای مثال، میتوانیم یک چرخش ۹۰ درجه به چپ را اینطور بسازیم.
make_turtle()
forward(50)
left(90)
forward(50)
(خروجی: دو پارهخط به طول ۵۰ که زاویهی قائمه میسازند؛ اولی به سمت راست و دومی به سمت بالا.)
این برنامه لاکپشت را اول به شرق و بعد به شمال میبرد و دو پارهخط به جا میگذارد. قبل از ادامه، ببینید میتوانید برنامهی قبلی را طوری تغییر دهید که مربع بسازد.
۴.۲. ساختن یک مربع
این یک راه برای ساختن مربع است.
make_turtle()
forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
forward(50)
left(90)
(خروجی: یک مربع به ضلع ۵۰.)
چون این برنامه یک جفت خط یکسان را چهار بار تکرار میکند، میتوانیم همین کار را مختصرتر با یک حلقهی for انجام دهیم.
make_turtle()
for i in range(4):
forward(50)
left(90)
۴.۳. کپسولهسازی و تعمیم
بیایید کد رسم مربع از بخش قبل را بگیریم و در تابعی به نام square بگذاریم.
def square():
for i in range(4):
forward(50)
left(90)
حالا میتوانیم تابع را اینطور فراخوانی کنیم.
make_turtle()
square()
پیچیدن یک تکه کد در یک تابع کپسولهسازی (encapsulation) نامیده میشود. یکی از مزایای کپسولهسازی این است که یک نام به کد میدهد و این نام نوعی مستندسازی است. مزیت دیگر این است که اگر بخواهید دوباره از کد استفاده کنید، دو بار فراخوانی یک تابع مختصرتر از کپی و پیست کردن بدنه است!
در نسخهی فعلی، اندازهی مربع همیشه 50 است. اگر بخواهیم مربعهایی با اندازههای مختلف بکشیم، میتوانیم طول ضلعها را بهعنوان پارامتر بگیریم.
def square(length):
for i in range(4):
forward(length)
left(90)
حالا میتوانیم مربعهایی با اندازههای مختلف بکشیم.
make_turtle()
square(30)
square(60)
(خروجی: دو مربع تودرتو که گوشهی پایینچپشان مشترک است؛ ضلعها ۳۰ و ۶۰.)
افزودن یک پارامتر به تابع تعمیم (generalization) نامیده میشود، چون تابع را کلیتر میکند: با نسخهی قبلی مربع همیشه هماندازه است؛ با این نسخه میتواند هر اندازهای داشته باشد.
اگر پارامتر دیگری اضافه کنیم، میتوانیم آن را باز هم کلیتر کنیم. تابع زیر چندضلعیهای منتظم با تعداد ضلع دادهشده را میکشد.
def polygon(n, length):
angle = 360 / n
for i in range(n):
forward(length)
left(angle)
در یک چندضلعی منتظم با n ضلع، زاویهی بین ضلعهای مجاور 360 / n درجه است.
مثال زیر یک چندضلعی 7 ضلعی با طول ضلع 30 میکشد.
make_turtle()
polygon(7, 30)
وقتی تابعی بیش از چند آرگومان عددی دارد، بهراحتی فراموش میشود که آنها چه بودند یا به چه ترتیبی باید باشند. ایدهی خوبی است که نام پارامترها را در فهرست آرگومانها بنویسید.
make_turtle()
polygon(n=7, length=30)
به اینها گاهی «آرگومانهای نامدار» میگویند چون نام پارامترها را دارند. اما در پایتون بیشتر به آنها آرگومان کلیدواژهای (keyword argument) میگویند (با کلمههای کلیدی پایتون مثل for و def اشتباه نگیرید).
این استفاده از عملگر انتساب، =، یادآوری میکند که آرگومانها و پارامترها چگونه کار میکنند – وقتی تابعی را فراخوانی میکنید، آرگومانها به پارامترها نسبت داده میشوند.
۴.۴. تقریب یک دایره
حالا فرض کنید میخواهیم یک دایره بکشیم. میتوانیم این کار را، بهصورت تقریبی، با کشیدن یک چندضلعی با تعداد زیادی ضلع انجام دهیم، بهطوری که هر ضلع آنقدر کوچک باشد که دیدنش سخت شود. این تابعی است که از polygon برای کشیدن یک چندضلعی 30 ضلعی که دایره را تقریب میزند استفاده میکند.
import math
def circle(radius):
circumference = 2 * math.pi * radius
n = 30
length = circumference / n
polygon(n, length)
circle شعاع دایره را بهعنوان پارامتر میگیرد. circumference را حساب میکند که محیط دایرهای با شعاع دادهشده است. n تعداد ضلعهاست، پس circumference / n طول هر ضلع است.
اجرای این تابع ممکن است زمان زیادی ببرد. میتوانیم با فراخوانی make_turtle با یک آرگومان کلیدواژهای به نام delay آن را سریعتر کنیم؛ این آرگومان زمانی را، بر حسب ثانیه، که لاکپشت بعد از هر قدم صبر میکند تعیین میکند. مقدار پیشفرض 0.2 ثانیه است – اگر آن را 0.02 بگذاریم حدود ۱۰ برابر سریعتر اجرا میشود.
make_turtle(delay=0.02)
circle(30)
(خروجی: یک دایرهی تقریبی با شعاع ۳۰.)
محدودیت این راهحل این است که n یک مقدار ثابت است؛ یعنی برای دایرههای بسیار بزرگ ضلعها خیلی بلند میشوند و برای دایرههای کوچک وقت را برای کشیدن ضلعهای خیلی کوتاه تلف میکنیم. یک راه این است که تابع را با گرفتن n بهعنوان پارامتر تعمیم دهیم. اما فعلاً ساده نگهش میداریم.
۴.۵. بازآرایی (Refactoring)
حالا نسخهی کلیتری از circle بنویسیم، به نام arc، که پارامتر دومی به نام angle میگیرد و کمانی از یک دایره را میکشد که زاویهی دادهشده را میپوشاند. برای مثال، اگر angle برابر 360 درجه باشد، یک دایرهی کامل میکشد. اگر angle برابر 180 درجه باشد، نیمدایره میکشد.
برای نوشتن circle توانستیم از polygon دوباره استفاده کنیم، چون یک چندضلعی با ضلعهای زیاد تقریب خوبی از دایره است. اما نمیتوانیم از polygon برای نوشتن arc استفاده کنیم.
در عوض، نسخهی کلیتر polygon را میسازیم که polyline نام دارد.
def polyline(n, length, angle):
for i in range(n):
forward(length)
left(angle)
polyline اینها را بهعنوان پارامتر میگیرد: تعداد پارهخطهایی که باید کشیده شوند، n، طول پارهخطها، length، و زاویهی بین آنها، angle.
حالا میتوانیم polygon را طوری بازنویسی کنیم که از polyline استفاده کند.
def polygon(n, length):
angle = 360.0 / n
polyline(n, length, angle)
و میتوانیم از polyline برای نوشتن arc استفاده کنیم.
def arc(radius, angle):
arc_length = 2 * math.pi * radius * angle / 360
n = 30
length = arc_length / n
step_angle = angle / n
polyline(n, length, step_angle)
arc شبیه circle است، با این تفاوت که arc_length را حساب میکند که کسری از محیط یک دایره است.
سرانجام، میتوانیم circle را طوری بازنویسی کنیم که از arc استفاده کند.
def circle(radius):
arc(radius, 360)
برای بررسی اینکه این توابع همانطور که انتظار میرود کار میکنند، از آنها برای کشیدن چیزی شبیه حلزون استفاده میکنیم. با delay=0 لاکپشت تا حد امکان سریع اجرا میشود.
make_turtle(delay=0)
polygon(n=20, length=9)
arc(radius=70, angle=70)
circle(radius=10)
(خروجی: یک چندضلعی ۲۰ ضلعی، یک کمان ۷۰ درجهای که از آن بیرون میآید و یک دایرهی کوچک در انتهای کمان؛ شبیه حلزون.)
در این مثال با کدی که کار میکرد شروع کردیم و آن را با توابع مختلف دوباره سازماندهی کردیم. تغییراتی مثل این، که کد را بهبود میدهند بدون اینکه رفتارش را تغییر دهند، بازآرایی (refactoring) نامیده میشوند.
اگر از قبل برنامهریزی کرده بودیم، شاید ابتدا polyline را مینوشتیم و از بازآرایی دوری میکردیم، اما اغلب در آغاز یک پروژه آنقدر نمیدانید که همهی توابع را طراحی کنید. وقتی شروع به کدنویسی میکنید، مسئله را بهتر میفهمید. گاهی بازآرایی نشانهی این است که چیزی یاد گرفتهاید.
۴.۶. نمودار پشته
وقتی circle را فراخوانی میکنیم، arc را فراخوانی میکند، که polyline را فراخوانی میکند. میتوانیم از یک نمودار پشته برای نشان دادن این دنبالهی فراخوانیها و پارامترهای هر یک استفاده کنیم.
(در کتاب اصلی اینجا نمودار پشته رسم شده است: سه قاب، circle با radius=30، arc با radius=30 و angle=360 و polyline با n=60 و length=3.04 و angle=5.8.)
دقت کنید که مقدار angle در polyline با مقدار angle در arc فرق دارد. پارامترها محلیاند، یعنی میتوانید همان نام پارامتر را در توابع مختلف به کار ببرید؛ در هر تابع یک متغیر متفاوت است و میتواند به مقدار متفاوتی اشاره کند.
۴.۷. یک طرح توسعه
طرح توسعه (development plan) فرایندی برای نوشتن برنامههاست. فرایندی که در این فصل به کار بردیم «کپسولهسازی و تعمیم» است. مراحل این فرایند عبارتاند از:
-
با نوشتن یک برنامهی کوچک بدون تعریف تابع شروع کنید.
-
وقتی برنامه کار کرد، یک بخش منسجم از آن را مشخص کنید، آن بخش را در یک تابع کپسوله کنید و نامی به آن بدهید.
-
با افزودن پارامترهای مناسب تابع را تعمیم دهید.
-
مراحل ۱ تا ۳ را تکرار کنید تا مجموعهای از توابع کارآمد داشته باشید.
-
به دنبال فرصتهایی برای بهبود برنامه با بازآرایی بگردید. برای مثال، اگر در چند جا کد مشابه دارید، فکر کنید آن را به یک تابع بهاندازهی کافی کلی تبدیل کنید.
این فرایند معایبی دارد – بعداً جایگزینهایی را میبینیم – اما اگر از قبل نمیدانید چطور برنامه را به توابع تقسیم کنید میتواند مفید باشد. این رویکرد به شما اجازه میدهد در حین پیشرفت طراحی کنید.
طراحی یک تابع دو بخش دارد:
-
رابط (interface) نحوهی استفاده از تابع است، شامل نام، پارامترهایی که میگیرد و کاری که قرار است تابع انجام دهد.
-
پیادهسازی (implementation) نحوهی انجام کاری است که تابع قرار است انجام دهد.
برای مثال، این اولین نسخهی circle است که نوشتیم و از polygon استفاده میکند.
def circle(radius):
circumference = 2 * math.pi * radius
n = 30
length = circumference / n
polygon(n, length)
و این نسخهی بازآراییشده است که از arc استفاده میکند.
def circle(radius):
arc(radius, 360)
این دو تابع رابط یکسانی دارند – پارامترهای یکسانی میگیرند و کار یکسانی میکنند – اما پیادهسازیهایشان متفاوت است.
۴.۸. رشتههای مستندساز (Docstring)
docstring رشتهای است در ابتدای یک تابع که رابط را توضیح میدهد («doc» مخفف «documentation» یعنی مستندات است). این یک مثال است:
def polyline(n, length, angle):
"""Draws line segments with the given length and angle between them.
n: integer number of line segments
length: length of the line segments
angle: angle between segments (in degrees)
"""
for i in range(n):
forward(length)
left(angle)
طبق قرارداد، docstringها رشتههایی با سه نقلقول هستند که رشتهی چندخطی (multiline string) هم نامیده میشوند، چون سه نقلقول اجازه میدهند رشته بیش از یک خط را بگیرد.
یک docstring باید:
-
بهطور مختصر توضیح دهد که تابع چه میکند، بدون اینکه وارد جزئیات نحوهی کارش شود،
-
توضیح دهد هر پارامتر چه تأثیری بر رفتار تابع دارد، و
-
اگر بدیهی نیست، نشان دهد هر پارامتر باید از چه نوعی باشد.
نوشتن این نوع مستندات بخش مهمی از طراحی رابط است. رابطی که خوب طراحی شده باشد باید توضیح دادنش ساده باشد؛ اگر توضیح دادن یکی از توابعتان برایتان سخت است، شاید رابط را بتوان بهتر کرد.
۴.۹. اشکالزدایی
رابط مثل یک قرارداد بین یک تابع و فراخوانندهاش است. فراخواننده توافق میکند آرگومانهای مشخصی را فراهم کند و تابع توافق میکند کار مشخصی را انجام دهد.
برای مثال، polyline سه آرگومان لازم دارد: n باید عدد صحیح باشد؛ length باید عددی مثبت باشد؛ و angle باید عدد باشد و در نظر گرفته میشود که بر حسب درجه است.
این الزامها پیششرط (precondition) نامیده میشوند، چون قرار است پیش از شروع اجرای تابع برقرار باشند. در مقابل، شرطهای پایان تابع پسشرط (postcondition) هستند. پسشرطها شامل اثر مورد نظر تابع (مثل کشیدن پارهخطها) و هر اثر جانبی (مثل حرکت دادن لاکپشت یا ایجاد تغییرات دیگر) هستند.
پیششرطها مسئولیت فراخوانندهاند. اگر فراخواننده پیششرطی را نقض کند و تابع درست کار نکند، باگ در فراخواننده است، نه در تابع.
اگر پیششرطها برقرار باشند و پسشرطها نه، باگ در تابع است. اگر پیششرطها و پسشرطهایتان روشن باشند، میتوانند به اشکالزدایی کمک کنند.
۴.۱۰. واژهنامه
طراحی رابط (interface design): فرایندی برای طراحی رابط یک تابع، که شامل پارامترهایی است که باید بگیرد.
بوم (canvas): پنجرهای که برای نمایش عناصر گرافیکی از جمله خط، دایره، مستطیل و شکلهای دیگر به کار میرود.
کپسولهسازی (encapsulation): فرایند تبدیل دنبالهای از دستورها به یک تعریف تابع.
تعمیم (generalization): فرایند جایگزین کردن چیزی که بیجهت خاص است (مثل یک عدد) با چیزی که بهاندازهی کافی کلی است (مثل یک متغیر یا پارامتر).
آرگومان کلیدواژهای (keyword argument): آرگومانی که نام پارامتر را هم دارد.
بازآرایی (refactoring): فرایند تغییر یک برنامهی کارآمد برای بهبود رابط توابع و ویژگیهای دیگر کد.
طرح توسعه (development plan): فرایندی برای نوشتن برنامهها.
docstring: رشتهای که در بالای تعریف یک تابع میآید تا رابط تابع را مستند کند.
رشتهی چندخطی (multiline string): رشتهای که داخل سه نقلقول قرار دارد و میتواند بیش از یک خط از برنامه را بگیرد.
پیششرط (precondition): الزامی که فراخواننده باید پیش از شروع تابع برآورده کند.
پسشرط (postcondition): الزامی که تابع باید پیش از پایانش برآورده کند.
۴.۱۱. تمرینها
# This cell tells Jupyter to provide detailed debugging information
# when a runtime error occurs. Run it before working on the exercises.
%xmode Verbose
برای تمرینهای زیر، چند تابع دیگر لاکپشت هم هست که شاید بخواهید به کار ببرید.
-
penupقلم خیالی لاکپشت را بلند میکند تا هنگام حرکت ردی به جا نگذارد. -
pendownقلم را دوباره پایین میگذارد.
تابع زیر از penup و pendown استفاده میکند تا لاکپشت را بدون به جا گذاشتن ردی حرکت دهد.
from jupyturtle import penup, pendown
def jump(length):
"""Move forward length units without leaving a trail.
Postcondition: Leaves the pen down.
"""
penup()
forward(length)
pendown()
۴.۱۱.۱. تمرین
تابعی به نام rectangle بنویسید که مستطیلی با طول ضلعهای دادهشده بکشد. برای مثال، این مستطیلی است که 80 واحد عرض و 40 واحد ارتفاع دارد.
# Solution goes here
میتوانید برای آزمودن تابعتان از کد زیر استفاده کنید.
make_turtle()
rectangle(80, 40)
۴.۱۱.۲. تمرین
تابعی به نام rhombus بنویسید که یک لوزی با طول ضلع و زاویهی داخلی دادهشده بکشد. برای مثال، این لوزی با طول ضلع 50 و زاویهی داخلی 60 درجه است.
# Solution goes here
میتوانید برای آزمودن تابعتان از کد زیر استفاده کنید.
make_turtle()
rhombus(50, 60)
۴.۱۱.۳. تمرین
حالا تابع کلیتری به نام parallelogram بنویسید که یک چهارضلعی با ضلعهای موازی بکشد. سپس rectangle و rhombus را طوری بازنویسی کنید که از parallelogram استفاده کنند.
# Solution goes here
# Solution goes here
# Solution goes here
میتوانید برای آزمودن توابعتان از کد زیر استفاده کنید.
make_turtle(width=400)
jump(-120)
rectangle(80, 40)
jump(100)
rhombus(50, 60)
jump(80)
parallelogram(80, 50, 60)
۴.۱۱.۴. تمرین
مجموعهی بهاندازهی کافی کلی از توابعی بنویسید که بتوانند شکلهایی مثل شکل زیر را بکشند.
(در کتاب اصلی اینجا تصویری آمده است: چند شکل «نانپیتزا»؛ دایرههایی که به قطاعهای مثلثی مساوی تقسیم شدهاند، با ۵، ۶ و ۷ قطاع. تصویر: https://github.com/AllenDowney/ThinkPython/raw/v3/jupyturtle_pie.png)
راهنمایی: تابعی به نام triangle بنویسید که یک قطاع مثلثی را بکشد، و بعد تابعی به نام draw_pie که از triangle استفاده کند.
# Solution goes here
# Solution goes here
میتوانید برای آزمودن توابعتان از کد زیر استفاده کنید.
turtle = make_turtle(delay=0)
jump(-80)
size = 40
draw_pie(5, size)
jump(2*size)
draw_pie(6, size)
jump(2*size)
draw_pie(7, size)
# Solution goes here
۴.۱۱.۵. تمرین
مجموعهی بهاندازهی کافی کلی از توابعی بنویسید که بتوانند گلهایی مثل شکل زیر را بکشند.
(در کتاب اصلی اینجا تصویری آمده است: چند گل با تعداد گلبرگهای متفاوت. تصویر: https://github.com/AllenDowney/ThinkPython/raw/v3/jupyturtle_flower.png)
راهنمایی: از arc برای نوشتن تابعی به نام petal استفاده کنید که یک گلبرگ بکشد.
# Solution goes here
# Solution goes here
میتوانید برای آزمودن توابعتان از کد زیر استفاده کنید.
چون راهحل پارهخطهای کوچک زیادی میکشد، حین اجرا کند میشود. برای جلوگیری از این، میتوانید آرگومان کلیدواژهای auto_render=False را اضافه کنید تا بعد از هر قدم نقاشی انجام نشود، و در پایان تابع render را فراخوانی کنید تا نتیجه نمایش داده شود.
هنگام اشکالزدایی، شاید بخواهید auto_render=False را حذف کنید.
from jupyturtle import render
turtle = make_turtle(auto_render=False)
jump(-60)
n = 7
radius = 60
angle = 60
flower(n, radius, angle)
jump(120)
n = 9
radius = 40
angle = 85
flower(n, radius, angle)
render()
# Solution goes here
۴.۱۱.۶. از یک دستیار مجازی بپرسید
در پایتون چند ماژول مثل jupyturtle وجود دارد، و ماژولی که در این فصل به کار بردیم برای این کتاب سفارشیسازی شده است. پس اگر از یک دستیار مجازی کمک بخواهید، نمیداند از کدام ماژول استفاده کند. اما اگر چند مثال به او بدهید، احتمالاً میتواند بفهمد. برای مثال، این پرامپت را امتحان کنید و ببینید میتواند تابعی بنویسد که یک مارپیچ بکشد:
The following program uses a turtle graphics module to draw a circle:
from jupyturtle import make_turtle, forward, left
import math
def polygon(n, length):
angle = 360 / n
for i in range(n):
forward(length)
left(angle)
def circle(radius):
circumference = 2 * math.pi * radius
n = 30
length = circumference / n
polygon(n, length)
make_turtle(delay=0)
circle(30)
Write a function that draws a spiral.
(ترجمهی جملهی اول و آخر پرامپت: «برنامهی زیر از یک ماژول گرافیک لاکپشتی برای کشیدن یک دایره استفاده میکند: … تابعی بنویس که یک مارپیچ بکشد.»)
به یاد داشته باشید که نتیجه ممکن است از قابلیتهایی استفاده کند که هنوز ندیدهایم، و ممکن است خطا داشته باشد. کد را از دستیار مجازی کپی کنید و ببینید میتوانید آن را به کار بیندازید. اگر چیزی را که میخواستید نگرفتید، سعی کنید پرامپت را تغییر دهید.
# Solution goes here
# Solution goes here