برنامه‌نویسی پایتون فهرست مطالب
فصل ۱۸

فایل‌ها و سیستم‌عامل

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

در این فصل می‌خوانید
  1. باز کردن و بستن فایل
  2. دستور with
  3. روش‌های خواندن
  4. پیمایش خط به خط
  5. مشکل کاراکتر خط جدید
  6. نوشتن در فایل
  7. افزودن به انتهای فایل
  8. حالت ساخت امن
  9. انکدینگ و متن فارسی
  10. ماژول os
  11. ماژول pathlib
  12. برنامه دفترچه یادداشت

۱. باز کردن و بستن فایل

# open() gives us a "file object". The default mode is 'r' (read).
# The path is relative to the folder the program is started from.
f = open('a.txt', encoding='utf-8')
# An absolute path works too:
# f = open('D:\\myprojects\\a.txt', encoding='utf-8')

content = f.read()      # read() returns the WHOLE file as one string
print(content)

f.close()               # the file must be closed - if we forget, data can be lost

شکل ابتدایی: نمونه 01_read_whole_file.py

شرح مثال

۲. دستور with

فراموش کردن close آن‌قدر رایج است که پایتون راه بهتری دارد. از این به بعد همیشه از این شکل استفاده کنید.

# 'with' closes the file automatically, even if an error happens inside.
# This is the only form you should use.
with open('a.txt', encoding='utf-8') as f:
    content = f.read()
    print(content)

# Here the file is already closed.
print('closed?', f.closed)

دستور with: نمونه 02_with_statement.py

چرا with بهتر است

فایل به طور خودکار بسته می‌شود، حتی اگر وسط بلوک خطایی رخ دهد یا return اجرا شود. با close دستی، یک خطا کافی است تا فایل باز بماند. در فصل بیست و چهارم می‌بینید که with فقط برای فایل نیست و می‌شود برای هر چیزی که «باز و بسته» می‌شود ساختش، مثل اتصال به پایگاه داده.

۳. روش‌های خواندن

# read(n) reads at most n characters instead of the whole file.
with open('a.txt', encoding='utf-8') as f:
    print(f.read(5))     # 'Hello'
    print(f.read(3))     # the reading continues where it stopped

خواندن چند کاراکتر: نمونه 03_read_n_chars.py

# readline() reads ONE line, including the '\n' at its end.
with open('a.txt', encoding='utf-8') as f:
    print(f.readline())
    print(f.readline())

# That is why the output looks double spaced: the line already ends with '\n'
# and print() adds another one. end='' fixes it.
print('--- without the extra blank line ---')
with open('a.txt', encoding='utf-8') as f:
    print(f.readline(), end='')
    print(f.readline(), end='')

خواندن خط به خط: نمونه 04_readline.py

روشبرمی‌گرداندمناسب
read()کل فایل، یک رشتهفایل کوچک
read(n)حداکثر n کاراکترخواندن تکه‌تکه
readline()یک خط، همراه \nوقتی ساختار فایل مشخص است
readlines()لیستی از همه خط‌هافایل کوچک
for line in fخط به خطبهترین روش، هر اندازه فایل

نکته مهم: فایل یک مکان‌نما دارد. هر بار که می‌خوانید، مکان‌نما جلو می‌رود. برای همین f.read(3) دوم، از جایی که اولی رها کرده ادامه می‌دهد.

۴. پیمایش خط به خط

# Looping directly over the file gives its lines one by one.
# This is the best way: it never loads the whole file into memory.
with open('numbers.txt', encoding='utf-8') as file:
    for line in file:
        print(line, end='')

print()

# readlines() puts every line into a list (only for small files).
with open('numbers.txt', encoding='utf-8') as file:
    lines = file.readlines()

print(lines)
print('number of lines:', len(lines))

پیمایش فایل: نمونه 05_loop_over_lines.py

