Hola. Solo quiero añadir que no basta con especificar la codificación en el código fuente, sino que además la codificación del archivo debe coincidir con la que se haya especificado.
"
coding: utf-8" no convierte la codificación del archivo fuente. Lo único que hace es indicarle al intérprete de Python con qué codificación debe interpretar los bytes del archivo fuente.
Ya te han indicado cual es la solución más coherente con la codificación por defecto que utiliza Python. Sin embargo, y como alternativa aislada para tu caso, puedes especificar: "
# coding: windows-1252" - eso lo escribes siempre la primera línea arriba del todo ya que seguramente en tu entorno hispanohablante por algún motivo estés creando los archivos con esa codificación, y de ahí viene el conflicto. Hazlo solo en caso de que prefieras evitar guardar el archivo con codificación UTF-8 como te han dicho. Aunque esto no sería una buena práctica hacerlo.
Por cierto, en el código que has mostrado estás realizando cientos y cientos de operaciones de escritura (I/O) sobre el archivo de texto cada vez que el búfer interno por defecto de Python (de muy pocos kilobytes, 8 me parece) se acumula y se vacía (
flush). 3,61 MB / 8 KB = aprox. 451 de escrituras en el archivo de texto.
Lo suyo es utilizar un búfer con tamaño arbitrario en memoria para reducir drásticamente la cantidad de
syscalls de escritura sobre el archivo físico:
Asegúrate de utilizar el parámetro para especificar un tamaño de búfer arbitrario:
buffering = 104857600 # 100 MB
...
with open("Mywords.txt", "w", encoding="utf-8", buffering=buffering) as txt:
En este ejemplo, usando 100 MB como búfer de memoria, directamente te aseguras una única escritura sobre el archivo, ya que el archivo resultante con todas las combinatorias de palabras no llega ni a los 4 MB de tamaño. ¿Es un
overkill? No si piensas escalar tu código actual para generar longitudes más grandes.
Aparte, la anidación de bucles 'for' también es mejorable y es un punto crítico en tu código que merma mucho la velocidad de ejecución. Con el código actual no vas a notar diferencia, pero con longitudes más grandes si lo notarías y de forma muy exponencial con periodos de tiempo extremadamente largos para completar la ejecución del programa ya que esos 'for' suponen varios problemas que añaden
overhead en Python.
Lo ideal es que utilices
itertools.product (implementado en el lenguaje 'C'):
# coding: utf-8
import itertools
def generate_valid_words():
charset = "abcdefghijklmnñopqrstuvwxyz"
prefix = "K"
output_path = "Mywords.txt"
all_combinations = itertools.product(charset, repeat=4)
with open(output_path, "w", encoding="utf-8") as text_file:
for combo in all_combinations:
if not (combo[0] == combo[1] == combo[2] or combo[1] == combo[2] == combo[3]):
valid_word = f"{prefix}{''.join(combo)}\n"
text_file.write(valid_word)
generate_valid_words()
input("Press Enter to exit...")
Pero como estoy muy oxidado en Python, le pedí a la IA Sonnet (en Claude) que lo optimizase todavía mucho más haciéndolo multi-core:
# coding: utf-8
import itertools
import multiprocessing
import time
FILENAME = "Mywords.txt"
ENCODING = "utf-8"
BUFFERING = 104857600 # 100 MB
CHARSET = "abcdefghijklmnñopqrstuvwxyz"
PREFIX = "K"
LENGTH = 4
BATCH = 300_000
def format_elapsed(seconds_total):
hh = int(seconds_total // 3600)
mm = int((seconds_total % 3600) // 60)
ss = int(seconds_total % 60)
ffff = int((seconds_total % 1) * 10000)
return f"{hh:02d}:{mm:02d}:{ss:02d}:{ffff:04d}"
def worker(first_char):
chars = list(CHARSET)
n = LENGTH - 1
lines = []
batch = []
join = "".join
for rest in itertools.product(CHARSET, repeat=n):
combo = (first_char,) + rest
valid = True
for i in range(LENGTH - 2):
if combo[i] == combo[i + 1] == combo[i + 2]:
valid = False
break
if valid:
lines.append(f"{PREFIX}{join(combo)}\n")
if len(lines) >= BATCH:
batch.append("".join(lines))
lines.clear()
if lines:
batch.append("".join(lines))
return batch
def main():
cores = multiprocessing.cpu_count()
print(f"Usando {cores} cores...")
start = time.perf_counter()
with open(FILENAME, "w", encoding=ENCODING, buffering=BUFFERING) as f:
with multiprocessing.Pool(processes=cores) as pool:
for chunks in pool.imap(worker, CHARSET):
for chunk in chunks:
f.write(chunk)
elapsed = time.perf_counter() - start
print(f"Tiempo total: {format_elapsed(elapsed)}")
if __name__ == "__main__":
main()
input("Press Enter to exit...")
Los resultados son claros:
Longitud = 4, tu enfoque actual: ~800 ms
Longitud = 4, optimización básica itertools.product: ~350 ms
Longitud = 4, optimización completa itertools.product multi-core: ~240 ms
(aquí es donde la cosa se pone seria)
Longitud = 5, tu enfoque actual (bucles anidados y adaptados para esta longitud): ~19 segundos.
Longitud = 5, optimización básica itertools.product: ~9 segundos.
Longitud = 5, optimización completa itertools.product multi-core: ~3 segundos.
Longitud = 6, tu enfoque actual (bucles anidados y adaptados para esta longitud): SIN MEDIR (estima de 30 minutos para arriba}.
Longitud = 6, optimización básica itertools.product: ~14 minutos.
Longitud = 6, optimización completa itertools.product multi-core: ~2 minutos.