Buscar..


Introducción

Python es un lenguaje destinado a ser claro y legible sin ambigüedades ni comportamientos inesperados. Desafortunadamente, estos objetivos no son alcanzables en todos los casos, y es por eso que Python tiene algunos casos de esquina en los que podría hacer algo diferente de lo que esperabas.

Esta sección le mostrará algunos problemas que puede encontrar al escribir código Python.

Cambiando la secuencia sobre la que estás iterando

Un bucle for repite en una secuencia, por lo que alterar esta secuencia dentro del bucle podría generar resultados inesperados (especialmente al agregar o eliminar elementos):

alist = [0, 1, 2]
for index, value in enumerate(alist):
    alist.pop(index)
print(alist)
# Out: [1]

Nota: list.pop() se está utilizando para eliminar elementos de la lista.

El segundo elemento no se eliminó porque la iteración pasa por los índices en orden. El bucle anterior se repite dos veces, con los siguientes resultados:

# Iteration #1
index = 0
alist = [0, 1, 2]
alist.pop(0) # removes '0'

# Iteration #2
index = 1
alist = [1, 2]
alist.pop(1) # removes '2'

# loop terminates, but alist is not empty:
alist = [1]

Este problema surge porque los índices cambian mientras se iteran en la dirección del índice creciente. Para evitar este problema, puede recorrer el bucle hacia atrás :

alist = [1,2,3,4,5,6,7]
for index, item in reversed(list(enumerate(alist))):
    # delete all even items
    if item % 2 == 0:
        alist.pop(index)
print(alist)
# Out: [1, 3, 5, 7]

Al recorrer el bucle que comienza al final, a medida que se eliminan (o agregan) los elementos, no afecta a los índices de los elementos que aparecen anteriormente en la lista. Por lo tanto, este ejemplo eliminará correctamente todos los elementos que sean pares de alist .


Un problema similar surge cuando se insertan o agregan elementos a una lista sobre la que está iterando , lo que puede dar lugar a un bucle infinito:

alist = [0, 1, 2]
for index, value in enumerate(alist):
    # break to avoid infinite loop:
    if index == 20:     
        break           
    alist.insert(index, 'a')
print(alist)
# Out (abbreviated): ['a', 'a', ..., 'a', 'a',  0,   1,   2]

Sin la condición de break , el bucle insertaría 'a' siempre que la computadora no se quede sin memoria y el programa pueda continuar. En una situación como esta, generalmente se prefiere crear una nueva lista y agregar elementos a la nueva lista a medida que recorre la lista original.


Cuando se utiliza un bucle for , no puede modificar los elementos de la lista con la variable de marcador de posición :

alist = [1,2,3,4]
for item in alist:
    if item % 2 == 0:
        item = 'even'
print(alist)
# Out: [1,2,3,4]

En el ejemplo anterior, cambiar el item no cambia realmente nada en la lista original . Debe usar el índice de alist[2] ( alist[2] ), y enumerate() funciona bien para esto:

alist = [1,2,3,4]
for index, item in enumerate(alist):
    if item % 2 == 0:
        alist[index] = 'even'
print(alist)
# Out: [1, 'even', 3, 'even']

Un while de bucle podría ser una mejor elección en algunos casos:

Si va a eliminar todos los elementos de la lista:

zlist = [0, 1, 2]
while zlist:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
#      2
# After: zlist = []

Aunque simplemente restablecer zlist logrará el mismo resultado;

zlist = []

El ejemplo anterior también se puede combinar con len() para detenerse después de un cierto punto, o para eliminar todos los elementos excepto x en la lista:

zlist = [0, 1, 2]
x = 1
while len(zlist) > x:
    print(zlist[0])
    zlist.pop(0)
print('After: zlist =', zlist)

# Out: 0
#      1
# After: zlist = [2]

O para recorrer una lista mientras elimina elementos que cumplen una determinada condición (en este caso, eliminar todos los elementos pares):

zlist = [1,2,3,4,5]
i = 0
while i < len(zlist):
    if zlist[i] % 2 == 0:
        zlist.pop(i)
    else:
        i += 1
print(zlist)
# Out: [1, 3, 5]

Observe que no incrementa i después de eliminar un elemento. Al eliminar el elemento en zlist[i] , el índice del siguiente elemento ha disminuido en uno, por lo que al marcar zlist[i] con el mismo valor para i en la siguiente iteración, estará verificando correctamente el siguiente elemento en la lista .


Una forma contraria de pensar en eliminar elementos no deseados de una lista, es agregar elementos deseados a una nueva lista . El ejemplo siguiente es una alternativa a este último while ejemplo de bucle:

zlist = [1,2,3,4,5]

z_temp = []
for item in zlist:
    if item % 2 != 0:
        z_temp.append(item)
zlist = z_temp
print(zlist)
# Out: [1, 3, 5]

Aquí estamos canalizando los resultados deseados en una nueva lista. De manera opcional, podemos reasignar la lista temporal a la variable original.

Con esta tendencia de pensamiento, puede invocar una de las funciones más elegantes y potentes de Python, listas de comprensión , que elimina las listas temporales y se desvía de la ideología de mutación de listas / índices in situ anteriormente discutida.

zlist = [1,2,3,4,5]
[item for item in zlist if item % 2 != 0]
# Out: [1, 3, 5]

Argumento predeterminado mutable

def foo(li=[]):
    li.append(1)
    print(li)

foo([2])
# Out: [2, 1]
foo([3])
# Out: [3, 1]

Este código se comporta como se espera, pero ¿y si no pasamos un argumento?

foo()
# Out: [1] As expected...

foo()
# Out: [1, 1]  Not as expected...

Esto se debe a que los argumentos predeterminados de las funciones y los métodos se evalúan en el momento de la definición en lugar del tiempo de ejecución. Así que solo tenemos una sola instancia de la lista li .

La forma de evitarlo es usar solo tipos inmutables para los argumentos predeterminados:

def foo(li=None):
    if not li:
        li = []
    li.append(1)
    print(li)

foo()
# Out: [1]

foo()
# Out: [1]

Si bien es una mejora y, if not li se evalúa correctamente como False , muchos otros objetos también lo hacen, como las secuencias de longitud cero. Los siguientes argumentos de ejemplo pueden causar resultados no deseados:

x = []
foo(li=x)
# Out: [1]

foo(li="")
# Out: [1]

foo(li=0) 
# Out: [1]

El enfoque idiomático es verificar directamente el argumento en contra del objeto None :

def foo(li=None):
    if li is None:
        li = []
    li.append(1)
    print(li)

foo()
# Out: [1]

Lista de multiplicación y referencias comunes.

Considere el caso de crear una estructura de lista anidada multiplicando:

li = [[]] * 3
print(li)
# Out: [[], [], []]

A primera vista, podríamos pensar que tenemos una lista que contiene 3 listas anidadas diferentes. Intentemos adjuntar 1 al primero:

li[0].append(1)
print(li)
# Out: [[1], [1], [1]]

1 obtuve adjunta a todas las listas de li .

La razón es que [[]] * 3 no crea una list de 3 list diferentes. Más bien, crea una list contiene 3 referencias al mismo objeto de list . Como tal, cuando agregamos a li[0] el cambio es visible en todos los subelementos de li . Esto es equivalente a:

li = []
element = [[]]
li = element + element + element
print(li)
# Out: [[], [], []]
element.append(1)
print(li)
# Out: [[1], [1], [1]]

Esto se puede corroborar aún más si imprimimos las direcciones de memoria de la list contenida usando id :

li = [[]] * 3
print([id(inner_list) for inner_list in li])
# Out: [6830760, 6830760, 6830760]

La solución es crear las listas internas con un bucle:

li = [[] for _ in range(3)]

En lugar de crear una list única y luego hacer 3 referencias a ella, ahora creamos 3 listas distintas diferentes. Esto, de nuevo, puede verificarse usando la función id :

print([id(inner_list) for inner_list in li])
# Out: [6331048, 6331528, 6331488]

También puedes hacer esto. Hace que se cree una nueva lista vacía en cada llamada append .

>>> li = []
>>> li.append([])
>>> li.append([])
>>> li.append([])
>>> for k in li: print(id(k))
... 
4315469256
4315564552
4315564808

No utilice el índice para recorrer una secuencia.

No hagas

for i in range(len(tab)):
    print(tab[i])

Hacer

for elem in tab:
    print(elem)

for automatizará la mayoría de las operaciones de iteración para usted.

Use enumerar si realmente necesita tanto el índice como el elemento .

for i, elem in enumerate(tab):
     print((i, elem))

Tenga cuidado al usar "==" para verificar si es verdadero o falso

if (var == True):
    # this will execute if var is True or 1, 1.0, 1L

if (var != True):
    # this will execute if var is neither True nor 1

if (var == False):
    # this will execute if var is False or 0 (or 0.0, 0L, 0j)

if (var == None):
    # only execute if var is None

if var:
    # execute if var is a non-empty string/list/dictionary/tuple, non-0, etc

if not var:
    # execute if var is "", {}, [], (), 0, None, etc.

if var is True:
    # only execute if var is boolean True, not 1

if var is False:
    # only execute if var is boolean False, not 0

if var is None:
    # same as var == None

No marque si puede, solo hágalo y maneje el error

Los pitonistas usualmente dicen "es más fácil pedir perdón que permiso".

No hagas

if os.path.isfile(file_path):
    file = open(file_path)
else:
    # do something

Hacer:

try:
    file = open(file_path)
except OSError as e:
    # do something

O incluso mejor con Python 2.6+ :

with open(file_path) as file:

Es mucho mejor porque es mucho más genérico. Puede aplicar try/except a casi cualquier cosa. No necesita preocuparse por lo que debe hacer para evitarlo, solo debe preocuparse por el error que está arriesgando.

No comprobar contra tipo

Python se escribe dinámicamente, por lo tanto, verificar el tipo hace que pierdas flexibilidad. En su lugar, utilice la escritura de pato comprobando el comportamiento. Si espera una cadena en una función, use str() para convertir cualquier objeto en una cadena. Si espera una lista, use list() para convertir cualquier iterable en una lista.

No hagas

def foo(name):
    if isinstance(name, str):
        print(name.lower())

def bar(listing):
    if isinstance(listing, list):
        listing.extend((1, 2, 3))
        return ", ".join(listing)

Hacer:

def foo(name) :
    print(str(name).lower())

def bar(listing) :
    l = list(listing)
    l.extend((1, 2, 3))
    return ", ".join(l)

Usando la última forma, foo aceptará cualquier objeto. bar aceptará cadenas, tuplas, conjuntos, listas y mucho más. Barato SECO.

No mezclar espacios y pestañas

Usa el objeto como primer padre

Esto es complicado, pero te morderá a medida que tu programa crezca. Hay clases antiguas y nuevas en Python 2.x Los viejos son, bueno, viejos. Carecen de algunas características, y pueden tener un comportamiento incómodo con la herencia. Para ser utilizable, cualquiera de su clase debe ser del "nuevo estilo". Para ello, hazlo heredar del object .

No hagas

class Father:
    pass

class Child(Father):
    pass

Hacer:

class Father(object):
    pass


class Child(Father):
    pass

En Python 3.x todas las clases son de estilo nuevo, por lo que no necesitas hacerlo.

No inicialice los atributos de clase fuera del método init

A las personas que vienen de otros idiomas les resulta tentador porque eso es lo que haces en Java o PHP. Escribe el nombre de la clase, luego enumera sus atributos y les asigna un valor predeterminado. Parece funcionar en Python, sin embargo, esto no funciona como piensas. Al hacerlo, se configurarán los atributos de clase (atributos estáticos), luego, cuando intente obtener el atributo de objeto, le dará su valor a menos que esté vacío. En ese caso devolverá los atributos de la clase. Implica dos grandes peligros:

  • Si se cambia el atributo de clase, entonces se cambia el valor inicial.

  • Si configura un objeto mutable como un valor predeterminado, obtendrá el mismo objeto compartido en todas las instancias.

No (a menos que quieras estática):

class Car(object):
    color = "red"
    wheels = [Wheel(), Wheel(), Wheel(), Wheel()]

Hacer

class Car(object):
    def __init__(self):
        self.color = "red"
        self.wheels = [Wheel(), Wheel(), Wheel(), Wheel()]

Identidad entera y de cadena

Python utiliza el almacenamiento en caché interno para un rango de enteros para reducir la sobrecarga innecesaria de su creación repetida.

En efecto, esto puede llevar a un comportamiento confuso al comparar identidades enteras:

>>> -8 is (-7 - 1)
False
>>> -3 is (-2 - 1)
True

y, usando otro ejemplo:

>>> (255 + 1) is (255 + 1)
True
>>> (256 + 1) is (256 + 1)
False

¿Esperar lo?

Podemos ver que la operación de identidad is True para algunos enteros ( -3 , 256 ) pero no para otros ( -8 , 257 ).

Para ser más específicos, los enteros en el rango [-5, 256] se almacenan en caché internamente durante el inicio del intérprete y solo se crean una vez. Como tales, son idénticos y la comparación de sus identidades con is rinde True ; los enteros fuera de este rango son (generalmente) creados sobre la marcha y sus identidades se comparan con False .

Este es un error común ya que este es un rango común para las pruebas, pero a menudo, el código falla en el proceso posterior de estadificación (o peor aún, en la producción) sin una razón aparente después de funcionar perfectamente en el desarrollo.

La solución es comparar siempre los valores utilizando el operador de igualdad ( == ) y no el operador de identidad ( is ).


Python también mantiene referencias a los usados comúnmente cuerdas y pueden resultar en un comportamiento de manera similar confuso cuando la comparación de identidades (es decir, utilizando is ) de cadenas.

>>> 'python' is 'py' + 'thon'
True

La cadena 'python' se usa comúnmente, por lo que Python tiene un objeto que usan todas las referencias a la cadena 'python' .

Para cadenas poco comunes, la comparación de identidad falla incluso cuando las cadenas son iguales.

>>> 'this is not a common string' is 'this is not' + ' a common string'
False
>>> 'this is not a common string' == 'this is not' + ' a common string'
True

Entonces, al igual que la regla para enteros, siempre compare valores de cadena utilizando el operador de igualdad ( == ) y no el operador de identidad ( is ).

Accediendo a los atributos de int literals.

Es posible que hayas oído que todo en Python es un objeto, incluso literales. Esto significa, por ejemplo, que 7 es un objeto, lo que significa que tiene atributos. Por ejemplo, uno de estos atributos es el bit_length . Devuelve la cantidad de bits necesarios para representar el valor al que se solicita.

x = 7
x.bit_length()
# Out: 3

Al ver que el código anterior funciona, podría pensar intuitivamente que 7.bit_length() también funcionaría, solo para descubrir que genera un SyntaxError . ¿Por qué? porque el intérprete debe diferenciar entre un acceso de atributo y un número flotante (por ejemplo, 7.2 o 7.bit_length() ). No puede, y es por eso que se levanta una excepción.

Hay algunas formas de acceder a los atributos de los literales int :

# parenthesis
(7).bit_length()
# a space
7 .bit_length()

El uso de dos puntos (como este 7..bit_length() ) no funciona en este caso, porque crea un literal float y los flotantes no tienen el método bit_length() .

Este problema no existe al acceder a los atributos de los literales float , ya que el intérprete es lo suficientemente "inteligente" para saber que un literal float no puede contener dos . , por ejemplo:

7.2.as_integer_ratio()
# Out: (8106479329266893, 1125899906842624)

Encadenamiento de u operador

Al probar para cualquiera de varias comparaciones de igualdad:

if a == 3 or b == 3 or c == 3:

es tentador abreviar esto a

if a or b or c == 3: # Wrong

Esto está mal; el operador or tiene una prioridad menor que == , por lo que la expresión se evaluará como if (a) or (b) or (c == 3): La forma correcta es verificando explícitamente todas las condiciones:

if a == 3 or b == 3 or c == 3:  # Right Way

Alternativamente, la función any() incorporada se puede usar en lugar de encadenados or operadores:

if any([a == 3, b == 3, c == 3]): # Right

O, para hacerlo más eficiente:

if any(x == 3 for x in (a, b, c)): # Right

O, para hacerlo más corto:

if 3 in (a, b, c): # Right

Aquí, usamos el operador in para probar si el valor está presente en una tupla que contiene los valores con los que queremos comparar.

Del mismo modo, es incorrecto escribir

if a == 1 or 2 or 3:

que debe ser escrito como

if a in (1, 2, 3):

sys.argv [0] es el nombre del archivo que se está ejecutando

El primer elemento de sys.argv[0] es el nombre del archivo python que se está ejecutando. Los elementos restantes son los argumentos del script.

# script.py
import sys

print(sys.argv[0])
print(sys.argv)

$ python script.py
=> script.py
=> ['script.py']

$ python script.py fizz
=> script.py
=> ['script.py', 'fizz']

$ python script.py fizz buzz
=> script.py
=> ['script.py', 'fizz', 'buzz']

Los diccionarios están desordenados.

Puede esperar que un diccionario de Python se ordene por claves como, por ejemplo, un C ++ std::map , pero este no es el caso:

myDict = {'first': 1, 'second': 2, 'third': 3}
print(myDict)
# Out: {'first': 1, 'second': 2, 'third': 3}

print([k for k in myDict])
# Out: ['second', 'third', 'first']

Python no tiene ninguna clase incorporada que ordene automáticamente sus elementos por clave.

Sin embargo, si la ordenación no es obligatoria y solo desea que su diccionario recuerde el orden de inserción de sus pares clave / valor, puede usar collections.OrderedDict :

from collections import OrderedDict

oDict = OrderedDict([('first', 1), ('second', 2), ('third', 3)])

print([k for k in oDict])
# Out: ['first', 'second', 'third']

