web-dev-qa-db-fra.com

Rendement dans une fonction récursive

J'essaie de faire quelque chose à tous les fichiers sous un chemin donné. Je ne veux pas collecter tous les noms de fichiers au préalable puis faire quelque chose avec eux, alors j'ai essayé ceci:

import os
import stat

def explore(p):
  s = ''
  list = os.listdir(p)
  for a in list:
    path = p + '/' + a
    stat_info = os.lstat(path )
    if stat.S_ISDIR(stat_info.st_mode):
     explore(path)
    else:
      yield path

if __== "__main__":
  for x in explore('.'):
    print '-->', x

Mais ce code saute les répertoires quand il les frappe, au lieu de donner leur contenu. Qu'est-ce que je fais mal?

50
Ali

Utilisez os.walk au lieu de réinventer la roue. 

En suivant les exemples de la documentation de la bibliothèque, voici une tentative non testée:

import os
from os.path import join

def hellothere(somepath):
    for root, dirs, files in os.walk(somepath):
        for curfile in files:
            yield join(root, curfile)


# call and get full list of results:
allfiles = [ x for x in hellothere("...") ]

# iterate over results lazily:
for x in hellothere("..."):
    print x
26
phooji

Les itérateurs ne fonctionnent pas de manière récursive comme ça. Vous devez renvoyer chaque résultat en remplaçant

explore(path)

avec quelque chose comme

for value in explore(path):
    yield value

Python 3.3 a ajouté la syntaxe yield from X, comme proposé dans PEP 380 , afin de répondre à cet objectif. Avec cela, vous pouvez le faire à la place:

yield from explore(path)

Si vous utilisez generators en tant que coroutines , cette syntaxe prend également en charge l'utilisation de generator.send() pour renvoyer des valeurs dans les générateurs invoqués de manière récursive. La simple boucle for ci-dessus ne le ferait pas.

124
Jeremy Banks

Le problème est cette ligne de code:

explore(path)

Qu'est ce que ça fait?

  • appelle explore avec la nouvelle path
  • explore s'exécute, créant un générateur
  • le générateur est renvoyé à l'endroit où explore(path) a été exécuté. . .
  • et est jeté

Pourquoi est-il jeté? Il n'a été affecté à rien, il n'a pas été itéré - il a été complètement ignoré.

Si vous voulez faire quelque chose avec les résultats, eh bien, vous devez faire quelque chose avec eux! ;)

Le moyen le plus simple de corriger votre code est:

for name in explore(path):
    yield name

Lorsque vous êtes certain de comprendre ce qui se passe, vous voudrez probablement utiliser os.walk() à la place.

Une fois que vous avez migré vers Python 3.3 (en supposant que tout fonctionne comme prévu), vous pourrez utiliser la nouvelle syntaxe yield from et le moyen le plus simple de corriger votre code à ce stade sera le suivant:

yield from explore(path)
35
Ethan Furman

Change ça:

explore(path)

Pour ça:

for subpath in explore(path):
    yield subpath

Ou utilisez os.walk, comme suggéré par phooji (qui est la meilleure option).

8
Dietrich Epp

Cela appelle explore comme une fonction. Ce que vous devriez faire, c'est l'itérer comme un générateur:

if stat.S_ISDIR(stat_info.st_mode):
  for p in explore(path):
    yield p
else:
  yield path

EDIT: Au lieu du module stat, vous pouvez utiliser os.path.isdir(path).

3
MRAB

Essaye ça:

if stat.S_ISDIR(stat_info.st_mode):
    for p in explore(path):
        yield p
2
satoru

Vous pouvez également implémenter la récursivité à l'aide d'une pile.

Cela ne présente toutefois aucun avantage, mis à part le fait que cela est possible. Si vous utilisez python en premier lieu, les gains de performances ne sont probablement pas intéressants.

import os
import stat

def explore(p):
    '''
    perform a depth first search and yield the path elements in dfs order
        -implement the recursion using a stack because a python can't yield within a nested function call
    '''
    list_t=type(list())
    st=[[p,0]]
    while len(st)>0:
        x=st[-1][0]
        print x
        i=st[-1][1]

        if type(x)==list_t:
            if i>=len(x):
                st.pop(-1)
            else:
                st[-1][1]+=1
                st.append([x[i],0])
        else:
            st.pop(-1)
            stat_info = os.lstat(x)
            if stat.S_ISDIR(stat_info.st_mode):
                st.append([['%s/%s'%(x,a) for a in os.listdir(x)],0])
            else:
                yield x

print list(explore('.'))
0
user1149913

os.walk est idéal si vous devez parcourir tous les dossiers et sous-dossiers. Si vous n'en avez pas besoin, c'est comme utiliser une arme à éléphant pour tuer une mouche.

Cependant, dans ce cas particulier, os.walk pourrait être une meilleure approche.

0
Robson França

Pour répondre à la question d'origine telle qu'elle a été posée, la clé est que l'instruction yield doit être propagée en dehors de la récursion (comme par exemple return). Voici une réimplémentation de travail de os.walk(). J'utilise ceci dans une implémentation de pseudo-VFS, où je remplace en plus os.listdir() et des appels similaires.

import os, os.path
def walk (top, topdown=False):
    items = ([], [])
    for name in os.listdir(top):
        isdir = os.path.isdir(os.path.join(top, name))
        items[isdir].append(name)
    result = (top, items[True], items[False])
    if topdown:
        yield result
    for folder in items[True]:
        for item in walk(os.path.join(top, folder), topdown=topdown):
            yield item
    if not topdown:
        yield result
0
Hlórriði