Búsqueda de sitios web

Cómo no tenerle más miedo a Python


Una inmersión en la documentación de referencia del idioma

Durante el primer año o dos, cuando comencé a codificar, pensé que aprender un idioma consistía únicamente en aprender la sintaxis. Entonces, eso es todo lo que hice.

No hace falta decir que no me convertí en un gran desarrollador. Estaba atorada. Entonces, un buen día, simplemente hizo clic. Me di cuenta de que estaba haciendo esto mal. Aprender la sintaxis debería ser la menor de mis preocupaciones. Lo que importa es todo lo demás sobre el idioma. ¿Qué es exactamente todo eso? Sigue leyendo.

Este artículo se divide en tres subpartes principales: el modelo de datos, el modelo de ejecución y el análisis léxico.

Este artículo es más una visión de cómo funcionan las cosas en Pythonland, en contraste con cómo aprender Python. Encontrará muchas fuentes de aprendizaje práctico en línea.

Lo que no encontré en línea fue una fuente única de "errores" comunes en Python. Una fuente que explica cómo funciona el idioma. Esto intenta resolver ese problema. Creo que me he quedado corto, ¡hay mucho que decir!

Todo aquí proviene de la documentación oficial. Lo condensé en los puntos importantes, reordené el material y agregué mis ejemplos. Todos los enlaces apuntan a la documentación.

Sin más preámbulos, aquí vamos.

Modelo de datos

Objetos, valores y tipos.

Los objetos son la abstracción de datos de Python.

Cada objeto tiene su identidad fija única, un tipo fijo y un valor fijo.

"Fijo" significa que la identidad y el tipo de un Objeto nunca pueden cambiar.

El valor puede cambiar. Los objetos cuyo valor puede cambiar se denominan mutables, mientras que los objetos cuyo valor no puede cambiar se denominan inmutables.

La mutabilidad está determinada por type :

  • Los números, cadenas y tuplas son inmutables
  • Las listas y diccionarios son mutables.

La identidad de los objetos se puede comparar mediante el operador is.

id() devuelve la identidad

tipo() devuelve el tipo

Nota: El valor de un objeto contenedor inmutable que contiene una referencia a un objeto mutable puede cambiar cuando se cambia el valor de este último. Sin embargo, el contenedor todavía se considera inmutable porque la colección de objetos que contiene no se puede cambiar. Entonces, la inmutabilidad no es estrictamente lo mismo que tener un valor inmutable.

Esta nota me hizo dar vueltas la cabeza las dos primeras veces que la leí.

Traducción simple: la inmutabilidad no es lo mismo que el valor inmutable. En el siguiente ejemplo, la tupla es inmutable, mientras que su value sigue cambiando (a medida que cambia la list).

Ejemplo :

>>> t = ("a", [1]) # a tuple of string and list
>>> id(t)
4372661064
>>> t
('a', [1])
>>> type(t)
<class 'tuple'>
>>> t[1]
[1]
>>> t[1].append(2)
>>> t
('a', [1, 2])
>>> id(t)
4372661064
>>> type(t)
<class 'tuple'>

La tupla es inmutable, aunque contenga un objeto mutable, una lista.

Compare esto con una cadena, donde cambiar la matriz existente cambia el objeto (ya que las cadenas son inmutables).

>>> x = "abc"
>>> id(x)
4371437472
>>> x += "d"
>>> x
'abcd'
>>> id(x)
4373053712

Aquí, el nombre, x está vinculado a otro objeto de tipo cadena. Esto también cambia su identificación.

El objeto original, al ser inmutable, permanece inmutable. La vinculación se explica con más detalle a continuación, lo que debería aclarar las cosas.

Tipos incorporados

Python viene con varios tipos integrados:

Ninguno

El tipo está representado por un único objeto y, por tanto, por un único valor. El único objeto con type=NoneType

>>> type(None)
<class 'NoneType'>

Números

Esta es una colección de clases base abstractas que se utilizan para representar números. No se pueden crear instancias de ellos y int, float heredan de numbers.Number.

Se crean mediante literales numéricos y operaciones aritméticas. Los objetos devueltos son inmutables, como hemos visto. La siguiente lista de ejemplos aclarará esto:

>>> a = 3 + 4
>>> type(a)
<class 'int'>
>>> isinstance(a, numbers.Number)
True
>>> isinstance(a, numbers.Integral)
True
>>> isinstance(3.14 + 2j, numbers.Real)
False
>>> isinstance(3.14 + 2j, numbers.Complex)
True

Secuencias

Estos representan conjuntos ordenados finitos indexados por números enteros no negativos. Como una matriz de otros idiomas.

len() devuelve la longitud de las secuencias. Cuando la longitud es n, el conjunto de índices tiene elementos de 0...n-1. Luego, el iésimo elemento se selecciona mediante seq[i-1].

Para una secuencia l, puede seleccionar elementos entre índices usando el corte: l[i:j].

Hay dos tipos de secuencias: mutables e inmutables.

  • Las secuencias inmutables incluyen: cadenas, tuplas y bytes.
  • Las secuencias mutables incluyen: listas y matrices de bytes.

Conjuntos

Estos representan conjuntos finitos y desordenados de objetos únicos e inmutables. No se pueden indexar, pero se pueden iterar. len() todavía devuelve el número de elementos del conjunto.

Hay dos tipos de conjuntos: mutables e inmutables.

  • Un conjunto mutable se crea mediante set().
  • Un conjunto inmutable es creado por frozenset().

Mapeos

Diccionario

Estos representan conjuntos finitos de objetos indexados por valores casi arbitrarios. Las claves no pueden ser objetos mutables. Eso incluye listas, otros diccionarios y otros objetos que se comparan por valor y no por identidad de objeto.

¡Esto significa que un frozenset también puede ser una clave de diccionario!

Módulos

Un objeto de módulo es una unidad organizativa básica en Python. El espacio de nombres se implementa como un diccionario. Las referencias de atributos son búsquedas en este diccionario.

Para un módulo m, el diccionario es de solo lectura y se accede mediante m.__dict__.

¡Es un diccionario normal, por lo que puedes agregarle claves!

Aquí hay un ejemplo, con el Zen de Python:

Estamos agregando nuestra función personalizada, figure() al módulo this.

>>> import this as t
>>> t.__dict__
{'__name__': 'this', '__doc__': None, '__package__': '',
.....
.....
's': "Gur Mra bs Clguba, ol Gvz Crgref\n\nOrnhgvshy vf orggre guna
vqrn.\nAnzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!",
'd': {'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 
'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'},
'c': 97,
'i': 25
}
>>> def figure():
...   print("Can you figure out the Zen of Python?")
... 
>>> t.fig = figure
>>> t.fig()
Can you figure out the Zen of Python?
>>> t.__dict__
{'__name__': 'this', '__doc__': None, '__package__': '',
.....
.....
's': "Gur Mra bs Clguba, ol Gvz Crgref\n\nOrnhgvshy vf orggre guna
vqrn.\nAnzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!",
'd': {'A': 'N', 'B': 'O', 'C': 'P', 'D': 'Q', 'E': 'R', 'F': 'S', 
'u': 'h', 'v': 'i', 'w': 'j', 'x': 'k', 'y': 'l', 'z': 'm'},
'c': 97,
'i': 25
'fig': <function figure at 0x109872620>
}
>>> print("".join([t.d.get(c, c) for c in t.s]))
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Tampoco es muy útil, pero es bueno saberlo.

Sobrecarga del operador

Python permite la sobrecarga de operadores.

Las clases tienen nombres de funciones especiales: métodos que pueden implementar para usar los operadores definidos de Python. Esto incluye cortes, operaciones aritméticas y subíndices.

Por ejemplo, __getitem__() se refiere a subíndices. Por lo tanto, x[i] es equivalente a type(x).__getitem__(x,i).

Por lo tanto, para usar el operador [] en una clase someClass: necesita definir __getitem__() en someClass .

>>> class operatorTest(object):
...     vals = [1,2,3,4]
...     def __getitem__(self, i):
...         return self.vals[i]
... 
>>> x = operatorTest()
>>> x[2]
3
>>> x.__getitem__(2)
3
>>> type(x)
<class '__main__.OperatorTest'>
>>> type(x).__getitem__(x,2)
3
>>> OperatorTest.__getitem__(x,2)
3

¿Confundido acerca de por qué todos ellos son equivalentes? Eso es para la siguiente parte, donde cubrimos las definiciones de clases y funciones.

Del mismo modo, la función __str__() determina la salida cuando se llama al método str() en un objeto de su clase.

