Python como el último caso de C ++. Parte 1/2

Del traductor

Brandon Rhodes es una persona muy modesta que se presenta en Twitter como "un programador de Python que paga un préstamo a la comunidad en forma de informes o ensayos". La cantidad de estos "informes y ensayos" es impresionante, al igual que la cantidad de proyectos gratuitos en los que Brandon fue o es colaborador. Y Brandon ha publicado dos libros y está escribiendo un tercero.


Muy a menudo encuentro en los comentarios sobre Habré un malentendido o rechazo fundamental de los lenguajes dinámicos, la tipificación dinámica, la programación generalizada y otros paradigmas. Estoy publicando esta traducción autorizada (abreviada) (transcripción) de uno de los informes de Brandon con la esperanza de que ayude a los programadores existentes en los paradigmas de los lenguajes estáticos a comprender mejor los lenguajes dinámicos, en particular Python.


Como es habitual en mí, le pido que me informe en PM de mis errores y errores tipográficos.


¿Qué significa la frase "caso marginal" en el título de mi informe? El caso limitante surge cuando itera sobre una secuencia de opciones hasta que alcanza el valor extremo. Por ejemplo, un polígono de n lados. Si n = 3, entonces este es un triángulo, n = 4 es un cuadrángulo, n = 5 es un pentágono, etc. A medida que n se acerca al infinito, los lados se vuelven más pequeños y más grandes, y el contorno del polígono se vuelve como un círculo. Por lo tanto, el círculo es el caso límite para los polígonos regulares. Esto es lo que sucede cuando cierta idea es llevada al límite.


Quiero hablar sobre Python como un caso extremo para C ++. Si tomas todas las buenas ideas de C ++ y las limpias hasta su conclusión lógica, estoy seguro de que terminarás con Python tan naturalmente como una serie de polígonos forma un círculo.


Activos no básicos


Me interesé en Python en los años 90: fue un período en mi vida cuando me deshice de los "activos no básicos", como lo llamo. Muchas cosas comenzaron a aburrirme. Interrupciones, por ejemplo. ¿Recuerda que una vez en muchas placas de computadora hubo tales contactos con puentes? ¿Y configura estos puentes en los manuales para que la tarjeta de video reciba una interrupción de mayor prioridad, para que su juego funcione más rápido? Entonces, estaba cansado de asignar y liberar memoria usando malloc() y free() casi al mismo tiempo que dejé de ajustar el rendimiento de mi computadora con puentes. Era 1997 más o menos.


Quiero decir, cuando estudiamos un proceso, generalmente nos esforzamos por obtener un control completo sobre él, para tener a la mano todas las palancas y botones posibles. Entonces algunas personas todavía están fascinadas por esta posibilidad de control. Pero mi personaje es que, tan pronto como me acostumbro a la administración y entiendo qué es qué, inmediatamente empiezo a buscar la oportunidad de renunciar a algunos de mis poderes, transferir palancas y botones a alguna máquina para que me asigne interrupciones.


Por lo tanto, a fines de los años 90, estaba buscando un lenguaje de programación que me permitiera concentrarme en el área temática y el modelado de tareas, en lugar de preocuparme por el área de memoria de mi computadora. ¿Cómo podemos simplificar C ++ sin repetir los pecados de los famosos lenguajes de script?


Por ejemplo, no podría usar Perl, y ¿sabes por qué? Este signo de dólar! Inmediatamente dejó en claro que el creador de Perl no entendía cómo funcionaban los lenguajes de programación. Utiliza el dólar en Bash para separar los nombres de las variables del resto de la cadena, porque un programa Bash consiste en comandos percibidos literalmente y sus parámetros. Pero después de conocer estos lenguajes de programación, en los que se colocan cadenas entre pares de caracteres pequeños llamados comillas, y no en todo el texto del programa, comienza a percibir $ como basura visual. El signo del dólar es inútil, es feo, ¡debe irse! Si desea diseñar un lenguaje para programación seria, no debe usar caracteres especiales para indicar variables.


Sintaxis


¿Qué pasa con la sintaxis? ¡Toma C como base! Funciona bastante bien Deje que la asignación se denote con un signo igual. Esta designación no se acepta en todos los idiomas, pero, de una forma u otra, muchos están acostumbrados. Pero no hagamos de la asignación una expresión. Los usuarios de nuestro lenguaje no solo serán programadores profesionales, sino también escolares, científicos o científicos de datos (si no sabe cuál de estas categorías de usuarios escribe el peor código, entonces insinuaré que no son escolares). No daremos a los usuarios la oportunidad de cambiar el estado de las variables en lugares inesperados, y haremos de la asignación un operador.


