¿Qué es `1 ..__ truediv__`? ¿Python tiene una syntax de notación (“punto punto”)?

Hace poco me encontré con una syntax que nunca había visto antes cuando aprendí python ni en la mayoría de los tutoriales, la notación .. , se parece a esto:

 f = 1..__truediv__ # or 1..__div__ for python 2 print(f(8)) # prints 0.125 

Pensé que era exactamente lo mismo que (excepto que es más largo, por supuesto):

 f = lambda x: (1).__truediv__(x) print(f(8)) # prints 0.125 or 1//8 

Pero mis preguntas son:

  • ¿Cómo puede hacer eso?
  • ¿Qué significa realmente con los dos puntos?
  • ¿Cómo se puede usar en una statement más compleja (si es posible)?

Esto probablemente me ahorrará muchas líneas de código en el futuro … 🙂

Lo que tienes es un literal float sin el cero final, al que luego accedes al método __truediv__ de. No es un operador en sí mismo; el primer punto es parte del valor flotante, y el segundo es el operador de punto para acceder a las propiedades y métodos de los objetos.

Puedes llegar al mismo punto haciendo lo siguiente.

 >>> f = 1. >>> f 1.0 >>> f.__floordiv__  

Otro ejemplo

 >>> 1..__add__(2.) 3.0 

Aquí agregamos 1.0 a 2.0, lo que obviamente produce 3.0.

La pregunta ya está suficientemente respondida (es decir, la respuesta de @Paul Rooney ) pero también es posible verificar la exactitud de estas respuestas.

Permítanme recapitular las respuestas existentes: ¡El .. no es un solo elemento de syntax!

Puede verificar cómo el código fuente es “tokenizado” . Estas fichas representan cómo se interpreta el código:

 >>> from tokenize import tokenize >>> from io import BytesIO >>> s = "1..__truediv__" >>> list(tokenize(BytesIO(s.encode('utf-8')).readline)) [... TokenInfo(type=2 (NUMBER), string='1.', start=(1, 0), end=(1, 2), line='1..__truediv__'), TokenInfo(type=53 (OP), string='.', start=(1, 2), end=(1, 3), line='1..__truediv__'), TokenInfo(type=1 (NAME), string='__truediv__', start=(1, 3), end=(1, 14), line='1..__truediv__'), ...] 

Así que la cadena 1. se interpreta como número, el segundo . es un OP (un operador, en este caso el operador “obtener atributo”) y el __truediv__ es el nombre del método. Así que esto es solo acceder al método __truediv__ del float 1.0 .

Otra forma de ver el código de bytes generado es dis . Esto realmente muestra las instrucciones que se realizan cuando se ejecuta algún código:

 >>> import dis >>> def f(): ... return 1..__truediv__ >>> dis.dis(f) 4 0 LOAD_CONST 1 (1.0) 3 LOAD_ATTR 0 (__truediv__) 6 RETURN_VALUE 

Lo que básicamente dice lo mismo. Carga el atributo __truediv__ de la constante 1.0 .


Con respecto a su pregunta

¿Y cómo puede usarlo en una statement más compleja (si es posible)?

Aunque es posible que nunca debas escribir código así, simplemente porque no está claro qué está haciendo el código. Así que por favor no lo uses en declaraciones más complejas. Incluso iría tan lejos que no deberías usarlo en declaraciones tan “simples”, al menos deberías usar paréntesis para separar las instrucciones:

 f = (1.).__truediv__ 

Esto sería definitivamente más legible, pero algo similar a:

 from functools import partial from operator import truediv f = partial(truediv, 1.0) 

sería aún mejor!

El enfoque que usa partial también preserva el modelo de datos de python (¡el enfoque 1..__truediv__ no lo hace!) Que se puede demostrar con este pequeño fragmento:

 >>> f1 = 1..__truediv__ >>> f2 = partial(truediv, 1.) >>> f2(1+2j) # reciprocal of complex number - works (0.2-0.4j) >>> f2('a') # reciprocal of string should raise an exception TypeError: unsupported operand type(s) for /: 'float' and 'str' >>> f1(1+2j) # reciprocal of complex number - works but gives an unexpected result NotImplemented >>> f1('a') # reciprocal of string should raise an exception but it doesn't NotImplemented 

