O que é `1 ..__ truediv__`? O Python possui uma sintaxe de notação .. ("dot dot")?

190

Recentemente, deparei com uma sintaxe que nunca vi antes quando aprendi python, nem na maioria dos tutoriais, a ..notação, é algo parecido com isto:

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

print(f(8)) # prints 0.125 

Achei que era exatamente o mesmo que (exceto que é mais longo, é claro):

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

Mas minhas perguntas são:

  • Como isso pode ser feito?
  • O que realmente significa com os dois pontos?
  • Como você pode usá-lo em uma declaração mais complexa (se possível)?

Provavelmente isso me salvará muitas linhas de código no futuro ... :)

abccd
fonte
14
Nota: (1).__truediv__não é realmente o mesmo que 1..__truediv__, como o primeiro chama int.__truediv__enquanto o último chama float.__truediv__. Como alternativa, você também pode usar 1 .__truediv__(com um espaço) `
tobias_k 19/04/19
7
Observe que isso não 1//8ocorre nas duas versões do Python. 00.125
precisa saber é o seguinte
1
lembra-me deif (x <- 3) {...}
Não sei
7
Aqui está um exemplo disso em uso.
Eamonn Olive
3
@KeithC As respostas e comentários de alta qualidade mostram que o código de amostra precisa de insight para compreender, é surpreendente para muitos, tem alternativas mais claras, mais gerais e pelo menos tão eficientes. Minha queixa principal é que a legibilidade conta. Economize inteligência para onde é mais necessário - comunicando-se com seres humanos.
Peter Wood

Respostas:

212

O que você tem é um floatliteral sem o zero à direita, ao qual você acessa o __truediv__método Não é um operador em si; o primeiro ponto faz parte do valor flutuante e o segundo é o operador de ponto para acessar as propriedades e os métodos dos objetos.

Você pode chegar ao mesmo ponto, fazendo o seguinte.

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

Outro exemplo

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

Aqui nós adicionamos 1.0 a 2.0, o que obviamente gera 3.0.

Paul Rooney
fonte
165
Então, o que descobrimos é um desenvolvedor que sacrificou muita clareza por um pouco de brevidade e aqui estamos nós.
TemporalWolf
11
Talvez que alguém está salvando seu código fonte para disquete 5.5" ?
Thomas Ayoub
10
@ThomasAyoub seria 5,25 "iirc ;-)
jjmontes
9
@TemporalWolf Ele pode ter encontrado isso recentemente neste código de submissão de golfe .
precisa saber é o seguinte
2
Curiosidade, você também pode fazer isso em JavaScript:1..toString()
Derek朕會功夫
74

A pergunta já está suficientemente respondida (por exemplo, a resposta de @Paul Rooney ), mas também é possível verificar a exatidão dessas respostas.

Deixe-me recapitular as respostas existentes: O ..elemento não é um único sintaxe!

Você pode verificar como o código-fonte é "tokenizado" . Esses tokens representam como o código é interpretado:

>>> 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__'),
 ...]

Portanto, a string 1.é interpretada como número, a segunda .é um OP (um operador, neste caso o operador "get attribute") e __truediv__é o nome do método. Então, isso é apenas acessar o __truediv__método do float 1.0.

Outra maneira de visualizar o bytecode gerado é montá- lo. Na verdade, isso mostra as instruções executadas quando algum código é executado: 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

O que basicamente diz o mesmo. Carrega o atributo __truediv__da constante 1.0.


Quanto à sua pergunta

E como você pode usá-lo em uma declaração mais complexa (se possível)?

Mesmo que seja possível, você nunca deve escrever um código como esse, simplesmente porque não está claro o que o código está fazendo. Portanto, não o use em declarações mais complexas. Eu iria tão longe que você não deve usá-lo em declarações "simples", pelo menos você deve usar parênteses para separar as instruções:

f = (1.).__truediv__

isso seria definitivamente mais legível - mas algo como:

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

seria ainda melhor!

A abordagem usando partialtambém preserva o modelo de dados do python (a 1..__truediv__abordagem não!), O que pode ser demonstrado por este pequeno trecho:

>>> 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

Isso ocorre porque 1. / (1+2j)não é avaliado por, float.__truediv__mas com complex.__rtruediv__- operator.truedivgarante que a operação reversa seja chamada quando a operação normal retornar, NotImplementedmas você não terá esses fallbacks quando operar __truediv__diretamente. Essa perda de "comportamento esperado" é a principal razão pela qual você (normalmente) não deve usar métodos mágicos diretamente.

MSeifert
fonte
40

Dois pontos juntos podem ser um pouco estranhos no começo:

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

Mas é o mesmo que escrever:

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

Como os floatliterais podem ser escritos em três formas:

normal_float = 1.0
short_float = 1.  # == 1.0
prefixed_float = .1  # == 0.1
sobolevn
fonte
Isso é surpreendente, por que essas sintaxe são válidas, mas 1.__truediv__não são?
Alex Hall
3
@AlexHall Veja aqui . O .parece ser analisado como parte do número e, em seguida, o .acessador do método está ausente.
tobias_k
7
Mas como é uma sintaxe incômoda e pouco clara, provavelmente deve ser evitada.
DrMcCleod 19/04
11

O que é f = 1..__truediv__?

fé um método especial vinculado em um float com o valor one. Especificamente,

1.0 / x

no Python 3, chama:

(1.0).__truediv__(x)

Evidência:

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

e:

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

Se nós fizermos:

f = one.__truediv__

Mantemos um nome vinculado a esse método vinculado

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

Se estivéssemos fazendo essa pesquisa pontilhada em um loop apertado, isso poderia economizar um pouco de tempo.

Analisando a Árvore de Sintaxe Abstrata (AST)

Podemos ver que a análise do AST da expressão nos diz que estamos obtendo o __truediv__atributo no número do ponto flutuante 1.0:

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

Você pode obter a mesma função resultante de:

f = float(1).__truediv__

Ou

f = (1.0).__truediv__

Dedução

Também podemos chegar lá por dedução.

Vamos construir.

1 por si só é um int:

>>> 1
1
>>> type(1)
<type 'int'>

1 com um período após ser flutuado:

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

O próximo ponto por si só seria um SyntaxError, mas inicia uma pesquisa pontilhada na instância do float:

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

Ninguém mais mencionou isso - Agora é um "método vinculado" no float 1.0:

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

Poderíamos realizar a mesma função com muito mais facilidade:

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

atuação

A desvantagem da divide_one_byfunção é que ela requer outro quadro de pilha Python, tornando-o um pouco mais lento que o método bound:

>>> 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]

Obviamente, se você pode usar literais simples, isso é ainda mais 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
fonte