Списочный ввод-вывод и namelist

Списочный I/O печатает и читает данные без явного формата, а namelist превращает группу переменных в самоописывающий конфигурационный блок.

Списочный (list-directed) ввод-вывод — режим print * / read *, где формат выбирает сама библиотека по типам значений. namelist — именованная группа переменных, читаемая и записываемая в самоописывающем формате имя=значение.

Списочный вывод: быстро и небрежно

Звёздочка вместо строки формата — print * или write(*,*) — означает «печатай как сочтёшь нужным». Библиотека сама выбирает ширину и форму для каждого значения, разделяя их пробелами. Это идеально для отладки и черновых распечаток: не нужно подбирать дескрипторы, достаточно перечислить переменные.

program demo_list
  implicit none
  integer :: n = 7
  real    :: x = 2.5, y = -0.001
  character(len=*), parameter :: name = "опыт"

  print *, "n=", n, " x=", x
  print *, "y=", y, " имя=", name
  print *, "массив:", [1, 2, 3, 4]
end program demo_list

Вывод (точная ширина зависит от компилятора):

 n=           7  x=   2.50000000    
 y=  -1.00000005E-03  имя=опыт
 массив:           1           2           3           4

Платой за удобство является непредсказуемость: точное число пробелов и знаков зависит от компилятора, поэтому списочный вывод не годится для файлов, которые читают другие программы по фиксированным позициям, и для отчётов, где важна аккуратная вёрстка. Его ниша — диагностика и неформальный вывод. Заметьте также: ведущий пробел в начале каждой строки — наследие исторического «управления кареткой», его учитывают при сравнении вывода.

Списочный ввод: чтение свободных данных

Зеркальный режим read * разбирает входной поток по разделителям (пробелы, запятые, перевод строки), присваивая значения переменным по порядку. Это самый удобный способ читать данные, записанные «как есть», без жёсткой колоночной структуры.

integer :: k
real    :: a, b
read *, k, a, b      ! вход: "5  1.5, 2.0" → k=5, a=1.5, b=2.0

Списочный ввод понимает несколько приятных соглашений. Разделителем служат пробелы и/или запятые. Повторяющееся значение можно задать как 3*0.0 (три нуля). Звёздочка-«пропуск» r* пропускает позиции, оставляя переменные без изменения. Строку в кавычках читают как символьное значение. Эти правила делают списочный ввод гибким для научных входных файлов, набитых человеком.

namelist: самоописывающая конфигурация

Когда у программы десятки параметров, передавать их позиционным списком неудобно и хрупко: добавили параметр — сместились все позиции. namelist решает это, связывая значения с именами. Сначала объявляют группу: namelist /имя_группы/ список_переменных. Затем эту группу можно целиком записать или прочитать одним оператором, а во входном файле параметры задаются как имя=значение в любом порядке, и лишние можно опускать.

program demo_namelist
  implicit none
  integer :: max_iter = 100
  real    :: tol = 1.0e-6
  logical :: verbose = .false.
  character(len=20) :: method = "gauss"

  namelist /config/ max_iter, tol, verbose, method

  ! записать текущие значения в namelist-формате:
  write(*, nml=config)
end program demo_namelist

Вывод:

&CONFIG
 MAX_ITER=         100,
 TOL=  9.99999997E-07,
 VERBOSE= F,
 METHOD="gauss",
 /

Блок начинается с &CONFIG и заканчивается косой чертой /. Каждая строка — ИМЯ=значение. Этот же текст можно прочитать обратно. Соответствующий входной файл, например run.nml, выглядит так:

&config
  max_iter = 500
  tol = 1.0e-8
  verbose = .true.
/

А программа читает его одним оператором:

integer :: u
open(newunit=u, file="run.nml", status="old", action="read")
read(u, nml=config)
close(u)
! теперь max_iter=500, tol=1e-8, verbose=.true., method остался "gauss"

Ключевые достоинства namelist: параметры именованы (порядок не важен), опущенные сохраняют значения по умолчанию, формат человекочитаем и редактируется в любом текстовом редакторе. Именно поэтому namelist десятилетиями служит стандартным механизмом конфигурации научных кодов — от моделей климата до решателей CFD: входной .nml-файл задаёт сотни параметров запуска, и его удобно править и хранить в системе контроля версий.

Как работает под капотом