Entonces, ¿qué se debe usar para denotar la igualdad si el signo igual ya se ha usado para la asignación? Por supuesto, doble asignación, como se hace en C! Muchos ya están acostumbrados. También tomaremos prestada de C la notación para todas las operaciones aritméticas y bit a bit, porque estas anotaciones funcionan, y muchas están muy contentas con ellas.


Por supuesto, podemos mejorar algo. ¿Qué piensas cuando ves el signo de porcentaje en el texto del programa? Sobre la interpolación de cuerdas, por supuesto! Aunque % es principalmente un operador de captura de módulos, simplemente no estaba definido para cadenas. Y si es así, ¿por qué no reutilizarlo?


Literales numéricos y de cadena que controlan secuencias con barras invertidas: todo esto se verá en C.


Control de flujo de ejecución? Lo mismo if , de lo else , while , se break y continue . Por supuesto, agregaremos un poco de diversión cooptando lo bueno for iterar sobre estructuras de datos y rangos de valores. Esto se propondrá más adelante en C ++ 11, pero en Python, el operador for inicialmente encapsuló todas las operaciones para calcular tamaños, atravesar enlaces, incrementar el contador, etc., en otras palabras, hacer todo lo necesario para proporcionar al usuario un elemento de la estructura de datos. ¿Qué tipo de estructuras? No importa, solo pásalo for , lo resolverá.


También tomaremos prestadas excepciones de C ++, pero las haremos tan baratas en términos de consumo de recursos que pueden usarse no solo para manejar errores, sino también para controlar el flujo de ejecución. Haremos que la indexación sea más interesante agregando segmentación, la capacidad de indexar no solo elementos individuales de estructuras de datos secuenciales, sino también sus rangos.


Oh si! Arreglaremos el defecto de diseño original en C: ¡agregue una coma colgante!


Esta historia comenzó con Pascal, un lenguaje terrible en el que se utiliza un punto y coma como delimitador de expresiones. Esto significa que el usuario debe poner un punto y coma al final de cada expresión en el bloque, excepto la última . Por lo tanto, cada vez que cambie el orden de las expresiones en un programa en Pascal, corre el riesgo de recibir un error de sintaxis si no se asegura de eliminar el punto y coma de la última línea y agregarlo al final de la línea que solía ser la última.


 If (n = 0) then begin writeln('N is now zero'); func := 1 end 

Kernigan y Ritchie hicieron lo correcto cuando definieron el punto y coma en C como el terminador de la expresión, en lugar del delimitador, creando esa maravillosa simetría cuando cada línea del programa, incluida la última, termina igual y puede intercambiarse libremente. Desafortunadamente, en el futuro, un sentido de armonía cambió para ellos, e hicieron de la coma un separador en inicializadores estáticos. Esto se ve bien cuando la expresión se ajusta en una línea:


 int a[] = {4, 5, 6}; 

pero cuando su inicializador se alarga y lo organiza verticalmente, obtiene la misma asimetría incómoda que en Pascal:


 int a[] = { 4, 5, 6 }; 

En una etapa temprana de su desarrollo, Python hizo que la coma suspendida en las estructuras de datos fuera completamente opcional, independientemente de cómo estén organizados los elementos de esta estructura: horizontal o verticalmente. Por cierto, esto es muy conveniente para la generación automática de código: no necesita tratar el último elemento como un caso especial.


Más tarde, los estándares C99 y C ++ 11 también corrigieron el malentendido inicial, permitiéndole agregar una coma después del último literal en el inicializador.


Espacios de nombres


También necesitamos implementar en nuestro lenguaje de programación cosas como espacios de nombres o espacios de nombres. Esta es una parte crítica del lenguaje que debería salvarnos de errores como los conflictos de nombres. Lo haremos más fácil que C ++: en lugar de darle al usuario la capacidad de nombrar arbitrariamente el espacio de nombres, crearemos un espacio de nombres por módulo (archivo) y los designaremos con nombres de archivo. Por ejemplo, si crea el módulo foo.py , se le asignará el espacio de nombres foo .


Para trabajar con un modelo de espacios de nombres tan simplificado, un usuario solo necesita un operador.


Cree el directorio my_package , coloque el archivo my_module.py y declare la clase en el archivo:


 class C(object): READ = 1 WRITE = 2 

