A medida que esta serie crece, comencé a buscar problemas aparentemente simples para exponer su complejidad. Esta vez, pensé que sería interesante ver cómo convertir una cadena a minúsculas.

Resulta que hay una solución realmente sencilla ( lower()), pero creo que vale la pena mirar primero algunas soluciones caseras. Por ejemplo, podríamos intentar construir una cadena haciendo un bucle sobre cada carácter. Si eso suena interesante, consulte el resto de este artículo.

Descripción del problema

Si alguna vez ha intentado escribir código que manipule cadenas, sabe lo doloroso que puede ser un proceso. Por ejemplo, intente escribir código para invertir una cadena. Consejo profesional : no es tan fácil como crees. Lo sé porque agregué la inversión de cadenas como uno de los desafíos en nuestro repositorio de programas de muestra .

Cuando estaba construyendo ese repositorio, descubrí que no se puede simplemente comenzar al final de la cadena e imprimir los caracteres al revés. Eso funcionará para cadenas simples como la mayoría del texto de este artículo. Sin embargo, podría fallar para personajes más complejos como emojis.

Dicho todo esto, Python 3 hace un gran trabajo al abstraer caracteres, por lo que es posible que no tenga problemas. Por ejemplo, el siguiente código parece funcionar bien:

1
2
3
>>> hero = "😊"
>>> hero[::-1]
'😊'

Ahora, menciono esto porque hoy queremos hablar sobre la conversión de una cadena a minúsculas. Si ha estado en Python por un tiempo, sabrá que hay una manera rápida de hacerlo. Sin embargo, si no lo ha hecho, existe la posibilidad de que intente hacerlo usted mismo (o tenga que hacerlo usted mismo para un curso). Como resultado, estableceré una restricción para todo este artículo: asumir ASCII .

Esta restricción puede ahorrarnos mucho dolor y sufrimiento. Básicamente, nos restringe a los primeros 128 caracteres (o 256 según a quién le pregunte). De esa forma, no tenemos que preocuparnos por tratar con personajes de otros idiomas o emojis.

Suponiendo ASCII, deberíamos poder convertir una cadena como "All Might" a "All might" con bastante facilidad. En las secciones siguientes, veremos algunas soluciones que podrán hacer precisamente esto.

Soluciones

En esta sección, veremos cada solución que se me ocurra. Dado que este problema ha sido resuelto trivialmente por el lower()método, la mayoría de estas soluciones son esencialmente fuerza bruta. En otras palabras, cada solución pasa por una estrategia diferente para convertir una cadena a minúsculas a mano. Si eso no es lo tuyo, no dudes en pasar a la última solución. Para todos los demás, ¡echemos un vistazo a nuestra primera solución de fuerza bruta!

Convertir una cadena a minúscula por fuerza bruta

Ya que asumimos ASCII, podemos intentar convertir nuestra cadena a minúsculas mirando los valores ordinales de cada carácter. En otras palabras, a cada carácter se le asigna un número. Si la identificación de un personaje está dentro del rango de letras mayúsculas, deberíamos poder encontrar su identificación en minúscula correspondiente y reemplazarla. Eso es exactamente lo que hacemos a continuación:

1
2
3
4
5
6
7
hero = "All Might"
output = ""
for char in hero:
  if "A" <= char <= "Z":
    output += chr(ord(char) - ord('A') + ord('a'))
  else:
    output += char

Aquí, creamos una cadena llamada heroque almacena el nombre "All Might". Luego, creamos una cadena de salida vacía. Después de eso, recorremos cada carácter de la cadena para verificar si el carácter actual se encuentra en el rango de letras mayúsculas. Si es así, lo convertimos a minúsculas con esta pequeña expresión inteligente:

1
chr(ord(char) - ord('A') + ord('a'))

Restando ord('A'), obtenemos el índice del carácter en el alfabeto. Por ejemplo, si charfuera “C”, la expresión ord(char) - ord('A')sería 2. Entonces, todo lo que necesitamos saber es cuál es el valor ordinal de 'a' para cambiar nuestro índice al rango de letras minúsculas. En otras palabras, esta expresión convierte cualquier letra mayúscula en minúscula.

Una cosa que no me encanta de este algoritmo es la concatenación. En general, es una mala idea concatenar cadenas en un bucle como este. Como resultado, podríamos usar una lista en su lugar:

1
2
3
4
5
6
7
8
hero = "All Might"
output = []
for char in hero:
  if "A" <= char <= "Z":
    output.append(chr(ord(char) - ord('A') + ord('a')))
  else:
    output.append(char)
output = "".join(output)

En la sección de rendimiento, veremos si esto es importante. Sin embargo, por ahora, profundicemos en algunas opciones mejores.

Convertir una cadena a minúscula usando colecciones ASCII

En la solución anterior, calculamos matemáticamente valores en minúsculas. Sin embargo, ¿qué pasa si por casualidad tenemos las letras minúsculas y mayúsculas disponibles para nosotros como una colección? Como resultado, la biblioteca de cadenas nos tiene cubiertos:

1
from string import ascii_lowercase, ascii_uppercase

Si tiene curiosidad por saber cómo se ven estos valores, lo verifiqué:

1
2
3
4
>>> ascii_lowercase
'abcdefghijklmnopqrstuvwxyz'
>>> ascii_uppercase
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

Como podemos ver, cada valor es una cadena que contiene el alfabeto. Ahora, es solo una cuestión de mapear de un conjunto a otro dado un índice:

1
2
3
4
5
6
7
8
hero = "All Might"
output = []
for char in hero:
  if char in ascii_uppercase:
    output.append(ascii_lowercase[ascii_uppercase.index(char)])
  else:
    output.append(char)
output = "".join(output)

Nuevamente, recorremos cada carácter de nuestra cadena. Por supuesto, esta vez comprobamos si ese carácter está en mayúsculas. Si es así, buscamos el carácter en minúscula correspondiente y lo agregamos a nuestra cadena final. De lo contrario, agregamos el carácter original.

Personalmente, me gusta un poco más esta solución porque estamos tratando de manera más explícita con ciertos conjuntos de personajes. Dicho esto, todavía hay una mejor solución por delante.

Convertir una cadena a minúscula usando una comprensión de lista

Mirando las soluciones anteriores, pensé que podría ser divertido intentar usar una lista de comprensión. No es bonito, pero hace el trabajo:

1
2
3
4
5
from string import ascii_uppercase, ascii_lowercase
 
hero = "All Might"
output = [ascii_lowercase[ascii_uppercase.index(char)] if char in ascii_uppercase else char for char in hero]
output = "".join(output)

Si prefiere algo un poco más legible, aquí está la misma lista de comprensión con la expresión separada del bucle:

1
2
3
4
5
6
[
  ascii_lowercase[ascii_uppercase.index(char)]
    if char in ascii_uppercase
    else char
  for char in hero
]

Básicamente, decimos que para cada carácter en hero, asumimos que vamos a convertir mayúsculas a minúsculas. De lo contrario, deje el carácter sin cambios.

Honestamente, esto podría ser un poco más claro si sacamos la expresión a una función:

1
2
3
4
5
def to_lowercase(char: str):
  if char in ascii_uppercase:
    return ascii_lowercase[ascii_uppercase.index(char)]
  else:
    return char

Entonces, podríamos llamar a esta función en lugar de ese lío:

1
[to_lowercase(char) for char in hero]

¡Eso es mucho más limpio! Por supuesto, definitivamente hay una mejor solución a seguir. Dicho esto, si le gustan las listas por comprensión y desea obtener más información sobre ellas, consulte mi artículo sobre cómo escribir listas por comprensión .

Convertir una cadena a minúscula usando el lower()método

Hasta este punto, intentamos desplegar nuestra propia función en minúsculas. Debido a la complejidad de las cadenas, resultó ser un asunto no trivial. Afortunadamente, los desarrolladores de Python sabían que esta sería una solicitud popular, por lo que escribieron un método para nosotros:

1
2
hero = "All Might"
hero.lower()

¡Y eso es! En una línea, podemos convertir una cadena a minúsculas.

Dado que asumimos ASCII hasta este punto, no hay mucho que decir en términos de los beneficios de esta solución. Seguro, lower()es probablemente más conveniente y más rápido que nuestras soluciones anteriores, pero nuestra suposición nos ha impedido hablar sobre el beneficio real: funciona más allá de ASCII.

A diferencia de nuestras soluciones anteriores, esta solución funcionará básicamente para cualquier lugar donde los conceptos de mayúsculas y minúsculas tengan sentido. En otras palabras, lower()debería funcionar en contextos más allá de ASCII. Si está interesado en cómo funciona bajo el capó, consulte la sección 3.13 del estándar Unicode .

Actuación

En este punto, echemos un vistazo a cómo se compara cada solución en términos de rendimiento. Si ha estado presente un tiempo, sabe que comenzamos con las pruebas almacenando cada solución en una cadena. Si es la primera vez que ve uno de estos tutoriales, puede ponerse al día con las pruebas de rendimiento con este artículo . De lo contrario, aquí están las cadenas:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
setup = """
hero = "All Might"
from string import ascii_lowercase, ascii_uppercase
"""
 
brute_force_concat = """
output = ""
for char in hero:
  if "A" <= char <= "Z":
    output += chr(ord(char) - ord('A') + ord('a'))
  else:
    output += char
"""
 
brute_force_list = """
output = []
for char in hero:
  if "A" <= char <= "Z":
    output.append(chr(ord(char) - ord('A') + ord('a')))
  else:
    output.append(char)
output = "".join(output)
"""
 
ascii_collection = """
output = []
for char in hero:
  if char in ascii_uppercase:
    output.append(ascii_lowercase[ascii_uppercase.index(char)])
  else:
    output.append(char)
output = "".join(output)
"""
 
list_comp = """
output = [ascii_lowercase[ascii_uppercase.index(char)] if char in ascii_uppercase else char for char in hero]
output = "".join(output)
"""
 
lower_method = """
output = hero.lower()
"""

Luego, si queremos probar el rendimiento de estas soluciones, podemos importar la timeitbiblioteca y ejecutar el repeat()método:

01
02
03
04
05
06
07
08
09
10
11
>>> import timeit
>>> min(timeit.repeat(setup=setup, stmt=brute_force_concat))
1.702892600000041
>>> min(timeit.repeat(setup=setup, stmt=brute_force_list))
1.9661427000000913
>>> min(timeit.repeat(setup=setup, stmt=ascii_collection))
1.5348989000001438
>>> min(timeit.repeat(setup=setup, stmt=list_comp))
1.4514239000000089
>>> min(timeit.repeat(setup=setup, stmt=lower_method))
0.07294070000011743

Como era de esperar, el lower()método es increíblemente rápido. Estamos hablando de 100 veces más rápido que nuestras soluciones de fuerza bruta. Dicho esto, me sorprendió la pequeña mejora en la velocidad que tiene la concatenación con respecto al uso de una lista en nuestro ejemplo. Como resultado, decidí usar una cadena más grande para probar:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
>>> setup = """
hero = "If you feel yourself hitting up against your limit remember for what cause you clench your fists... remember why you started down this path, and let that memory carry you beyond your limit."
from string import ascii_lowercase, ascii_uppercase
"""
>>> min(timeit.repeat(setup=setup, stmt=brute_force_concat))
22.304970499999996
>>> min(timeit.repeat(setup=setup, stmt=brute_force_list))
24.565209700000025
>>> min(timeit.repeat(setup=setup, stmt=ascii_collection))
19.60345490000003
>>> min(timeit.repeat(setup=setup, stmt=list_comp))
13.309821600000078
>>> min(timeit.repeat(setup=setup, stmt=lower_method))
0.16421549999995477

De alguna manera, la concatenación sigue siendo un poco más rápida que usar una lista. Esto me sorprendió mucho. Después de todo, casi toda la literatura apunta a que la concatenación es una mala idea , así que estaba un poco perplejo. Como resultado, fui tan lejos como para duplicar el código de prueba de ese artículo anterior para ver si estaba haciendo algo mal en mi prueba:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
>>> setup = """
hero = "All Might"
loop_count = 500
from string import ascii_lowercase, ascii_uppercase
 
def method1():
  out_str = ''
  for num in range(loop_count):
    out_str += str(num)
  return out_str
 
def method4():
  str_list = []
  for num in range(loop_count):
    str_list.append(str(num))
  return ''.join(str_list)
"""
>>> min(timeit.repeat(setup=setup, stmt="method1()"))
156.1076584
>>> min(timeit.repeat(setup=setup, stmt="method4()"))
124.92521890000012

