Прощай, 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
/
Работа с файлами
Чтобы создавать файлы и записывать в них данные, вам больше не нужно использовать функцию 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
не вызовет никаких сигналов тревоги, если файл не существует.
Работа с каталогами
Есть несколько изящных приёмов для работы с каталогами в 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
для гораздо более простых и удобочитаемых операций с путями.