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

Continuará Comenzando en Python como el último caso de C ++. Parte 1/2 ".


Variables y tipos de datos


Ahora que finalmente hemos descubierto las matemáticas, decidamos qué variables deben significar en nuestro idioma.


En C ++, un programador tiene una opción: usar variables automáticas ubicadas en la pila o mantener los valores en la memoria de datos del programa, colocando solo punteros a estos valores en la pila. ¿Qué sucede si elegimos solo una de estas opciones para Python?


Por supuesto, no siempre podemos usar solo los valores de las variables, ya que las grandes estructuras de datos no caben en la pila, o su movimiento constante en la pila creará problemas de rendimiento. Por lo tanto, solo utilizaremos punteros en Python. Esto simplificará conceptualmente el lenguaje.


Entonces la expresión


a = 3 

significará que creamos un objeto "3" en la memoria de datos del programa (el llamado "montón") e hicimos referencia al nombre "a". Y la expresión


 b = a 

en este caso, significará que forzamos a la variable "b" a referirse al mismo objeto en la memoria que "a" se refiere, en otras palabras, copiamos el puntero.


Si todo es un puntero, ¿cuántos tipos de lista necesitamos implementar en nuestro idioma? ¡Por supuesto, solo uno es una lista de punteros! Puede usarlo para almacenar enteros, cadenas, otras listas, lo que sea, después de todo, estos son punteros.


¿Cuántos tipos de tablas hash necesitamos implementar? (En Python, este tipo se llama "diccionario" - dict .) ¡Uno! Deje que asocie punteros a claves con punteros a valores.


Por lo tanto, no necesitamos implementar en nuestro lenguaje una gran parte de la especificación de C ++: plantillas, ya que realizamos todas las operaciones en objetos, y los objetos siempre son accesibles por puntero. Por supuesto, los programas escritos en Python no tienen que limitarse a trabajar con punteros: hay bibliotecas como NumPy que ayudan a los científicos a trabajar con matrices de datos en la memoria, como lo harían en Fortran. Pero la base del lenguaje, expresiones como "a = 3", siempre funciona con punteros.


El concepto de "todo es un puntero" también simplifica la composición de los tipos hasta el límite. ¿Quieres una lista de diccionarios? ¡Simplemente cree una lista y ponga diccionarios allí! No necesita pedir permiso a Python, no necesita declarar tipos adicionales, todo funciona de forma inmediata.


Pero, ¿qué pasa si queremos usar objetos compuestos como claves? La clave en el diccionario debe tener un valor inmutable, de lo contrario, ¿cómo buscar valores por él? Las listas están sujetas a cambios, por lo tanto, no se pueden usar en esta capacidad. Para tales situaciones, Python tiene un tipo de datos que, como una lista, es una secuencia de objetos, pero, a diferencia de una lista, esta secuencia no cambia. Este tipo se llama tupla o tuple (se pronuncia "tupla" o "tupla").


Las tuplas en Python resuelven un problema de lenguaje de secuencias de comandos de larga data. Si no está impresionado con esta función, probablemente nunca haya intentado usar lenguajes de secuencias de comandos para un trabajo serio con datos, en el que puede usar solo cadenas o solo tipos primitivos como clave en las tablas hash.


Otra posibilidad que nos dan las tuplas es devolver varios valores de una función sin tener que declarar tipos de datos adicionales para esto, como debe hacer en C y C ++. Además, para facilitar el uso de esta función, el operador de asignación estaba dotado de la capacidad de desempaquetar automáticamente las tuplas en variables separadas.


 def get_address(): ... return host, port host, port = get_address() 

Desempacar tiene varios efectos secundarios útiles, por ejemplo, el intercambio de valores variables se puede escribir de la siguiente manera:


 x, y = y, x 