Списочный вывод реализуется как форматный, где библиотека сама подбирает дескриптор под тип и значение: для целого выбирает достаточную ширину I, для вещественного — представление, гарантирующее обратную читаемость (часто близкое к полной точности типа). Поэтому списочный вывод вещественных нередко даёт «некрасивые» хвосты вроде 2.50000000 или -1.00000005E-03 — библиотека печатает достаточно цифр, чтобы значение можно было прочитать назад без потерь. namelist устроен как именованный словарь: при записи библиотека для каждой переменной печатает её имя, знак равенства и значение; при чтении — разбирает текст, ищет переменные группы по именам и присваивает. Неизвестное имя во входе обычно вызывает ошибку (зависит от компилятора), а отсутствующее — просто оставляет переменную нетронутой. Это и даёт устойчивость к добавлению параметров: старые входные файлы продолжают работать.

Тонкости списочного ввода: разделители и неполные строки

Списочный ввод обладает рядом полезных, но не всем известных соглашений, понимание которых избавляет от загадочных ошибок чтения. Во-первых, перевод строки эквивалентен разделителю: один оператор read *, a, b, c прочитает три значения, даже если они записаны на трёх отдельных строках входного файла — список ввода «жадно» забирает столько значений, сколько ему нужно, пересекая границы строк. Это удобно, но иногда неожиданно: лишнее значение на строке уйдёт в следующую переменную следующего read. Во-вторых, пустое значение (две запятые подряд 1,,3) означает «оставить эту переменную без изменения» — то есть пропуск, сохраняющий предыдущее или начальное значение. В-третьих, слэш во входе (/) досрочно завершает список ввода: оставшиеся переменные не считываются и сохраняют свои значения. Эти правила превращают списочный ввод в гибкий инструмент для рукописных файлов данных, но требуют дисциплины: смешав привычки разных языков, легко получить сдвиг значений.

Отдельно стоит сказать про строки в списочном вводе. Символьное значение, содержащее пробелы или запятые, обязательно заключают в апострофы или кавычки, иначе разделители «разрежут» его на части. Без кавычек строка читается до первого разделителя. Это частая ловушка при чтении, например, имён файлов или текстовых меток из данных: read *, name на входе my file.dat прочитает в name только my. Правильный вход — 'my file.dat'. Понимание этих мелочей отличает надёжный код чтения данных от хрупкого, ломающегося на «неудобных» входах.

namelist в практике научных кодов

Чтобы оценить, почему namelist десятилетиями остаётся стандартом конфигурации научного ПО, полезно взглянуть на реальный масштаб его применения. Большие модели — атмосферы, океана, плазмы, реакторов — имеют сотни управляющих параметров: шаги сетки, физические константы, флаги включения подмоделей, пути к данным, параметры численных схем. Передавать их аргументами командной строки невозможно, а зашивать в код — значит пересобирать программу при каждом изменении сценария. namelist решает это элегантно: все параметры собраны в один (или несколько) текстовых .nml-файлов, которые задают конкретный прогон. Учёный правит .nml в редакторе, кладёт его рядом с результатами, версионирует в git — и прогон полностью воспроизводим по этому файлу.

! Несколько namelist-групп в одном файле run.nml:
&grid
  nx = 256
  ny = 256
  dx = 0.01
/
&physics
  viscosity = 1.0e-5
  use_gravity = .true.
/
&output
  save_every = 100
  outdir = "results/run42"
/

Программа читает группы по очереди, каждую своим read(u, nml=имя), и параметры аккуратно распределяются по модулям: группа grid настраивает модуль сетки, physics — модуль физики, и так далее. Несколько групп в одном файле — обычная практика, отражающая модульную структуру самого кода. Устойчивость namelist к изменениям тут особенно ценна: добавили в новую версию модели параметр — старые .nml-файлы продолжают работать, новый параметр просто берёт значение по умолчанию. Это позволяет годами накапливать архив входных файлов экспериментов, оставаясь совместимым. Дополнительный плюс — самодокументируемость: записав текущую конфигурацию через write(*, nml=...), программа выдаёт полный снимок всех своих настроек, который можно сохранить в лог прогона. Когда спустя годы нужно понять, с какими параметрами получен тот или иной результат, этот снимок бесценен. Именно сочетание читаемости, версионируемости, устойчивости и самодокументируемости сделало namelist де-факто стандартом, который не вытеснили ни XML, ни JSON, ни YAML — для научного Fortran-кода он остаётся самым естественным и проверенным способом конфигурации.