Para operaciones de comparación, los nombres de funciones especiales son:

  • object.__lt__(self, other) para < (“menos que”)
  • object.__le__(self, other) para (“menor o igual a”)
  • object.__eq__(self, other) para == (“igual a”)
  • object.__ne__(self, other) para != (“no es igual a”)
  • object.__gt__(self, other) para > (“mayor que”)
  • object.__ge__(self, other) para (“mayor o igual a”)

Entonces, por ejemplo, x < y se llama x.__lt__(y)

También hay funciones especiales para operaciones aritméticas, como object.__add__(self, other).

Por ejemplo, x+y se llama x.__add__(y)

Otra función interesante es __iter__().

Llama a este método cuando necesitas un iterador para un contenedor. Devuelve un nuevo objeto iterador que puede iterar sobre todos los objetos del contenedor.

Para las asignaciones, debe iterar sobre las claves del contenedor.

El objeto iterador en sí admite dos métodos:

  • iterator.__iter__() : Devuelve el objeto en sí.

Esto hace que los iteradores y los contenedores sean equivalentes.

Esto permite que el iterador y los contenedores se utilicen en declaraciones for y in.

  • iterator.__next__(): Devuelve el siguiente elemento del contenedor. Si no hay más elementos, genera la excepción StopIteration.
class IterableObject(object):    # The iterator object class
     vals = []
     it = 0
     def __init__(self, val):
         self.vals = val
         it = 0
 
     def __iter__(self):
         return self
 
     def __next__(self):
         if self.it < len(self.vals):
             index = self.it
             self.it += 1
             return self.vals[index]
         raise StopIteration
 
 class IterableClass(object):    # The container class
       vals = [1,2,3,4]
 
       def __iter__(self):
         return iterableObject(self.vals)
>>> iter_object_example = IterableObject([1,2,3])
>>> for val in iter_object_example:
...   print(val)
... 
1
2
3
>>> iter_container_example = IterableClass()
>>> for val in iter_container_example:
...  print(val)
... 
1
2
3
4

Cosas geniales, ¿verdad? También existe un equivalente directo en Javascript.

Los administradores de contexto también se implementan mediante la sobrecarga de operadores.

con open(nombre de archivo, 'r') como f

open(filename, 'r') es un objeto administrador de contexto que implementa

object.__enter__(self) y

object.__exit__(self, exc_type, exc_value, traceback)
Los tres parámetros anteriores son nulos cuando el error es Ninguno.

class MyContextManager(object):
    def __init__(self, some_stuff):
        self.object_to_manage = some_stuff
    def __enter__(self):
        print("Entering context management")
        return self.object_to_manage # can do some transforms too
    
    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            print("Successfully exited")
            # Other stuff to close
>>> with MyContextManager("file") as f:
...     print(f)
... 
Entering context management
file
Successfully exited

Esto no es útil, pero transmite la idea. ¿Eso lo hace útil de todos modos?

Modelo de ejecución

Un bloque es un fragmento de código ejecutado como una unidad en un marco de ejecución.

Ejemplos de bloques incluyen:

  • Módulos, que son bloques de nivel superior.
  • cuerpo funcional
  • Definición de clase
  • Pero NO bucles for y otras estructuras de control

¿Recuerdas que todo es un objeto en Python?

Bueno, tienes nombres vinculados a estos objetos. Estos nombres son lo que usted considera variables.

>>> x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined

La vinculación o asignación de nombres se produce en un bloque.

Ejemplos de vinculación de nombres: estos son intuitivos:

  • Los parámetros de las funciones están vinculados a los nombres definidos en la función.
  • Las declaraciones de importación vinculan el nombre del módulo
  • Las definiciones de clases y funciones vinculan el nombre a los objetos de clase/función
  • Administradores de contexto: con ... como f: f es el nombre vinculado al objeto ...

Los nombres vinculados a un bloque son locales para ese bloque. Eso significa que las variables globales son simplemente nombres vinculados al módulo.

Las variables utilizadas en un bloque sin estar definidas son variables libres.

Los ámbitos definen la visibilidad de un nombre en un bloque. El alcance de una variable incluye el bloque en el que está definida, así como todos los bloques contenidos dentro del bloque que la define.

¿Recuerdas que los bucles for no son bloques? Es por eso que las variables de iteración definidas en el bucle son accesibles después del bucle, a diferencia de C++ y JavaScript.

>>> for i in range(5):
...   x = 2*i
...   print(x, i)
... 
0 0
2 1
4 2
6 3
8 4
>>> print(x, i)    # outside the loop! x was defined inside.
8 4

