open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 02 · 01

Файлы и аргументы командной строки

Настоящий Python-скрипт читает аргументы командной строки, открывает файлы в with-блоке, который закрывает их даже при исключении, потоково читает большие файлы построчно и завершается кодом возврата, на который реагирует shell.

PY Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Ночная задача, которая «работала на моей машине», портит каждое имя с диакритикой в проде. Причина — одна строка: open("names.csv"). На macOS разработчика open по умолчанию брал UTF-8; на Linux-сервере под локалью C — почти-ASCII latin-1, и José превратилось в мусор. Через два дня другой скрипт падает на середине записи и оставляет недосброшенный, обрезанный выходной файл, потому что дескриптор никто не закрыл. Оба бага — один и тот же урок: у файлового и CLI-ввода-вывода в Python есть умолчания, которые тихо различаются на разных машинах, и скрипт, который их игнорирует, — это скрипт, который падает в CI, а не на твоём ноутбуке.

К концу урока ты будешь знать, почему оба продовых бага случаются, в чём однострочный фикс для каждого и как сшить их в скрипт, который ведёт себя одинаково на любой машине.

with: закрытие, которое переживает исключение

open() возвращает файловый объект, держащий ресурс ОС — файловый дескриптор и буфер записи. Если его не закрыть, гниют две вещи. Дескриптор течёт (ОС ограничивает, сколько их можно держать; долгоживущий процесс рано или поздно упирается в Too many open files), а у файла на запись твои буферизованные байты могут так и не дойти до диска — вывод тихо обрезается. Наивный фикс — f.close() в конце, но если что-то между open и close бросит исключение, строка close пропускается, и ты всё равно течёшь. Это тот же провал, что и в JavaScript-коде, который забыл finally.

Инструкция with решает это структурно. open() — это менеджер контекста: он определяет __enter__ (выполняется на входе) и __exit__ (на выходе). Python гарантирует, что __exit__ отработает, когда блок завершается по любой причине — обычный проход, return или брошенное исключение, выходящее наружу. Для файла __exit__ вызывает close(), который сбрасывает буфер и освобождает дескриптор. В этой гарантии весь смысл: ты получаешь корректную очистку без try/finally и без надежды на то, что вспомнишь сам.

# Течёт дескриптор и может обрезать вывод, если transform() бросит исключение:
f = open("out.txt", "w", encoding="utf-8")
f.write(transform(data))   # если тут исключение, close() ниже не выполнится
f.close()

# Правильно: __exit__ выполняется (и закрывает/сбрасывает), даже если transform() бросит:
with open("out.txt", "w", encoding="utf-8") as f:
    f.write(transform(data))
# файл закрыт и сброшен здесь, с исключением или без
Почему это работает

«Закрыт при исключении» не значит «глотает исключение». Простой with выполняет __exit__ (закрывая файл), а затем даёт исключению лететь дальше — очистка происходит, но ошибка всё равно роняет скрипт, что тебе и нужно. Подавляет ошибку только тот менеджер контекста, чей __exit__ явно возвращает истинное значение, а у open() он так не делает. Так что with даёт и гарантированную очистку, и честные падения.

Текстовый режим против бинарного и кодировка, которую надо назвать

Почему это важнее, чем кажется? Потому что твой скрипт будет деплоиться в контейнеры, CI-пайплайны и на машины коллег — ни одну из которых ты не контролируешь. У open() два режима. Текстовый (по умолчанию, "r" / "w") декодирует байты в str при чтении и кодирует str в байты при записи, используя кодировку. Бинарный ("rb" / "wb") даёт сырые bytes без декодирования — то, что нужно для изображений, архивов и любой не-текстовой нагрузки.

Ловушка — кодировка по умолчанию. Когда ты опускаешь encoding=, текстовый режим берёт locale.getpreferredencoding(), а она зависит от локального окружения машины — UTF-8 на типичном Mac, но, возможно, cp1252 на Windows или ascii/latin-1 под голой локалью C в контейнере. Тогда один и тот же файл декодируется по-разному на разных хостах — ровно тот прод-баг из хука. Правило безусловно: для текстовых файлов всегда передавай encoding="utf-8" (или ту кодировку, которой файл реально является). Сделай кодировку явной — и скрипт поведёт себя одинаково везде.

