Python: Деревья

Теория: Манипуляции с виртуальной файловой системой

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

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

Базовые операции с узлами

В пакете python-immutable-fs-trees есть набор функций для работы с уже созданными файлами и директориям. Они позволяют не лезть во внутреннюю структуру самого дерева:

from hexlet import fs

tree = fs.mkdir("/", [fs.mkfile("hexlet.log")], {"hidden": True})
fs.get_name(tree)
# '/'
fs.get_meta(tree).get("hidden")
# True
[file] = fs.get_children(tree)
fs.get_name(file)
# 'hexlet.log'
fs.get_meta(file).get("unknown")

# А вот так делать не надо
# У файлов нет потомков
fs.get_children(file)

Дополнительно в пакете есть две функции для проверки типа. С их помощью можно выборочно работать с файлами и директориями:

from hexlet import fs

tree = fs.mkdir("/", [fs.mkfile("hexlet.log")], {"hidden": True})
fs.is_directory(tree)
# True
fs.is_file(tree)
# False
[file] = fs.get_children(tree)
fs.is_file(file)
# True
fs.is_directory(file)
# False

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

Обработка

Любая обработка в неизменяемом стиле сводится к формированию новых данных на основе старых. Ниже мы реализуем некоторые варианты преобразования, раскрывающие эту идею.

Изменение имени файла

Фактически можно создать новый файл с метаданными старого:

from hexlet import fs
import copy

file = fs.mkfile("one", {"size": 35})
# При переименовании важно сохранить метаданные
new_meta = copy.deepcopy(fs.get_meta(file))
new_file = fs.mkfile("new name", new_meta)

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

Как только мы захотим внести изменения в новое, мы сломаем старое:

file = fs.mkfile("one", {"size": 35})
new_meta = fs.get_meta(file)
new_meta["size"] = 15
new_file = fs.mkfile("new name", new_meta)
fs.get_meta(new_file)
# {'size': 15}

# Бум! У file тоже поменялись метаданные
fs.get_meta(file)
# {'size': 15}

Сортировка содержимого директории

Также данные внутри директории можно отсортировать:

tree = fs.mkdir(
    "/",
    [
        fs.mkfile("one"),
        fs.mkfile("two"),
        fs.mkdir("three"),
    ],
)

children = fs.get_children(tree)
new_meta = copy.deepcopy(fs.get_meta(tree))
# Reverse изменяет массив, поэтому клонируем
new_children = children[:]
# Делаем сортировку в обратном порядке, то есть разворачиваем список
new_children.reverse()
tree2 = fs.mkdir(fs.get_name(tree), new_children, new_meta)
list(map(fs.get_name, fs.get_children(tree2)))
# ['three', 'two', 'one']

Обновление содержимого директории

Еще мы можем обновить содержимое директории:

tree = fs.mkdir(
    "/",
    [
        fs.mkfile("oNe"),
        fs.mkfile("Two"),
        fs.mkdir("THREE"),
    ],
)


# Приводим к нижнему регистру имена директорий и файлов внутри конкретной директории
def to_lower(node):
    name = fs.get_name(node)
    new_meta = copy.deepcopy(fs.get_meta(node))
    if fs.is_directory(node):
        return fs.mkdir(name.lower(), fs.get_children(node), new_meta)
    return fs.mkfile(name.lower(), new_meta)


children = fs.get_children(tree)
new_children = list(map(to_lower, children))
# Обязательно копируем метаданные
new_meta = copy.deepcopy(fs.get_meta(tree))
tree2 = fs.mkdir(fs.get_name(tree), new_children, new_meta)
list(map(fs.get_name, fs.get_children(tree2)))
# ['one', 'two', 'three']

Удаление файлов внутри директории

Кроме того, файлы можно удалять:

tree = fs.mkdir(
    "/",
    [
        fs.mkfile("one"),
        fs.mkfile("two"),
        fs.mkdir("three"),
    ],
)

children = fs.get_children(tree)
new_children = list(filter(fs.is_directory, children))
new_meta = copy.deepcopy(fs.get_meta(tree))
fs.mkdir(fs.get_name(tree), new_children, new_meta)
# {'name': '/', 'children': [{'name': 'three', 'children': [], 'meta': {}, 'type': 'directory'}], 'meta': {}, 'type': 'directory'}

Рекомендуемые программы