TransWikia.com

¿Cómo finalizar varias llamadas recursivas a una función? Python

Stack Overflow en español Asked by user166844 on August 26, 2021

El caso es que tengo una estructura de archivos, y desarrollé un algoritmo de búsqueda de un archivo para estructura multi-directorio. La estructura es la siguiente:

path
    dir0
        dir2
            file3
    dir1
        file2
    file0
    file1

Estoy usando esta función:

def searchfile(paths: list,search: str):
    for path in paths:
        print("Directorio actual: {}".format(path))

        contents = os.listdir(path)
        print("Contenidos: {}".format(contents))

        if search in contents:
            print("Archivo encontrado! n")
            return "{}".format(os.path.abspath(search))
            sys.exit()

        dirs = ["{}{}/".format(path,x) for x in contents if os.path.isdir("{}/{}/".format(path,x)) == True]
        print("Directorios: {} n".format(dirs))

        searchfile(dirs,search)

Como se puede ver, emplea una llamada recursiva dentro de la propia definición de función.

¿Por que uso una llamada recursiva?

Debido a que esta función se debe de adaptar a estructura de una longitud indefinida, es decir que a una estructura que puede tener una cantidad indefinida de directorios dentro de un directorio, y esos directorios una cantidad indefinida de directorios dentro de cada uno. El algoritmo debe de ser capaz de realizar una búsqueda en todos los directorios de estructura.

dir
    dir
        dir
            dir
            ...
        dir
        dir
    dir
        dir
            ..
        dir
        dir

Donde "dir" es un directorio X

Lo que pasa es que la única manera que encontré de iterar sobre el contenido de cada directorio, es invocando la misma función en cada elemento de la lista generada por dirs. Entonces, de esa manera se ejecutará la función hasta que la distancia a un un punto nulo se igual a 0. En esto se define un punto nulo como un directorio que solo contenga archivos o bien que esté vació. Si el directorio solo contiene archivos el filtro que realiza la expresión generadora de dirs equivaldrá a [], por lo tanto la siguiente llamada de la función será una iteración sobre una lista vacía (osea nula).

¿Dónde está el problema?

El condicional que uso para validar si el archivo se encontró (100% de eficacia):

if search in contents:
    print("Archivo encontrado! n")
    return "{}".format(os.path.abspath(search))
    sys.exit()

Se ejecuta, pero no cumple mi objetivo. Este objetivo es terminar el programa con sys.exit(). El problema surge ya que se realizan llamadas recursivas dentro de una iteración es decir que a lo mejor una llamada a la función puede evaluar un directorio, y por más que el archivo esté en ese directorio siempre va aquedar la otra iteración pendiente. Ocurre algo así:

dir*
    dir*
        dir*
            busqueda
    dir
        dir
            ...

Donde "busqueda" es el archivo a encontrar

Como se puede ver los directorios que el tienen asterisco es la ruta sobre la que se está iterando, aunque se llegue a "busqueda" va a quedar pendiente evaluar los directorios sin el asterisco.

¿Cómo se sabe con certeza que el condicional si se ejecuta?

Al ejecutar la función con un ejemplo

print(searchfile(["path/"],"file3.txt"))

obtengo

Directorio actual: path/
Contenidos: ['dir0', 'dir1', 'file0.txt', 'file1.txt']
Directorios: ['path/dir0/', 'path/dir1/']

Directorio actual: path/dir0/
Contenidos: ['dir2']
Directorios: ['path/dir0/dir2/']

Directorio actual: path/dir0/dir2/
Contenidos: ['file3.txt']
Archivo encontrado!

Directorio actual: path/dir1/
Contenidos: ['file2.txt']
Directorios: []

None

Se puede notar clarmente que aparece el mensaje de Archivo encontrado! y no se muestra el contenido del directorio. Esto deja como conclusión que el condicional si que se está ejecutando y el sys.exit() solo tiene influencia sobre esa iteración, no sobre todo el bucle y mucho menos sobre todo el programa.

Otra cosa a notar es que retorna None, osea que la linea return "{}".format(os.path.abspath(search)) no tiene efecto. Ni siquiera por que la casteo a un string.

Leyendo la referencia en la documentación de Python:

Exit from Python. This is implemented by raising the SystemExit
exception, so cleanup actions specified by finally clauses of try
statements are honored, and it is possible to intercept the exit
attempt at an outer level.

Dice que claramente "Exit from Python". Entonces me surgen varias preguntas.

  • ¿Que está pasando?
  • ¿Por qué realmente sys.exit() no tiene influencia sobre todo el programa (o función)?
  • ¿De qué manera puedo parar varias llamadas recursivas?
  • ¿sys.exit() es adecuada para este tipo de casos?

Agradecería cualquier explicación o comentario. Muchas gracias de antemano, saludos.

Edición:

Por lo visto el programa no se está deteniendo debido a que existe un return antes del sys.exit(). Esto provoca que dicha función (sys.exit()) no se ejecute, ya que return da como resultado el final de una función.

Al tratarse de una invocación dentro de un bucle for tiene sentido que el programa no parara. Entonces me surge esta pregunta:

¿Existe una forma de detener la función y retornar un valor?

Ahora el verdadero problema surge con las iteraciones y lo que implican:

  • Retornar el valor y terminar el programa

Si hay un return significa el final de la función, pero al tratarse de una invocación en un ciclo… Siempre se va ejecutar la otra. Además el valor se va a perder y va terminar siendo None, por que la función que se invoca es la que se espera que retorne un valor, no las invocadas a base de esta.

Si hay un sys.exit() después del retorno de la función no se ejecutará, si no el siguiente ciclo.

  • Retornar un valor y romper el ciclo con break

Pasa lo mismo que con sys.exit(), no se llegará a ejecutar. Por otro lado, si fuera posible no rompería el ciclo. Esto por que se está hablando del ciclo de una función que invoca a la función actual. Son ciclos for anidados por funciones, pasaría algo así:

funcion():
    for i in iterable:#1
        funcion():
            for i in iterable: #2
                funcion():
                    ...
                        for i in iterable: #X
                            if condicion:
                                break

Como se puede ver, el break del ciclo for #X solo rompería este mismo ciclo. No tendría la capacidad de romperá ciclo en la función que esta invocando la función de dicho break. Se queda corto.

No importa de que manera si se usa return o break para acabar la función o ciclo, tampoco si se usa sys.exit() ya que no se puede obtener un valor por la función si se usa.

Se puede llegar a la conclusión de que el problema no está en ¿Cómo detener el programa?, si no más bien en el ¿Cómo obtener el el valor rertornado de una llamada recursiva y usarlo como valor de retorno de la función "padre"?

Por ende, se puede saber la manera adecuada es usando return sin más, esto significa detener la función actual y obtener el valor en cuestión. Por eso mismo intenté especificar que el valor de la llamada a la función dentro de la función era el valor de la función:

def searchfile(paths: list,search: str):
    for path in paths:
        print("Actual path: {}".format(path))

        contents = os.listdir(path)
        print("Contents: {}".format(contents))

        if search in contents:
            print("File found! n")
            return os.path.abspath(search)

        dirs = ["{}{}/".format(path,x) for x in contents if os.path.isdir("{}/{}/".format(path,x)) == True]
        print("Directories: {} n".format(dirs))

        return searchfile(dirs,search)

Esto suena muy bien, debido a que funciona con estructuras de archivos con longitud indefinida. Esto debido a que este retorno es recursivo. Cuando intento con:

print(searchfile(["path/"],"file3.txt"))

Obtengo:

Actual path: path/
Contents: ['dir0', 'dir1', 'file0.txt', 'file1.txt']
Directories: ['path/dir0/', 'path/dir1/']

Actual path: path/dir0/
Contents: ['dir2']
Directories: ['path/dir0/dir2/']

Actual path: path/dir0/dir2/
Contents: ['file3.txt']
File found!

C:UsersaliacDesktopfile3.txt

Un resultado "correcto". Sin embargo a la hora de intentar con un archivo en otra ubicación.

print(searchfile(["path/"],"file2.txt"))

Obtengo:

Actual path: path/
Contents: ['dir0', 'dir1', 'file0.txt', 'file1.txt']
Directories: ['path/dir0/', 'path/dir1/']

Actual path: path/dir0/
Contents: ['dir2']
Directories: ['path/dir0/dir2/']

