Прощай, os.path: 15 хитростей Pathlib для быстрого освоения файловой системы на Python

Pathlib, возможно, моя любимая библиотека (очевидно, после Sklearn). А учитывая, что в мире насчитывается более 130 тысяч библиотек, это о чём-то да говорит. Pathlib помогает мне превратить подобный код, написанный в os.path

import os

dir_path = "/home/user/documents"

# Find all text files inside a directory
files = [os.path.join(dir_path, f) for f in os.listdir(dir_path) \
        if os.path.isfile(os.path.join(dir_path, f)) and f.endswith(".txt")]

…в это:

from pathlib import Path

# Find all text files inside a directory
files = list(dir_path.glob("*.txt"))

Pathlib появился в Python 3.4 в качестве замены кошмара, которым был os.path. Это также стало важной вехой для языка Python в целом: они, наконец, превратили каждую отдельную вещь в объект.

Самый большой недостаток os.path заключался в рассмотрении системных путей как строк, что приводило к нечитаемому, беспорядочному коду и крутой кривой обучения.

Представляя пути как полноценные объекты, Pathlib решает все эти проблемы и привносит элегантность, согласованность и глоток свежего воздуха в обработку путей.

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

Изучение этих функций Pathlib упростит вам, как специалисту по обработке данных, всё, что связано с путями и файлами, особенно во время рабочих процессов обработки данных, когда вам приходится перемещать тысячи изображений, CSV-файлов или аудиофайлов.

Давайте начнём!

Работа с путями

1. Создание путей

Почти все функции pathlib доступны через его класс Path, который вы можете использовать для создания путей к файлам и каталогам.

Есть несколько способов, которыми вы можете создавать пути с помощью Path. Во-первых, существуют методы класса, такие как cwd и home, для текущего рабочего каталога и домашнего каталога пользователя:

from pathlib import Path

Path.cwd()
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib')
Path.home()
PosixPath('/home/bexgboost')

Вы также можете создавать пути из строковых путей:

p = Path("documents")

p
PosixPath('documents')

Объединение путей в Pathlib очень просто реализовать с помощью слэша:

data_dir = Path(".") / "data"
csv_file = data_dir / "file.csv"

print(data_dir)
print(csv_file)
data
data/file.csv

Пожалуйста, не позволяйте никому когда-либо поймать вас на использовании os.path.join после этого.

Чтобы проверить, существует ли путь, вы можете использовать логическую функцию exists:

data_dir.exists()
True
csv_file.exists()
True

Иногда весь объект Path не будет виден, и вам придётся проверить, является ли он каталогом или файлом. Итак, вы можете использовать функции is_dir или is_file для этого:

data_dir.is_dir()
True
csv_file.is_file()
True

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

csv_file.absolute()
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/data/file.csv')

Наконец, если вам не повезло работать с библиотеками, которым всё ещё требуются строковые пути, вы можете вызвать функцию str(path):

str(Path.home())
'/home/bexgboost'

Большинство библиотек в стеке данных уже давно поддерживают объекты Path, включая sklearn, pandas, matplotlib, seaborn и т.д.

2. Атрибуты пути

Объекты Path обладают множеством полезных атрибутов. Давайте посмотрим несколько примеров использования объекта path, который указывает на файл изображения:

image_file = Path("images/midjourney.png").absolute()

image_file
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/images/midjourney.png')

Давайте начнём с parent. Он возвращает объект path, который находится на один уровень выше текущего рабочего каталога.

image_file.parent
PosixPath('/home/bexgboost/articles/2023/4_april/1_pathlib/images')

Иногда вам может понадобиться только имя файла (name) вместо полного пути. Для этого есть атрибут…

image_file.name
'midjourney.png'

…который возвращает только имя файла с расширением.

Существует также stem для имени файла без суффикса:

image_file.stem
'midjourney'

Или suffix для обозначения расширения файла:

image_file.suffix
'.png'

Если вы хотите разделить путь на его компоненты, вы можете использовать parts вместо str.split('/'):

image_file.parts
('/',
 'home',
 'bexgboost',
 'articles',
 '2023',
 '4_april',
 '1_pathlib',
 'images',
 'midjourney.png')

Если вы хотите, чтобы эти компоненты сами по себе были объектами Path, вы можете использовать атрибут parents, который создает генератор:

for i in image_file.parents:
    print(i)
/home/bexgboost/articles/2023/4_april/1_pathlib/images
/home/bexgboost/articles/2023/4_april/1_pathlib
/home/bexgboost/articles/2023/4_april
/home/bexgboost/articles/2023
/home/bexgboost/articles
/home/bexgboost
/home
/

Работа с файлами

Прощай, os.path: 15 хитростей Pathlib для быстрого освоения файловой системы на Python

Чтобы создавать файлы и записывать в них данные, вам больше не нужно использовать функцию open. Просто создайте объект Path и write_text или write_btyes для них:

markdown = data_dir / "file.md"

# Create (override) and write text
markdown.write_text("# This is a test markdown")

Или, если у вас уже есть файл, вы можете использовать read_text или read_bytes:

markdown.read_text()
'# This is a test markdown'
len(image_file.read_bytes())
1962148

Однако, обратите внимание, что write_text или write_bytes переопределяют существующее содержимое файла.

# Write new text to existing file
markdown.write_text("## This is a new line")
# The file is overridden
markdown.read_text()
'## This is a new line'

Чтобы добавить новую информацию к существующим файлам, вы должны использовать метод open объектов Path в режиме a (добавить):

# Append text
with markdown.open(mode="a") as file:
    file.write("\n### This is the second line")

markdown.read_text()
'## This is a new line\n### This is the second line'

Также часто используется переименование файлов. Метод rename принимает путь назначения для переименованного файла.

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

renamed_md = markdown.with_stem("new_markdown")

markdown.rename(renamed_md)
PosixPath('data/new_markdown.md')

Выше показано, как file.md превращается в new_markdown.md .

Давайте посмотрим размер файла с помощью stat().st_size:

# Display file size
renamed_md.stat().st_size
49 # in bytes

или последний раз, когда файл был изменён, что произошло несколько секунд назад:

from datetime import datetime

modified_timestamp = renamed_md.stat().st_mtime

datetime.fromtimestamp(modified_timestamp)
datetime.datetime(2023, 4, 3, 13, 32, 45, 542693)

st_mtime возвращает временную метку, которая является отсчётом секунд с 1 января 1970 года. Чтобы сделать его читаемым, вы можете использовать функцию fromtimestamp.

Чтобы удалить ненужные файлы, вы можете разорвать с ними связь (unlink):

renamed_md.unlink(missing_ok=True)

Установка missing_ok в значение True не вызовет никаких сигналов тревоги, если файл не существует.

Работа с каталогами

Прощай, os.path: 15 хитростей Pathlib для быстрого освоения файловой системы на Python

Есть несколько изящных приёмов для работы с каталогами в Pathlib. Сначала давайте посмотрим, как создавать каталоги рекурсивно.

new_dir = (
    Path.cwd()
    / "new_dir"
    / "child_dir"
    / "grandchild_dir"
)

new_dir.exists()
False

new_dir не существует, поэтому давайте создадим его со всеми его дочерними элементами:

new_dir.mkdir(parents=True, exist_ok=True)

По умолчанию mkdir создаёт последний дочерний элемент заданного пути. Если промежуточные родители не существуют, вы должны установить для parents значение True.

Чтобы удалить пустые каталоги, вы можете использовать rmdir. Если указанный объект path является вложенным, удаляется только последний дочерний каталог:

# Removes the last child directory
new_dir.rmdir()

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

for p in Path.home().iterdir():
    print(p)
/home/bexgboost/.python_history
/home/bexgboost/word_counter.py
/home/bexgboost/.azure
/home/bexgboost/.npm
/home/bexgboost/.nv
/home/bexgboost/.julia
...

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

Например, ниже мы найдём все текстовые файлы внутри моего домашнего каталога с помощью glob("*.txt"):

home = Path.home()
text_files = list(home.glob("*.txt"))

len(text_files)
3 # Only three

Для рекурсивного поиска текстовых файлов, то есть также внутри всех дочерних каталогов, вы можете использовать rglob:

all_text_files = [p for p in home.rglob("*.txt")]

len(all_text_files)
5116 # Now much more

Вы можете прочитать о регулярных выражениях здесь

Вы также можете использовать rglob('*') для рекурсивного перечисления содержимого каталога. Это похоже на перегруженную версию iterdir().

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

Чтобы сделать это, мы импортируем класс Counter из коллекций и предоставляем ему все файловые суффиксы в папке articles в home:

from collections import Counter

file_counts = Counter(
    path.suffix for path in (home / "articles").rglob("*")
)

file_counts
Counter({'.py': 12,
         '': 1293,
         '.md': 1,
         '.txt': 7,
         '.ipynb': 222,
         '.png': 90,
         '.mp4': 39})

Различия в операционной системе

Извините, но мы должны поговорить об этой кошмарной проблеме.

До сих пор мы имели дело с объектами PosixPath, которые используются по умолчанию в UNIX-подобных системах:

type(Path.home())
pathlib.PosixPath

Если бы вы использовали Windows, вы бы получили объект WindowsPath:

from pathlib import WindowsPath

# User raw strings that start with r to write windows paths
path = WindowsPath(r"C:\users")
path
NotImplementedError: cannot instantiate 'WindowsPath' on your system

Создание экземпляра пути к другой системе приводит к ошибке, подобной описанной выше.

Но что, если бы вы были вынуждены работать с путями из другой системы, например, с кодом, написанным коллегами, которые используют Windows?

В качестве решения pathlib предлагает объекты чистого пути, такие как PureWindowsPath или PurePosixPath:

from pathlib import PurePosixPath, PureWindowsPath

path = PureWindowsPath(r"C:\users")
path
PureWindowsPath('C:/users')

Это примитивные объекты path. У вас есть доступ к некоторым методам и атрибутам path, но, по сути, объект path остаётся строкой:

path / "bexgboost"
PureWindowsPath('C:/users/bexgboost')
path.parent
PureWindowsPath('C:/')
path.stem
'users'
path.rename(r"C:\losers") # Unsupported
AttributeError: 'PureWindowsPath' object has no attribute 'rename'

Заключение

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

Я просто не хотел вас напугать.

Но я надеюсь, что убедил вас достаточно, чтобы отказаться от os.path и начать использовать pathlib для гораздо более простых и удобочитаемых операций с путями.

Прощай, os.path: 15 хитростей Pathlib для быстрого освоения файловой системы на Python

+1
1
+1
4
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *