Преобразование нехвостовой рекурсивной функции Python, добавляющей информацию о подпапках в цикл

Я написал функцию для получения общего размера и общего количества файлов/подкаталогов всего диска или определенной папки. Рекурсивная функция была очень простой:

# Make the python dirSize function a non recursive one:
from pathlib import Path

obj = {}
def dirSize(dir: Path, size = 0, files = 0, dirs = 0):
    for sub_dir in dir.glob('*'):
        if sub_dir.is_dir():
            dirs += 1
            res_size, res_files, res_dirs = dirSize(sub_dir)
            size += res_size
            files += res_files
            dirs += res_dirs
        elif sub_dir.is_file():
            files += 1
            size += sub_dir.stat().st_size
    
    obj[dir.as_posix()] = {
        'size': size,
        'files': files,
        'dirs': dirs,
    }
    
    return size, files, dirs

dirSize(Path('~/Documents').expanduser())
#Line above is for tests. Final code should run in elevated cmd:
#dirSize(Path('C:/'))

with open(Path('~/Desktop/results.tsv').expanduser().as_posix(), 'w') as file:
    file.write('Directory\tBytes\tFiles\tDirs\n')
    for key in sorted(obj):
        dir = obj[key]
        file.write(key)
        file.write('\t')
        file.write(str(dir['size']))
        file.write('\t')
        file.write(str(dir['files']))
        file.write('\t')
        file.write(str(dir['dirs']))
        file.write('\n')

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

size += res_size
files += res_files
dirs += res_dirs

Итак, я попробовал стековый цикл, как в алгоритме Дейкстры, но оказалось, что я не смог найти простого решения для вычисления суммы снизу вверх. В качестве альтернативы я мог бы добавить свойства level/parent/child в переменную obj, чтобы получить сумму снизу вверх после обработки очереди. Но это кажется очень громоздким. Есть ли более простой способ добиться этого?




Отвечаем на вопросы, заданные ниже:

  1. Почему бы вам просто не использовать os.walk?
  • Это немедленно устраняет рекурсию, но не помогает подсчитать сумму снизу вверх - сгенерированный отчет показывает, сколько подкаталогов, дискового пространства и файлов имеет каждый каталог.
  1. В чем проблема с рекурсивной функцией?
  • Никаких проблем, все работает идеально! Но мой босс считает, что не все разработчики умеют работать с рекурсией, поэтому он попросил меня сделать нерекурсивную версию, и я изо всех сил стараюсь не усложнять ее слишком сильно.

почему бы тебе просто не использовать os.walk?

juanpa.arrivillaga 21.06.2024 21:36

также важно отметить, что хвостовая рекурсия не имеет особого статуса в CPython (нет оптимизации хвостового вызова)

juanpa.arrivillaga 21.06.2024 21:40

В чем проблема с рекурсивной функцией? Если глубина каталогов не превышает 1000 уровней, вам не следует сталкиваться с ограничением рекурсии Python.

Barmar 21.06.2024 21:57

«Но не все разработчики умеют работать с рекурсией» << Почему бы и нет?

Stef 22.06.2024 21:53

Я бы ответил, что единственное, что потенциально можно назвать слишком сложным в вашей функции, это то, что она делает две вещи: (1) ходит по дереву каталогов (2) накапливает статистику; а затем я бы предложил разделить его на две функции: одну, которая ходит, и другую, которая принимает статистику. Но тогда функция, которая ходит, будет просто os.walk и, видимо, вы не хотите ее использовать?

Stef 22.06.2024 21:55
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
5
94
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

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

obj = {}