entonces el acceso a los atributos de clase será el siguiente:


 import my_package.my_module my_package.my_module.C.READ 

No se preocupe, no obligaremos al usuario a imprimir el nombre completo cada vez. Le daremos la oportunidad de usar varias versiones de la declaración de import para variar el grado de "proximidad" del espacio de nombres:


 import my_package.my_module my_package.my_module.C.READ from my_package import my_module my_module.C.READ from my_package.my_module import C C.READ 

Por lo tanto, los mismos nombres dados en diferentes paquetes nunca entrarán en conflicto:


 import json j = json.load(file) import pickle p = pickle.load(file) 

El hecho de que cada módulo tenga su propio espacio de nombres también significa que no necesitamos un modificador static . Sin embargo, recordamos una función que realizó static : encapsular variables internas. Para mostrar a los colegas que un nombre de pila (variable, clase o módulo) no es público, lo comenzamos con un guión bajo, por ejemplo, _ignore_this . También puede ser una señal para que el IDE no use este nombre en la finalización automática.


Sobrecarga de funciones


No implementaremos la sobrecarga de funciones en nuestro idioma. El mecanismo de sobrecarga es demasiado complicado. En su lugar, utilizaremos argumentos opcionales con valores predeterminados que se pueden omitir de la llamada, así como argumentos con nombre para "saltar" sobre los argumentos opcionales con valores predeterminados válidos y establecer solo aquellos valores que difieran de los predeterminados. Es importante destacar que la falta de sobrecarga nos salvará de la necesidad de determinar qué función del conjunto de funciones sobrecargadas se acaba de llamar, cómo funcionó el administrador de llamadas: la función siempre es una en este módulo, es fácil de encontrar por nombre.


API del sistema


Le daremos al usuario acceso completo a muchas API del sistema, incluidos los sockets. No entiendo por qué los autores de lenguajes de script siempre ofrecen sus propias formas ingeniosas de abrir un socket. Sin embargo, nunca se dan cuenta de la API completa de Unix Socket. Implementan 5-6 funciones que entienden y desechan todo lo demás. Python, a diferencia de ellos, tiene módulos estándar para interactuar con el sistema operativo que implementan cada llamada estándar del sistema. Eso significa que puede abrir el libro de Stevens ahora mismo y comenzar a escribir código. Y todos sus enchufes, procesos y horquillas funcionarán exactamente como se dice. Sí, es posible que Guido o los primeros contribuyentes de Python hicieran eso, porque eran demasiado flojos para escribir su implementación de las bibliotecas del sistema, demasiado flojos para explicar nuevamente a los usuarios cómo funcionan los sockets. Pero como resultado, lograron un efecto maravilloso: puede transferir todo su conocimiento de UNIX adquirido en C y C ++ al entorno Python.


Por lo tanto, decidimos qué características "prestaremos" de C ++ para crear nuestro lenguaje de script simple. Ahora tenemos que decidir qué queremos arreglar.


Comportamiento indefinido


Comportamiento desconocido, comportamiento indefinido, comportamiento definido por la implementación ... Todas estas son malas ideas para el lenguaje que serán utilizados por escolares, científicos y científicos de datos. Y la ganancia de rendimiento para la que se permiten tales cosas a menudo es insignificante en comparación con las molestias. En cambio, anunciaremos que cualquier programa sintácticamente correcto produce el mismo resultado en cualquier plataforma. Describiremos el lenguaje estándar con frases como "Python evalúa todas las expresiones de izquierda a derecha" en lugar de tratar de reordenar los cálculos según el procesador, el sistema operativo o la fase lunar. Si el usuario está seguro de que el orden de los cálculos es importante, tiene derecho a reescribir correctamente el código: al final, el usuario es el principal.


Prioridades de operación


Debes haber encontrado errores similares: expresión


 oflags & 0x80 == nflags & 0x80 

siempre devuelve 0, porque las comparaciones en C tienen prioridad sobre las operaciones bit a bit. En otras palabras, esta expresión se evalúa como


 oflags & (0x80 == nflags) & 0x80 

Oh, que C!


Eliminaremos la causa potencial de tales errores en nuestro lenguaje de script simple, poniendo la prioridad de las operaciones de comparación detrás de la aritmética y la manipulación de bits, de modo que la expresión de nuestro ejemplo se calcule de manera más intuitiva:


 (oflags & 0x80) == (nflags & 0x80) 

Otras mejoras


