ساختمان داده‌ها

تاپل در پایتون: ساخت، برش، تغییرناپذیری و unpacking

۶ دقیقه مطالعه · به‌روزرسانی ۲۸ مهر ۱۴۰۵

تا اینجا هر متغیر فقط یک مقدار نگه می‌داشت: یک عدد، یک رشته، یک True/False. اما خیلی وقت‌ها می‌خواهیم چند مقدار را کنار هم نگه داریم: نمره‌های یک کلاس، نام و سن یک نفر، مختصات یک نقطه.

در پایتون به چیزهایی مثل int و float و bool که جزء کوچک‌تری ندارند اسکالر می‌گوییم، و به چیزهایی مثل رشته که از تکه‌های کوچک‌تر ساخته شده‌اند غیراسکالر. ساختمان‌های دادهٔ پایتون — تاپل، لیست، دیکشنری و مجموعه — همگی غیراسکالرند. از تاپل شروع می‌کنیم.

تاپل چیست

تاپل یک دنبالهٔ مرتب از عنصرهاست که:

  • با پرانتز نوشته می‌شود،
  • می‌تواند انواع مختلف را با هم داشته باشد (عدد، رشته، حتی تاپل دیگر)،
  • و تغییرناپذیر (immutable) است: بعد از ساخته‌شدن نمی‌توانید عنصرهایش را عوض کنید.
a = (1, 5, 10)
print(a)
print(type(a))

khali = ()
print(khali, type(khali))

خروجی:

(1, 5, 10)
<class 'tuple'>
() <class 'tuple'>

دام تاپل تک‌عنصری

اینجا یک نکتهٔ ریز ولی مهم هست. پرانتز در پایتون کار دیگری هم دارد: تعیین اولویت در عبارت‌های ریاضی. پس وقتی فقط یک چیز داخل پرانتز بگذارید، پایتون فکر می‌کند منظورتان پرانتز ریاضی است:

c = (12)
print(c, type(c))      # عدد ساده است، نه تاپل!

c = (12,)              # با یک ویرگول در انتها
print(c, type(c))      # حالا تاپل است

خروجی:

12 <class 'int'>
(12,) <class 'tuple'>

💡 قاعده: چیزی که تاپل بودن را مشخص می‌کند ویرگول است، نه پرانتز. برای تاپل خالی () کافی است، برای تاپل تک‌عنصری حتماً (12,).

جمع، برش و پیمایش

تاپل‌ها را می‌توان با + به هم چسباند. نتیجه یک تاپل جدید است:

b = ()
c = (12,)
a = (1, 5, 10)

d = b + c + a
print(d)
print(len(d))

خروجی:

(12, 1, 5, 10)
4

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

d = (12, 1, 5, 10)

print(d[0])        # اولی
print(d[-1])       # آخری
print(d[0:3])      # از صفر تا قبل از سه
print(d[0:4:2])    # دوتا دوتا (یکی در میان)

خروجی:

12
10
(12, 1, 5)
(12, 5)

و مثل هر دنبالهٔ دیگری می‌توان روی آن حلقهٔ for زد:

d = (12, 1, 5, 10)
for item in d:
    print(item)

تغییرناپذیری: چرا تاپل را نمی‌شود عوض کرد

این خط خطا می‌دهد:

d = (12, 1, 5, 10)
d[2] = 100
TypeError: 'tuple' object does not support item assignment

خواندن d[2] اشکالی ندارد؛ نوشتن روی آن ممنوع است. به این ویژگی immutable (تغییرناپذیر) می‌گوییم.

ولی شاید بگویید «من که این کار را کردم و جواب داد»:

t = (5, 12, 8)
print(id(t))        # آدرس t در حافظه

t = (1000,) + t[1:]
print(t)
print(id(t))        # آدرس عوض شده است

اینجا هیچ تاپلی تغییر نکرده. سمت راست یک تاپل کاملاً جدید ساخته شده و اسم t را روی آن گذاشته‌ایم. تابع id() آدرس شیء در حافظه را می‌دهد (عددش روی سیستم شما فرق می‌کند) و می‌بینید که قبل و بعد یکی نیست. تاپل قبلی دست‌نخورده باقی مانده؛ فقط دیگر کسی به آن اشاره نمی‌کند.

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

پک و آنپک

اگر چند مقدار را با ویرگول جدا کنید، پایتون خودش آن‌ها را در یک تاپل بسته‌بندی (pack) می‌کند — حتی بدون پرانتز:

a = 1, 2, 5
print(a, type(a))

عکس این کار هم ممکن است: یک تاپل را در چند متغیر باز (unpack) کنید. فقط تعداد متغیرهای سمت چپ باید با تعداد عنصرهای تاپل یکی باشد:

name_and_age = ("sara", 30)

name, age = name_and_age      # unpack
print(name)
print(age)

خروجی:

sara
30
a, b, c = name_and_age        # ValueError: not enough values to unpack

کاربرد ۱: جابه‌جایی دو متغیر

در بیشتر زبان‌های برنامه‌نویسی برای جابه‌جا کردن مقدار دو متغیر به یک متغیر کمکی احتیاج دارید:

a = 12
b = 13

temp = a
a = b
b = temp
print(a, b)

در پایتون، به لطف پک و آنپک، یک خط کافی است:

a = 12
b = 13

a, b = b, a
print(a, b)

چرا کار می‌کند؟ اول سمت راست حساب می‌شود و تاپل (13, 12) ساخته می‌شود، بعد همان تاپل در a و b باز می‌شود. هیچ متغیر اضافه‌ای لازم نیست.

کاربرد ۲: برگرداندن چند مقدار از تابع

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

def calc_sum_and_avg(a, b, c):
    total = a + b + c
    return total, total / 3     # یک تاپل دوتایی

jam, mean = calc_sum_and_avg(12, 10, 9)
print(jam)
print(mean)

خروجی:

31
10.333333333333334

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

def quotient_and_remainder(x, y):
    q = x // y
    r = x % y
    return q, r                  # پرانتز لازم نیست

quot, rem = quotient_and_remainder(17, 5)
print(quot, rem)

💡 اگر دوست داشتید می‌توانید خروجی را یکجا هم بگیرید: result = quotient_and_remainder(17, 5). آن‌وقت result یک تاپل است و result[0] خارج‌قسمت.

تاپلی از تاپل‌ها

عنصرهای یک تاپل می‌توانند خودشان تاپل باشند. این ساده‌ترین راه نگه‌داشتن یک جدول کوچک است:

students = (("ali", 20), ("sara", 18), ("gholam", 10), ("changiz", 7), ("sadra", 16))

print(len(students))      # چند دانشجو
print(students[:2])       # دو تای اول
print(students[-1])       # آخری
print(students[-1][1])    # نمرهٔ آخری

len() فقط تعداد عنصرهای تاپل بیرونی را می‌دهد؛ کاری به ساختار داخلی ندارد.

حالا نمره‌ها را چاپ و میانگین کلاس را حساب کنیم:

students = (("ali", 20), ("sara", 18), ("gholam", 10), ("changiz", 7), ("sadra", 16))

total = 0
for s in students:
    print(f"{s[0]} got: {s[1]}")
    total += s[1]

avg = total / len(students)
print("-" * 10)
print("میانگین کلاس:", avg)

خروجی:

ali got: 20
sara got: 18
gholam got: 10
changiz got: 7
sadra got: 16
----------
میانگین کلاس: 14.2

💡 اسم متغیر را sum نگذارید. sum اسم یک تابع آمادهٔ پایتون است و با این کار آن را خراب می‌کنید. total یا jam انتخاب بهتری است.

و اگر فقط نمره‌ها را جدا کنیم، min() و max() آماده‌اند:

students = (("ali", 20), ("sara", 18), ("gholam", 10), ("changiz", 7), ("sadra", 16))

nomarat = ()
for s in students:
    nomarat = nomarat + (s[1],)      # تاپل تک‌عنصری را می‌چسبانیم

print(nomarat)
print("بیشترین:", max(nomarat), "کمترین:", min(nomarat))

دقت کنید: (s[1],) همان تاپل تک‌عنصری است که اول درس دیدیم. هر بار یک تاپل جدید ساخته می‌شود و دوباره اسم nomarat رویش می‌نشیند — که برای کار مکرر، راه چندان بهینه‌ای نیست. در درس بعد می‌بینیم لیست دقیقاً برای همین ساخته شده است.

پس تاپل به چه درد می‌خورد؟

وقتی می‌خواهید مطمئن باشید داده‌تان به اشتباه عوض نمی‌شود. یک نقطه با مختصات (x, y)، یک رکورد (نام، نمره)، یا مقدارهایی که یک تابع برمی‌گرداند — این‌ها چیزهایی هستند که بعد از ساخته‌شدن قرار نیست تغییر کنند و تاپل این را تضمین می‌کند. هر جا که لازم است داده رشد کند یا عوض شود، سراغ لیست بروید.

تمرین

یک تابع بنویسید که یک تاپل از تاپل‌های (نام، نمره) بگیرد و سه چیز برگرداند: کمترین نمره، بیشترین نمره، و تعداد نام‌های غیرتکراری. بعد خروجی را با unpack در سه متغیر بگیرید و چاپ کنید.

جواب
def get_data(aTuple):
    nums = ()
    names = ()
    for t in aTuple:
        nums = nums + (t[1],)
        if t[0] not in names:        # اگر قبلاً نیامده
            names = names + (t[0],)
    return min(nums), max(nums), len(names)

students = (("ali", 20), ("sara", 18), ("ali", 15), ("changiz", 7))

min_n, max_n, unique = get_data(students)
print("کمترین:", min_n)
print("بیشترین:", max_n)
print("تعداد افراد غیرتکراری:", unique)

خروجی:

کمترین: 7
بیشترین: 20
تعداد افراد غیرتکراری: 3