def dirSizeIterative(dir: Path):
    dir_stack = [(dir, 'root')]
    obj['root'] = {'size': 0, 'files': 0, 'dirs': 0}

    while dir_stack:
        sub_dir, parent_dir_name = dir_stack.pop(0)
        parent_info = obj[parent_dir_name]

        if sub_dir.is_dir():
            # if this exists, then we are processing this dir for the second time, after children have all been computed
            if sub_info := obj.get(sub_dir.as_posix()):
                parent_info['size'] += sub_info['size']
                parent_info['files'] += sub_info['files']
                parent_info['dirs'] += sub_info['dirs']

                continue

            sub_info = {'size': 0, 'files': 0, 'dirs': 0}
            obj[sub_dir.as_posix()] = sub_info

            parent_info['dirs'] += 1

            # add this dir back to the stack for processing after children have been computed
            dir_stack.insert(0, (sub_dir, parent_dir_name))

            for child in sub_dir.glob('*'):
                dir_stack.insert(0, (child, sub_dir.as_posix()))
        elif sub_dir.is_file():
            parent_info['files'] += 1
            parent_info['size'] += sub_dir.stat().st_size

    del obj['root']

dirSizeIterative(Path('~/Documents').expanduser())

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

Marquinho Peli 23.06.2024 19:28

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

Marquinho Peli 29.06.2024 16:29

Но не все разработчики умеют работать с рекурсией.

Контрапункт: возможно, все разработчики умеют работать с рекурсией, но ваша рекурсивная функция слишком сложна, потому что:

  • он делает три вещи одновременно: рекурсивно просматривает подкаталоги, обновляет глобальный словарь и возвращает тройку;
  • использование аргументов по умолчанию становится трудным для понимания;
  • он опирается на глобальный диктат;
  • его возвращаемое значение, тройка, не является чем-то, что вызывающая сторона ожидает получить, поскольку вызывающую сторону интересует только dict

Я предлагаю разделить код на две функции:

  • очень простая рекурсивная функция ходьбы;
  • нерекурсивная функция, которая создает и заполняет словарь, возвращает этот словарь и не возвращает тройку.

Что-то вроде этого:

from collections import defaultdict

def walk(dir: Path):
    '''
    Walk directory recursively and yield (node, parent_name) pairs
    '''
    for sub_dir in dir.glob('*'):
        if sub_dir.is_dir():
            yield from walk(sub_dir)
        yield (sub_dir, dir.as_posix())

def dirSize(dir: Path):
    obj = defaultdict(lambda (): {'size': 0, 'files': 0, 'dirs': 0})
    for (node, parent_name) in walk(dir):
        size, files = (node.stat().st_size, 1) if node.is_file() else (0, 0)
        dirs = 1 if node.is_dir() else 0
        node_dict = obj[node.as_posix()]
        parent_dict = obj[parent_name]
        for d in (node_dict, parent_dict):
            d['size'] += size
            d['files'] += files
            d['dirs'] += dirs
    return obj

Это не то, о чем спрашивали, но на самом деле это очень полезно, поскольку значительно упрощает функцию.

Marquinho Peli 23.06.2024 19:24

Основываясь на ответе @ awarrier99, я придумал эту версию, которая немного упрощает логику. Я надеюсь, что это поможет будущим исследователям:

def dirSizeIterative(dir: Path):
    dirs = [(dir, {'size': 0, 'files': 0, 'dirs': 0}, None)]
    i = 0

    #step 1, read dir/files info and add to array
    while i < len(dirs):
        dir, info, _ = dirs[i]
        i += 1
        
        for sub_dir in dir.glob('*'):
            if sub_dir.is_dir():
                info['dirs'] += 1
                dirs.append((sub_dir, {'size': 0, 'files': 0, 'dirs': 0}, info))
            elif sub_dir.is_file():
                info['files'] += 1
                info['size'] += sub_dir.stat().st_size

    #step 2, bottom up sum using the parent
    for _, info, parent in dirs[-1:0:-1]:
        parent['size'] += info['size']
        parent['files'] += info['files']
        parent['dirs'] += info['dirs']
    
    return {dir.as_posix(): info for dir, info, _ in dirs}

Другие вопросы по теме