پیمایش مستقیم بهترین روش است، چون کل فایل را در حافظه نمی‌ریزد. برای فایلی با یک گیگابایت حجم، read() برنامه را از کار می‌اندازد ولی حلقه بی‌مشکل کار می‌کند.

۵. مشکل کاراکتر خط جدید

# Every line read from a file ends with '\n'. strip() removes it
# (together with the spaces at both ends).
with open('numbers.txt', encoding='utf-8') as file:
    for line in file:
        clean = line.strip()
        if clean == '':          # skip the empty lines
            continue
        number = int(clean)
        print(number, number * 2)

پاک کردن خط جدید: نمونه 06_strip_lines.py

مهم‌ترین دام کار با فایل

هر خطی که از فایل می‌خوانید، کاراکتر \n در انتهایش دارد. پس 'ali\n' با 'ali' برابر نیست و مقایسه‌تان همیشه نادرست می‌شود. راه حل، همیشه strip() است. این دقیقاً یکی از باگ‌هایی بود که در فایل‌های اصلی این دوره وجود داشت.

رد کردن خط‌های خالی هم لازم است: بیشتر فایل‌های متنی با یک خط خالی تمام می‌شوند و int('') خطا ایجاد می‌کند.

۶. نوشتن در فایل

# Mode 'w' creates the file, and ERASES it completely when it already exists.
with open('h.txt', 'w', encoding='utf-8') as f:
    f.write('It deletes the existing file!\n')
    f.write('Second line.\n')          # write() does NOT add '\n' by itself

# writelines() writes a list of strings (still without adding '\n').
lines = ['Ali\n', 'Sara\n', 'Reza\n']
with open('names.txt', 'w', encoding='utf-8') as f:
    f.writelines(lines)

نوشتن: نمونه 07_write_file.py

حالت w بی‌رحم است

باز کردن فایل با حالت 'w' محتوای قبلی‌اش را کاملاً پاک می‌کند، بدون هیچ سؤال و هشداری. اگر اشتباهی نام فایل مهمی را بنویسید، از دستش داده‌اید. پیش از هر open(..., 'w') یک بار نام فایل را دوباره بخوانید.

دو نکته دیگر: write برخلاف print خودش خط جدید اضافه نمی‌کند، پس اگر \n ننویسید همه چیز پشت سر هم روی یک خط می‌چسبد. و writelines هم با وجود نامش، بین خط‌ها چیزی اضافه نمی‌کند.

۷. افزودن به انتهای فایل

# Mode 'a' adds to the END of the file and never erases anything.
# Run this program several times and watch the file grow.
with open('g.txt', 'a', encoding='utf-8') as f:
    f.write('This is a text file!\n')

with open('g.txt', encoding='utf-8') as f:
    print(f.read())

افزودن: نمونه 08_append_file.py

حالتاگر فایل نباشداگر باشدکاربرد
'r'خطامی‌خواندخواندن. پیش‌فرض
'w'می‌سازدپاک می‌کندنوشتن از نو
'a'می‌سازدبه آخرش اضافه می‌کندگزارش و لاگ
'x'می‌سازدخطاساخت امن

۸. حالت ساخت امن

# Mode 'x' creates the file, but raises FileExistsError when it already exists.
# It is the safe way of saying "I do not want to overwrite anything".
try:
    with open('i.txt', 'x', encoding='utf-8') as f:
        f.write('Created for the first time.\n')
    print('The file was created.')
except FileExistsError:
    print('The file already exists - nothing was touched.')

حالت x: نمونه 09_create_only.py

حالت 'x' راه صریح گفتن «نمی‌خواهم چیزی را خراب کنم» است. ساختار try اینجا آمده تا خطا برنامه را از کار نیندازد؛ فصل بعد کاملاً درباره همین است.

۹. انکدینگ و متن فارسی

# Text files store bytes; the ENCODING says how those bytes become characters.
# Without encoding='utf-8', Persian text can be unreadable on Windows.
persian = 'سلام، به موسسه کادوس خوش آمدید'

