Comparacion de metodos para crear nombres de archivo unico

Solicitud inicial

Buenas, estoy haciendo un script que recorre un directorio con muchos subniveles y archivos, y a estos archivos los tiene que copiar a otro directorio (afuera del que ya estoy recorriendo), pero todos en el mismo nivel. Es decir, tengo algo así para recorrer:

DirA
 |
 |-DirB
    |-readme.txt
    |-archivo.txt
 |-DirC
    |-readme.txt
    |-otro-archivo.txt
 ...

Y genero un nuevo directorio así

NuevoDir
 |-readme.txt
 |-readme.txt-0
 |-archivo.txt
 |-otro-archivo.txt
 ...

El problema obvio es que hay nombres que se pueden repetir y tengo que armar un nuevo nombre (Esto puede ocurrir un numero de veces indeterminado). Recorro el directorio con os.walk, así que en principio no tengo el listado completo de archivos (y de hecho, cada tanto cambia).

Simplificando un poco, lo resolví con este código:

sufijo = 0
nombre_archivo_original = nombre_archivo
while os.path.exists(nombre_archivo):
    nombre_archivo = nombre_archivo_original + "-" + str(sufijo)
    sufijo += 1

Cuando sale de ese bucle, tengo un nombre "valido" (o sea, funciona), pero me gustaría saber si existe una forma mas "pythonica" de generar ese nuevo nombre.

Requerimientos emergentes

Si bien no mencionados, algunas caracteristicas deseables del script son las siguientes:

  • Es importante que el metodo sea eficiente, debido a que la cantidad de archivos y la jerarquia de directorios tiende a crecer cada vez mas.
  • No se necesita recordar informacion adicional y debe intentar minimizar el consumo de memoria.

Comparacion

Se comienza con un setup basico para las pruebas

In [66]:
import timeit
In [67]:
REPETICION = 10000 # Establece el numero de veces que se ejecutara cada test

Discriminante basico: Contador

La forma mas sencilla de discriminante es utilizando un contador. Sin mantener estado, solo sirve para el script en ejecucion y no es reutilizable (En principio).

In [68]:
 timeit.timeit('c = 0; c += 1', number=REPETICION)
Out[68]:
0.0005691051483154297

La forma mas sencilla, se prueba crear el archivo con el nombre original. Si falla, se reintenta con '0', si vuelve a fallar se reintenta con '1', y asi. El consumo de memoria es minimo (un integer), pero para n archivos con el mismo nombre, hay que reintentar la creacion del archivo n - 1 veces. A esto le decimos que no guarda estado. Una mejora sencilla seria que el contador sea global. Se pierde asi la referencia de cuantas veces se repite cada nombre de archivo, pero se evitan los reintentos innecesarios.

Uso de uuid

Propuesta de Manuel: Uso del modulo uuid, implementacion del RFC 4122

In [69]:
timeit.timeit('uuid.uuid1().hex', 'import uuid', number=REPETICION)
Out[69]:
0.28641295433044434

Es acertado y cumple con lo pedido. No es demasiado eficiente en cuanto al tiempo de generacion, pero es entendible respecto al procesamiento que realiza.

Uso de mkstemp

Propuesta de Miguel: Uso del modulo mkstemp. Para los fines de test, es conveniente crear una carpeta de prueba porque genera mucha cantidad de archivos.

In [70]:
mkdir 'delete-after-test'
mkdir: cannot create directory ‘delete-after-test’: File exists
In [71]:
timeit.timeit("(fh, name) = tempfile.mkstemp(dir='./delete-after-test/', prefix='readme-',suffix='.txt');os.close(fh);", "import os; import tempfile", number=REPETICION)
Out[71]:
0.464108943939209

Crea el archivo vacio, y en este caso el archivo tiene que ser borrado para que el otro sea copiado posteriormente desde su origen. Ademas, genera un IO cada vez que se ejecuta, y como hay que hacerlo por cada archivo a copiar, se duplican la cantidad de escrituras al Disco Rigido. Parece una buena idea para resolver el problema de una generacion eventual de nombre, pero no para un copiado masivo.

Armar el nombre en base al namespace de origen

Propuesta de German: Usar el path completo para construir el nombre de archivo.

In [72]:
timeit.timeit("'-'.join(path_completo.split('/'))", "path_completo = 'DirA/DirB/DirC/readme.txt'", number=REPETICION)
Out[72]:
0.004395008087158203

La solucion es interesante, cumple con lo que se necesita y gracias a que se usa una restriccion del FS asegura la unicidad de los nombres.

Mantener un estado por sufijo

Propuesta de Marian: Armar un dic con los sufijos. Si no entendi mal, es similar a la propuesta del contandor, pero el script va manteniendo en un diccionario cada uno de los nombres de archivo repetidos, y para cada uno cuenta la repeticion, que a su vez se usa como sufijo.

In [73]:
timeit.timeit("f_dic['key'] = f_dic['key'] + 1 if f_dic.has_key('key') else 0; sufijo = f_dic['key'];", "f_dic = {}", number=REPETICION)
Out[73]:
0.002296924591064453

Permite mantener el estado, cosa que no ocurria con el contador. Para ello necesita una estructura adicional (el diccionario). Si la cantidad de archivos no es masiva, es un costo que se puede asumir, siempre que interese saber cuantas veces se repite cada nombre de archivo.

Graficas

Se utilizo la lib timeit_plot para graficar los tiempos de los diferentes metodos.

In [74]:
import timeit_plot as tp
In [75]:
from matplotlib import pyplot as plt
In [76]:
%matplotlib inline
In [77]:
functions = ['c = 0; c += 1',
             'uuid.uuid1().hex',
             "(fh, name) = tempfile.mkstemp(dir='./delete-after-test/', prefix='readme-',suffix='.txt');os.close(fh);",
             "'-'.join(path_completo.split('/'))",
             "f_dic['key'] = f_dic['key'] + 1 if f_dic.has_key('key') else 0; sufijo = f_dic['key'];"]
In [78]:
setup_code = ['pass',
              'import uuid;',
              "import os; import tempfile;",
              "path_completo = 'DirA/DirB/DirC/readme.txt';",
              "f_dic = {}"]
In [79]:
data = tp.timeit_compare(functions, range(10,101,10), setups=setup_code, number=REPETICION)
testing c = 0; c += 1...
testing uuid.uuid1().hex...
testing (fh, name) = tempfile.mkstemp(dir='./delete-after-test/', prefix='readme-',suffix='.txt');os.close(fh);...
testing '-'.join(path_completo.split('/'))...
testing f_dic['key'] = f_dic['key'] + 1 if f_dic.has_key('key') else 0; sufijo = f_dic['key'];...
In [80]:
tp.timeit_plot2D(data, 'list length', 'Comparacion')
Out[80]:
In [81]:
functions = ['c = 0; c += 1',
             "'-'.join(path_completo.split('/'))",
             "f_dic['key'] = f_dic['key'] + 1 if f_dic.has_key('key') else 0; sufijo = f_dic['key'];"]
In [82]:
setup_code = ['pass',
              "path_completo = 'DirA/DirB/DirC/readme.txt';",
              "f_dic = {}"]
In [83]:
data = tp.timeit_compare(functions, range(10,101,10), setups=setup_code, number=REPETICION)
testing c = 0; c += 1...
testing '-'.join(path_completo.split('/'))...
testing f_dic['key'] = f_dic['key'] + 1 if f_dic.has_key('key') else 0; sufijo = f_dic['key'];...
In [84]:
tp.timeit_plot2D(data, 'list lengt', 'Comparacion')
Out[84]:
In [84]: