lenguaje - python tutorial
Python 2.x gotchas y minas terrestres (22)
Python 2 tiene un comportamiento sorprendente con comparaciones:
>>> print x
0
>>> print y
1
>>> x < y
False
¿Que esta pasando? repr()
al rescate:
>>> print "x: %r, y: %r" % (x, y)
x: ''0'', y: 1
El propósito de mi pregunta es fortalecer mi base de conocimiento con Python y obtener una mejor idea de ello, que incluye conocer sus fallas y sorpresas. Para mantener las cosas específicas, solo me interesa el intérprete CPython.
Estoy buscando algo similar a lo que aprendí de mi pregunta sobre las minas antipersonal de PHP , donde algunas de las respuestas eran bien conocidas para mí, pero una pareja era terriblemente horrible.
Actualización: aparentemente una o dos personas están molestas por haber hecho una pregunta que ya se respondió parcialmente fuera de Stack Overflow. Como una especie de compromiso aquí está la URL http://www.ferg.org/projects/python_gotchas.html
Tenga en cuenta que una o dos respuestas aquí ya son originales de lo que estaba escrito en el sitio al que se hace referencia anteriormente.
Si asigna una variable dentro de una función, Python supone que la variable está definida dentro de esa función:
>>> x = 1
>>> def increase_x():
... x += 1
...
>>> increase_x()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in increase_x
UnboundLocalError: local variable ''x'' referenced before assignment
Use global x
(o nonlocal x
en Python 3) para declarar que desea establecer una variable definida fuera de su función.
La única sorpresa con la que he tratado es con GIL de CPython. Si por alguna razón esperas que los hilos de python en CPython se ejecuten al mismo tiempo ... bueno, no lo son, y esto está bastante bien documentado por la multitud de Python e incluso el propio Guido.
Una explicación larga pero exhaustiva del enhebrado de CPython y algunas de las cosas que suceden bajo el capó y por qué no es posible una verdadera concurrencia con CPython. http://jessenoller.com/2009/02/01/python-threads-and-the-global-interpreter-lock/
La vinculación dinámica hace que los errores tipográficos en los nombres de las variables sean sorprendentemente difíciles de encontrar. Es fácil pasar media hora arreglando un error trivial.
EDITAR: un ejemplo ...
for item in some_list:
... # lots of code
... # more code
for tiem in some_other_list:
process(item) # oops!
Debe tener en cuenta cómo se manejan las variables de clase en Python. Considere la siguiente jerarquía de clases:
class AAA(object):
x = 1
class BBB(AAA):
pass
class CCC(AAA):
pass
Ahora, verifique el resultado del siguiente código:
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
>>> print AAA.x, BBB.x, CCC.x
3 2 3
¿Sorprendido? No lo serás si recuerdas que las variables de clase se manejan internamente como diccionarios de un objeto de clase. Si no se encuentra un nombre de variable en el diccionario de la clase actual, se buscan las clases principales. Entonces, el siguiente código nuevamente, pero con explicaciones:
# AAA: {''x'': 1}, BBB: {}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 1 1
>>> BBB.x = 2
# AAA: {''x'': 1}, BBB: {''x'': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
1 2 1
>>> AAA.x = 3
# AAA: {''x'': 3}, BBB: {''x'': 2}, CCC: {}
>>> print AAA.x, BBB.x, CCC.x
3 2 3
Lo mismo ocurre con el manejo de variables de clase en instancias de clase (trate este ejemplo como una continuación del anterior):
>>> a = AAA()
# a: {}, AAA: {''x'': 3}
>>> print a.x, AAA.x
3 3
>>> a.x = 4
# a: {''x'': 4}, AAA: {''x'': 3}
>>> print a.x, AAA.x
4 3
Las expresiones en los argumentos predeterminados se calculan cuando se define la función, no cuando se llama.
Ejemplo: considere por defecto un argumento para la hora actual:
>>>import time
>>> def report(when=time.time()):
... print when
...
>>> report()
1210294387.19
>>> time.sleep(5)
>>> report()
1210294387.19
El argumento when
no cambia Se evalúa cuando defines la función. No cambiará hasta que la aplicación se reinicie.
Estrategia: no se tropezará con esto si tiene argumentos por defecto a None
y luego hará algo útil cuando lo vea:
>>> def report(when=None):
... if when is None:
... when = time.time()
... print when
...
>>> report()
1210294762.29
>>> time.sleep(5)
>>> report()
1210294772.23
Ejercicio: para asegurarse de haber entendido: ¿por qué está sucediendo esto?
>>> def spam(eggs=[]):
... eggs.append("spam")
... return eggs
...
>>> spam()
[''spam'']
>>> spam()
[''spam'', ''spam'']
>>> spam()
[''spam'', ''spam'', ''spam'']
>>> spam()
[''spam'', ''spam'', ''spam'', ''spam'']
James Dumay me recordó de manera elocuente otro truco de Python:
No todas las "baterías incluidas" de Python son maravillosas .
El ejemplo específico de James fueron las bibliotecas HTTP: httplib
, urllib
, urllib2
, urlparse
, mimetools
y ftplib
. Parte de la funcionalidad está duplicada, y parte de la funcionalidad que esperaría está completamente ausente, por ejemplo, el manejo de redirección. Francamente, es horrible.
Si alguna vez tengo que tomar algo a través de HTTP estos días, utilizo el módulo urlgrabber bifurcado del proyecto Yum.
Los flotantes no se imprimen con la máxima precisión por defecto (sin repr
):
x = 1.0 / 3
y = 0.333333333333
print x #: 0.333333333333
print y #: 0.333333333333
print x == y #: False
repr
imprime demasiados dígitos:
print repr(x) #: 0.33333333333333331
print repr(y) #: 0.33333333333300003
print x == 0.3333333333333333 #: True
Sin incluir un __init__
.py en sus paquetes. Ese todavía me atrapa a veces.
Una de las mayores sorpresas que he tenido con Python es esta:
a = ([42],)
a[0] += [43, 44]
Esto funciona como uno podría esperar, ¡excepto para generar un TypeError después de actualizar la primera entrada de la tupla! Entonces a
será ([42, 43, 44],)
después de ejecutar el enunciado +=
, pero de todos modos habrá una excepción. Si intentas esto por otro lado
a = ([42],)
b = a[0]
b += [43, 44]
no obtendrás un error
def f():
x += 1
x = 42
f()
da como resultado un UnboundLocalError
, porque los nombres locales se detectan estáticamente. Un ejemplo diferente sería
def f():
print x
x = 43
x = 42
f()
try:
int("z")
except IndexError, ValueError:
pass
La razón por la que esto no funciona es porque IndexError es el tipo de excepción que está capturando, y ValueError es el nombre de la variable a la que está asignando la excepción.
El código correcto para detectar múltiples excepciones es:
try:
int("z")
except (IndexError, ValueError):
pass
La división de listas me ha causado mucha pena. Realmente considero el siguiente comportamiento como un error.
Definir una lista x
>>> x = [10, 20, 30, 40, 50]
Índice de acceso 2:
>>> x[2]
30
Como esperabas
Cortar la lista del índice 2 y hasta el final de la lista:
>>> x[2:]
[30, 40, 50]
Como esperabas
Índice de acceso 7:
>>> x[7]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
De nuevo, como esperabas.
Sin embargo , intente dividir la lista del índice 7 hasta el final de la lista:
>>> x[7:]
[]
???
El remedio es realizar muchas pruebas al usar la división de listas. Me gustaría obtener un error en su lugar. Mucho más fácil de depurar.
No puede usar locals () [''x''] = lo que sea para cambiar los valores de las variables locales como podría esperar.
This works:
>>> x = 1
>>> x
1
>>> locals()[''x''] = 2
>>> x
2
BUT:
>>> def test():
... x = 1
... print x
... locals()[''x''] = 2
... print x # *** prints 1, not 2 ***
...
>>> test()
1
1
Esto realmente me quemó en una respuesta aquí en SO, ya que lo había probado fuera de una función y obtuve el cambio que quería. Luego, lo encontré mencionado y contrastado con el caso de globals () en "Dive Into Python". Ver ejemplo 8.12. (Aunque no nota que el cambio a través de los lugareños () funcionará en el nivel superior como se muestra arriba).
Bucles y lambdas (o cualquier cierre, realmente): las variables están vinculadas por su nombre
funcs = []
for x in range(5):
funcs.append(lambda: x)
[f() for f in funcs]
# output:
# 4 4 4 4 4
Una solución alternativa es crear una función separada o pasar los argumentos por nombre:
funcs = []
for x in range(5):
funcs.append(lambda x=x: x)
[f() for f in funcs]
# output:
# 0 1 2 3 4
Usar variables de clase cuando desee variables de instancia. La mayoría de las veces esto no causa problemas, pero si se trata de un valor mutable causa sorpresas.
class Foo(object):
x = {}
Pero:
>>> f1 = Foo()
>>> f2 = Foo()
>>> f1.x[''a''] = ''b''
>>> f2.x
{''a'': ''b''}
Casi siempre desea variables de instancia, que requieren que asigne dentro de __init__
:
class Foo(object):
def __init__(self):
self.x = {}
Los valores de range(end_val)
no solo son estrictamente más pequeños que end_val
, sino estrictamente más pequeños que int(end_val)
. Para un argumento float
al range
, este podría ser un resultado inesperado:
from future.builtins import range
list(range(2.89))
[0, 1]
x += [...]
no es lo mismo que x = x + [...]
cuando x
es una lista "
>>> x = y = [1,2,3]
>>> x = x + [4]
>>> x == y
False
>>> x = y = [1,2,3]
>>> x += [4]
>>> x == y
True
Uno crea una nueva lista mientras que el otro se modifica en su lugar
Lista de repetición con listas anidadas
Esto me sorprendió hoy y desperdicié una hora de mi depuración de tiempo:
>>> x = [[]]*5
>>> x[0].append(0)
# Expect x equals [[0], [], [], [], []]
>>> x
[[0], [0], [0], [0], [0]] # Oh dear
Explicación: problema de la lista de Python
Hubo un gran debate sobre las características del lenguaje oculto hace un tiempo: hidden-features-of-python . Donde se mencionaron algunos escollos (y algunas de las cosas buenas también).
También es posible que desee verificar Python Warts .
Pero para mí, la división entera es una gotcha:
>>> 5/2
2
Probablemente querías:
>>> 5*1.0/2
2.5
Si realmente quieres este comportamiento (tipo C), debes escribir:
>>> 5//2
2
Como eso también funcionará con flotadores (y funcionará cuando finalmente vaya a Python 3 ):
>>> 5*1.0//2
2.0
GvR explica cómo la división de enteros comenzó a funcionar como lo hace en la historia de Python .
Mezclar sin querer las clases de estilo antiguo y nuevo puede causar errores aparentemente misteriosos.
Supongamos que tiene una jerarquía de clases simple que consiste en la superclase A y la subclase B. Cuando se instancia B, se debe llamar primero al constructor de A. El siguiente código hace esto correctamente:
class A(object):
def __init__(self):
self.a = 1
class B(A):
def __init__(self):
super(B, self).__init__()
self.b = 1
b = B()
Pero si olvida hacer A una clase de estilo nuevo y definirlo así:
class A:
def __init__(self):
self.a = 1
obtienes este rastreo:
Traceback (most recent call last):
File "AB.py", line 11, in <module>
b = B()
File "AB.py", line 7, in __init__
super(B, self).__init__()
TypeError: super() argument 1 must be type, not classobj
Otras dos preguntas relacionadas con este tema son 489269 y 770134
Debido a la ''verdad'' esto tiene sentido:
>>>bool(1)
True
pero puede que no esperes que vaya por el otro lado:
>>>float(True)
1.0
Esto puede ser un problema si está convirtiendo cadenas en numéricas y sus datos tienen valores True / False.