# Читается корректно на любом хосте, независимо от локали:
with open("names.csv", "r", encoding="utf-8") as f:
    header = f.readline()

# Бинарно: без декодирования, ты получаешь bytes:
with open("logo.png", "rb") as f:
    magic = f.read(8)        # b'\x89PNG\r\n\x1a\n'

Паттерны чтения: не грузи файл на 4 ГБ в RAM

Как ты читаешь важно не меньше того, что ты читаешь. f.read() возвращает весь файл одной строкой — нормально для маленького конфига, катастрофа для многогигабайтного лога, потому что тянет всё в память разом. f.readlines() так же жаден: он строит список всех строк в памяти. Безопасный по памяти паттерн — итерировать файловый объект напрямую: for line in f: — он потоково отдаёт по одной строке за раз и не держит больше одной строки плюс буфер, так что файл на 4 ГБ обрабатывается в постоянной памяти.

# НЕ ДЕЛАЙ на больших файлах — оба грузят всё в RAM:
data = f.read()              # весь файл одной str
lines = f.readlines()        # список всех строк

# ДЕЛАЙ — потоково, построчно, постоянная память:
with open("app.log", "r", encoding="utf-8") as f:
    for line in f:           # по одной строке за раз
        if "ERROR" in line:
            print(line, end="")

pathlib.Path: пути как объекты, а не строки

Манипулировать путями через конкатенацию строк — это способ отгрузить "logs" + "/" + name, который ломается на Windows и сдваивает слэши на краевых случаях. pathlib.Path (современный, с 3.4) моделирует путь как объект с операторами и методами, заменяя большинство старых строковых функций os.path. Оператор / склеивает сегменты переносимо, а Path несёт .exists(), .read_text(), .glob() и компанию.

from pathlib import Path

src = Path("data") / "input.csv"     # os.path.join(..) — но читаемо и кроссплатформенно
if src.exists():
    text = src.read_text(encoding="utf-8")   # open+read+close одним вызовом
out = src.with_suffix(".out.csv")            # data/input.out.csv

Если ты из Node, отображение прямое: path.join становится оператором /, fs.existsSync — это .exists(), а fs.readFileSync(p, "utf8") — это Path(p).read_text(encoding="utf-8").

CLI-ввод: sys.argv, затем переход на argparse

Скрипт получает аргументы из sys.argv — списка, где sys.argv[0] — имя скрипта, а остальное — слова пользователя. Разбирать его руками нормально для разовой задачи, но плохо масштабируется: ни --флагов, ни преобразования типов, ни сообщения об использовании, а выход за границы индекса — это необработанный IndexError.

import sys
# Хрупко: только позиционные, падает на пропущенном аргументе, всё — str
infile = sys.argv[1]          # IndexError, если пользователь его забыл
limit = int(sys.argv[2])      # ValueError на плохом вводе, без подсказки

Правильный инструмент в момент, когда у скрипта появляются реальные опции, — argparse (стандартная библиотека). Ты объявляешь позиционные и --флаги, задаёшь им type= для преобразования, а argparse парсит sys.argv, валидирует, преобразует типы, генерирует -h/--help, а при неверном вызове печатает usage в stderr и завершается кодом 2 — всё бесплатно.

import argparse

parser = argparse.ArgumentParser(description="Filter and transform a CSV.")
parser.add_argument("infile", help="path to the input CSV")          # позиционный
parser.add_argument("-o", "--out", help="output path (default: stdout)")
parser.add_argument("-n", "--limit", type=int, default=0,            # авто-преобразование в int
                    help="max rows to emit (0 = all)")
args = parser.parse_args()    # валидирует, заполняет умолчания, выходит с 2 при ошибке
# args.infile, args.out, args.limit готовы к использованию

Грубо говоря, process.argv плюс библиотека флагов в Node схлопываются здесь в argparse — разбор аргументов, приведение типов и экран --help в одном объявлении.

stdin/stdout/stderr и код возврата, который читает CI

