Рубріки: Теория

Лучшая практика: работа с путями и файлами в Python

Игорь Грегорченко

Это не совсем обычный пост по «питону». Здесь мы не только решаем частую проблему при работе с путями и файлами в Python, объясняя, как это сделать максимально правильно. Здесь мы также попытаемся рассказать, как мыслит опытный программист, наглядно покажем, как постепенно он дорабатывает свой код. Увидев и поняв, как это работает, вы получите возможность значительно поднять свой профессиональный уровень. Не верите? Прочитайте и попробуйте!

Этот большой пост – вольный перевод вот этой оригинальной статьи (с нашими дополнениями в местах, где это показалось нужным), которую написал Майкл Аллгувер. В ней автор со знанием дела делится своим стилем программирования, рассказывая о правильном подходе к Python.

Проблема: перечисление папок и дисков

Недавно во время работы над проектом коллега спросил меня, можно ли в Python вывести список содержимого дисков. Конечно, можно. Более того, поскольку это совсем не сложно, я хотел бы рассмотреть этот случай подробно, чтобы проиллюстрировать лучшие практики, рекомендуемые для работы с путями к дискам.

Поверьте, это не голая теория — подобные вещи очень часто встречаются в реальной жизни. Давайте посмотрим, как эти проблемы решать правильно.

Шаг 1: Как ввести правильный путь?

Предполагая, что вы хотите точно получить листинг файлов от конкретного пути, начнем с выбора пользовательского каталога в системе Windows 10:

path_dir: str = "C:\Users\sselt\Documents\blog_demo"

Переменные, назначенные при выполнении, немедленно вызывают ошибку:

SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape

Интерпретатор не понимает последовательность символов \U, так как она инициирует символы Unicode аналогичной последовательности. Эта распространенная проблема, которую постоянно гуглят тысячи начинающих питонистов, возникает потому, что в системе Windows в качестве разделителя путей используется обратная косая черта «\» (бэкслеш), а в Linux — обычная косая черта «/» (слеш).

К сожалению, поскольку разделитель Windows также является инициатором для различных специальных символов или escape-последовательностью в Unicode, это явно все запутывает и сбивает с толку. Один и тот же код работает на одних системах и творит чудеса на вторых, хотя внешне все выглядит одинаково.

Так же, как мы не ожидаем в ближайшее время согласованности в использовании десятичных разделителей в разных странах, наш единственный выбор — это одно из трех решений.

Решение 1 — отвратительный вариант

Просто избегайте разделителя Windows и вместо этого пишите путь, используя только разделители Linux:

path_dir: str = "C:/Users/sselt/Documents/blog_demo"

После этого интерпретатор распознает правильный путь, считая, что это Linux-система. Как минимум на серверных системах это должно работать как надо… Но универсальностью здесь и не пахнет.

Решение 2 — еще более отвратительный вариант

Давайте не будем останавливаться, и покажем, до чего порой додумываются некоторые новички, решая эту проблему.

Используйте экранирующие последовательности — в интернете полно подобных советов.

path_dir: str = "C:\\Users\sselt\Documents\\blog_demo"

Помимо неразборчивости такого кода, меня беспокоит то, что не нужно использовать экранирующие последовательности в каждой комбинации символов-разделителей, а только перед «U» и «b».

Слабо держать все это под контролем (особенно если ваша программа размером на несколько десяток тысяч строк кода)?

Решение 3 — элегантное

Используйте необработанные строки с «r» в качестве префикса, чтобы указать, что специальные символы не должны оцениваться.

path_dir: str = r"C:\Users\sselt\Documents\blog_demo"

Как тебе такое решение, Маск? Все просто и универсально. Попробуйте найти в интернете аналог моего решения — это будет сложно, поверьте.

Шаг 2: Сканирование файлов

Вернемся к нашей типичной задаче — перечислить все элементы в папке. Мы уже знаем путь, а также то, как его правильно записать без косяков.

Простая команда os.listdir перечисляет все строки, то есть только имена файлов по пути. Здесь и во всех других примерах я использую подсказки типов для дополнительного документирования кода, что сразу приучает нас писать качественно. Этот синтаксис стал доступен, начиная с Python 3.5.

import os

from typing import List

path_dir: str = r"C:\Users\sselt\Documents\blog_demo"

content_dir: List[str] = os.listdir(path_dir)

Отображение файлов в порядке, но меня больше интересует статистика файлов, для которой у нас есть os.stat.

Шаг 3: Конкатенация путей

Продираемся дальше в решение вроде бы простой и частой задачи.

Чтобы передать путь к файлу, мы должны сначала объединить имя файла и путь. Я часто встречал следующие конструкции в гугле и даже использовал их, когда был молодой. Например, распространены такие варианты:

path_file: str = path_dir + "/" + filename                         
path_file: str = path_dir + "\\" + filename                         
path_file: str = "{}/{}".format(path_dir, filename)                         
path_file: str = f"{path_dir}/{filename}"

Первые два варианта сверху — отвратительны, потому что они конкатенируют строки со знаком «+», который в новом Python не нужен.

Особенно все это отвратительно, потому что в Windows нужен двойной разделитель, иначе он будет оценен как управляющая последовательность для закрывающей кавычки. Что создает непредсказуемый «оопс» в процессе эксплуатации такого кода.

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

Смотрите сами:

filename = "some_file"

print("{}/{}".format(path_dir, filename))

...: 'C:\\Users\\sselt\\Documents\\blog_demo/some_file'

Правильное решение, не зависящее от ОС

Решение из Python — os.sep или os.path.sep. Оба возвращают разделитель путей соответствующей системы. Функционально они идентичны, но второй, более явный синтаксис сразу показывает задействованный разделитель.

Это означает, что можно написать так:

path_file = "{}{}{}".format(path_dir, os.sep, filename)

Результат будет лучше, но за счет сложного кода, если вы хотите объединить несколько сегментов пути. Поэтому принято объединять элементы пути через конкатенацию строк. Это еще короче и универсальнее:

path_file = os.sep.join([path_dir, filename])

В качестве домашнего задания погуглите триллионов решений в гугле для этой задачи и сравните с нашим подходом — теперь вы сразу увидите косяки во множестве чужих решений.

Первый полный запуск

Перейдем к каталогу:

for filename in os.listdir(path_dir):
    path_file = os.sep.join([path_dir, filename])

    print(os.stat(path_file))

Один из результатов — st_atime — время последнего обращения к файлу, st_mtime — время последней модификации и st_ctime — время создания. Кроме того, st_size дает размер файла в байтах. В данный момент мне нужны только размер и дата последней модификации, поэтому я решил сохранить простой формат списка.

import os

from typing import List, Tuple


filesurvey: List[Tuple] = []

content_dir: List[str] = os.listdir(path_dir)

for filename in content_dir:

    path_file = os.sep.join([path_dir, filename])

    stats = os.stat(path_file)

    filesurvey.append((path_dir, filename, stats.st_mtime, stats.st_size))

Конечная функция с рекурсией

Полученный результат поначалу кажется удовлетворительным, большинство на этом бы и остановилось. Но почесав как следует репу, мы осознаем, что возникают две новые проблемы. Listdir не делает различий между файлами и папками, обращается только к уровню папок и не обрабатывает вложенные папки.

Следовательно, нам нужна рекурсивная функция, которая различает файлы и папки и спускается вниз для сканирования низких уровней. os.path.isdir как раз проверяет, есть ли папка ниже пути.

def collect_fileinfos(path_directory: str, filesurvey: List[Tuple]):

    content_dir: List[str] = os.listdir(path_directory)

    for filename in content_dir:

        path_file = os.sep.join([path_directory, filename])

        if os.path.isdir(path_file):

            collect_fileinfos(path_file, filesurvey)

        else:

            stats = os.stat(path_file)

            filesurvey.append((path_directory, filename, stats.st_mtime, stats.st_size))


filesurvey: List[Tuple] = []

collect_fileinfos(path_dir, filesurvey)

 

Сделать результаты более полезными

Готово! Мы решили проблему менее чем за 10 строк. Проблема только в том, что если мы запустим такой скрипт на большой файловой системе, то мы получим большой (нет, ОГРОМНЫЙ) монотонный вывод из строчек. Как все это понять и переварить в реальном мире? Я сразу обещал, что мы решаем не академические задачки, а разбираем решения из реального мира.

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

import pandas as pd

df: pd.DataFrame = pd.DataFrame(filesurvey, columns=('path_directory', 'filename', 'st_mtime', 'st_size'))

… но, к сожалению, и это не самая лучшая практика.

Я знаю, этот пост сразу обещал решать проблемы с помощью только лучших практик. Поэтому придется снова все переписать.

Далее я собираюсь снова обратиться к этому сценарию и решить его по-настоящему элегантно.

Одна и та же проблема: перечисление папок и дисков

Выше мы использовали рекурсивную функцию для решения, состоящего менее чем из 10 строк, для сканирования папок и обеспечения возможности оценки файлов по дате модификации и размеру. Теперь я собираюсь несколько поднять планку для этого примера, показав лучшие альтернативы.

Конкатенация пути с помощью Pathlib

Старое вино в новых бутылках? Решение предыдущего примера с помощью склеивания путей было следующим:

path_file = os.sep.join([path_dir, filename])

Преимущество этого способа в том, что решение не зависит от операционной системы, и не нужно объединять строки знаком «+» или как-то париться с их форматированием.