Cuando se utiliza un nombre en un bloque, se resuelve utilizando el ámbito adjunto más cercano.

Nota: Si se produce una operación de vinculación de nombres en cualquier lugar dentro de un bloque de código, todos los usos del nombre dentro del bloque se tratan como referencias al bloque actual. Esto puede provocar errores cuando se utiliza un nombre dentro de un bloque antes de vincularlo.

Por ejemplo:

>>> name = "outer_scope"
>>> def foo():
...     name = "inner_function" if name == "outer_scope" \
                else "not_inner_function"
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'name' referenced before assignment

Este es un maravilloso rastreo que debería tener sentido ahora.

Tenemos el bloque de nivel superior, el módulo, en el que hay otro bloque, la función. ¡Cada enlace dentro de la función tiene la función como su alcance de nivel superior!

Por lo tanto, cuando vincula el nombre name al objeto "inner_function": antes del enlace, verifica su valor. La regla dice que no se puede hacer referencia a él antes de la vinculación. Exactamente el motivo del UnboundLocalError.

Análisis léxico

Python te permite usar uniones de líneas. Para continuar líneas explícitamente, utilice una barra invertida.

No se permiten comentarios después de unir líneas.

if a < 10 and b < 10 \ # Comment results in SyntaxError
and c < 10: # Comment okay
    return True
else:
    return False

Implícitamente, la unión de líneas se produce por sí sola cuando los elementos están dentro de llaves. Se permiten comentarios aquí.

month_names = ['Januari', 'Februari', 'Maart',      # These are the
               'April',   'Mei',      'Juni',       # Dutch names
               'Juli',    'Augustus', 'September',  # for the months
               'Oktober', 'November', 'December']   # of the year

Sangría

La cantidad de espacios/tabulaciones en la sangría no importa, siempre y cuando aumente para las cosas que deben tener sangría. La primera línea no debe tener sangría.

La regla de los cuatro espacios es una convención definida por PEP 8: Guía de estilo. Es una buena práctica seguirlo.

# Compute the list of all permutations of l.
def perm(l):
        # Comment indentation is ignored
    if len(l) <= 1:
                  return [l]
    r = []
    for i in range(len(l)):
             s = l[:i] + l[i+1:]     # Indentation level chosen
             p = perm(s)             # Must be same level as above
             for x in p:
              r.append(l[i:i+1] + x) # One space okay
    return r

También hay algunos identificadores reservados.

  • _ para importación: las funciones/variables que comienzan con _ no se importan.
  • __*__ para nombres definidos por el sistema, definidos por implementación: hemos visto algunos de estos. ( __str__(), __iter__(), __add__())

Python también ofrece concatenación literal de cadenas implícitas

>>> def name():
...   return "Neil" "Kakkar"
...
>>> name()
'Neil Kakkar'

Formatear cadenas

El formato de cadenas es una herramienta útil en Python.

Las cadenas pueden tener { expr } en el literal de cadena donde expr es una expresión. La expresión evaluación se sustituye en su lugar.

Se pueden especificar conversiones para convertir el resultado antes de formatear.

!r llama a repr(), !s llama a str() y !a > llama a ascii()

>>> name = "Fred"
>>> f"He said his name is {name!r}."
"He said his name is 'Fred'."
>>> f"He said his name is {repr(name)}."  # repr() is equiv. to !r
"He said his name is 'Fred'."
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal("12.34567")
>>> f"result: {value:{width}.{precision}}"  # nested fields
'result:      12.35'
# This is same as "{decf:10.4f}".format(decf=float(value))
>>> today = datetime(year=2017, month=1, day=27)
>>> f"{today:%B %d, %Y}"  # using date format specifier
'January 27, 2017'
>>> number = 1024
>>> f"{number:#0x}"  # using hexadecimal format specifier
'0x400'

Es una sintaxis más limpia para usar str.format()

Resumen

Con esto, hemos cubierto los pilares principales de Python. El modelo de datos del objeto, el modelo de ejecución con sus alcances y bloques y algunos bits en cadenas. Saber todo esto te sitúa por delante de cualquier desarrollador que sólo conozca la sintaxis. Ese es un número más alto de lo que piensas.

Otras historias de esta serie:

  • Cómo no tenerle más miedo a Vim
  • Cómo no tenerle más miedo a GIT

¿Disfrutaste esto? No te pierdas ninguna publicación nuevamente: ¡suscríbete a mi lista de correo!

Artículos relacionados