Esto se debe a que 1. / (1+2j) no se evalúa mediante float.__truediv__ pero con complex.__rtruediv__operator.truediv se asegura de que se llame a la operación inversa cuando la operación normal devuelve NotImplemented pero no tiene estos fallbacks cuando operar en __truediv__ directamente. Esta pérdida de “comportamiento esperado” es la razón principal por la que (normalmente) no debería usar métodos mágicos directamente.

Dos puntos juntos pueden ser un poco incómodos al principio:

 f = 1..__truediv__ # or 1..__div__ for python 2 

Pero es lo mismo que escribir:

 f = 1.0.__truediv__ # or 1.0.__div__ for python 2 

Porque los literales float se pueden escribir en tres formas:

 normal_float = 1.0 short_float = 1. # == 1.0 prefixed_float = .1 # == 0.1 

¿Qué es f = 1..__truediv__ ?

f es un método especial encuadernado en un flotador con un valor de uno. Específicamente,

 1.0 / x 

en Python 3, invoca:

 (1.0).__truediv__(x) 

Evidencia:

 class Float(float): def __truediv__(self, other): print('__truediv__ called') return super(Float, self).__truediv__(other) 

y:

 >>> one = Float(1) >>> one/2 __truediv__ called 0.5 

Si lo hacemos:

 f = one.__truediv__ 

Conservamos un nombre vinculado a ese método enlazado

 >>> f(2) __truediv__ called 0.5 >>> f(3) __truediv__ called 0.3333333333333333 

Si estuviéramos haciendo esa búsqueda punteada en un circuito cerrado, esto podría ahorrar un poco de tiempo.

Análisis del árbol de syntax abstracta (AST)

Podemos ver que el análisis del AST para la expresión nos dice que estamos obteniendo el atributo __truediv__ en el número de punto flotante, 1.0 :

 >>> import ast >>> ast.dump(ast.parse('1..__truediv__').body[0]) "Expr(value=Attribute(value=Num(n=1.0), attr='__truediv__', ctx=Load()))" 

Podría obtener la misma función resultante de:

 f = float(1).__truediv__ 

O

 f = (1.0).__truediv__ 

Deducción

También podemos llegar por deducción.

Vamos a construirlo.

1 por sí mismo es un int :

 >>> 1 1 >>> type(1)  

1 con un período después de que es un flotador:

 >>> 1. 1.0 >>> type(1.)  

El siguiente punto por sí mismo sería un SyntaxError, pero comienza una búsqueda punteada en la instancia del flotador:

 >>> 1..__truediv__  

Nadie más ha mencionado esto . Este es ahora un “método vinculado” en el flotador, 1.0 :

 >>> f = 1..__truediv__ >>> f  >>> f(2) 0.5 >>> f(3) 0.33333333333333331 

Podríamos realizar la misma función mucho más fácilmente:

 >>> def divide_one_by(x): ... return 1.0/x ... >>> divide_one_by(2) 0.5 >>> divide_one_by(3) 0.33333333333333331 

Actuación

La desventaja de la función divide_one_by es que requiere otro marco de stack de Python, lo que lo hace algo más lento que el método enlazado:

 >>> def f_1(): ... for x in range(1, 11): ... f(x) ... >>> def f_2(): ... for x in range(1, 11): ... divide_one_by(x) ... >>> timeit.repeat(f_1) [2.5495760687176485, 2.5585621018805469, 2.5411816588331888] >>> timeit.repeat(f_2) [3.479687248616699, 3.46196088706062, 3.473726342237768] 

Por supuesto, si puedes usar literales simples, eso es aún más rápido:

 >>> def f_3(): ... for x in range(1, 11): ... 1.0/x ... >>> timeit.repeat(f_3) [2.1224895628296281, 2.1219930218637728, 2.1280188256941983]