Para mí, está sucediendo una de dos cosas:

  • O mi prueba es mala
  • O hay algún punto de cruce donde el join()método es mejor

Como resultado, decidí probar el mismo código para varias cantidades de loop_count:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
# Loop count = 10
>>> min(timeit.repeat(setup=setup, stmt="method1()"))
2.665588600000774
>>> min(timeit.repeat(setup=setup, stmt="method4()"))
3.069867900000645
 
# Loop count = 25
>>> min(timeit.repeat(setup=setup, stmt="method1()"))
6.647211299999981
>>> min(timeit.repeat(setup=setup, stmt="method4()"))
6.649540800000068
 
# Loop count = 50
>>> min(timeit.repeat(setup=setup, stmt="method1()"))
12.666602099999182
>>> min(timeit.repeat(setup=setup, stmt="method4()"))
12.962779500000579
 
# Loop count = 100
>>> min(timeit.repeat(setup=setup, stmt="method1()"))
25.012076299999535
>>> min(timeit.repeat(setup=setup, stmt="method4()"))
29.01509150000038

Mientras realizaba estas pruebas, tuve una revelación repentina: no se pueden ejecutar otros programas mientras se prueba el código. En este caso, las pruebas estaban tardando tanto que decidí jugar Overwatch mientras esperaba. ¡Mala idea! Torció todas mis pruebas. Como resultado, decidí volver a probar todas nuestras soluciones en las mismas condiciones exactas. Estos son los resultados donde los paréntesis indican la longitud de la cadena bajo prueba:

SoluciónTiempo (10)Tiempo (25)Tiempo (50)Tiempo (100)
Concatenación de fuerza bruta0,949443.728148.3357917.56751
Lista de fuerza bruta1.275674.454639.3325820.43046
Colección ASCII1.234414.262189.2658819.34155
Comprensión de listas1.032742.994146.1363412.71114
Método inferior0.071210.085750.110290.163998

Para ser honesto, no pude aislar la discrepancia. Mi conjetura es que en algún momento la concatenación empeora; Simplemente no he podido probarlo. Dicho esto, no me he encontrado construyendo cadenas masivas, así que no imagino que realmente importe. Por supuesto, probablemente haya alguna aplicación donde lo haga.

En cualquier caso, está claro que el lower()método es casi con certeza el camino a seguir (a menos que tenga algún tipo de asignación de clase que diga lo contrario). Por supuesto, tome estas medidas con cautela. Por contexto, estoy en un sistema Windows 10 con Python 3.8.2.

Desafío

Dado que pasamos todo el artículo hablando de convertir cadenas a minúsculas, pensé que para el desafío podíamos probar algo un poco diferente. Para hacer las cosas más interesantes, pensé que incluso podría ser divertido especificar un par de desafíos:

  1. Convertir una cadena a mayúsculas (por ejemplo, "todos podrían" -> "TODOS LOS PODERÍAN")
  2. Convertir una cadena en un caso de sarcasmo (p. Ej., "All Might" -> "AlL miGhT")
    • Para este, no estaba seguro de si tenía más sentido alternar o simplemente poner en mayúsculas y minúsculas cada letra. ¡Tu puedes decidir!
  3. Convertir una cadena en mayúsculas y minúsculas (p. Ej., "Todos podrían" -> "Todos podrían")

Cada uno de estos desafíos viene con un conjunto único de problemas. Siéntase libre de compartir una solución para cualquiera de ellos a continuación en los comentarios. Como siempre, también dejaré uno allí para que empiecen.

Un pequeño resumen

Con todo lo dicho, creo que hemos terminado por hoy. Aquí están todas las soluciones de este artículo en un lugar conveniente:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
dieciséis
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from string import ascii_lowercase, ascii_uppercase
 
hero = "All Might"
 
# Brute force using concatenation
output = ""
for char in hero:
  if "A" <= char <= "Z":
    output += chr(ord(char) - ord('A') + ord('a'))
  else:
    output += char
 
# Brute force using join
output = []
for char in hero:
  if "A" <= char <= "Z":
    output.append(chr(ord(char) - ord('A') + ord(