Todo es un puntero, lo que significa que las funciones y los tipos de datos se pueden usar como datos. Si está familiarizado con el libro "Patrones de diseño" de "The Gang of Four", debe recordar qué métodos complejos y confusos ofrece para parametrizar la elección del tipo de objeto creado por su programa en tiempo de ejecución. De hecho, en muchos lenguajes de programación esto es difícil de hacer. En Python, todas estas dificultades desaparecen, porque sabemos que una función puede devolver un tipo de datos, que tanto las funciones como los tipos de datos son solo enlaces, y los enlaces se pueden almacenar, por ejemplo, en diccionarios. Esto simplifica la tarea hasta el límite.


David Wheeler dijo: "Todos los problemas de programación se resuelven creando un nivel adicional de indirección". El uso de enlaces en Python es el nivel de indirección que tradicionalmente se ha utilizado para resolver muchos problemas en muchos lenguajes, incluido C ++. Pero si se usa explícitamente allí, y esto complica los programas, entonces en Python se usa implícitamente, de manera uniforme con respecto a datos de todo tipo, y es fácil de usar.


Pero si todo es un enlace, ¿a qué se refieren estos enlaces? Lenguajes como C ++ tienen muchos tipos. Dejemos en Python solo un tipo de datos: ¡un objeto! Los especialistas en el campo de la teoría de tipos sacuden la cabeza con desaprobación, pero creo que un tipo de datos fuente, del que se derivan todos los otros tipos en el idioma, es una buena idea que garantiza la uniformidad del lenguaje y su facilidad de uso.


Para contenidos de memoria específicos, varias implementaciones de Python (PyPy, Jython o MicroPython) pueden administrar la memoria de diferentes maneras. Pero para comprender mejor cómo se implementa la simplicidad y uniformidad de Python, para formar el modelo mental correcto, es mejor recurrir a la implementación de referencia de Python en C llamada CPython, que podemos descargar en python.org .


 struct { struct _typeobject *ob_type; /* followed by object's data */ } 

Lo que veremos en el código fuente de CPython es una estructura que consiste en un puntero a la información sobre el tipo de una variable dada y una carga útil que define el valor específico de la variable.


¿Cómo funciona la información de tipo? Vamos a profundizar en el código fuente de CPython nuevamente.


 struct _typeobject { /* ... */ getattrfunc tp_getattr; setattrfunc tp_setattr; /* ... */ newfunc tp_new; freefunc tp_free; /* ... */ binaryfunc nb_add; binaryfunc nb_subtract; /* ... */ richcmpfunc tp_richcompare; /* ... */ } 

Vemos punteros a funciones que proporcionan todas las operaciones que son posibles para un tipo dado: suma, resta, comparación, acceso a atributos, indexación, segmentación, etc. Estas operaciones saben cómo trabajar con la carga útil que se encuentra en la memoria debajo de un puntero para escribir información, ya sea un entero, una cadena o un objeto de un tipo creado por el usuario.


Esto es radicalmente diferente de C y C ++, en el que la información de tipo está asociada con nombres, no con valores de variables. En Python, todos los nombres están asociados con enlaces. El valor por referencia, a su vez, es de tipo. Esta es la esencia de los lenguajes dinámicos.


Para comprender todas las características del lenguaje, es suficiente para nosotros definir dos operaciones en los enlaces. Una de las más obvias es la copia. Cuando asignamos un valor a una variable, un espacio en un diccionario o un atributo de un objeto, copiamos los enlaces. Esta es una operación simple, rápida y completamente segura: copiar enlaces no cambia el contenido del objeto.


La segunda operación es una llamada a una función o método. Como mostramos anteriormente, un programa Python puede interactuar con la memoria solo a través de métodos implementados en objetos incorporados. Por lo tanto, no puede causar un error relacionado con el acceso a la memoria.


Puede tener una pregunta: si todas las variables contienen referencias, ¿cómo puedo proteger el valor de una variable de los cambios pasándolo a la función como parámetro?


 n = 3 some_function(n) # Q: I just passed a pointer! # Could some_function() have changed “3”? 

La respuesta es que los tipos simples en Python son inmutables: simplemente no implementan el método responsable de cambiar su valor. El inmutable (inmutable) int , float , tuple o str proporciona en lenguajes como "todo es un puntero" el mismo efecto semántico que las variables automáticas proporcionan en C.