Actual path: path/dir0/dir2/
Contents: ['file3.txt']
Directories: []

None

Esto deja en evidencia dos cosas:

  1. Solo se está iterando sobre el primer elemento de la lista argumento path en cada llamada. Pasa algo como esto:

    func()
        func()
            func()
    
        func($)
            func($)
    

Las funciones representadas con func() que tiene como argumento $ son las que nunca se van a ejecutar. Por que cada función "hijo" (funciones llamadas por otras funciones), van a retornar un valor y con mi intento de código significa que el el "padre" (función que invoca a otra función) va a retornar un valor. Entonces una función que tenga que hacer llamadas recursivas a otra función por medio de una iteración solo va a llamar a la primera iteración, ya que return significa el final de una función. Si hay un final en la primera llamada va a haber un final en la función que la llama (según mi intento)

  1. Devuelve el valor None

Nunca encuentra el archivo si está en una ubicación que no corresponde al primer subdirectorio del "directorio actual" (sobre el que se está iterando). Esto ocurre por la primera razón, visualizando;

dir #
    sdir #
        sdir #
            ...
        sdir
            sdir
    sdir
        sdir

Donde los dir o sdir que tenga una almohadilla (#) son los que la función tomará en cuenta, en cualquier caso. Esto ocurre básicamente por el problema del retorno en la primera iteración.

dirC[0] => sdirC[0] => sdirC[0] ... sdirC[0]

La C al final de cada elemento para representar el contenido de dicho elemento

Esto da la conclusión de que mi intento no es válido.

Entonces:

  • ¿Se puede especificar cuál valor se considera como válido de retorno y especificar que es el retorno de la función "padre"?
  • ¿De que manera se puede detener la función padre y retornar un valor de esta?
  • ¿Hay una mejor manera de hacer lo que estoy intentando hacer (retornar la ruta exacta de el archivo encontrado, una vez ya recorrida la parte de la estructura necesaria para encontrarlo)?

Sinceramente no sé si me dí a entender, no soy muy bueno explicándome. Si la pregunta está mal planteada, agradecería una corrección.

One Answer

Hay varias consideraciones:

Una instrucción que sigue a un return no se ejecuta nunca.

return "{}".format(os.path.abspath(search))
sys.exit()

El sys.exit() es como si no estuviera; puedes borrarlo.

Estas llamando recursivamente sin recuperar el valor que la llamada puede haber generado. Si encuentras el archivo en un subdirectorio, su nombre simplemente se pierde.

La verdad es que para este tipo de operaciones sobre directorios, el algoritmo general es más simple: se toma cada entrada del directorio. Si es el archivo buscado, se retorna. Si es un directorio, se invoca recursivamente la función y luego se examina el resultado. Si lo buscado, se retorna con eso.

Simplifique tambièn la llamada. No es necesario pasar una lista de directorios.

En resumen:

import os
    
def searchfile(path: str, search: str):

    for f in os.listdir(path):
        item=os.path.join(path, f)
        if os.path.isfile(item) and f == search:
            break
        elif os.path.isdir(item):
            item = searchfile(item,search)
            if item:
                break
    else:
        item = None

    return item

print(searchfile("/home/candid/bin/", "fonts.css"))

produce

candid@dell ~ $ python3 dir.py
/home/candid/bin/arduino-1.8.5/reference/www.arduino.cc/fonts/fonts.css
a

Edit

En general, es mejor escribir métodos que hagan una sola cosa, la mínima posible, pues es más fácil agregar que quitar. También es más fácil combinar que dividir.

En este caso, la función busca en un solo directorio, pero ¿Qué pasa si uno quiere revisar varios directorios no relacionados?

La solución es hacer una llamada por directorio:

directorios = ["dir1", "dir2", ..., "dirN"]
for dir in directorios:
   archivo = searchfile(dir, nombre_archivo)
   if archivo:
      ... hacer algo con el archivo ...

o también puedes armar una lista con los archivos encontrados:

lista_archivos = [searchfile(dir, nombre_archivo) for dir in directorios]

o incluso crear otra función:

def busca_en_dirs(lista_dir, nombre_archivo):
    return [searchfile(dir, nombre_archivo) for dir in lista_dir]

Answered by Candid Moe on August 26, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP