Preguntas frecuentes sobre diseño e historia

Contenido

¿Por qué Python usa indentación para agrupar declaraciones?

Guido van Rossum cree que usar indentación para agrupar es extremadamente elegante y contribuye mucho a la claridad del programa Python promedio. La mayoría de las personas aprenden a amar esta característica después de un tiempo.

Como no hay corchetes de inicio/fin, no puede haber un desacuerdo entre la agrupación percibida por el analizador y el lector humano. Ocasionalmente, los programadores de C encontrarán un fragmento de código como este:

if (x <= y)
        x++;
        y--;
z++;

Solo se ejecuta la instrucción x ++ si la condición es verdadera, pero la indentación lo lleva a creer lo contrario. Incluso los programadores experimentados de C a veces lo miran durante mucho tiempo preguntándose por qué y se está disminuyendo incluso para x > y.

Debido a que no hay corchetes de inicio/fin, Python es mucho menos propenso a conflictos de estilo de codificación. En C hay muchas formas diferentes de colocar las llaves. Si está acostumbrado a leer y escribir código que usa un estilo, se sentirá al menos un poco incómodo cuando lea (o le pidan que escriba) otro estilo.

Muchos estilos de codificación colocan corchetes de inicio / fin en una línea por sí mismos. Esto hace que los programas sean considerablemente más largos y desperdicia un valioso espacio en la pantalla, lo que dificulta obtener una buena visión general de un programa. Idealmente, una función debería caber en una pantalla (por ejemplo, 20-30 líneas). 20 líneas de Python pueden hacer mucho más trabajo que 20 líneas de C. Esto no se debe únicamente a la falta de corchetes de inicio/fin – la falta de declaraciones y los tipos de datos de alto nivel también son responsables – sino también la indentación basada en la sintaxis ciertamente ayuda.

¿Por qué los cálculos de punto flotante son tan inexactos?

Los usuarios a menudo se sorprenden por resultados como este:

>>> 1.2 - 1.0
0.19999999999999996

y creo que es un error en Python. No es. Esto tiene poco que ver con Python, y mucho más con la forma en que la plataforma subyacente maneja los números de punto flotante.

El tipo float en CPython usa una C double para el almacenamiento. Un valor del objeto float se almacena en coma flotante binaria con una precisión fija (típicamente 53 bits) y Python usa operaciones C, que a su vez dependen de la implementación de hardware en el procesador, para realizar operaciones de coma flotante. Esto significa que, en lo que respecta a las operaciones de punto flotante, Python se comporta como muchos lenguajes populares, incluidos C y Java.

Muchos números que se pueden escribir fácilmente en notación decimal no se pueden expresar exactamente en coma flotante binaria. Por ejemplo, después de:

>>> x = 1.2

el valor almacenado para x es una aproximación (muy buena) al valor decimal 1.2, pero no es exactamente igual a él. En una máquina típica, el valor real almacenado es:

1.0011001100110011001100110011001100110011001100110011 (binary)

que es exactamente:

1.1999999999999999555910790149937383830547332763671875 (decimal)

La precisión típica de 53 bits proporciona flotantes Python con 15–16 dígitos decimales de precisión.

Para obtener una explicación más completa, consulte el capítulo aritmética de coma flotante en el tutorial de Python.

¿Por qué las cadenas de caracteres de Python son inmutables?

Hay varias ventajas.

Una es el rendimiento: saber que una cadena es inmutable significa que podemos asignarle espacio en el momento de la creación, y los requisitos de almacenamiento son fijos e inmutables. Esta es también una de las razones para la distinción entre tuplas y listas.

Otra ventaja es que las cadenas en Python se consideran tan «elementales» como los números. Ninguna cantidad de actividad cambiará el valor 8 a otra cosa, y en Python, ninguna cantidad de actividad cambiará la cadena «ocho» a otra cosa.

¿Por qué debe usarse “self” explícitamente en las definiciones y llamadas de métodos?