Los tipos y métodos unificados simplifican el uso de la programación generalizada, o genéricos, tanto como sea posible. Las funciones min() , max() , sum() y similares están integradas, no es necesario importarlas. Y funcionan con cualquier tipo de datos en el que se implementan operaciones de comparación para min() y max() , adiciones para sum() , etc.


Crear objetos


Descubrimos en términos generales cómo deben comportarse los objetos. Ahora determinaremos cómo los crearemos. Esta es una cuestión de sintaxis del lenguaje. C ++ admite al menos tres formas de crear un objeto:


  1. Automático, al declarar una variable de esta clase:
     my_class c(arg); 
  2. Usando el new operador:
     my_class *c = new my_class(arg); 
  3. Factory, llamando a una función arbitraria que devuelve un puntero:
     my_class *c = my_factory(arg); 

Como ya habrás adivinado, habiendo estudiado la forma de pensar de los creadores de Python en los ejemplos anteriores, ahora debemos elegir uno de ellos.


Del mismo libro, The Gangs of Four, aprendimos que una fábrica es la forma más flexible y universal de crear objetos. Por lo tanto, solo este método se implementa en Python.


Además de la universalidad, este método es bueno porque no necesita sobrecargarse con una sintaxis innecesaria para garantizarlo: una llamada a la función ya está implementada en nuestro idioma, y ​​una fábrica no es más que una función.


Otra regla para crear objetos en Python es esta: cualquier tipo de datos es su propia fábrica. Por supuesto, puede escribir cualquier cantidad de fábricas personalizadas adicionales (que serán funciones o métodos ordinarios, por supuesto), pero la regla general seguirá siendo válida:


 # Let's make type objects # their own type's factories! c = MyClass() i = int('7') f = float(length) s = str(bytes) 

Todos los tipos se denominan objetos, y todos devuelven valores de su tipo, determinados por los argumentos pasados ​​en la llamada.


Por lo tanto, utilizando solo la sintaxis básica del lenguaje, cualquier manipulación al crear objetos, como los patrones "Arena" o "Adaptativos", puede encapsularse, ya que otra gran idea tomada de C ++ es que el tipo en sí mismo determina cómo sucede. engendrando sus objetos, cómo el new operador trabaja para él.


¿Qué tal NULL?


El manejo de un puntero nulo agrega complejidad al programa, por lo que prohibimos NULL. La sintaxis de Python hace que sea imposible crear un puntero nulo. Dos operaciones elementales sobre punteros, de las que hablamos anteriormente, se definen de tal manera que cualquier variable apunta a algún objeto.


Como resultado de esto, el usuario no puede usar Python para crear un error relacionado con el acceso a la memoria, como un error de segmentación o fuera de los límites del búfer. En otras palabras, los programas Python no se ven afectados por los dos tipos de vulnerabilidades más peligrosos que amenazan la seguridad de Internet en los últimos 20 años.


Puede preguntar: "Si la estructura de operaciones en los objetos no ha cambiado, como vimos anteriormente, entonces, ¿cómo crearán los usuarios sus propias clases, con métodos y atributos que no figuran en esta estructura?"


La magia radica en el hecho de que para las clases personalizadas Python tiene una "preparación" muy simple con una pequeña cantidad de métodos implementados. Aquí están los más importantes:


 struct _typeobject { getattrfunc tr_getattr; setattrfunc tr_setattr; /* ... */ newfunc tp_new; /* ... */ } 

tp_new() crea una tabla hash para la clase de usuario, igual que para el tipo dict . tp_getattr() extrae algo de esta tabla hash, y tp_setattr() , por el contrario, pone algo allí. Por lo tanto, la capacidad de las clases arbitrarias para almacenar cualquier método y atributo no se proporciona a nivel de estructuras de lenguaje C, sino a un nivel superior: una tabla hash. (Por supuesto, con la excepción de algunos casos relacionados con la optimización del rendimiento).


