En realidad, el título de este maravilloso artículo de Jeff Knapp, el autor del libro "
Writing Idiomatic Python " refleja plenamente su esencia. Lea atentamente y no dude en comentar.
Como realmente no queríamos dejar el término importante en letras latinas en el texto, nos permitimos traducir la palabra "docstring" como "docstring", después de haber descubierto este
término en
varias fuentes en
ruso .
En Python, como en la mayoría de los lenguajes de programación modernos, la función es el método principal de abstracción y encapsulación. Usted, como desarrollador, probablemente ya haya escrito cientos de funciones. Pero funciones a funciones - discordia. Además, si escribe funciones "malas", esto afectará inmediatamente la legibilidad y el soporte de su código. Entonces, ¿qué es una función "mala" y, lo que es más importante, cómo hacerla "buena"?
Actualizar el tema
Las matemáticas están repletas de funciones, sin embargo, es difícil recordarlas. Así que volvamos a nuestra disciplina favorita: el análisis. Probablemente has visto fórmulas como
f(x) = 2x + 3
. Esta es una función llamada
f
que toma un argumento
x
y luego "devuelve" dos veces
x + 3
. Aunque no es muy similar a las funciones a las que estamos acostumbrados en Python, es completamente similar al siguiente código:
def f(x): return 2*x + 3
Las funciones han existido durante mucho tiempo en las matemáticas, pero en ciencias de la computación están completamente transformadas. Sin embargo, este poder no se da en vano: tienes que superar varias trampas. Analicemos qué debe ser una función "buena" y qué "campanas y silbatos" son típicos para funciones que pueden requerir refactorización.
Secretos de buena función
¿Qué distingue una función Python "buena" de una función mediocre? Se sorprenderá de cuántas interpretaciones permite la palabra "bueno". Como parte de este artículo, consideraré la función de Python "buena" si satisface la
mayoría de los elementos de la siguiente lista (a veces no es posible completar todos los elementos para una función en particular):
- Se llama claramente
- Cumple con el principio del deber único
- Contiene muelle
- Valor devuelto
- Consiste en no más de 50 líneas
- Ella es idempotente y, si es posible, pura.
Para muchos de ustedes, estos requisitos pueden parecer demasiado duros. Sin embargo, lo prometo: si sus funciones cumplen con estas reglas, resultarán tan hermosas que incluso perforarán un unicornio con una lágrima. A continuación dedicaré una sección a cada uno de los elementos de la lista anterior, y luego completaré la historia contando cómo se armonizan entre sí y ayudan a crear buenas funciones.
NombrarAquí está mi cita favorita sobre este tema, a menudo atribuida erróneamente a Donald, pero en realidad propiedad de
Phil Carleton :
Hay dos desafíos para la informática: invalidación de caché y nombres.
No importa cuán tonto suene, nombrar es realmente algo complicado. Aquí hay un ejemplo de un nombre de función "malo":
def get_knn_from_df(df):
Ahora, los malos nombres me llegan casi a todas partes, pero este ejemplo está tomado del campo de la ciencia de datos (más precisamente, el aprendizaje automático), donde los profesionales generalmente escriben código en un cuaderno Jupyter y luego intentan ensamblar un programa digerible a partir de estas celdas.
El primer problema con el nombre de esta función es que usa abreviaturas.
Es mejor usar palabras completas en inglés, en lugar de abreviaturas y abreviaturas no conocidas . La única razón por la que quiero acortar las palabras es para no perder el tiempo escribiendo demasiado texto, pero
cualquier editor moderno tiene una función de autocompletado , por lo que debe escribir el nombre completo de la función solo una vez. La abreviatura es un problema, porque a menudo es específica para un área temática. En el código anterior,
knn
significa "K-vecinos más cercanos", y
df
significa "DataFrame", una estructura de datos comúnmente utilizada en la biblioteca de
pandas . Si un programador que no conoce estas abreviaturas lee el código, entonces no entenderá casi nada en el nombre de la función.
Hay dos defectos menores más en el nombre de esta función. En primer lugar, la palabra
"get"
redundante. En la mayoría de las funciones con nombre competente, está claro de inmediato que esta función devuelve algo, que se refleja específicamente en el nombre. El elemento
from_d
f tampoco es necesario. Ya sea en la base de funciones o (si está en la periferia), el tipo del parámetro se describirá en la anotación de tipo si esta información
no es obvia del nombre del parámetro .
Entonces, ¿cómo cambiamos el nombre de esta función? Solo:
def k_nearest_neighbors(dataframe):
Ahora, incluso un lego entiende lo que se está calculando en esta función, y el nombre del parámetro
(dataframe)
no deja dudas sobre qué argumento se le debe pasar.
Responsabilidad exclusiva
Desarrollando la idea de Bob Martin, diré que el
principio de responsabilidad exclusiva se aplica a funciones no menos que clases y módulos (sobre los cuales el Sr. Martin escribió originalmente). De acuerdo con este principio (en nuestro caso), una función debe tener una sola responsabilidad. Es decir, ella debe hacer una y solo una cosa. Una de las razones más convincentes para esto: si una función hace solo una cosa, entonces tendrá que reescribirse en el único caso: si esto mismo tiene que hacerse de una manera nueva. También queda claro cuándo se puede eliminar una función; si, haciendo cambios en otro lugar, entendemos que el único deber de una función ya no es relevante, entonces simplemente nos desharemos de ella.
Es mejor dar un ejemplo. Aquí hay una función que hace más de una "cosa":
def calculate_and print_stats(list_of_numbers): sum = sum(list_of_numbers) mean = statistics.mean(list_of_numbers) median = statistics.median(list_of_numbers) mode = statistics.mode(list_of_numbers) print('-----------------Stats-----------------') print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean) print('MEDIAN: {}'.format(median) print('MODE: {}'.format(mode)
A saber, dos: calcula un conjunto de estadísticas en una lista de números y los muestra en
STDOUT
. Una función viola una regla: debe haber una única razón específica por la que deba modificarse. En este caso, hay dos razones obvias por las que esto es necesario: o necesita calcular estadísticas nuevas o diferentes, o necesita cambiar el formato de salida. Por lo tanto, es mejor reescribir esta función en forma de dos funciones separadas: una realizará cálculos y devolverá sus resultados, y la otra recibirá estos resultados y los mostrará en la consola.
Una función (o más bien, tiene dos responsabilidades) con menudillos da la palabra y en su nombre .
Esta separación también simplifica enormemente la prueba de la función, y también le permite no solo dividirla en dos funciones dentro del mismo módulo, sino incluso separar estas dos funciones en módulos completamente diferentes, si corresponde. Esto contribuye aún más a pruebas más limpias y simplifica el soporte de código.
De hecho, las funciones que realizan exactamente dos cosas son raras. Más a menudo, te encuentras con funciones que realizan muchas, muchas más operaciones. Nuevamente, por razones de legibilidad y comprobabilidad, tales funciones de "múltiples estaciones" deben dividirse en tareas únicas, cada una de las cuales contiene un solo aspecto del trabajo.
Docstrings
Parece que todos saben que hay un documento
PEP-8 que da recomendaciones sobre el estilo del código Python, pero hay muchas menos personas entre nosotros que conocen
PEP-257 , en el que se dan las mismas recomendaciones con respecto a las cadenas de acoplamiento. Para no volver a contar el contenido de PEP-257, lo envío a este documento usted mismo, lea en su tiempo libre. Sin embargo, sus ideas principales son las siguientes:
- Cada función necesita una cadena de documentación.
- Debe observar gramática y puntuación; escribir oraciones completas
- La cadena de documentación comienza con una breve descripción (en una oración) de lo que hace la función.
- La cadena de documentos se formula en un estilo prescriptivo en lugar de descriptivo
Todos estos puntos son fáciles de seguir al escribir características. Simplemente escribir docstrings debería convertirse en un hábito e intentar escribirlos antes de continuar con el código de la función en sí. Si no puede escribir una cadena de documentos clara que describa la función, esta es una buena razón para pensar por qué está escribiendo esta función.
Valores de retorno
Las funciones pueden (y
deben ) interpretarse como pequeños programas autónomos. Toman alguna entrada en forma de parámetros y devuelven el resultado. Los parámetros, por supuesto, son opcionales.
Pero los valores de retorno son necesarios desde el punto de vista de la estructura interna de Python . Si incluso intenta escribir una función que no devuelve un valor, no puede. Si la función ni siquiera devuelve valores, entonces el intérprete de Python la "obligará" a devolver
None
. No lo creo? Pruébalo tú mismo:
❯ python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True
Como puede ver, el valor de
b
es esencialmente
None
. Entonces, incluso si escribe una función sin una declaración de devolución, aún devolverá algo. Y deberia. Después de todo, este es un programa pequeño, ¿verdad? ¿Qué tan útiles son los programas de los cuales no hay conclusión, y por lo tanto es imposible juzgar si este programa se ejecutó correctamente? Pero lo más importante, ¿cómo vas a
probar dicho programa?
Ni siquiera tengo miedo de decir lo siguiente: cada función debería devolver un valor útil, al menos en aras de la capacidad de prueba. El código que escribo debe ser probado (esto no se discute). Solo imagine lo torpe que puede resultar la prueba de la función de
add
anterior (pista: tendrá que redirigir la entrada / salida, después de lo cual todo saldrá mal pronto). Además, al devolver un valor, podemos encadenar métodos y, por lo tanto, escribir código como este:
with open('foo.txt', 'r') as input_file: for line in input_file: if line.strip().lower().endswith('cat'):
Cadena
if line.strip().lower().endswith('cat'):
funciona porque cada uno de los métodos de cadena (
strip()
,
lower()
,
endswith()
) devuelve una cadena como resultado de llamar a la función.
Aquí hay algunas razones comunes que un programador puede darle al explicar por qué una función que escribe no devuelve un valor:
“Es solo [algún tipo de operación relacionada con la entrada / salida, por ejemplo, almacenar un valor en una base de datos]. Aquí no puedo devolver nada útil.
No estoy de acuerdo La función puede devolver True si la operación se completó con éxito.
“Aquí cambiamos uno de los parámetros disponibles, lo usamos como parámetro de referencia”. ""
Aquí hay dos puntos. Primero, haz tu mejor esfuerzo para no hacer esto. En segundo lugar, proporcionar una función con algún tipo de argumento solo para descubrir que ha cambiado es sorprendente en el mejor de los casos, y simplemente peligroso en el peor. En cambio, al igual que con los métodos de cadena, intente devolver una nueva instancia del parámetro que ya refleje los cambios que se le aplicaron. Incluso si esto no se puede hacer, ya que la creación de una copia de algún parámetro está cargada de costos excesivos, aún puede volver a la opción "Volver
True
si la operación se completó con éxito" propuesta anteriormente.
“Necesito devolver múltiples valores. No hay un valor único que en este caso sería aconsejable devolver ".
Este argumento es un poco exagerado, pero lo he escuchado. La respuesta, por supuesto, es precisamente lo que el autor quería hacer, pero no sabía cómo:
usar una tupla para devolver múltiples valores .
Finalmente, el argumento más fuerte de que es mejor devolver un valor útil en cualquier caso es que la persona que llama siempre puede ignorar justificadamente estos valores. En resumen, devolver un valor de una función es casi seguro una buena idea, y es muy poco probable que dañemos algo de esta manera, incluso en las bases de código existentes.
Longitud de la función
Admití más de una vez que soy bastante tonto. Puedo tener alrededor de tres cosas en mi cabeza al mismo tiempo. Si me permite leer la función de 200 líneas y preguntar qué hace, probablemente la miraré durante al menos 10 segundos.
La longitud de una función afecta directamente su legibilidad y, por lo tanto, su soporte . Por lo tanto, trate de mantener sus funciones cortas. 50 líneas: un valor tomado completamente del techo, pero me parece razonable. (Espero) que la mayoría de las funciones que escribes serán mucho más cortas.
Si una función cumple con el Principio de responsabilidad exclusiva, es probable que sea lo suficientemente breve. Si es de lectura o idempotente (hablaremos de esto) a continuación, entonces, probablemente, también será breve. Todas estas ideas se combinan armoniosamente entre sí y ayudan a escribir un código bueno y limpio.
Entonces, ¿qué hacer si su función es demasiado larga?
REFACTOR! Probablemente tenga que refactorizar todo el tiempo, incluso si no conoce el término.
Refactorizar es simplemente cambiar la estructura de un programa, sin cambiar su comportamiento. Por lo tanto, extraer varias líneas de código de una función larga y convertirlas en una función independiente es uno de los tipos de refactorización. Resulta que esta es también la forma más común y rápida de acortar productivamente las funciones largas. Como le está dando a estas nuevas funciones nombres apropiados, el código resultante es mucho más fácil de leer. Escribí un libro completo sobre refactorización (de hecho, lo hago todo el tiempo), así que no entraré en detalles aquí. Solo sepa que si tiene una función que es demasiado larga, debe refactorizarla.
Idempotencia y limpieza funcional
El título de esta sección puede parecer un poco intimidante, pero conceptualmente la sección es simple. Una función idempotente con el mismo conjunto de argumentos siempre devuelve el mismo valor, independientemente de cuántas veces se llame. El resultado no depende de variables no locales, la variabilidad de los argumentos o de ningún dato proveniente de flujos de entrada / salida. La siguiente función
add_three(number)
es idempotente:
def add_three(number): """ ** + 3.""" return number + 3
Independientemente de cuántas veces llamemos
add_three(7)
, la respuesta siempre será 10. Pero otro caso es una función que no es idempotente:
def add_three(): """ 3 + , .""" number = int(input('Enter a number: ')) return number + 3
Esta función francamente ideada no es idempotente, ya que el valor de retorno de la función depende de la entrada / salida, es decir, del número ingresado por el usuario. Por supuesto, con diferentes llamadas a
add_three()
valores de retorno serán diferentes. Si llamamos a esta función dos veces, el usuario en el primer caso puede ingresar 3, y en el segundo - 7, y luego dos llamadas a
add_three()
devolverán 6 y 10, respectivamente.
Fuera de la programación, también hay ejemplos de idempotencia: por ejemplo, el botón arriba del elevador está diseñado de acuerdo con este principio. Al presionarlo por primera vez, "notificamos" al elevador que queremos subir. Como el botón es idempotente, no importa cuánto lo presiones más tarde, no pasará nada malo. El resultado siempre será el mismo.
¿Por qué es tan importante la idempotencia?
Capacidad de prueba y usabilidad. Las funciones idempotentes son fáciles de probar, ya que se garantiza que devolverán el mismo resultado en cualquier caso si las llama con los mismos argumentos. Las pruebas se reducen a verificar que con una variedad de llamadas, la función siempre devuelve el valor esperado. Además, estas pruebas serán rápidas: la velocidad de prueba es un tema importante que a menudo se pasa por alto en las pruebas unitarias. Y refactorizar cuando se trabaja con funciones idempotentes es generalmente una caminata fácil. No importa cómo cambie el código fuera de la función: el resultado de llamarlo con los mismos argumentos siempre será el mismo.
¿Qué es una función "pura"?
En la programación funcional, una función se considera pura si, en
primer lugar , es idempotente y, en
segundo lugar , no causa los
efectos secundarios observados. No lo olvide: una función es idempotente si siempre devuelve el mismo resultado con un conjunto específico de argumentos. Sin embargo, esto no significa que la función no pueda afectar a otros componentes, por ejemplo, variables no locales o flujos de entrada / salida. Por ejemplo, si la versión idempotente de la función
add_three(number)
anterior
add_three(number)
el resultado en la consola, y solo luego lo devuelve, aún se consideraría idempotente, porque cuando accede a la secuencia de entrada / salida, esta operación de acceso no afecta el valor devuelto de la función La llamada
print()
es solo un
efecto secundario : interacción con el resto del programa o sistema como tal, que ocurre junto con el valor de retorno.
Desarrollemos un poco nuestro ejemplo con
add_three(number)
. Puede escribir el siguiente código para determinar cuántas veces se ha llamado a
add_three(number)
:
add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """, *add_three*.""" return add_three_calls
Ahora ejecutamos la salida a la consola (esto es un efecto secundario) y cambiamos la variable no local (otro efecto secundario), pero dado que ninguno de los dos afecta el valor devuelto por la función, de todos modos es idempotente.
La función pura no tiene efectos secundarios. No solo no utiliza ningún "dato externo" al calcular el valor, sino que no interactúa con el resto del programa / sistema, solo calcula y devuelve el valor especificado. Por lo tanto, aunque nuestra nueva definición de
add_three(number)
sigue siendo idempotente, esta función ya no es pura.
En las funciones puras no hay instrucciones de registro o llamadas
print()
. Cuando trabajan, no acceden a la base de datos y no usan conexiones a Internet. No acceda ni modifique variables no locales.
Y no llame a otras funciones no puras .
En resumen, no tienen una "acción terrible de largo alcance", como lo expresan las palabras de Einstein (pero en el contexto de la informática, no de la física). No alteran de ninguna manera el resto del programa o sistema. En la
programación imperativa (que es lo que haces cuando escribes código en Python), tales funciones son las más seguras. Son conocidos por su capacidad de prueba y facilidad de soporte; Además, dado que son idempotentes, se garantiza que probar tales funciones sea tan rápido como ejecutarlo. Las pruebas en sí también son simples: no tiene que conectarse a la base de datos o simular ningún recurso externo, preparar la configuración inicial del código y, al final del trabajo, no necesita limpiar nada.
Honestamente, la idempotencia y la limpieza son muy deseables, pero no requeridas. , , , . , , , , . , , .
Conclusión
Eso es todo , – . . , . – ! . , , , « ». .