La idea fue tomada de Modula-3. Resulta ser muy útil, por una variedad de razones.

Primero, es más obvio que está utilizando un método o atributo de instancia en lugar de una variable local. Leer self.x o self.meth() deja absolutamente claro que se usa una variable de instancia o método incluso si no conoce la definición de clase de memoria. En C++, puede darse cuenta de la falta de una declaración de variable local (suponiendo que los globales son raros o fácilmente reconocibles) – pero en Python, no hay declaraciones de variables locales, por lo que debería buscar la definición de clase para estar seguro. Algunos estándares de codificación de C++ y Java requieren que los atributos de instancia tengan un prefijo m_, porque el ser explícito también es útil en esos lenguajes.

En segundo lugar, significa que no es necesaria una sintaxis especial si desea hacer referencia explícita o llamar al método desde una clase en particular. En C++, si desea usar un método de una clase base que se anula en una clase derivada, debe usar el operador :: – en Python puede escribir baseclass.methodname(self, <argument list>). Esto es particularmente útil para métodos __init__(), y en general en los casos en que un método de clase derivada quiere extender el método de clase base del mismo nombre y, por lo tanto, tiene que llamar al método de clase base de alguna manera.

Finalmente, para las variables de instancia se resuelve un problema sintáctico con la asignación: dado que las variables locales en Python son (¡por definición!) Aquellas variables a las que se asigna un valor en un cuerpo de función (y que no se declaran explícitamente como globales), tiene que haber una forma de decirle al intérprete que una asignación estaba destinada a asignar a una variable de instancia en lugar de a una variable local, y que preferiblemente debería ser sintáctica (por razones de eficiencia). C++ hace esto a través de declaraciones, pero Python no tiene declaraciones y sería una pena tener que presentarlas solo para este propósito. Usar el self.var explícito resuelve esto muy bien. Del mismo modo, para usar variables de instancia, tener que escribir self.var significa que las referencias a nombres no calificados dentro de un método no tienen que buscar en los directorios de la instancia. Para decirlo de otra manera, las variables locales y las variables de instancia viven en dos espacios de nombres diferentes, y debe decirle a Python qué espacio de nombres usar.

¿Por qué no puedo usar una tarea en una expresión?

¡A partir de Python 3.8, se puede!

Asignación de expresiones usando el operador de morsa := asigna una variable en una expresión:

while chunk := fp.read(200):
   print(chunk)

Ver PEP 572 para más información.

¿Por qué Python usa métodos para alguna funcionalidad (por ejemplo, list.index()) pero funciones para otra (por ejemplo, len(list))?

Como dijo Guido:

(a) Para algunas operaciones, la notación de prefijo solo se lee mejor que postfijo – las operaciones de prefijo (e ¡infijo!) tienen una larga tradición en matemáticas que le gustan las anotaciones donde las imágenes ayudan al matemático a pensar en un problema. Compare lo fácil con que reescribimos una fórmula como x*(a+b) en x*a + x*b con la torpeza de hacer lo mismo usando una notación OO sin procesar.

(b) Cuando leo un código que dice len(x), que está pidiendo la longitud de algo. Esto me dice dos cosas: el resultado es un número entero y el argumento es algún tipo de contenedor. Por el contrario, cuando leo x.len (), ya debo saber que x es algún tipo de contenedor que implementa una interfaz o hereda de una clase que tiene un estándar len(). Sea testigo de la confusión que ocasionalmente tenemos cuando una clase que no está implementando una asignación tiene un método get() o keys(), o algo que no es un archivo tiene un método write().

https://mail.python.org/pipermail/python-3000/2006-November/004643.html

¿Por qué join() es un método de cadena de caracteres en lugar de un método de lista o tupla?

Las cadenas de caracteres se volvieron mucho más parecidas a otros tipos estándar a partir de Python 1.6, cuando se agregaron métodos que brindan la misma funcionalidad que siempre ha estado disponible utilizando las funciones del módulo de cadenas. La mayoría de estos nuevos métodos han sido ampliamente aceptados, pero el que parece hacer que algunos programadores se sientan incómodos es:

", ".join(['1', '2', '4', '8', '16'])

que da el resultado:

"1, 2, 4, 8, 16"

Hay dos argumentos comunes en contra de este uso.

El primero corre a lo largo de las líneas de: «Se ve realmente feo el uso de un método de un literal de cadena (constante de cadena)», a lo que la respuesta es que sí, pero un literal de cadena es solo un valor fijo. Si se permiten los métodos en nombres vinculados a cadenas, no hay razón lógica para que no estén disponibles en literales.

La segunda objeción generalmente se presenta como: «Realmente estoy diciéndole a una secuencia que una a sus miembros junto con una constante de cadena». Lamentablemente, no lo estas haciendo. Por alguna razón, parece ser mucho menos difícil tener split() como método de cadena, ya que en ese caso es fácil ver que:

"1, 2, 4, 8, 16".split(", ")

es una instrucción a un literal de cadena para retornar las subcadenas de caracteres delimitadas por el separador dado (o, por defecto, ejecuciones arbitrarias de espacio en blanco).

join() es un método de cadena de caracteres porque al usarlo le está diciendo a la cadena de separación que itere sobre una secuencia de cadenas y se inserte entre elementos adyacentes. Este método se puede usar con cualquier argumento que obedezca las reglas para los objetos de secuencia, incluidas las clases nuevas que pueda definir usted mismo. Existen métodos similares para bytes y objetos bytearray.

¿Qué tan rápido van las excepciones?

Un bloque try/except es extremadamente eficiente si no se generan excepciones. En realidad, capturar una excepción es costoso. En versiones de Python anteriores a la 2.0, era común usar este modismo:

try:
    value = mydict[key]
except KeyError:
    mydict[key] = getvalue(key)
    value = mydict[key]

Esto solo tenía sentido cuando esperaba que el dict tuviera la clave casi todo el tiempo. Si ese no fuera el caso, lo codificó así:

if key in mydict:
    value = mydict[key]
else:
    value = mydict[key] = getvalue(key)

Para este caso específico, también podría usar value = dict.setdefault(key, getvalue(key)), pero solo si la llamada getvalue() es lo suficientemente barata porque se evalúa en todos los casos.

¿Por qué no hay un switch o una declaración case en Python?

Puede hacer esto fácilmente con una secuencia de if... elif... elif... else. Ha habido algunas propuestas para cambiar la sintaxis de la declaración, pero todavía no hay consenso sobre si y cómo hacer pruebas de rango. Ver PEP 275 para detalles completos y el estado actual.

Para los casos en los que necesita elegir entre una gran cantidad de posibilidades, puede crear un diccionario que asigne valores de casos a funciones para llamar. Por ejemplo:

def function_1(...):
    ...

functions = {'a': function_1,
             'b': function_2,
             'c': self.method_1, ...}

func = functions[value]
func()

Para invocar métodos en objetos, puede simplificar aún más utilizando getattr() incorporado para recuperar métodos con un nombre particular:

def visit_a(self, ...):
    ...
...

def dispatch(self, value):
    method_name = 'visit_' + str(value)
    method = getattr(self, method_name)
    method()

Se sugiere que utilice un prefijo para los nombres de los métodos, como visit_ en este ejemplo. Sin dicho prefijo, si los valores provienen de una fuente no confiable, un atacante podría invocar cualquier método en su objeto.

¿No puede emular hilos en el intérprete en lugar de confiar en una implementación de hilos específica del sistema operativo?

Respuesta 1: Desafortunadamente, el intérprete empuja al menos un marco de pila C para cada marco de pila de Python. Además, las extensiones pueden volver a llamar a Python en momentos casi aleatorios. Por lo tanto, una implementación completa de subprocesos requiere soporte de subprocesos para C.

Respuesta 2: Afortunadamente, existe Python sin pila, que tiene un bucle de intérprete completamente rediseñado que evita la pila C.

¿Por qué las expresiones lambda no pueden contener sentencias?

Las expresiones lambda de Python no pueden contener declaraciones porque el marco sintáctico de Python no puede manejar declaraciones anidadas dentro de expresiones. Sin embargo, en Python, este no es un problema grave. A diferencia de las formas lambda en otros lenguajes, donde agregan funcionalidad, las lambdas de Python son solo una notación abreviada si eres demasiado vago para definir una función.

Las funciones ya son objetos de primera clase en Python y pueden declararse en un ámbito local. Por lo tanto, la única ventaja de usar una lambda en lugar de una función definida localmente es que no es necesario inventar un nombre para la función, sino que es solo una variable local para la cual el objeto de función (que es exactamente el mismo tipo de se asigna un objeto que produce una expresión lambda)

¿Se puede compilar Python en código máquina, C o algún otro lenguaje?

Cython compila una versión modificada de Python con anotaciones opcionales en extensiones C. Nuitka es un compilador prometedor de Python en código C ++, con el objetivo de soportar el lenguaje completo de Python. Para compilar en Java puede considerar VOC.

¿Cómo gestiona Python la memoria?

Los detalles de la administración de memoria de Python dependen de la implementación. La implementación estándar de Python, CPython, utiliza el recuento de referencias para detectar objetos inaccesibles, y otro mecanismo para recopilar ciclos de referencia, ejecutando periódicamente un algoritmo de detección de ciclos que busca ciclos inaccesibles y elimina los objetos involucrados. El módulo gc proporciona funciones para realizar una recolección de basura, obtener estadísticas de depuración y ajustar los parámetros del recolector.

Sin embargo, otras implementaciones (como Jython o PyPy) pueden confiar en un mecanismo diferente, como recolector de basura. Esta diferencia puede causar algunos problemas sutiles de portabilidad si su código de Python depende del comportamiento de la implementación de conteo de referencias.

En algunas implementaciones de Python, el siguiente código (que está bien en CPython) probablemente se quedará sin descriptores de archivo:

for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

De hecho, utilizando el esquema de conteo de referencias y destructor de CPython, cada nueva asignación a f cierra el archivo anterior. Sin embargo, con un GC tradicional, esos objetos de archivo solo se recopilarán (y cerrarán) a intervalos variables y posiblemente largos.

Si desea escribir código que funcione con cualquier implementación de Python, debe cerrar explícitamente el archivo o utilizar una declaración with; esto funcionará independientemente del esquema de administración de memoria:

for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)

¿Por qué CPython no utiliza un esquema de recolección de basura más tradicional?

Por un lado, esta no es una característica estándar de C y, por lo tanto, no es portátil. (Sí, sabemos acerca de la biblioteca Boehm GC. Tiene fragmentos de código de ensamblador para la mayoría de las plataformas comunes, no para todas ellas, y aunque es principalmente transparente, no es completamente transparente; se requieren parches para obtener Python para trabajar con eso)

El GC tradicional también se convierte en un problema cuando Python está integrado en otras aplicaciones. Mientras que en un Python independiente está bien reemplazar el estándar malloc() y free() con versiones proporcionadas por la biblioteca GC, una aplicación que incruste Python puede querer tener su propio sustituto de malloc() y free(), y puede No quiero a Python. En este momento, CPython funciona con todo lo que implementa malloc() y free() correctamente.

¿Por qué no se libera toda la memoria cuando sale CPython?

Los objetos a los que se hace referencia desde los espacios de nombres globales de los módulos de Python no siempre se desasignan cuando Python sale. Esto puede suceder si hay referencias circulares. También hay ciertos bits de memoria asignados por la biblioteca de C que son imposibles de liberar (por ejemplo, una herramienta como Purify se quejará de estos). Python es, sin embargo, agresivo sobre la limpieza de la memoria al salir e intenta destruir cada objeto.

Si desea forzar a Python a eliminar ciertas cosas en la desasignación, use el módulo atexit para ejecutar una función que obligará a esas eliminaciones.