Однако это чревато ошибками, поскольку можно случайно или по ошибке определить путь к каталогу с закрывающим разделителем путей.

path_dir: str = r"C:/Users/sselt/Documents/blog_demo/"  # abschließender Trenner

filename: str = "some_file"

path_file = os.sep.join([path_dir, filename])

# C:/Users/sselt/Documents/blog_demo/\some_file

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

Начиная с Python 3.4, появилось лучшее решение — модуль pathlib. Он обрабатывает функции файлов и папок модуля os в Python с помощью объектно-ориентированного подхода.

Напомню старый вариант:

import os

path = "C:/Users/sselt/Documents/blog_demo/"

os.path.isdir(path)

os.path.isfile(path)

os.path.getsize(path)

А вот новая альтернатива:

from pathlib import Path

path: Path = Path("C:/Users/sselt/Documents/blog_demo/")

path.is_dir()

path.is_file()

path.stat().st_size

Оба варианта дают абсолютно одинаковый результат. Так почему же второй вариант намного лучше? Давайте пораскинем мозгами.

Объектно-ориентированный и более устойчивый к ошибкам

Вызовы в основном объектно-ориентированы, и это может быть или не быть вашим предпочтением — но мне так нравится гораздо больше. Здесь у нас есть объект, например, определение пути, у которого есть атрибуты и методы. Это правильный подход для энтерпрайз-программирования.

Однако пример, примененный здесь для перегрузки операторов, более интересен:

filename: Path = Path("some_file.txt")

path: Path = Path("C:/Users/sselt/Documents/blog_demo")

print( path / filename )

# C:\Users\sselt\Documents\blog_demo\some_file.txt

Сначала разделение на два пути кажется недопустимым. Однако объект path был просто перегружен таким образом, что функционирует как объединяющий путь.

В дополнение к этому синтаксическому сахару, объекты path будут перехватывать и другие типичные ошибки:

filename: Path = Path("some_file.txt")

path: Path = Path("C:/Users/sselt/Documents/blog_demo/")

path: Path = Path("C:/Users/sselt/Documents/blog_demo//")

path: Path = Path("C:\\Users/sselt\\Documents/blog_demo")  

print(path/filename)

# C:\Users\sselt\Documents\blog_demo\some_file.txt

Этот вариант не только красивее, но и устойчивее к ложным вводам. В дополнение к другим преимуществам код также не зависит от операционной системы. Определяется только общий объект пути, который в системе Windows выглядит как WindowsPath, а в системе Linux — как PosixPath.

Большинство функций, которые обычно ожидают строку в качестве пути, могут работать непосредственно с путем. В редких случаях вам может понадобиться разрешить объект просто с помощью str(Path).

Обработка пути с помощью os.walk

В моем последнем решении я использовал os.listdir, os.path.isdir и рекурсивную функцию для итерации по дереву путей и различения папок и файлов.

Но os.walk предлагает лучшее решение. Этот метод создает не список, а итератор, который можно вызывать построчно. Результат содержит соответствующий путь к папке и список всех файлов данных в пределах этого пути. Все это происходит рекурсивно, так что вы получаете все файлы одним вызовом. Не правда ли круто?

Лучшее решение с os.walk и Pathlib

Если вы объедините две вышеупомянутые техники, вы получите более простое решение, полностью независимое от ОС, более устойчивое к непоследовательным форматам путей и свободное от явных рекурсий:

filesurvey = []

for row in os.walk(path):   # row beinhaltet jeweils einen Ordnerinhalt

    for filename in row[2]:  # row[2] ist ein tupel aus Dateinamen

        full_path: Path = Path(row[0]) / Path(filename)   # row[0] ist der Ordnerpfad

        filesurvey.append([path, filename, full_path.stat().st_mtime, full_path.stat().st_size])

 

Подведение итогов: методологическая задача поста

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

Если вы можете дополнить этот пример лучшей практикой, не стесняйтесь, напишите коммент под этим постом.

И в заключение самое главное — этот пост не про сканирование файлов и папок, хотя именно этим мы и занимались. Помимо решения задачи, я попытался показать, КАК ДУМАЕТ программист, когда развивает свое решение, которое, как правило, улучшает последовательными итерациями, постепенно достигая некоего приемлемого уровня стабильности.

Это приходит с опытом, но теперь увидев как это работает, вы посмотрите на чужой код из интернета совсем другими глазами — глазами человека, который учится предугадывать дефекты и видеть неявные слабые места. А не просто бездумно копипастит чужие примеры, «потому что они ведь работают». Возможно, такой подход усложнит вам жизнь и замедлит в плане работы, но зато именно он и даст толчок к вашему профессиональному росту как программиста.

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023