Когда ты пишешь скрипт, тот, кто его запускает, — часто не человек, а cron, Makefile или шаг в GitHub Actions. Хорошо ведущий себя CLI-инструмент участвует в работе shell. Обычный вывод идёт в stdout (print() пишет туда по умолчанию), чтобы его можно было передать по пайпу. Диагностика — ошибки, прогресс, предупреждения — идёт в stderr (print(..., file=sys.stderr)), чтобы не портить данные в пайпе. И скрипт завершается кодом возврата: 0 — успех, любое ненулевое — провал. Shell отдаёт его как $?, цепочки && ветвятся на нём, а CI помечает шаг проваленным при ненулевом — так что код возврата это то, как любой автоматический вызывающий узнаёт, сработал ли твой скрипт.

sys.exit(code) его устанавливает. Передай int как статус; передай строку — Python печатает её в stderr и завершается кодом 1. Возврат из main() без вызова sys.exit завершает с 0. Непойманное исключение завершает кодом 1 и трейсбэком в stderr.

import sys

if not args.infile:
    print("error: no input file", file=sys.stderr)   # диагностика → stderr
    sys.exit(2)                                       # ненулевое → вызывающий видит провал
print(result)                                         # данные → stdout
sys.exit(0)                                           # явный успех
Node / JSPythonЗамечание
fs.readFileSync(p, “utf8”)open(p, encoding=“utf-8”).read()Оберни в with; всегда называй кодировку
createReadStream + разбивка строкfor line in f:Потоково читает большие файлы в постоянной памяти
process.argvsys.argv / argparseargv[0] — имя скрипта; argparse для реальных флагов
path.join, fs.existsSyncPath / ”..”, .exists()pathlib моделирует пути как объекты
console.errorprint(…, file=sys.stderr)Держи диагностику вне stdout
process.exit(code)sys.exit(code)0 = ок; ненулевое = провал для shell/CI

Собери части вместе — и настоящий скрипт это конвейер: разобрать аргументы, прочитать вход, преобразовать, записать выход (файл или stdout) и завершиться статусом, на который вызывающий может ветвиться.

Викторина

Почему `with open(...)` предпочтительнее, чем `f = open(...)` и потом `f.close()`?

Викторина

Скрипт вызывает sys.exit(0), хотя файла не было и он напечатал ошибку. Что ломается?

Выбери лучший вариант

Нужно обработать лог приложения на 6 ГБ, оставив только строки ERROR. Как его читать?

Вспомните перед уходом
  1. 01
    Объясни точно, почему `with open(...)` закрывает файл при брошенном исключении, и что пошло бы не так с обычной парой open()/close().
  2. 02
    Что сообщает код возврата скрипта, как его выставить в Python и почему он важнее напечатанного сообщения?
Итог

Скрипт промышленного уровня относится к файловому и CLI-вводу-выводу как к тому, что должно вести себя одинаково на каждом хосте, а не только на твоём ноутбуке. Открывай файлы под with-блоком: open() — менеджер контекста, чей __exit__ отрабатывает — закрывая и сбрасывая файл — когда блок завершается по любой причине, включая выходящее наружу исключение, так что ты никогда не утечёшь дескриптор и не отгрузишь обрезанный вывод. Всегда передавай encoding="utf-8" для текста, потому что кодировка по умолчанию зависит от локали машины и порождает целый класс багов «работает на моём Mac, ломается в контейнере»; к бинарному режиму тянись только для не-текстовой нагрузки. Большие файлы читай, итерируя файловый объект построчно, а не через read()/readlines(), которые грузят всё в память. Используй pathlib.Path, чтобы строить пути как объекты оператором / вместо хрупкой конкатенации строк. Бери аргументы через argparse, как только появляются реальные флаги — он парсит, преобразует типы, валидирует и генерирует --help бесплатно, — откатываясь на сырой sys.argv только для одноразовых. Наконец, направляй данные в stdout, а диагностику — в stderr и вызывай sys.exit с ненулевым кодом при провале, чтобы shell, цепочки && и CI могли ветвиться на том, сработал ли твой скрипт. Теперь, когда встретишь скрипт, который в проде молча портит имена или оставляет полузаписанный вывод, — ты знаешь, за какими двумя строками тянуться первым делом.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.