¿Por qué hay tipos de datos separados de tuplas y listas?

Las listas y las tuplas, si bien son similares en muchos aspectos, generalmente se usan de maneras fundamentalmente diferentes. Se puede pensar que las tuplas son similares a los registros Pascal o estructuras C; son pequeñas colecciones de datos relacionados que pueden ser de diferentes tipos que funcionan como un grupo. Por ejemplo, una coordenada cartesiana se representa adecuadamente como una tupla de dos o tres números.

Las listas, por otro lado, son más como matrices en otros lenguajes. Tienden a contener un número variable de objetos, todos los cuales tienen el mismo tipo y que se operan uno por uno. Por ejemplo, os.listdir('.') Retorna una lista de cadenas de caracteres que representan los archivos en el directorio actual. Las funciones que operan en esta salida generalmente no se romperían si agregara otro archivo o dos al directorio.

Las tuplas son inmutables, lo que significa que una vez que se ha creado una tupla, no puede reemplazar ninguno de sus elementos con un nuevo valor. Las listas son mutables, lo que significa que siempre puede cambiar los elementos de una lista. Solo los elementos inmutables se pueden usar como claves de diccionario y, por lo tanto, solo las tuplas y no las listas se pueden usar como claves.

¿Cómo se implementan las listas en Python?

Las listas de CPython son realmente matrices de longitud variable, no listas enlazadas al estilo Lisp. La implementación utiliza una matriz contigua de referencias a otros objetos y mantiene un puntero a esta matriz y la longitud de la matriz en una estructura de encabezado de lista.

Esto hace que indexar una lista a[i] una operación cuyo costo es independiente del tamaño de la lista o del valor del índice.

Cuando se añaden o insertan elementos, la matriz de referencias cambia de tamaño. Se aplica cierta inteligencia para mejorar el rendimiento de la adición de elementos repetidamente; cuando la matriz debe crecer, se asigna un espacio extra para que las próximas veces no requieran un cambio de tamaño real.

¿Cómo se implementan los diccionarios en CPython?

Los diccionarios de CPython se implementan como tablas hash redimensionables. En comparación con los árboles B (B-trees), esto proporciona un mejor rendimiento para la búsqueda (la operación más común con diferencia) en la mayoría de las circunstancias, y la implementación es más simple.

Los diccionarios funcionan calculando un código hash para cada clave almacenada en el diccionario utilizando la función incorporada hash(). El código hash varía ampliamente según la clave y una semilla por proceso; por ejemplo, «Python» podría dividir en hash a -539294296 mientras que «python», una cadena que difiere en un solo bit, podría dividir en 1142331976. El código de resumen se usa para calcular una ubicación en una matriz interna donde se almacenará el valor . Suponiendo que está almacenando claves que tienen valores hash diferentes, esto significa que los diccionarios toman tiempo constante – O(1), en notación Big-O – para recuperar una clave.

¿Por qué las claves del diccionario deben ser inmutables?

La implementación de la tabla hash de los diccionarios utiliza un valor hash calculado a partir del valor clave para encontrar la clave. Si la clave fuera un objeto mutable, su valor podría cambiar y, por lo tanto, su hash también podría cambiar. Pero dado que quien cambie el objeto clave no puede decir que se estaba utilizando como clave de diccionario, no puede mover la entrada en el diccionario. Luego, cuando intente buscar el mismo objeto en el diccionario, no se encontrará porque su valor hash es diferente. Si trató de buscar el valor anterior, tampoco lo encontraría, porque el valor del objeto que se encuentra en ese hash bin sería diferente.

Si desea un diccionario indexado con una lista, simplemente convierta la lista a una tupla primero; La función tuple(L) crea una tupla con las mismas entradas que la lista L. Las tuplas son inmutables y, por lo tanto, pueden usarse como claves de diccionario.

Algunas soluciones inaceptables que se han propuesto:

  • Listas de hash por su dirección (ID de objeto). Esto no funciona porque si construye una nueva lista con el mismo valor, no se encontrará; por ejemplo:

    mydict = {[1, 2]: '12'}
    print(mydict[[1, 2]])
    

    generaría una excepción KeyError porque la identificación del [1, 2] usado en la segunda línea difiere de la de la primera línea. En otras palabras, las claves del diccionario deben compararse usando ==, no usando is.

  • Hacer una copia cuando use una lista como clave. Esto no funciona porque la lista, al ser un objeto mutable, podría contener una referencia a sí misma, y luego el código de copia se ejecutaría en un bucle infinito.

  • Permitir listas como claves pero decirle al usuario que no las modifique. Esto permitiría una clase de errores difíciles de rastrear en los programas cuando olvidó o modificó una lista por accidente. También invalida una invariante importante de diccionarios: cada valor en d.keys() se puede usar como una clave del diccionario.

  • Marcar las listas como de solo lectura una vez que se usan como clave de diccionario. El problema es que no solo el objeto de nivel superior puede cambiar su valor; podría usar una tupla que contiene una lista como clave. Ingresar cualquier cosa como clave en un diccionario requeriría marcar todos los objetos accesibles desde allí como de solo lectura – y nuevamente, los objetos autoreferenciados podrían causar un bucle infinito.

Hay un truco para evitar esto si lo necesita, pero úselo bajo su propio riesgo: puede envolver una estructura mutable dentro de una instancia de clase que tenga un método __eq__() y a __hash__() . Luego debe asegurarse de que el valor hash para todos los objetos de contenedor que residen en un diccionario (u otra estructura basada en hash) permanezca fijo mientras el objeto está en el diccionario (u otra estructura).

class ListWrapper:
    def __init__(self, the_list):
        self.the_list = the_list

    def __eq__(self, other):
        return self.the_list == other.the_list

    def __hash__(self):
        l = self.the_list
        result = 98767 - len(l)*555
        for i, el in enumerate(l):
            try:
                result = result + (hash(el) % 9999999) * 1001 + i
            except Exception:
                result = (result % 7777777) + i * 333
        return result

Tenga en cuenta que el cálculo de hash se complica por la posibilidad de que algunos miembros de la lista sean inquebrantables y también por la posibilidad de desbordamiento aritmético.

Además, siempre debe darse el caso de que si o1 == o2 (es decir, o1.__eq__(o2) is True), entonces hash(o1) == hash(o2) (es decir, o1.__hash__() == o2.__hash__()), independientemente de si el objeto está en un diccionario o no. Si no cumple con estas restricciones, los diccionarios y otras estructuras basadas en hash se comportarán mal.

En el caso de ListWrapper, siempre que el objeto contenedor esté en un diccionario, la lista ajustada no debe cambiar para evitar anomalías. No haga esto a menos que esté preparado para pensar detenidamente sobre los requisitos y las consecuencias de no cumplirlos correctamente. Considérese advertido.

¿Por qué list.sort() no retorna la lista ordenada?

En situaciones donde el rendimiento es importante, hacer una copia de la lista solo para ordenarlo sería un desperdicio. Por lo tanto, list.sort() ordena la lista en su lugar. Para recordarle ese hecho, no retorna la lista ordenada. De esta manera, no se dejará engañar por sobreescribir accidentalmente una lista cuando necesite una copia ordenada, pero también deberá mantener la versión sin ordenar.

Si desea retornar una nueva lista, use la función incorporada sorted() en su lugar. Esta función crea una nueva lista a partir de un iterativo proporcionado, la ordena y la retorna. Por ejemplo, a continuación se explica cómo iterar sobre las teclas de un diccionario en orden ordenado:

for key in sorted(mydict):
    ...  # do whatever with mydict[key]...

¿Cómo se especifica y aplica una especificación de interfaz en Python?

Una especificación de interfaz para un módulo proporcionada por lenguajes como C++ y Java describe los prototipos para los métodos y funciones del módulo. Muchos sienten que la aplicación en tiempo de compilación de las especificaciones de la interfaz ayuda en la construcción de grandes programas.

Python 2.6 agrega un módulo abc que le permite definir clases base abstractas (ABC). Luego puede usar isinstance() y issubclass() para verificar si una instancia o una clase implementa un ABC en particular. El módulo collections.abc define un conjunto de ABC útiles como Iterable, Container y MutableMapping.

For Python, many of the advantages of interface specifications can be obtained by an appropriate test discipline for components.

Un buen conjunto de pruebas para un módulo puede proporcionar una prueba de regresión y servir como una especificación de interfaz de módulo y un conjunto de ejemplos. Muchos módulos de Python se pueden ejecutar como un script para proporcionar una simple «autocomprobación». Incluso los módulos que usan interfaces externas complejas a menudo se pueden probar de forma aislada utilizando emulaciones triviales de «stub» de la interfaz externa. Los módulos doctest y unittest o marcos de prueba de terceros se pueden utilizar para construir conjuntos de pruebas exhaustivas que ejercitan cada línea de código en un módulo.

Una disciplina de prueba adecuada puede ayudar a construir grandes aplicaciones complejas en Python, así como tener especificaciones de interfaz. De hecho, puede ser mejor porque una especificación de interfaz no puede probar ciertas propiedades de un programa. Por ejemplo, se espera que el método append() agregue nuevos elementos al final de alguna lista interna; una especificación de interfaz no puede probar que su implementación append() realmente haga esto correctamente, pero es trivial verificar esta propiedad en un conjunto de pruebas.

Escribir conjuntos de pruebas es muy útil, y es posible que desee diseñar su código con miras a que sea fácilmente probado. Una técnica cada vez más popular, el desarrollo dirigido por pruebas, requiere escribir partes del conjunto de pruebas primero, antes de escribir el código real. Por supuesto, Python te permite ser descuidado y no escribir casos de prueba.

¿Por qué no hay goto?

In the 1970s people realized that unrestricted goto could lead to messy «spaghetti» code that was hard to understand and revise. In a high-level language, it is also unneeded as long as there are ways to branch (in Python, with if statements and or, and, and if-else expressions) and loop (with while and for statements, possibly containing continue and break).

One can also use exceptions to provide a «structured goto» that works even across function calls. Many feel that exceptions can conveniently emulate all reasonable uses of the «go» or «goto» constructs of C, Fortran, and other languages. For example:

class label(Exception): pass  # declare a label

try:
    ...
    if condition: raise label()  # goto label
    ...
except label:  # where to goto
    pass
...

Esto no le permite saltar a la mitad de un bucle, pero de todos modos eso generalmente se considera un abuso de goto. Utilizar con moderación.

¿Por qué las cadenas de caracteres sin formato (r-strings) no pueden terminar con una barra diagonal inversa?

Más precisamente, no pueden terminar con un número impar de barras invertidas: la barra invertida no emparejada al final escapa el carácter de comillas de cierre, dejando una cadena sin terminar.

Las cadenas de caracteres sin formato se diseñaron para facilitar la creación de entradas para procesadores (principalmente motores de expresión regular) que desean realizar su propio procesamiento de escape de barra invertida. Tales procesadores consideran que una barra invertida sin par es un error de todos modos, por lo que las cadenas de caracteres sin procesar no lo permiten. A cambio, le permiten pasar el carácter de comillas de cadena escapándolo con una barra invertida. Estas reglas funcionan bien cuando las cadenas de caracteres r (r-strings) se usan para el propósito previsto.

Si está intentando construir nombres de ruta de Windows, tenga en cuenta que todas las llamadas al sistema de Windows también aceptan barras diagonales:

f = open("/mydir/file.txt")  # works fine!

Si está tratando de construir una ruta para un comando de DOS, intente por ejemplo uno de los siguientes:

dir = r"\this\is\my\dos\dir" "\\"
dir = r"\this\is\my\dos\dir\ "[:-1]
dir = "\\this\\is\\my\\dos\\dir\\"

¿Por qué Python no tiene una declaración «with» para las asignaciones de atributos?

Python tiene una declaración “with” que envuelve la ejecución de un bloque, llamando al código en la entrada y salida del bloque. Algunos lenguajes tienen una construcción que se ve así:

with obj:
    a = 1               # equivalent to obj.a = 1
    total = total + 1   # obj.total = obj.total + 1

En Python, tal construcción sería ambigua.

Otros lenguajes, como Object Pascal, Delphi y C ++, utilizan tipos estáticos, por lo que es posible saber, de manera inequívoca, a qué miembro se le está asignando. Este es el punto principal de la escritura estática: el compilador siempre conoce el alcance de cada variable en tiempo de compilación.

Python usa tipos dinámicos. Es imposible saber de antemano a qué atributo se hará referencia en tiempo de ejecución. Los atributos de los miembros pueden agregarse o eliminarse de los objetos sobre la marcha. Esto hace que sea imposible saber, a partir de una simple lectura, a qué atributo se hace referencia: ¿uno local, uno global o un atributo miembro?

Por ejemplo, tome el siguiente fragmento incompleto:

def foo(a):
    with a:
        print(x)

El fragmento supone que «a» debe tener un atributo miembro llamado «x». Sin embargo, no hay nada en Python que le diga esto al intérprete. ¿Qué debería suceder si «a» es, digamos, un número entero? Si hay una variable global llamada «x», ¿se usará dentro del bloque with? Como puede ver, la naturaleza dinámica de Python hace que tales elecciones sean mucho más difíciles.

Sin embargo, el beneficio principal de «with» y características de lenguaje similares (reducción del volumen del código) se puede lograr fácilmente en Python mediante la asignación. En vez de:

function(args).mydict[index][index].a = 21
function(args).mydict[index][index].b = 42
function(args).mydict[index][index].c = 63

escribe esto:

ref = function(args).mydict[index][index]
ref.a = 21
ref.b = 42
ref.c = 63

Esto también tiene el efecto secundario de aumentar la velocidad de ejecución porque los enlaces de nombres se resuelven en tiempo de ejecución en Python, y la segunda versión solo necesita realizar la resolución una vez.

¿Por qué se requieren dos puntos para las declaraciones if/while/def/class?

Los dos puntos se requieren principalmente para mejorar la legibilidad (uno de los resultados del lenguaje ABC experimental). Considera esto:

if a == b
    print(a)

versus

if a == b:
    print(a)

Observe cómo el segundo es un poco más fácil de leer. Observe más a fondo cómo los dos puntos establecen el ejemplo en esta respuesta de preguntas frecuentes; Es un uso estándar en inglés.

Otra razón menor es que los dos puntos facilitan a los editores con resaltado de sintaxis; pueden buscar dos puntos para decidir cuándo se debe aumentar la indentación en lugar de tener que hacer un análisis más elaborado del texto del programa.

¿Por qué Python permite comas al final de las listas y tuplas?

Python le permite agregar una coma final al final de las listas, tuplas y diccionarios:

[1, 2, 3,]
('a', 'b', 'c',)
d = {
    "A": [1, 5],
    "B": [6, 7],  # last trailing comma is optional but good style
}

Hay varias razones para permitir esto.

Cuando tiene un valor literal para una lista, tupla o diccionario distribuido en varias líneas, es más fácil agregar más elementos porque no tiene que recordar agregar una coma a la línea anterior. Las líneas también se pueden reordenar sin crear un error de sintaxis.

La omisión accidental de la coma puede ocasionar errores difíciles de diagnosticar. Por ejemplo:

x = [
  "fee",
  "fie"
  "foo",
  "fum"
]

Parece que esta lista tiene cuatro elementos, pero en realidad contiene tres: «fee», «fiefoo» y «fum». Agregar siempre la coma evita esta fuente de error.

Permitir la coma final también puede facilitar la generación de código programático.