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

لیست در پایتون: ساخت، تغییرپذیری، append و extend

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

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

ساخت لیست

لیست را با کروشه می‌نویسیم:

a = []                      # لیست خالی
b = [1, 2, 5, 10]

print(a, type(a))
print(b, len(b))
print(b[0], b[2], b[-1])

خروجی:

[] <class 'list'>
[1, 2, 5, 10] 4
1 5 10

برخلاف آرایه در خیلی از زبان‌های دیگر، لازم نیست همهٔ عنصرها هم‌نوع باشند (هرچند در عمل معمولاً هستند):

b_list = [2, 'a', 4, True]
print(b_list)

اندیس می‌تواند یک متغیر یا حتی یک محاسبه باشد — فقط باید عدد صحیح از آب دربیاید:

L = [2, 1, 3]
i = 2
print(L[i - 1])      # یعنی L[1]

و اگر اندیسی بدهید که وجود ندارد، IndexError می‌گیرید.

تفاوت اصلی: لیست تغییرپذیر است

این همان خطی است که روی تاپل خطا می‌داد:

b = [1, 2, 5, 10]
b[2] = 5000
print(b)

خروجی:

[1, 2, 5000, 10]

نکتهٔ ظریف این است که لیست در جای خودش عوض شده، نه اینکه لیست جدیدی ساخته شده باشد. با id() که آدرس شیء در حافظه را می‌دهد، می‌شود این را دید:

L = [1, 2, 5, 10]
print(id(L))

L[0] = 200
print(L)
print(id(L))      # همان آدرس قبلی

عدد id روی سیستم شما فرق می‌کند، ولی قبل و بعد یکی است. حالا همین را با تاپل مقایسه کنید:

t = (5, 12, 8)
print(id(t))

t = (1000,) + t[1:]     # مجبوریم تاپل جدید بسازیم
print(id(t))            # آدرس عوض شده

به تغییر «در جای خود» جهش (mutation) می‌گوییم. لیست جهش‌پذیر (mutable) است و تاپل نیست. این تفاوت در درس بعد عواقب جدی‌تری هم پیدا می‌کند.

💡 کدام را انتخاب کنم؟ اگر قرار است داده عوض شود یا رشد کند، لیست. اگر قرار است ثابت بماند (مختصات، یک رکورد، خروجی چندتایی یک تابع)، تاپل.

پیمایش لیست

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

L = [2, 1, 3]

total = 0
for i in L:            # روی خود عنصرها
    total += i
print(total)

total = 0
for i in range(len(L)):   # روی اندیس‌ها
    total += L[i]
print(total)

هر دو جواب یکی می‌دهند. اولی خواناتر است؛ دومی وقتی به خود اندیس احتیاج دارید به کار می‌آید. یادتان باشد اندیس‌های لیست از 0 تا len(L) - 1 هستند و range(n) هم دقیقاً از 0 تا n-1 می‌رود.

افزودن عنصر: append

append یک عنصر به انتهای لیست اضافه می‌کند و خود لیست را جهش می‌دهد:

a = [12, 5, 9]
a.append(10)
print(a, len(a))

خروجی:

[12, 5, 9, 10] 4

💡 آن نقطه از کجا آمد؟ لیست در پایتون یک شیء (object) است؛ شیء هم داده دارد و هم کارهایی که بلد است انجام دهد (متد). a.append(10) یعنی «ای لیست، این را به خودت اضافه کن». در بخش شی‌گرایی خودمان هم شیء می‌سازیم.

اینجا یک اشتباه خیلی رایج هست. append یک عنصر می‌گیرد؛ اگر به آن یک لیست بدهید، کل آن لیست می‌شود یک خانه:

a = [12, 5, 9]
b = [100, 200, 300]

a.append(b)
print(a)
print(len(a))     # ۴ شد، نه ۶
print(a[-1])      # خانهٔ آخر خودش یک لیست است

خروجی:

[12, 5, 9, [100, 200, 300]]
4
[100, 200, 300]

چسباندن دو لیست: + و extend

اگر می‌خواهید عنصرهای لیست دوم یکی‌یکی به انتهای اولی اضافه شوند، دو راه بهتر دارید.

راه اول، عملگر + که یک لیست جدید می‌سازد:

a = [12, 5, 9]
b = [100, 200, 300]

c = a + b
print(c, len(c))
print(a)          # دست‌نخورده

خروجی:

[12, 5, 9, 100, 200, 300] 6
[12, 5, 9]

راه دوم، متد extend که لیست را در جای خودش گسترش می‌دهد:

l1 = [12, 9, 100]
l2 = [10, 100, 1000]

l1.extend(l2)
print(l1)
print(l2)        # دست‌نخورده

خروجی:

[12, 9, 100, 10, 100, 1000]
[10, 100, 1000]

جمع‌بندی این سه:

کار نتیجه لیست اصلی
a.append(b) b به‌عنوان یک خانه اضافه می‌شود جهش می‌کند
a + b لیست جدید با همهٔ عنصرها دست‌نخورده می‌ماند
a.extend(b) عنصرهای b یکی‌یکی اضافه می‌شوند جهش می‌کند

⚠️ a + b فقط بین دو لیست کار می‌کند. a + 12 خطا می‌دهد؛ باید بنویسید a + [12].

لیست تودرتو

عنصرهای یک لیست می‌توانند خودشان لیست یا تاپل باشند، تا هر عمقی که بخواهید:

a = [12, [15, 9, 10], "salam", [12, (6, (7, 8)), 10, 9], True]

print(len(a))        # ۵ خانه در لیست بیرونی
print(a[2])
print(a[1][1])
print(a[3][1][1][1])

خروجی:

5
salam
9
8

برای a[3][1][1][1] از چپ به راست پیش بروید: a[3] همان لیست چهارم است، [1] تاپل (6, (7, 8))، [1] بعدی (7, 8) و [1] آخر 8.

ساختن لیست با ورودی کاربر

رایج‌ترین کاربرد روزمرهٔ لیست همین است: چیزی نداریم، لیست خالی می‌سازیم و در حلقه پُرش می‌کنیم.

grades = []
for i in range(3):
    g = float(input("نمره: "))
    grades.append(g)

print(grades)

total = 0
for g in grades:
    total += g
print("میانگین:", total / len(grades))

💡 چرا append و نه grades[i] = g؟ چون آن خانه هنوز وجود ندارد و اندیس‌دهی به خانهٔ نساخته خطا می‌دهد. append خودش خانه را می‌سازد.

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

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

students = [("sara", 12), ("ali", 20), ("sadra", 9)]
print(students[1])
print(students[1][0], students[1][1])

حالا همین را از کاربر بگیریم. در حلقهٔ اول همه را جمع می‌کنیم، در حلقهٔ دوم گزارش می‌دهیم:

students = []
for i in range(2):          # عدد ۲ را به هر تعدادی که خواستید تغییر دهید
    name = input("اسم: ")
    grade = float(input("نمره: "))
    students.append((name, grade))      # یک تاپل دوتایی، یک خانه

print(students)
print("-" * 20)

for item in students:
    name = item[0]
    grade = item[1]
    print(f"{name} has got {grade}")

دقت کنید students.append((name, grade)) دو جفت پرانتز دارد: بیرونی مال صدازدن تابع است و درونی تاپل را می‌سازد. اگر یکی را بردارید، append دو آرگومان می‌گیرد و خطا می‌دهد.

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

for i in range(len(students)):
    item = students[i]
    print(f"{item[0]} has got {item[1]}")

تمرین

برنامه‌ای بنویسید که نام و نمرهٔ ۵ دانشجو را بگیرد و در یک لیست از تاپل‌ها نگه دارد، سپس نام و نمرهٔ همه را چاپ کند و در آخر بگوید بیشترین نمره مال چه کسی بوده است.

جواب
students = []
for i in range(5):
    name = input("اسم: ")
    grade = float(input("نمره: "))
    students.append((name, grade))

best_name = students[0][0]
best_grade = students[0][1]

for name, grade in students:        # unpack مستقیم در حلقه
    print(f"{name}: {grade}")
    if grade > best_grade:
        best_grade = grade
        best_name = name

print("-" * 20)
print(f"بهترین: {best_name} با نمرهٔ {best_grade}")

for name, grade in students همان unpack تاپل است که در درس قبل دیدیم، این بار داخل حلقه. خیلی خواناتر از item[0] و item[1] است.