Para todos los residentes de Habrach que tienen la sensación de deja vu: el artículo "Introducción a Python" me instó a escribir esta publicación y comentarla. Desafortunadamente, la calidad de este ejem de "introducción" ... no hablemos de cosas tristes. Pero fue aún más triste observar disputas en los comentarios, de la categoría "C ++ es más rápido que Python", "Rust es aún más rápido que C ++", "Python no es necesario", etc. ¡Es sorprendente que no recordaran a Ruby!
Como dijo Bjarn Stroustrup:
"Solo hay dos tipos de lenguajes de programación: los que la gente jura todo el tiempo y los que nadie usa".
¡Bienvenido a todos los que deseen familiarizarse con Python sin caer en maldiciones sucias!
La mañana en las montañas del Cáucaso Oriental estuvo marcada por gritos. Dos jóvenes se sentaron en una gran roca y discutieron vigorosamente sobre algo, haciendo un gesto activo. Un minuto después, comenzaron a empujarse entre sí, y luego lucharon y cayeron de una roca en (como resultó) un arbusto de ortiga. Aparentemente, este arbusto creció allí por una razón: inmediatamente apaciguó a los luchadores y trajo una tregua a su insaciable disputa. Como probablemente habrás adivinado, yo fui uno de los debatientes, el otro fue mi mejor amigo (¡hola, Quaker_t!), Pero el tema de nuestra pequeña charla fue Visual Basic vs. Delphi !
¿Te reconoces a ti mismo? ¡A veces convertimos nuestros lenguajes de programación favoritos en un culto y estamos listos para defenderlo hasta el final! Pero pasan los años y llega el momento en que "A vs. B" del tema de las disputas se convierte en "Me siento más cómodo trabajando con A, pero si es necesario aprenderé a trabajar con B, C, D, E y, en general, con cualquier cosa ". Eso es solo cuando nos encontramos con nuevos lenguajes de programación, los viejos hábitos y la cultura pueden no dejarnos ir por mucho tiempo.
Me gustaría presentarle Python y ayudarlo a transferir su experiencia a una nueva dirección. Como cualquier tecnología, tiene sus propias fortalezas y debilidades. Python, como C ++, Rust, Ruby, JS y todos los demás, es una herramienta. Se adjuntan instrucciones a cualquier instrumento y debe aprender a usar cualquier instrumento correctamente .
"Autor, no tienes cerebro, ¿ibas a presentarnos a Python?" ¡Vamos a conocernos!
Python es un lenguaje de programación dinámico y de alto nivel para propósitos generales. Python es un lenguaje de programación maduro con un rico ecosistema y tradición. Aunque el lenguaje fue lanzado en 1991, su aspecto moderno comenzó a tomar forma a principios de la década de 2000. Python es un lenguaje cargado , en su biblioteca estándar hay soluciones para muchas ocasiones. Python es un lenguaje de programación popular : Dropbox, Reddit, Instagram, Disqus, YouTube, Netflix, maldita sea, incluso Eve Online y muchos otros usan Python activamente.
¿Cuál es la razón de esta popularidad? Con su permiso, presentaré mi propia versión.
Python es un lenguaje de programación simple . Escritura dinámica Recolector de basura. Funciones de orden superior. Sintaxis simple para trabajar con diccionarios, conjuntos, tuplas y listas (incluso para obtener rebanadas). Python es ideal para principiantes: permite comenzar con la programación de procedimientos, cambiar lentamente a OOP y probar la programación funcional. Pero esta simplicidad es como la punta de un iceberg. Vale la pena sumergirse en las profundidades cuando se encuentra con la filosofía de Python: Zen Of Python . Sumérgete aún más, y te encontrarás en un conjunto de reglas claras para el diseño de códigos: Guía de estilo para Python Code . Buceo, el programador profundiza gradualmente en el concepto de "Python way" o "Pythonic". En esta sorprendente etapa de aprendizaje de idiomas, comienzas a entender por qué los buenos programas de Python se escriben de esta manera y no de otra manera. Por qué el lenguaje ha evolucionado en esta dirección, y no en otra. Python no tuvo éxito en la velocidad. Pero tuvo éxito en el aspecto más importante de nuestro trabajo: la legibilidad. "Escribir código para personas, no para automóviles": esta es la base de los conceptos básicos de Python.
Un buen código de Python se ve hermoso. Y para escribir un código hermoso, ¿qué no es una ocupación agradable?
Consejo 0: Antes de seguir leyendo, eche un vistazo a una esquina de Zen Python . El lenguaje se basa en estos postulados y nuestra comunicación será mucho más agradable si está familiarizado con ellos.
¿A qué hombre inteligente se le ocurrió la sangría?
El primer shock para aquellos que nunca han visto el código en Python es la sangría del cuerpo de instrucciones:
def main(): ins = input('Please say something') for w in ins.split(' '): if w == 'hello': print('world!')
Recuerdo las noches en el albergue del Politécnico de San Petersburgo cuando mi vecino, VlK , con ojos ardientes me dijo que había descubierto algo nuevo en Python. "¿Cuerpo de indentación? ¿En serio?" - Fue mi reacción. De hecho, para una persona que pasó de Visual Basic ( if ... end if
) a C # (llaves) a través de C, C ++ y Java, este enfoque parecía, por decirlo suavemente, extraño. "¿Está formateando el código con sangría?", Preguntó VlK . Por supuesto que lo formateé. Más precisamente, el espiral Studio Visual lo hizo por mí. Ella lo hizo muy bien. Nunca pensé en formatear y sangrar: aparecieron en el código por sí mismos y parecían ser algo ordinario y familiar. Pero no había nada que ocultar: el código siempre estaba formateado con sangría. "Entonces, ¿por qué necesitas llaves? Si el cuerpo de las instrucciones se desplaza hacia la derecha?"
Esa noche me senté con Python. Mirando hacia atrás, puedo decir con certeza qué ayudó exactamente a absorber rápidamente el material nuevo. Era un editor de código. Influenciado por el mismo VlK , poco antes de los eventos descritos anteriormente, cambié de Windows a Ubuntu y Emacs como editor (en el patio de 2007, a PyCharm, Atom, VS Code y otros, muchos años más). "Bueno, ahora Emacs PR ..." - usted dice. Solo un poco :) Tradicionalmente, la tecla <tab>
en Emacs no agrega pestañas, pero sirve para alinear la línea de acuerdo con las reglas de este modo. Presionó <tab>
- y la línea de código se desplazó a la siguiente posición apropiada:

De esta manera, nunca tendrá que pensar si alineó el código correctamente.
Consejo 1: Cuando conozca Python, use un editor que se encargue de la sangría.
¿Sabes qué efecto secundario tiene toda esta desgracia? El programador intenta evitar construcciones largas. Tan pronto como el tamaño de la función va más allá de los bordes verticales de la pantalla, se hace más difícil distinguir a qué diseño pertenece el bloque de código dado. Y cuantas más inversiones, más difícil. Como resultado, intenta escribir de la manera más concisa posible, separando los cuerpos largos de funciones, bucles, transiciones condicionales, etc.
Oh bueno tu escritura dinámica
¡Oh, esta discusión existe casi mientras exista el concepto de "programación"! La escritura dinámica no es mala ni buena. La escritura dinámica también es nuestra herramienta. En Python, la escritura dinámica proporciona una tremenda libertad de acción. Y donde hay una mayor libertad de acción, es más probable que te dispares en el pie.
Vale la pena aclarar que escribir en Python es estricto y agregar un número a una cadena no funciona:
1 + '1' >>> TypeError: unsupported operand type(s) for +: 'int' and 'str'
Python también verifica la firma de la función cuando se llama y lanzará una excepción si la firma de la llamada no es verdadera:
def sum(x, y): return x + y sum(10, 20, 30) >>> TypeError: sum() takes 2 positional arguments but 3 were given
Pero al cargar un script, Python no le dirá que la función espera un número y no una cadena que le pase. Y solo lo aprendes en tiempo de ejecución:
def sum(x, y): return x + y sum(10, '10') >>> TypeError: can only concatenate str (not "int") to str
Cuanto más fuerte es el desafío para el programador, especialmente al escribir proyectos grandes . Modern Python ha respondido a este desafío con un motor de anotación y una biblioteca de tipos, y la comunidad ha desarrollado programas que realizan la verificación de tipos estáticos . Como resultado, el programador se entera de tales errores antes de ejecutar el programa:
Python no otorga ninguna importancia a las anotaciones, aunque las almacena en el atributo __annotations__
. La única condición es que las anotaciones deben ser valores válidos en términos de lenguaje. Desde su aparición en la versión 3.0 (¡que fue hace más de diez años!), Fueron los esfuerzos de la comunidad los que comenzaron a usar anotaciones para el marcado tipeado de variables y argumentos.
Otro ejemplo, más complicado. Consejo 2: en la práctica, la mayoría de los tipos dinámicos causan problemas al leer y depurar código. Especialmente si este código se escribió sin anotaciones y tiene que pasar mucho tiempo descubriendo los tipos de variables. ¡No tiene que indicar y documentar los tipos de todo y todo, pero el tiempo dedicado a una descripción detallada de las interfaces públicas y las secciones más críticas del código se recompensará por cien!
Quack! Pato escribiendo
A veces, los entusiastas de Python fingen ser misteriosos y hablan sobre "Mecanografía pato".
La escritura de pato es el uso de la prueba de pato en la programación:
Si un objeto grazna como un pato, vuela como un pato y camina como un pato, entonces lo más probable es que sea un pato.
Considere un ejemplo:
class RpgCharacter: def __init__(self, weapon) self.weapon = weapon def battle(self): self.weapon.attack()
Aquí está la inyección de dependencia clásica. La clase RpgCharacter
recibe el objeto de weapon
en el constructor y más tarde, en el método battle()
, llama a weapon.attack()
. Pero RpgCharacter
no depende de la implementación específica del weapon
. Puede ser una espada, un BFG 9000 o una ballena con una maceta, lista para aterrizar en la cabeza del enemigo en cualquier momento. Es importante que el objeto tenga un método de attack()
, Python no está interesado en todo lo demás.
Estrictamente hablando, la escritura de patos no es única. Está presente en todos los lenguajes dinámicos (familiares para mí) que implementan OOP.
Este es otro ejemplo de cómo programar cuidadosamente en el mundo de la escritura dinámica. Método mal nombrado? Ambiguamente llamado una variable? Su colega, o usted mismo, después de aproximadamente medio año, estará feliz de descifrar dicho código :)
¿Qué pasaría si utilizamos Java condicional? interface IWeapon { void attack(); } public class Sword implements IWeapon { public void attack() {
Y habría una tipificación estática clásica, con verificación de tipo en la etapa de compilación. Precio: la incapacidad de usar un objeto que tiene un método de attack()
, pero no implementa explícitamente la interfaz IWeapon
.
Consejo 3 : si lo desea, puede describir la interfaz creando su propia clase abstracta con métodos y propiedades . Mejor aún, pase tiempo probando y escribiendo documentación para usted y sus usuarios de código.
Enfoque de procedimiento y __ métodos_ especiales __ ()
Python es un lenguaje orientado a objetos y la clase de object
está en la raíz de la jerarquía de herencia:
isinstance('abc', object) >>> True isinstance(10, object) >>> True
Pero donde se obj.ToString()
en Java y C #, habrá una llamada a str(obj)
en Python. O, por ejemplo, en lugar de myList.length
, Python tendrá len(my_list)
. El creador del lenguaje, Guido van Rossum, explicó esto de la siguiente manera:
Cuando leo el código que dice len(x)
, sé que se solicita la longitud de algo. Esto inmediatamente me dice que el resultado será un número entero, y el argumento es algún tipo de contenedor. Por el contrario, cuando leo x.len()
, necesito saber que x
es algún tipo de contenedor que implementa una interfaz específica o hereda de una clase que tiene el método len()
. [Fuente]
Sin embargo, dentro de sí mismo, las funciones len()
, str()
y algunas otras invocarán ciertos métodos del objeto:
class User: def __init__(self, name, last_name): self.name = name self.last_name = last_name def __str__(self): return f"Honourable {self.name} {self.last_name}" u = User('Alex', 'Black') label = str(u) print(label) >>> Honourable Alex Black
Los operadores de lenguaje, tanto matemáticos como booleanos, también usan métodos especiales, así como for ... in ...
operadores de bucle, with
operador de contexto, operador de índice []
, etc.
Por ejemplo, un protocolo iterador consta de dos métodos: __iter__()
y __next__()
:
Bueno, digamos métodos especiales. ¿Pero por qué se ven tan retorcidos? Guido explicó esto por el hecho de que tenían los nombres habituales sin subrayar, los programadores mismos no los redefinirían, al menos tarde o temprano. Es decir ____()
es un tipo de protección contra el tonto. Como ha demostrado el tiempo, la protección es efectiva :)
Consejo 4: Observe de cerca las funciones integradas y los métodos de objetos especiales . Son una parte integral del lenguaje, sin el cual es imposible hablarlo completamente.
¿Dónde está la encapsulación? ¿Dónde está mi privado? ¿Dónde está mi cuento de hadas?
Python no tiene modificadores de acceso para los atributos de clase. Los interiores de los objetos están abiertos para acceder sin restricciones. Sin embargo, existe una convención según la cual los atributos con el prefijo _
se consideran privados, por ejemplo:
import os class MyFile:
Por qué
No hay nada privado en Python. Ni la clase ni su instancia te ocultarán lo que hay dentro (gracias a lo cual es posible la introspección más profunda). Python confía en ti. Él dice: "Amigo, si quieres hurgar en rincones oscuros, no hay problema. Creo que hay buenas razones para esto y espero que no rompas nada.
Al final, todos somos adultos aquí.
- Karl Fast [Fuente] .
Pero, ¿cómo evitar las colisiones de nombres durante la herencia?¡Python tiene un mecanismo especial para destrozar el nombre de los atributos que comienzan con un guión bajo doble y no terminan con un guión bajo doble ( __my_attr
)! Esto se hace para evitar colisiones de nombres durante la herencia. Para llamar a los métodos de clase fuera del cuerpo, Python agrega el prefijo ___
. Pero para el acceso interno, nada cambia:
class C: def __init__(self): self.__x = 10 def get_x(self): return self.__x c = C() c.__x >>> 'C' object has no attribute '__x' print(c.get_x()) >>> 10 print(c._C__x) >>> 10
Veamos una aplicación práctica. Por ejemplo, para la clase File
, que lee archivos del sistema de archivos local, queremos agregar capacidades de almacenamiento en caché. Nuestro colega logró escribir una clase mixin para estos fines. Pero para aislar métodos y atributos de posibles conflictos, un colega agregó el prefijo __
a sus nombres:
class BaseFile: def __init__(self, path): self.path = path class LocalMixin: def read_from_local(self): with open(self.path) as f: return f.read() class CachedMixin: class CacheMissError(Exception): pass def __init__(self):
Si está interesado en ver la implementación de este mecanismo en CPython, por favor, en Python / compile.c
Finalmente, debido a la presencia de propiedades en el lenguaje, no tiene sentido escribir getters y setters en el estilo Java: getX(), setX()
. Por ejemplo, en la clase originalmente escrita Coordinates
,
class Coordinates: def __init__(self, x, y): self.x = x self.y = y c = Coordinates(10, 10) print(cx, cy) >>> (10, 10)
Necesitaba controlar el acceso al atributo x
. El enfoque correcto sería reemplazarlo con property
, manteniendo así un contrato con el mundo exterior.
class Coordinates: _x = 0 def __init__(self, x, y): self.x = x self.y = y @property def x(self): return self._x @x.setter def x(self, val): if val > 10: self._x = val else: raise ValueError('x should be greater than 10') c = Coordinates(20, 10) cx = 5 >>> ValueError: x should be greater than 10
Consejo 5: Al igual que en Python, el concepto de campos privados y métodos de clase se basa en una convención establecida. No se ofenda por los autores de las bibliotecas si "todo ha dejado de funcionar" por el hecho de que ha utilizado activamente los campos privados de sus clases. Al final, todos somos adultos aquí :) .
Un poco sobre excepciones
La cultura Python tiene un enfoque único para las excepciones. Además de la intercepción y el procesamiento habituales a la C ++ / Java, encontrará el uso de excepciones en el contexto
"Es más fácil pedir perdón que un permiso: EAFP".
Parafraseando, no escriba demasiado if
, en la mayoría de los casos, la ejecución se realizará en esta rama. En cambio, envuelva la lógica en try..except
.
Ejemplo: imagine un controlador de solicitud POST que crea un usuario en una base de datos condicional. En la entrada de la función hay un diccionario (diccionario) de tipo clave-valor:
def create_user_handler(data: Dict[str, str]): try: database.user.persist( username=data['username'], password=data['password'] ) except KeyError: print('There was a missing field in data passed for user creation')
No contaminamos el código con verificaciones "si el username
o la password
están contenidos en los data
". Esperamos que probablemente estén allí. No pedimos "permiso" para usar estos campos, sino que nos "disculpamos" cuando el próximo kulhacker publica un formulario con datos faltantes.
¡Simplemente no lo lleves al punto del absurdo!Por ejemplo, le gustaría comprobar si el apellido del usuario está presente en los datos y, si no, establecerlo en un valor vacío. if
aquí fuera mucho más apropiado:
def create_user_handler(data): if 'last_name' not in data: data['last_name'] = '' try: database.user.persist( username=data['username'], password=data['password'], last_name=data['last_name'] ) except KeyError: print('There was a missing field in data passed for user creation')
Los errores nunca deben pasar en silencio. ¡No ignore las excepciones! Modern Python tiene un raise from
maravilloso raise from
construcción que le permite mantener el contexto de la cadena de excepción. Por ejemplo:
class MyProductError(Exception): def __init__(self): super().__init__('There has been a terrible product error') def calculate(x): try: return 10 / x except ZeroDivisionError as e: raise MyProductError() from e
Sin raise from e
cadena de excepción se rompe en MyProductError
y no podemos averiguar cuál fue exactamente la causa de este error. Con el raise from X
, la razón (es decir, X
) de la excepción lanzada se almacena en el atributo __cause__
:
try: calculate(0) except MyProductError as e: print(e.__cause__) >>> division by zero
Pero hay un pequeño matiz en el caso de la iteración: StopIterationEn el caso de una iteración, lanzar una excepción StopIteration es la forma oficial de indicar que el iterador está completo.
class PositiveIntegers: def __init__(self, limit): self.counter = 0 self.limit = limit def __iter__(self): return self def __next__(self): self.counter += 1 if self.counter == self.limit:
Consejo 6: Pagamos el manejo de excepciones solo en situaciones excepcionales. ¡No los descuides!
Debe haber uno, y preferiblemente solo uno, la forma anterior de hacerlo.
switch
o coincidencia de patrones? - use if
y diccionarios. do-
? - Para esto hay un while
y for
. goto
? Creo que tú mismo lo has adivinado. Lo mismo se aplica a algunas técnicas de diseño y patrones que parecen darse por sentado en otros idiomas. Lo más sorprendente es que no hay restricciones técnicas en su implementación, es solo "no lo aceptamos".
Por ejemplo, en Python no suele ver el patrón "Generador". En cambio, utiliza la capacidad de pasar y solicitar explícitamente argumentos de nombre a la función. En cambio
human = HumanBuilder.withName("Alex").withLastName("Black").ofAge(20).withHobbies(['tennis', 'programming']).build()
será
human = Human( name="Alex" last_name="Black" age=20 hobbies=['tennis', 'programming'] )
La biblioteca estándar no utiliza cadenas de métodos para trabajar con colecciones . Recuerdo cómo un colega que vino del mundo de Kotlin me mostró el código del siguiente sentido (tomado de la documentación oficial de Kotlin):
val shortGreetings = people .filter { it.name.length < 10 } .map { "Hello, ${it.name}!" }
En Python, map()
, filter()
y muchos otros son funciones, no métodos de recopilación. Reescribiendo este código uno por uno, obtenemos:
short_greetings = map(lambda h: f"Hello, {h.name}", filter(lambda h: len(h.name) < 10, people))
En mi opinión, se ve horrible. Por lo tanto, para paquetes largos como .takewhile().filter().map().reduce()
es mejor usar el llamado inclusión (comprensiones), o buenos viejos ciclos. Por cierto, el mismo ejemplo en Kotlin se da en forma de la comprensión de la lista correspondiente. Y en Python se ve así:
short_greetings = [ f"Hello {h.name}" for h in people if len(h.name) < 10 ]
Para los que extrañan las cadenas.Hay bibliotecas como Pipe o py_linq !
Las cadenas de métodos se usan donde son más eficientes que las herramientas estándar. Por ejemplo, en el marco web de Django, las cadenas se utilizan para construir un objeto de consulta de base de datos:
query = User.objects \ .filter(last_visited__gte='2019-05-01') \ .order_by('username') \ .values('username', 'last_visited') \ [:5]
Consejo 7: Antes de hacer algo muy familiar de la experiencia pasada, pero no familiar en Python, pregúntese qué decisión tomaría un pitonista experimentado.
Python lento
Si
Sí, cuando se trata de la velocidad de ejecución en comparación con los idiomas compilados y tipados estáticamente.
¿Pero parece querer una respuesta detallada?
La implementación de referencia de Python (CPython) está lejos de ser su implementación más efectiva. Una de las razones importantes es el deseo de los desarrolladores de no complicarlo. Y la lógica es comprensible: el código no demasiado abstruso significa menos errores, una mejor oportunidad para realizar cambios y, al final, más personas que desean leer, comprender y complementar este código.
Jake VanderPlas en su blog analiza lo que sucede en CPython debajo del capó al agregar dos variables que contienen valores enteros:
a = 1 b = 2 c = a + b
Incluso si no profundizamos en la jungla de CPython, podemos decir que para almacenar las variables a
, c
, el intérprete tendrá que crear tres objetos en el montón, en los que se almacenarán los valores de tipo y (punteros a); Vuelva a determinar el tipo y los valores durante la operación de suma para llamar a algo como binary_add<int, int>(a->val, b->val)
; escriba el resultado en c
.
Esto es terriblemente ineficiente en comparación con un programa C similar.
Otro problema con CPython es el llamado Global Interpreter Lock (GIL). Este mecanismo, esencialmente un valor booleano encerrado por un mutex, se utiliza para sincronizar la ejecución de bytecode. GIL simplifica el desarrollo del código que se ejecuta en un entorno multiproceso: CPython no necesita pensar en sincronizar el acceso a variables o puntos muertos. Debe pagar esto porque solo un hilo obtiene acceso y ejecuta el código de bytes en un momento dado:
UPD: ¡ Pero esto no significa que el programa en Python funcionará mágicamente en un entorno multiproceso! ¡El código en Python no se transfiere al bytecode uno por uno y no hay garantías sobre la compatibilidad del bytecode entre versiones! Por lo tanto, aún tiene que sincronizar los hilos en el código. Afortunadamente, aquí Python tiene un rico conjunto de herramientas, por ejemplo, que le permite cambiar entre un modelo de ejecución multiproceso y multiproceso.
Si tiene curiosidad sobre qué esfuerzos se están haciendo para erradicar el GIL ?
- . ( CFFI ) . API (extensions) C/C++. , Rust, Go Kotlin Native !
- , :
8: , . , IO (, , ) , , , , :)
? Linux MacOS, 95% . , 3., 2.7. Windows . : Docker, Windows Subsystem for Linux, Cygwin, , .
9: . , — - .
"Hello world" ? Genial machine learning- - Python Package Index (PyPI).
(packages), .. (virtual environments). , . - . pip
. pip
. , pipenv
poetry
— npm, bundler, cargo ..
0xA: — pip
virtualenv
. — , , . , — sys.path
— , .
?
? . :
Dive into python...
, . , , :)
, !