with open('persian.txt', 'w', encoding='utf-8') as f:
    f.write(persian + '\n')

with open('persian.txt', encoding='utf-8') as f:
    print(f.read())

# Always pass encoding='utf-8' when you open a text file, for reading
# and for writing.

انکدینگ: نمونه 10_encoding.py

قانونی که هرگز نباید بشکنید

همیشه encoding='utf-8' بنویسید. اگر ننویسید، پایتون از انکدینگ پیش‌فرض سیستم استفاده می‌کند که در ویندوز فارسی معمولاً UTF-8 نیست. نتیجه، متن به‌هم‌ریخته یا خطای UnicodeDecodeError است. بدتر اینکه برنامه روی کامپیوتر شما کار می‌کند و روی کامپیوتر دیگری خراب می‌شود. در همه کدهای اصلاح‌شده این دوره، این پارامتر اضافه شده است.

۱۰. ماژول os

import os

# Does a file exist?
print(os.path.exists('a.txt'))

# Delete a file - but only after checking, otherwise FileNotFoundError.
if os.path.exists('to_delete.txt'):
    os.remove('to_delete.txt')
else:
    print('to_delete.txt does not exist')

# Where am I?
print('current folder:', os.getcwd())

# What is in this folder?
print(os.listdir('.'))

# Building a path that works on Windows, Linux and macOS.
path = os.path.join('data', 'students', 'list.txt')
print(path)

# Splitting a path.
print(os.path.basename(path))     # list.txt
print(os.path.dirname(path))      # data/students
print(os.path.splitext('app.py')) # ('app', '.py')

# Creating and removing folders.
os.makedirs('demo_folder', exist_ok=True)   # exist_ok avoids an error
print(os.path.isdir('demo_folder'))
os.rmdir('demo_folder')                     # only works on an EMPTY folder

# Size of a file, in bytes.
print(os.path.getsize('a.txt'))

ماژول os: نمونه 11_os_module.py

شرح مثال

۱۱. ماژول pathlib

pathlib جانشین مدرن بیشتر کارهای os.path است. به جای رشته، با یک شیء مسیر کار می‌کنید که خودش همه کارها را بلد است.

# pathlib is the modern replacement for most of os.path.
# A Path object knows how to do almost everything by itself.
from pathlib import Path

path = Path('a.txt')

print(path.exists())
print(path.name)          # a.txt
print(path.stem)          # a
print(path.suffix)        # .txt
print(path.resolve())     # the absolute path

# Reading and writing in one line.
content = path.read_text(encoding='utf-8')
print(content)

Path('hello.txt').write_text('Written with pathlib\n', encoding='utf-8')

# '/' builds a path - much nicer than os.path.join.
data_folder = Path('demo_data')
data_folder.mkdir(exist_ok=True)
(data_folder / 'note.txt').write_text('a note\n', encoding='utf-8')

# Listing the .txt files of the current folder.
for txt_file in Path('.').glob('*.txt'):
    print(txt_file.name, txt_file.stat().st_size, 'bytes')

ماژول pathlib: نمونه 12_pathlib.py

با osبا pathlib
os.path.exists(p)Path(p).exists()
os.path.join(a, b)Path(a) / b
os.path.basename(p)Path(p).name
os.makedirs(p, exist_ok=True)Path(p).mkdir(exist_ok=True)
باز کردن و خواندن با withPath(p).read_text(encoding='utf-8')

استفاده از عملگر تقسیم برای ساختن مسیر در نگاه اول عجیب است ولی خیلی زود به آن عادت می‌کنید. تابع glob('*.txt') هم همه فایل‌های با آن الگو را پیدا می‌کند.

۱۲. برنامه دفترچه یادداشت

برنامه کاملی که همه چیز این فصل و فصل‌های قبل را کنار هم می‌گذارد: منو، حلقه، تابع، لیست و فایل.