La legibilidad del código es importante para nosotros. Si las operaciones aritméticas del lenguaje C son familiares para el usuario incluso por la aritmética escolar, entonces la confusión entre las operaciones lógicas y las bit a bit es una fuente clara de errores. Reemplazaremos el doble ampersand con la palabra and , y la doble línea vertical con la palabra or , para que nuestro lenguaje se parezca más al discurso humano que al piquete de los caracteres de "computadora".


Dejaremos la posibilidad de computación abreviada a nuestros operadores lógicos ( https://en.wikipedia.org/wiki/Short-circuit_evaluation ), pero también les daremos la capacidad de devolver el valor final de cualquier tipo, no solo booleano. Entonces expresiones como


 s = error.message or 'Error' 

En este ejemplo, la variable se establecerá en error.message si no está vacía, de lo contrario, la cadena 'Error'.


Extendemos la idea de C de que 0 es equivalente a falso a otros objetos que no sean enteros. Por ejemplo, en líneas y contenedores vacíos.


Destruiremos el desbordamiento de enteros. Nuestro lenguaje será consistente en la implementación y fácil de usar, por lo que nuestros usuarios no necesitarán recordar un valor especial sospechosamente cercano a los dos mil millones, después de lo cual todo, aumentado en uno, cambia repentinamente de signo. Implementamos esos enteros que se comportarán como enteros hasta que agoten toda la memoria disponible.


Escritura estricta vs débil


Otro tema importante en el diseño del lenguaje de secuencias de comandos: el rigor de la escritura. ¿Muchos en la audiencia están familiarizados con JavaScript? ¿Qué sucede si el número 3 se resta de la cadena '4'?


 js> '4' - 3 1 

Genial ¿Y si agrega el número 3 a la cadena '4'?


 js> '4' + 3 "43" 

Esto se llama tipeo laxo (o débil). Esto es algo así como un complejo de inferioridad cuando un lenguaje de programación piensa que un programador lo condenará si no puede devolver el resultado de cualquier expresión, incluso obviamente sin sentido, al emitir tipos repetidamente. El problema es que la conversión de tipos, que un lenguaje de tipo débil produce automáticamente, rara vez conduce a un resultado significativo. Probemos conversiones un poco más complejas:


 js> [] + [] "" js> [] + {} "[object Object]" 

Esperamos que la operación de suma sea conmutativa, pero ¿qué sucede si cambiamos los términos en el último caso?


 js> {} + [] 0 

JavaScript no está solo en sus problemas. Perl en una situación similar también intenta devolver al menos algo:


 perl> "3" + 1 4 

Y awk hará algo así:


 $ echo | awk '{print "3" + 1}' 4 

Los creadores de los lenguajes de secuencias de comandos han creído tradicionalmente que la escritura suelta es conveniente . Se equivocaron: ¡la escritura suelta es terrible ! Viola el principio de localidad. Si hay un error en el código, entonces el lenguaje de programación debe informar al usuario al respecto, causando una excepción lo más cerca posible del lugar problemático en el código. Pero en todos estos idiomas, que emiten tipos sin fin, hasta que algo se resuelve, el control generalmente llega al final, y obtenemos el resultado, a juzgar por lo cual, en nuestro programa, algo está mal en alguna parte. Y tenemos que depurar todo nuestro programa, una línea tras otra, para encontrar este error.


La escritura suelta también degrada la legibilidad del código, porque incluso si usamos correctamente la conversión de tipos implícita en un programa, esto sucede inesperadamente para otro programador.


En Python, como en C ++, tales expresiones devolverán un error.


 >>> '4' - 3 TypeError >>> '4' + 3 TypeError 

Debido a que la conversión de tipos, si es realmente necesaria, es fácil de escribir explícitamente:


 >>> int('4') + 3 7 >>> '4' + str(3) '43' 

Este código es fácil de leer y mantener, deja en claro qué sucede exactamente en el programa, lo que conduce a este resultado. Esto se debe a que los programadores de Python creen que explícito es mejor que implícito, y el error no debe pasar desapercibido.


Python es un lenguaje fuertemente tipado, y la única conversión de tipo implícita en este ocurre durante operaciones aritméticas en enteros, cuyo resultado debe expresarse como un número fraccionario. Quizás esto tampoco debería permitirse en el programa, pero en este caso demasiados usuarios tendrían que explicar inmediatamente la diferencia entre números enteros y números de coma flotante, lo que complicaría sus primeros pasos en Python.


Continúa: “ Python como el último caso de C ++. Parte 2/2 ".

Source: https://habr.com/ru/post/464385/


All Articles