Modificadores de acceso


¿Qué hacemos con todas esas reglas y conceptos que se construyen alrededor de palabras clave de C ++ private y protected ? Python, siendo un lenguaje de script, no los necesita. Ya tenemos partes "protegidas" del lenguaje: estos son datos de tipos incorporados. ¡En ningún caso Python permitirá que un programa, por ejemplo, manipule los bits de un número de coma flotante! Este nivel de encapsulación es suficiente para mantener la integridad del lenguaje en sí. Nosotros, los creadores de Python, creemos que la integridad del lenguaje es el único buen pretexto para ocultar información. Todas las demás estructuras y los datos del programa de usuario se consideran públicos.


Puede escribir un guión bajo ( _ ) al comienzo del nombre de un atributo de clase para advertir a un colega: no debe confiar en este atributo. Pero el resto de Python aprendió las lecciones de principios de los 90: muchos creyeron que la razón principal por la que escribimos programas hinchados, ilegibles y con errores es la falta de variables privadas. Creo que los próximos 20 años han convencido a todos en la industria de la programación: las variables privadas no son las únicas y están lejos de ser el remedio más efectivo para los programas hinchados y con errores. Por lo tanto, los creadores de Python decidieron ni siquiera preocuparse por las variables privadas y, como pueden ver, no fallaron.


Gestión de la memoria


¿Qué les sucede a nuestros objetos, números y cadenas en un nivel inferior? ¿Cómo se almacenan exactamente en la memoria, cómo CPython les proporciona acceso compartido, cuándo y en qué condiciones se destruyen?


Y en este caso, elegimos la forma más general, predecible y productiva de trabajar con la memoria: desde el lado del programa C, todos nuestros objetos son punteros compartidos .


Con este conocimiento en mente, las estructuras de datos que examinamos anteriormente en la sección "Variables y tipos de datos" deben complementarse de la siguiente manera:


 struct { Py_ssize_t ob_refcnt; struct { struct _typeobject *ob_type; /* followed by object's data */ } } 

Entonces, cada objeto en Python (nos referimos a la implementación de CPython, por supuesto) tiene su propio contador de referencia. Una vez que se convierte en cero, el objeto se puede eliminar.


El mecanismo de conteo de enlaces no se basa en cálculos adicionales o procesos en segundo plano: un objeto puede destruirse instantáneamente. Además, proporciona una alta localidad de datos: a menudo, la memoria comienza a usarse nuevamente inmediatamente después de liberarse. El objeto recién destruido probablemente se usó recientemente, lo que significa que estaba en la memoria caché del procesador. Por lo tanto, el objeto recién creado permanecerá en la memoria caché. Estos dos factores, la simplicidad y la localidad, hacen que el conteo de enlaces sea una forma muy productiva de recolección de basura.


(Debido al hecho de que los objetos en los programas reales a menudo se refieren entre sí, el contador de referencia en ciertos casos no puede caer a cero incluso cuando los objetos ya no se usan en el programa. Por lo tanto, CPython también tiene un segundo mecanismo de recolección de basura, uno de fondo, basado en en generaciones de objetos - aprox. transl. )


Errores de desarrollador de Python


Intentamos desarrollar un lenguaje que fuera lo suficientemente simple para los principiantes, pero también lo suficientemente atractivo para los profesionales. Al mismo tiempo, no pudimos evitar errores en la comprensión y el uso de las herramientas que nosotros mismos creamos.


Python 2, debido a la inercia del pensamiento asociado con los lenguajes de secuencias de comandos, intentó convertir los tipos de cadenas, como lo haría un lenguaje con escritura débil. Si intenta combinar una cadena de bytes con una cadena en Unicode, el intérprete convierte implícitamente la cadena de bytes en Unicode utilizando la tabla de códigos que está disponible en el sistema y presenta el resultado en Unicode:


 >>> 'byte string ' + u'unicode string' u'byte string unicode string' 

Como resultado, algunos sitios web funcionaron bien mientras sus usuarios usaban el inglés, pero produjeron errores crípticos al usar caracteres de otros alfabetos.


