Che cos'è `1 ..__ truediv__`? Python ha una sintassi della notazione .. ("punto punto")?


190

Di recente mi sono imbattuto in una sintassi che non avevo mai visto prima quando ho imparato Python né nella maggior parte dei tutorial, la ..notazione, sembra qualcosa del genere:

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

print(f(8)) # prints 0.125 

Ho pensato che fosse esattamente lo stesso di (tranne che è più lungo, ovviamente):

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

Ma le mie domande sono:

  • Come può farlo?
  • Cosa significa in realtà con i due punti?
  • Come puoi usarlo in una dichiarazione più complessa (se possibile)?

Questo probabilmente mi farà risparmiare molte righe di codice in futuro ... :)


14
Nota: (1).__truediv__non è proprio lo stesso di 1..__truediv__, come il primo chiama int.__truediv__mentre il secondo lo fa float.__truediv__. In alternativa, puoi anche usare 1 .__truediv__(con uno spazio) `
tobias_k

7
Si noti che 1//8è 0, non è 0.125, in entrambe le versioni di Python.
mkrieger1,

1
mi ricordaif (x <- 3) {...}
Dunno,

7
Ecco un esempio di questo in uso.
Éamonn Olive,

3
@KeithC Le risposte e i commenti di alta qualità mostrano che il codice di esempio ha bisogno di informazioni da comprendere, è sorprendente per molti, ha alternative che sono più chiare, più generali e almeno altrettanto efficienti. La mia lamentela principale è che la leggibilità conta. Risparmia intelligenza dove è più necessario: comunicare con gli umani.
Peter Wood,

Risposte:


212

Quello che hai è un floatletterale senza lo zero finale, di cui accedi al __truediv__metodo di. Non è un operatore in sé; il primo punto fa parte del valore float e il secondo è l'operatore punto per accedere alle proprietà e ai metodi degli oggetti.

È possibile raggiungere lo stesso punto procedendo come segue.

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

Un altro esempio

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

Qui aggiungiamo 1.0 a 2.0, che ovviamente produce 3.0.


165
Quindi quello che abbiamo scoperto è un dev che ha sacrificato molta chiarezza per un po 'di brevità ed eccoci qui.
TemporalWolf

11
Forse qualcuno sta salvando il suo codice sorgente su disco floppy da 5,5 "?
Thomas Ayoub,

10
@ThomasAyoub sarebbe 5.25 "iirc ;-)
jjmontes,

9
@TemporalWolf Potrebbe averlo trovato in questo recente invio del codice golf .
Brian McCutchon,

2
1..toString()
Curiosità

74

Alla domanda è già stata data una risposta sufficiente (ovvero la risposta di @Paul Rooney ) ma è anche possibile verificare la correttezza di queste risposte.

Consentitemi di ricapitolare le risposte esistenti: ..non si tratta di un singolo elemento di sintassi!

Puoi controllare come il codice sorgente è "tokenizzato" . Questi token rappresentano la modalità di interpretazione del codice:

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

Quindi la stringa 1.viene interpretata come numero, il secondo .è un OP (un operatore, in questo caso l'operatore "ottieni attributo") e il __truediv__nome del metodo. Quindi questo sta solo accedendo al __truediv__metodo del float 1.0.

Un altro modo di visualizzare il bytecode generato è assemblarlo . Questo in realtà mostra le istruzioni che vengono eseguite quando viene eseguito del codice: 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

Che sostanzialmente dice lo stesso. Carica l'attributo __truediv__della costante 1.0.


Per quanto riguarda la tua domanda

E come puoi usarlo in una dichiarazione più complessa (se possibile)?

Anche se è possibile non scrivere mai un codice del genere, semplicemente perché non è chiaro cosa stia facendo il codice. Quindi, per favore, non usarlo in dichiarazioni più complesse. Vorrei anche andare così lontano che non dovresti usarlo in affermazioni così "semplici", almeno dovresti usare la parentesi per separare le istruzioni:

f = (1.).__truediv__

questo sarebbe sicuramente più leggibile - ma qualcosa sulla falsariga di:

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

sarebbe ancora meglio!

L'approccio che utilizza partialconserva anche il modello di dati di Python (l' 1..__truediv__approccio no!) Che può essere dimostrato da questo piccolo frammento:

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

Questo perché 1. / (1+2j)non viene valutato da float.__truediv__ma con complex.__rtruediv__- si operator.truedivassicura che l'operazione inversa venga chiamata quando ritorna l'operazione normale NotImplementedma non si hanno questi fallback quando si opera __truediv__direttamente. Questa perdita di "comportamento previsto" è la ragione principale per cui (normalmente) non dovresti usare direttamente i metodi magici.


40

All'inizio due punti insieme possono essere un po 'imbarazzanti:

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

Ma è lo stesso della scrittura:

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

Perché i floatletterali possono essere scritti in tre forme:

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

Ciò è sorprendente, perché queste sono sintassi valide ma 1.__truediv__non lo sono?
Alex Hall,

3
@AlexHall Vedi qui . L' .sembra essere analizzato come parte del numero, e quindi la .per il metodo di accesso è mancante.
tobias_k,

7
Ma poiché è una sintassi scomoda e poco chiara, probabilmente dovrebbe essere evitata.
DrMcCleod

11

Che cosa è f = 1..__truediv__?

fè un metodo speciale associato su un float con un valore di uno. In particolare,

1.0 / x

in Python 3, invoca:

(1.0).__truediv__(x)

Prova:

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 lo facciamo:

f = one.__truediv__

Conserviamo un nome associato a quel metodo associato

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

Se stessimo eseguendo quella ricerca punteggiata in un ciclo stretto, questo potrebbe far risparmiare un po 'di tempo.

Parsing the Abstract Syntax Tree (AST)

Possiamo vedere che l'analisi del AST per l'espressione ci dice che stiamo ottenendo l' __truediv__attributo del numero in virgola mobile, 1.0:

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

È possibile ottenere la stessa funzione risultante da:

f = float(1).__truediv__

O

f = (1.0).__truediv__

Deduzione

Possiamo anche arrivarci per detrazione.

Costruiamolo.

1 da solo è un int:

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

1 con un punto dopo che è un float:

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

Il punto successivo da solo sarebbe un SyntaxError, ma inizia una ricerca punteggiata sull'istanza del float:

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

Nessun altro ha parlato di questo - Questo è ormai un "metodo vincolato" sul galleggiante, 1.0:

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

Potremmo svolgere la stessa funzione in modo molto più leggibile:

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

Prestazione

Il rovescio della medaglia della divide_one_byfunzione è che richiede un altro frame stack Python, rendendolo un po 'più lento del metodo associato:

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

Certo, se puoi semplicemente usare letterali semplici, è ancora più veloce:

>>> def f_3():
...     for x in range(1, 11):
...         1.0/x
...         
>>> timeit.repeat(f_3)
[2.1224895628296281, 2.1219930218637728, 2.1280188256941983]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.