'''A complete little program: a note book stored in a text file.'''

import os

NOTES_FILE = 'notes.txt'


def load_notes() -> list[str]:
    'Return the notes stored in the file (an empty list when there is none).'
    if not os.path.exists(NOTES_FILE):
        return []

    with open(NOTES_FILE, encoding='utf-8') as f:
        return [line.strip() for line in f if line.strip() != '']


def save_notes(notes: list[str]) -> None:
    'Write the whole list back to the file.'
    with open(NOTES_FILE, 'w', encoding='utf-8') as f:
        for note in notes:
            f.write(note + '\n')


def show_notes(notes: list[str]) -> None:
    'Print the notes with their number.'
    if len(notes) == 0:
        print('(no note yet)')
        return
    for index, note in enumerate(notes, start=1):
        print(f'{index}. {note}')


def main() -> None:
    notes = load_notes()

    while True:
        print('\n1) Show the notes')
        print('2) Add a note')
        print('3) Delete a note')
        print('0) Exit')

        choice = input('Your choice: ')

        if choice == '1':
            show_notes(notes)
        elif choice == '2':
            text = input('The note: ')
            if text.strip() != '':
                notes.append(text.strip())
                save_notes(notes)
                print('Saved.')
        elif choice == '3':
            show_notes(notes)
            number = input('Number of the note to delete: ')
            if number.isdigit() and 1 <= int(number) <= len(notes):
                removed = notes.pop(int(number) - 1)
                save_notes(notes)
                print('Deleted:', removed)
            else:
                print('Wrong number.')
        elif choice == '0':
            print('Goodbye!')
            break
        else:
            print('Unknown option.')


if __name__ == '__main__':
    main()

دفترچه یادداشت: نمونه 17_notes_app.py

شرح مثال

فایل‌های تمرینی این فصل

پوشه S18-files-and-os شامل نمونه‌های 01_read_whole_file.py تا 17_notes_app.py، به‌علاوه فایل‌های داده a.txt و numbers.txt و friends.txt.

خلاصه فصل

ابزاروظیفه آن
with open(...) as f:تنها شکل درست باز کردن فایل
'r' 'w' 'a' 'x'خواندن، نوشتن از نو، افزودن، ساخت امن
encoding='utf-8'همیشه بنویسیدش، مخصوصاً برای فارسی
for line in fبهترین راه خواندن، بدون پر کردن حافظه
line.strip()پاک کردن \n انتهای هر خط
f.write(text + '\n')نوشتن. خودش خط جدید اضافه نمی‌کند
os.path.existsبررسی وجود پیش از باز کردن
os.path.joinساختن مسیر مستقل از سیستم‌عامل
Path(p) / 'x'همان کار، به شکل مدرن
Path(p).read_text()خواندن کل فایل در یک خط

تمرین‌های فصل هجده

  1. برنامه‌ای بنویسید که محتوای یک فایل را در فایل دیگری کپی کند.
  2. بشمارید یک فایل متنی چند خط، چند کلمه و چند کاراکتر دارد.
  3. numbers.txt را بخوانید و فایل تازه‌ای بسازید که فقط اعداد مثبتش را دارد.
  4. برنامه‌ای بنویسید که هر بار اجرا شود، یک خط با تاریخ و ساعت به فایل گزارش اضافه کند.
  5. فهرستی از نام‌ها را از فایل بخوانید و الفبایی مرتب چاپشان کنید.
  6. برنامه‌ای بنویسید که نام یک پوشه را بگیرد و همه فایل‌های py. داخلش را چاپ کند.
  7. یک مدیر رمز ساده بنویسید که رمزها را به شکل site:password در هر خط فایل نگه دارد.
  8. برنامه دفترچه یادداشت را طوری کامل کنید که بتواند داخل یادداشت‌ها جست‌وجو هم بکند.
فصل قبل: ماژول‌ها و بسته‌ها فصل بعد: خطاها و استثناها