Что такое `1 ..__ truediv__`? Имеет ли Python синтаксис нотации (точка-точка)?

190

Недавно я столкнулся с синтаксисом, который никогда не видел прежде, когда я изучал python, ни в большинстве учебных пособий .., это выглядит примерно так:

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

print(f(8)) # prints 0.125 

Я подумал, что это точно так же, как (за исключением, конечно, дольше):

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

Но мои вопросы:

  • Как это может сделать это?
  • Что это на самом деле означает с двумя точками?
  • Как вы можете использовать его в более сложном утверждении (если это возможно)?

Это, вероятно, сэкономит мне много строк кода в будущем ... :)

abccd
источник
14
Примечание: на (1).__truediv__самом деле это не то же самое 1..__truediv__, что первый вызов, в int.__truediv__то время как последний делает float.__truediv__. Кроме того, вы также можете использовать 1 .__truediv__(с пробелом) `
tobias_k
7
Обратите внимание, что 1//8это 0не так 0.125в любой версии Python.
mkrieger1
1
напоминает мне оif (x <- 3) {...}
Незнайка
7
Вот пример этого в использовании.
Эмонн Олив
3
@KeithC Высококачественные ответы и комментарии показывают, что пример кода нуждается в понимании для понимания, удивляет многих, имеет альтернативы, которые являются более четкими, более общими и, по крайней мере, такими же эффективными. Моя главная проблема в том, что читаемость имеет значение. Сохраните ум там, где это больше всего нужно - общение с людьми.
Питер Вуд

Ответы:

212

То, что у вас есть, это floatлитерал без конечного нуля, к которому вы затем обращаетесь по __truediv__методу. Это не оператор сам по себе; первая точка является частью значения с плавающей точкой, а вторая является оператором точки для доступа к свойствам и методам объектов.

Вы можете достичь той же точки, выполнив следующие действия.

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

Другой пример

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

Здесь мы добавляем 1,0 к 2,0, что, очевидно, дает 3,0.

Пол Руни
источник
165
Итак, мы нашли разработчика, который пожертвовал большой ясностью ради краткости, и вот мы здесь.
TemporalWolf
11
Может быть, кто-то сохраняет свой исходный код на 5,5-
Томас Аюб,
10
@ThomasAyoub это будет 5.25 "iirc ;-)
jjmontes
9
@TemporalWolf Он, возможно, нашел это в этой недавней подаче кода гольфа .
Брайан МакКатчон
2
Забавный факт, вы также можете сделать это в JavaScript:1..toString()
Дерек 朕 會
74

На вопрос уже получен достаточный ответ (т. Е. Ответ Пола Руни ), но также можно проверить правильность этих ответов.

Позвольте мне повторить существующие ответы: Это ..не единственный элемент синтаксиса!

Вы можете проверить, как исходный код «токенизируется» . Эти токены представляют, как код интерпретируется:

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

Таким образом, строка 1.интерпретируется как число, вторым .является OP (оператор, в данном случае оператор «get attribute»), а __truediv__это имя метода. Так что это просто доступ к __truediv__методу поплавка 1.0.

Другой способ просмотра сгенерированного байт-кода заключается в его сборке . Это фактически показывает инструкции, которые выполняются, когда выполняется некоторый код: 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

Который в основном говорит то же самое. Загружает атрибут __truediv__константы 1.0.


По вашему вопросу

И как вы можете использовать его в более сложном утверждении (если это возможно)?

Хотя возможно, что вы никогда не должны писать такой код просто потому, что неясно, что делает код. Поэтому, пожалуйста, не используйте его в более сложных утверждениях. Я бы даже зашел так далеко, что вы не должны использовать его в таких «простых» выражениях, по крайней мере, вы должны использовать скобки для разделения инструкций:

f = (1.).__truediv__

это было бы определенно более читабельно - но что-то вроде:

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

было бы еще лучше!

Подход, использующий подход, partialтакже сохраняет модель данных Python ( 1..__truediv__подход не делает!), Что можно продемонстрировать с помощью этого небольшого фрагмента:

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

Это потому, что 1. / (1+2j)не оценивается, float.__truediv__а с complex.__rtruediv__- operator.truedivгарантирует, что обратная операция вызывается, когда нормальная операция возвращается, NotImplementedно у вас нет этих откатов, когда вы работаете __truediv__напрямую. Эта потеря «ожидаемого поведения» является основной причиной, по которой вы (обычно) не должны использовать магические методы напрямую.

MSeifert
источник
40

Поначалу две точки могут быть немного неловкими:

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

Но это так же, как писать:

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

Потому что floatлитералы могут быть написаны в трех формах:

normal_float = 1.0
short_float = 1.  # == 1.0
prefixed_float = .1  # == 0.1
sobolevn
источник
Это удивительно, почему это правильный синтаксис, но 1.__truediv__это не так?
Алекс Холл
3
@AlexHall Смотрите здесь . ., Как представляется, анализируются как часть номера, а затем .для метода доступа отсутствуют.
tobias_k
7
Но поскольку это неуклюжий и неясный синтаксис, его, вероятно, следует избегать.
DrMcCleod
11

Что такое f = 1..__truediv__?

fявляется связанным специальным методом для числа с плавающей запятой со значением один. В частности,

1.0 / x

в Python 3 вызывает:

(1.0).__truediv__(x)

Свидетельство:

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

и:

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

Если мы делаем:

f = one.__truediv__

Мы сохраняем имя, связанное с этим связанным методом

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

Если бы мы делали этот точечный поиск в узком цикле, это могло бы сэкономить немного времени.

Разбор абстрактного синтаксического дерева (AST)

Мы можем видеть, что анализ AST для выражения говорит нам, что мы получаем __truediv__атрибут по числу с плавающей запятой 1.0:

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

Вы можете получить ту же функцию из:

f = float(1).__truediv__

Или

f = (1.0).__truediv__

дедукция

Мы также можем получить там путем вычета.

Давайте построим это.

1 само по себе является int:

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

1 с точкой после поплавка:

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

Следующая точка сама по себе будет SyntaxError, но она начинает точечный поиск в экземпляре с плавающей точкой:

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

Никто другой не упомянул об этом - теперь это «связанный метод» на поплавке 1.0:

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

Мы могли бы выполнить ту же функцию гораздо более легко:

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

Производительность

Недостатком divide_one_byфункции является то, что для нее требуется еще один фрейм стека Python, что делает его несколько медленнее, чем связанный метод:

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

Конечно, если вы можете просто использовать обычные литералы, это еще быстрее:

>>> def f_3():
...     for x in range(1, 11):
...         1.0/x
...         
>>> timeit.repeat(f_3)
[2.1224895628296281, 2.1219930218637728, 2.1280188256941983]
Аарон Холл
источник