فایل‌ها و مدیریت خطا

دستور with در پایتون (Context Manager)، خواندن خط به خط و ساخت فایل CSV

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

در درس قبل دیدیم فراموش کردن close() چه دردسرهایی دارد. پایتون راه‌حلی دارد که بستن فایل را خودکار می‌کند.

دستور with

with open("test.txt", "w") as f:
    f.write("salam\n")
    f.write("bye")

print("good bye")
  • with open(...) as f: فایل را باز می‌کند و دستگیره‌اش را در f می‌گذارد.
  • همهٔ کار با فایل داخل بلوک تورفته انجام می‌شود.
  • به محض اینکه تورفتگی تمام شود، پایتون فایل را خودش می‌بندد؛ حتی اگر وسط بلوک خطایی رخ داده باشد.

پس print("good bye") وقتی اجرا می‌شود که فایل از قبل بسته و ذخیره شده است. هم کد کوتاه‌تر است، هم دیگر نمی‌شود بستن را فراموش کرد.

💡 به چیزهایی که با with کار می‌کنند مدیر زمینه (context manager) می‌گویند. فایل فقط یک نمونه است؛ اتصال پایگاه داده، قفل‌ها و … هم همین‌طورند. قاعده: برای فایل همیشه از with استفاده کنید.

حالت افزودن هم همین‌طور است:

with open("test.txt", "a") as f:
    f.write("\nnew_line")

with open("test.txt") as f:
    print(f.read())

خواندن: سه روش

اول یک فایل نمونه بسازیم:

with open("names.csv", "w") as f:
    f.write("name,age\nsara,20\nali,12\ngholi,80\nparsa,3\nreza,33\n")
print("ساخته شد")

(مثال‌های بعدی این صفحه از همین فایل استفاده می‌کنند؛ اگر خطای FileNotFoundError گرفتید، اول این بلوک را اجرا کنید.)

۱. کل فایل یکجا: read()

with open("names.csv", "r") as f:
    content = f.read()

print(content)

read() کل فایل را در یک رشته برمی‌گرداند. برای فایل‌های کوچک عالی است؛ ولی یک فایل ۱۰ گیگابایتی را یکجا در حافظه نریزید!

۲. خط به خط: for

with open("names.csv", "r") as f:
    for line in f:
        print(line, end="")

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

یادتان هست مشکل خط خالی اضافه چه بود؟ هر line یک \n در انتها دارد و print یک \n دیگر هم اضافه می‌کند. دو راه‌حل:

with open("names.csv", "r") as f:
    for line in f:
        print(line.strip())

راه دوم معمولاً بهتر است، چون بعداً می‌خواهید با خود خط کار کنید و آن \n مزاحم است.

۳. فهرست خطوط: readlines()

with open("names.csv") as f:
    lines = f.readlines()
print(lines)

فایل CSV

CSV (مخفف Comma-Separated Values) ساده‌ترین قالب ذخیرهٔ داده‌های جدولی است: یک فایل متنی معمولی که هر خطش یک سطر جدول است و ستون‌ها با کاما از هم جدا شده‌اند:

name,age
sara,20
ali,12

اکسل، گوگل‌شیت و کتابخانه‌هایی مثل Pandas مستقیم بازش می‌کنند. این فایل‌ها نوع داده ندارند، همه‌چیزشان متن است؛ ولی اکسل وقتی ببیند ستونی فقط عدد دارد، خودش آن را عدد در نظر می‌گیرد.

مسئله: ساخت CSV از ورودی کاربر

برنامه‌ای بنویسید که نام و سن ۵ نفر را بگیرد و یک فایل CSV بسازد که ستون اول نام و ستون دوم سن باشد.

نسخهٔ ۱:

handler = open("people.csv", "w")
handler.write("name,age\n")      # سطر عنوان (header)

for _ in range(5):
    name = input("Enter a name: ")
    age = input("Enter an age: ")
    handler.write(name + ",")
    handler.write(age + "\n")

handler.close()
print(open("people.csv").read())

💡 _ اسم متغیری است که برنامه‌نویس‌های پایتون وقتی به مقدار شمارنده نیازی ندارند به کار می‌برند. یعنی «فقط ۵ بار تکرار کن».

نسخهٔ ۲: تمیزتر با with و f-string

with open("people.csv", "w", encoding="utf-8") as f:
    f.write("name,age\n")
    for i in range(1, 6):
        print(f"Person #{i}:")
        name = input("  name: ")
        age = input("  age: ")
        f.write(f"{name},{age}\n")

with open("people.csv", encoding="utf-8") as f:
    print(f.read())

با f-string کل سطر در یک write نوشته می‌شود. range(1, 6) هم برای این است که شمارهٔ نفرات از ۱ شروع شود، نه ۰.

خواندن و پردازش CSV

برای جدا کردن ستون‌ها از متد split استفاده می‌کنیم:

with open("names.csv") as f:
    header = f.readline()          # سطر اول (عنوان‌ها) را جدا بخوان
    total_age = 0
    count = 0
    for line in f:
        name, age = line.strip().split(",")
        print(f"{name:>8} → {age} ساله")
        total_age += int(age)
        count += 1

print("میانگین سن:", total_age / count)

"sara,20".split(",") رشته را در کاماها می‌شکند و ["sara", "20"] برمی‌گرداند که با باز کردن چندتایی در دو متغیر می‌ریزیم.

⚠️ اگر خود داده کاما داشته باشد (مثلاً آدرس "Tehran, Iran")، این روش ساده خراب می‌شود. برای CSVهای واقعی از ماژول استاندارد csv یا کتابخانهٔ Pandas استفاده کنید که این حالت‌ها را درست مدیریت می‌کنند.

نوشتن چندخطی

با رشتهٔ سه‌کوتیشنی می‌شود چند خط را یکجا نوشت:

with open("notes.txt", "w") as f:
    f.write("""line one
line two
line three
""")

with open("notes.txt") as f:
    print(f.read())

تمرین: ماژول name_utils

دو تابع بنویسید:

  • write_names(file_name, number_of_names): به تعداد number_of_names اسم از کاربر بگیرد و در فایل file_name بنویسد (هر اسم در یک خط).
  • read_names(file_name): همهٔ اسامی فایل را به ترتیب (هر اسم در یک خط) نمایش دهد.

در آخر این دو تابع را در یک ماژول به اسم name_utils.py بگذارید و از کد اصلی استفاده کنید.

جواب
def write_names(file_name, number_of_names):
    with open(file_name, "w", encoding="utf-8") as f:
        for i in range(number_of_names):
            name = input("Enter a name: ")
            f.write(name + "\n")

def read_names(file_name):
    with open(file_name, "r", encoding="utf-8") as f:
        for name in f:
            print(name, end="")

write_names("a.txt", 2)
read_names("a.txt")

بعد این دو تابع را در فایل name_utils.py بگذارید و در کد اصلی:

from name_utils import write_names, read_names

write_names("b.txt", 2)
read_names("b.txt")