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

190

Recientemente me encontré con una sintaxis que nunca antes había visto 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 puede usarlo en una declaración más compleja (si es posible)?

Esto probablemente me ahorrará muchas líneas de código en el futuro ... :)

abccd
fuente
14
Nota: en (1).__truediv__realidad no es lo mismo 1..__truediv__que el primero llama int.__truediv__mientras que el segundo sí float.__truediv__. Alternativamente, también puede usar 1 .__truediv__(con un espacio) `
tobias_k
77
Tenga en cuenta que 1//8es 0, no 0.125, en cualquier versión de Python.
mkrieger1
1
me recuerda aif (x <- 3) {...}
Dunno
77
Aquí hay un ejemplo de esto en uso.
Éamonn Olive
3
@KeithC Las respuestas y comentarios de alta calidad muestran que el código de muestra necesita información para comprender, es sorprendente para muchos, tiene alternativas que son más claras, más generales y al menos tan eficientes. Mi queja principal es que la legibilidad cuenta. Guarde la inteligencia para donde más se necesita: comunicarse con los humanos.
Peter Wood

Respuestas:

212

Lo que tiene es un floatliteral sin el cero final, al que luego accede al__truediv__ método. 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.

Puede llegar al mismo punto haciendo lo siguiente.

>>> f = 1.
>>> f
1.0
>>> f.__floordiv__
<method-wrapper '__floordiv__' of float object at 0x7f9fb4dc1a20>

Otro ejemplo

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

Aquí agregamos 1.0 a 2.0, que obviamente rinde 3.0.

Paul Rooney
fuente
165
Entonces, lo que encontramos es un desarrollador que sacrificó mucha claridad por un poco de brevedad y aquí estamos.
TemporalWolf
11
¿Tal vez que alguien está guardando su código fuente en un disquete de 5.5 "?
Thomas Ayoub
10
@ThomasAyoub sería 5.25 "iirc ;-)
jjmontes
9
@TemporalWolf Es posible que lo haya encontrado en este reciente envío de código de golf .
Brian McCutchon el
2
Dato 1..toString()
curioso
74

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

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

Puede verificar cómo se "tokeniza" el código fuente . Estos tokens 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__'),
 ...]

Entonces, la cadena 1.se interpreta como un número, el segundo .es un OP (un operador, en este caso el operador "obtener atributo") y __truediv__es el nombre del método. Entonces esto es solo acceder al __truediv__método de flotación 1.0.

Otra forma de ver el bytecode generado es ensamblarlo . En realidad, esto muestra las instrucciones que se realizan cuando se ejecuta algún código: dis

>>> 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 declaración más compleja (si es posible)?

Aunque es posible, nunca debe escribir código como ese, simplemente porque no está claro qué está haciendo el código. Por lo tanto, no lo use 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 partialtambién conserva el modelo de datos de Python (¡el 1..__truediv__enfoque no lo hace!), Que puede demostrarse 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 1. / (1+2j)que no se evalúa por float.__truediv__pero con complex.__rtruediv__: operator.truedivse asegura de que se llame a la operación inversa cuando vuelve la operación normal, NotImplementedpero no tiene estos retrocesos cuando opera __truediv__directamente. Esta pérdida de "comportamiento esperado" es la razón principal por la cual (normalmente) no debería usar métodos mágicos directamente.

MSeifert
fuente
40

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 floatliterales se pueden escribir en tres formas:

normal_float = 1.0
short_float = 1.  # == 1.0
prefixed_float = .1  # == 0.1
sobolevn
fuente
Esto es sorprendente, ¿por qué son estas sintaxis válidas pero 1.__truediv__no lo son?
Alex Hall
3
@AlexHall Ver aquí . El .parece ser analizado como parte de la serie, y luego el .de descriptor de acceso método no se encuentra.
tobias_k
77
Pero dado que es una sintaxis incómoda y poco clara, probablemente debería evitarse.
DrMcCleod
11

¿Qué es f = 1..__truediv__?

fes un método especial vinculado en un flotante 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__

Retenemos un nombre vinculado a ese método vinculado

>>> 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 ahorrarnos un poco de tiempo.

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

Podemos ver que analizar el AST para la expresión nos dice que estamos obteniendo el __truediv__atributo en el número de coma 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()))"

Puede 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)
<type 'int'>

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

>>> 1.
1.0
>>> type(1.)
<type 'float'>

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

>>> 1..__truediv__
<method-wrapper '__truediv__' of float object at 0x0D1C7BF0>

Nadie más ha mencionado esto : ahora es un "método vinculado" en el flotador 1.0:

>>> f = 1..__truediv__
>>> f
<method-wrapper '__truediv__' of float object at 0x127F3CD8>
>>> f(2)
0.5
>>> f(3)
0.33333333333333331

Podríamos cumplir la misma función de manera mucho más legible:

>>> 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 divide_one_byfunción es que requiere otro marco de pila 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 solo puede 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]
Aaron Hall
fuente