Este error de diseño del lenguaje se ha corregido en Python 3:


 >>> b'byte string ' + u'unicode string' TypeError: can't concat bytes to str 

Un error similar en Python 2 se relacionó con la clasificación "ingenua" de listas que constan de elementos incomparables:


 >>> sorted(['b', 1, 'a', 2]) [1, 2, 'a', 'b'] 

Python 3 en este caso deja en claro al usuario que está tratando de hacer algo no muy significativo:


 >>> sorted(['b', 1, 'a', 2]) TypeError: unorderable types: int() < str() 

Abusos


Los usuarios de vez en cuando abusan de la naturaleza dinámica del lenguaje Python, y luego, en los años 90, cuando las mejores prácticas aún no se conocían ampliamente, esto sucedió especialmente a menudo:


 class Address(object): def __init__(self, host, port): self.host = host self.port = port 

"¡Pero esto no es óptimo!" - Algunos dijeron, - “¿Qué pasa si el puerto no difiere del valor predeterminado? ¡De todos modos, gastamos un atributo de clase entera en su almacenamiento! ” Y el resultado es algo como


 class Address(object): def __init__(self, host, port=None): self.host = host if port is not None: # so terrible self.port = port 

Por lo tanto, los objetos del mismo tipo aparecen en el programa, que, sin embargo, no se pueden operar de manera uniforme, ya que algunos tienen un atributo determinado, mientras que otros no. Y no podemos tocar este atributo sin verificar su presencia de antemano:


 # code was forced to use introspection # (terrible!) if hasattr(addr, 'port'): print(addr.port) 

Actualmente, la abundancia de hasattr() , isinstance() y otras introspecciones es un signo seguro de código incorrecto, y se considera la mejor práctica para hacer que los atributos siempre isinstance() presentes en el objeto. Esto proporciona una sintaxis más simple al acceder a ella:


 # today's best practice: # every atribute always present if addr.port is not None: print(addr.port) 

Entonces, los primeros experimentos con atributos agregados y eliminados dinámicamente terminaron, y ahora miramos las clases en Python de la misma manera que en C ++.


Otro mal hábito de Python temprano era el uso de funciones en las que un argumento puede tener tipos completamente diferentes. Por ejemplo, puede pensar que puede ser demasiado difícil para el usuario crear una lista de nombres de columna cada vez, y debe permitirle pasarlos también como una sola línea, donde los nombres de columnas individuales están separados por, por ejemplo, una coma:


 class Dataframe(object): def __init__(self, columns): if isinstance(columns, str): columns = columns.split(',') self.columns = columns 

Pero este enfoque puede dar lugar a sus problemas. Por ejemplo, ¿qué sucede si un usuario accidentalmente nos da una fila que no está destinada a usarse como una lista de nombres de columna? ¿O si el nombre de la columna debe contener una coma?


Además, dicho código es más difícil de mantener, depurar y especialmente probar: en las pruebas, solo se puede verificar uno de los dos tipos admitidos por nosotros, pero la cobertura seguirá siendo del 100% y no probaremos el otro tipo.


Como resultado, llegamos a la conclusión de que Python permite al usuario pasar argumentos de cualquier tipo a funciones, pero la mayoría de ellos en la mayoría de las situaciones usarán una función de la misma manera que lo harían en C: pasarle un argumento del mismo tipo.


La necesidad de usar eval() en un programa se considera un error de cálculo arquitectónico explícito. Lo más probable es que no hayas descubierto cómo hacer lo mismo de una manera normal. − , Jupyter notebook - − eval() , Python ! , C++ .


, ( getattr() , hasattr() , isinstance() ) . , , , , : , , , !



: , . 20 , C++ Python. , , . .


, shared_ptr TensorFlow 2016 2018 .


TensorFlow − C++-, Python- ( C++ − TensorFlow, ).


imagen


TensorFlow, shared_ptr , . , .


C++? . , ? , , C++ Python!

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


All Articles