Выбор механизма ввода под задачу

Подытоживая, выстроим ясную картину того, какой механизм ввода-вывода выбирать под какую задачу, — это практический навык, экономящий массу сил. Для отладочной печати и быстрых черновых распечаток — списочный вывод print *: минимум усилий, не нужно подбирать формат, результат читаем человеком. Для чтения свободных рукописных данных (несколько чисел, простые входные файлы) — списочный ввод read *: гибко разбирает значения по разделителям. Для конфигурации программы с множеством параметров — namelist: именованные параметры, порядок не важен, значения по умолчанию, устойчивость к добавлению, человекочитаемость и версионируемость. Для аккуратных отчётов и файлов фиксированного формата — форматный вывод с дескрипторами: точный контроль вёрстки. Для сохранения больших данных без потери точности — неформатный или stream-вывод: скорость и битовая точность. Для переносимого обмена научными данными между системами — специализированные форматы (NetCDF, HDF5): самоописываемость и независимость от платформы. Эта таблица соответствий — часть профессиональной грамотности вычислителя.

ЗадачаМеханизм
Отладка, черновая печатьсписочный print *
Чтение свободных данныхсписочный read *
Конфигурация программыnamelist
Отчёты, фиксированный форматформатный (дескрипторы)
Сохранение больших данныхнеформатный / stream
Переносимый обменNetCDF / HDF5

Главная ошибка новичков — применять один механизм для всего: например, форматировать вручную там, где идеален namelist, или использовать списочный вывод для файла, который потом читает другая программа по позициям. Понимание, что у Fortran есть богатый арсенал средств ввода-вывода, каждое со своей нишей, и умение выбрать подходящее под конкретную потребность — это то, что отличает зрелый расчётный код. Списочный ввод-вывод и namelist, рассмотренные в этом уроке, занимают в этом арсенале важные, но конкретные ниши: первый — для удобства и черновиков, второй — для конфигурации; и применять их стоит именно там, оставляя точную вёрстку форматному выводу, а объёмные данные — бинарным форматам.

Частые ошибки

  • Использовать списочный вывод для межпрограммного обмена. Его ширина и форма зависят от компилятора; для файлов, читаемых по позициям, нужен форматный или неформатный I/O.
  • Забыть завершающий / в namelist-файле. Блок namelist обязан кончаться косой чертой; без неё чтение зависнет в ожидании конца группы.
  • Опечатка в имени переменной namelist. Имя во входе должно точно совпадать с объявленным (регистр обычно не важен); неизвестное имя часто вызывает ошибку чтения.
  • Полагать, что namelist сохраняет точность как бинарный файл. Это текст; для точного сохранения состояния между прогонами используйте неформатный I/O.
  • Объявлять namelist-переменные внутри процедуры без save, ожидая, что значения по умолчанию переживут вызовы. Помните об области видимости и времени жизни переменных.

Итоги

  • Списочный I/O (print */read *) удобен и краток — идеален для отладки и свободных данных, но непредсказуем по форме.
  • Списочный ввод понимает пробелы/запятые-разделители, повторители n*v и пропуски — гибок для рукописных входных файлов.
  • namelist связывает значения с именами: блок &имя ... / с парами имя=значение, порядок не важен, опущенные берут значения по умолчанию.
  • namelist — стандартный механизм конфигурации научных программ: человекочитаемый, версионируемый, устойчивый к добавлению параметров.
  • Ни списочный, ни namelist не сохраняют двоичную точность — для этого нужен неформатный вывод.
Проверьте себя
1. Почему списочный вывод (print *) не подходит для файлов, читаемых другими программами по фиксированным колонкам?
AОн работает медленнее форматного
BТочное число пробелов и форма чисел зависят от компилятора и непредсказуемы
CОн не умеет выводить вещественные числа
DОн всегда обрезает данные
2. Какое ключевое преимущество namelist перед позиционным списочным вводом для конфигурации?
Anamelist хранит данные в двоичном виде
BПараметры именованы: порядок не важен, опущенные берут значения по умолчанию
Cnamelist быстрее читается
Dnamelist не требует объявления переменных