Tenga en cuenta que inicializar un OrderedDict con un diccionario estándar no ordenará el diccionario por usted. Todo lo que hace esta estructura es preservar el orden de inserción de la clave.

La implementación de los diccionarios se cambió en Python 3.6 para mejorar su consumo de memoria. Un efecto secundario de esta nueva implementación es que también conserva el orden de los argumentos de palabras clave que se pasan a una función:

Python 3.x 3.6
def func(**kw): print(kw.keys())

func(a=1, b=2, c=3, d=4, e=5) 
dict_keys(['a', 'b', 'c', 'd', 'e']) # expected order 

Advertencia : tenga en cuenta que " el aspecto de preservar el orden de esta nueva implementación se considera un detalle de implementación y no se debe confiar en él " , ya que puede cambiar en el futuro.

Bloqueo global de intérprete (GIL) y bloqueo de hilos

Se ha escrito mucho sobre GIL de Python . A veces puede causar confusión cuando se trata de aplicaciones de subprocesos múltiples (no confundir con multiproceso).

Aquí hay un ejemplo:

import math
from threading import Thread

def calc_fact(num):
    math.factorial(num)

num = 600000
t = Thread(target=calc_fact, daemon=True, args=[num])
print("About to calculate: {}!".format(num))
t.start()
print("Calculating...")
t.join()
print("Calculated")

Esperaría ver el Calculating... impreso inmediatamente después de comenzar el hilo, ¡queríamos que el cálculo se realizara en un nuevo hilo después de todo! Pero en realidad, usted ve que se imprime una vez que se completa el cálculo. Esto se debe a que el nuevo hilo se basa en una función C ( math.factorial ) que bloqueará la GIL mientras se ejecuta.

Hay un par de maneras de evitar esto. El primero es implementar tu función factorial en Python nativo. Esto permitirá que el hilo principal tome el control mientras estás dentro de tu bucle. El inconveniente es que esta solución será mucho más lenta, ya que ya no estamos utilizando la función C.

def calc_fact(num):
    """ A slow version of factorial in native Python """
    res = 1
    while num >= 1:
        res = res * num
        num -= 1
    return res

También puede sleep durante un período de tiempo antes de comenzar su ejecución. Nota: esto no permitirá que su programa interrumpa el cálculo que ocurre dentro de la función C, pero permitirá que su hilo principal continúe después del inicio, que es lo que puede esperar.

def calc_fact(num):
    sleep(0.001)
    math.factorial(num)

Fugas variables en listas de comprensión y para bucles.

Considere la siguiente lista de comprensión

Python 2.x 2.7
i = 0
a = [i for i in range(3)]
print(i) # Outputs 2

Esto ocurre solo en Python 2 debido a que la comprensión de la lista "filtra" la variable de control de bucle en el ámbito circundante ( fuente ). Este comportamiento puede provocar errores difíciles de encontrar y se ha corregido en Python 3 .

Python 3.x 3.0
i = 0
a = [i for i in range(3)]
print(i) # Outputs 0

Del mismo modo, para los bucles no tienen ámbito privado para su variable de iteración

i = 0
for i in range(3):
    pass
print(i) # Outputs 2

Este tipo de comportamiento ocurre tanto en Python 2 como en Python 3.

Para evitar problemas con las variables con fugas, use nuevas variables en la lista de comprensión y para los bucles, según corresponda.

Retorno múltiple

La función xyz devuelve dos valores a y b:

def xyz():
  return a, b

El código que llama a xyz almacena el resultado en una variable, asumiendo que xyz devuelve solo un valor:

t = xyz()

Valor de t es en realidad una tupla (a, b) por lo que cualquier acción en t suponiendo que no es una tupla puede fallar profundo en el código con un un error inesperado sobre tuplas.

TypeError: el tipo tuple no define ... el método

La solución sería hacer:

a, b = xyz()

¡Los principiantes tendrán problemas para encontrar el motivo de este mensaje al solo leer el mensaje de error de la tupla!

Teclas JSON pitónicas

my_var = 'bla';
api_key = 'key';
...lots of code here...
params = {"language": "en", my_var: api_key}

Si estás acostumbrado a JavaScript, la evaluación de variables en los diccionarios de Python no será lo que esperas que sea. Esta declaración en JavaScript daría como resultado el objeto params siguiente manera:

{
    "language": "en",
    "my_var": "key"
}

En Python, sin embargo, daría como resultado el siguiente diccionario:

{
    "language": "en",
    "bla": "key"
}

my_var se evalúa y su valor se utiliza como clave.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow