Pull to refresh

Comments 24

Если вам понравилась эта статья или у вас есть дополнительные вопросы, пожалуйста, оставьте свой комментарий ниже!

У меня вопрос — а где, собственно, обещанное в заголовке "глубокое погружение" ?

я извиняюсь. я так понял, для Вас это легкая тема. это круто. работаю с Python 5 лет и только недавно вообще в принципе задумался как работают эти операторы и разобрал эту тему. мне показалось, что есть такие же вокруг программисты. эта заметка написана для нас.

ждем статью "Глубокое погружение в магию '-' и '/'" от создателя "Глубокое погружение в магию '+' и '*'"

из-за таких токсичных людей Хабр превращается в токсичную клоаку старых снобов.

почему превращается? он всегда таким был) а статья на самом деле провальная

Да нет, тут не токсичность)

Просто ожидания от сттатьи по заголовку не оправдалось. "Глубокое" для хабра - это несколько поглубже) Куда-нибудь на уровень ассемблера, устройства объектов в памяти, нюансов работы сборщика или хотя бы тонкости, поиск которых нетривиален. А тут для большинства нет ничего нового и даже интересного. А кто это не знал - то цена вопроса 1 минута поиска в доке.

В общем, цена информации из статьи - околонулевая.

Не стоит воспринимать как негатив, просто делайте выводы. Надеюсь, следующая статья будет намного интереснее и встречена тепло! Успехов! )

За 5 лет?! Парень, постарайся понять меня правильно: использовать язык, который от рождения поддерживает ООП-парадигму, и лишь спустя годы узнать основы того, как же в языке реализуются операции над объектами — ну..., по меньшей мере, странно...

И да, что-либо магическое в программистской рутине видят только космически далёкие от программирования люди. На форуме, где обитают программисты, использование слова “магия” и подобных в заголовках статей о рутине — неработающий способ привлечения аудитории.

Вот да... Как в анекдоте. Старый, но не грех и процитировать.

Мужик. Фанат бокса. Сегодня - бой за звание чемпиона мира. За час - отпрашивается с работы. По дороге забегает в магазин. Покупает креветки и пиво.40 минут до матча - он дома. Бросает креветки в воду, пиво - в морозилку, чтобы быстрее.5 минут до матча. Достает пиво, вынимает креветки. Минута до матча. Мужик сидит перед телевизором, в левой руке - очищенная креветка, в правой руке - открытая бутылка. И... Гонг! Первый раунд, первый удар... Нокаут. Мужик сидит, сказать ничего не может. Только глазами лупает, да руками в воздухе махает. Оборачивается к двери в комнату, а там его жена стоит, сложив руки на груди, и спрашивает его:- Ну?! Теперь ты меня понимаешь?

Согласен, нету команд add, mul, sub, div, которые выполняются на самом дне

>> Если сложение между этими типами невозможно напрямую, Python пытается использовать __radd__ у второго объекта.

И чем radd сильнее add, и почему только у второго обьекта?

Потому что это "reverse add". Абстрактный пример: мы складываем число и строку. Число не знает как складываться со строкой, поэтому __add__ у первого объекта фейлится. Но возможно строка знает как складываться с числом? Поэтому вызывается __radd__ у второго объекта. Почему не вызвать __add__ у второго объекта? Потому что в Пайтоне операция сложения не коммутативна. A + B в общем случае не то же самое, что B + A. Поэтому второй объект должен знать что его операнд слева, а не справа. В этом и разница между __add__ и __radd__

Другой вопрос - почему автор не погрузился настолько глубоко, чтобы описать такие нюансы?

"Глубокое погружение" наверное даже подразумевало вхождение в дебри реализации на С (например, про длинную арифметику), но никак не "Операторы реализованы с помощью соответствующих специальных функций". Ещё бы можно было написать почему операторов инкремента и декремента нет.

что значит "фейлится"? кидает исключение? возвращает спец значение? вызывает спец функцию?

Возвращает специальное значение NotImplemented.

Спасибо за пояснение!

Попробуйте дополнить статью объяснением логики интерпретатора при вызовах __add__ и __radd__, как это сделали в комментариях выше

Можно ещё добавить пример того, как это можно использовать для объединения объектов

Уважаемый, "глубокое" погружение - это когда вы детально расписываете, как именно происходит низкоуровневая оптимизация сложения на Си. А то, что вы написали, знает любой грамотный джун-питонист.

Тут недавно статья была про программирование за "гранью", где взяли рандомные куски кода на js и python, про которые даже стажер знать должен.
Теперь глубокое погружение в + и * , когда про умножение и складывание строк рассказывают при первом изучении этих операторов, а про магические методы складывания и умножения узнают сразу, когда начинают изучать магические методы.

Глубокое погружение возможно было бы при рассказе про переполнения на python, а точнее его отсутствия в привычном виде и почему это так, или почему оно все же возможно при работе с pandas или numpy или при работе с float к примеру.

Глубокое погружение возможно было бы при рассказе о том как на самом деле складываются строки и какая алокация при этом происходит и почему складывание большого количества строк по средствам цикла не очень хорошая идея.

Ну или на крайний случай хотя бы пример какой-нибудь реализации магических методов. К примеру класс вектора реализующий метод складывания векторов. Уже было бы значительно полезнее. А так мы имеем, что новичок который прочитает статью вспомнит разве что про складывание строк, про которое он уже скорее всего знал, а про магические методы так ничего и не поймет. Тому же, кто знает про магические методы ваша статья тем более ничего не даст.

складывание большого количества строк по средствам цикла не очень хорошая идея.

Насколько мне известно CPython сам оптимизирует этот кейс.

code1 = """
from loremipsum import get_sentences
l = get_sentences(99999)
out = ''
for line in l:
    out += word
print(out)
"""
%timeit code1
# 11.9 ns ± 0.263 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

code2 = """
from loremipsum import get_sentences
l = get_sentences(99999)
out = ''.join(l)
print(out)
"""
%timeit code2
# 11.8 ns ± 0.0677 ns per loop (mean ± std. dev. of 7 runs, 100,000,000 loops each)

Извиняюсь, этого момента не знал, спасибо, что поправили. Хотя лично я предпочитаю не доверять интерпретаторам и компиляторам на 100%, так что знать про то как оно работает внутри в любом случае полезно. Ну и плюс интересно, как себя поведет cpython при более комплексной логике построения строки. В любом случае, в одной это ветке больше глубины погружения, чем во всей статье выше.

Погружение было бы еще глубже, если бы была упомянута роль NotImplemented в этих функциях.

Чтобы на этой странице было побольше полезно, то приведу два примера как быстро считать числа Фибоначчи при помощи матриц 2×2 и чисел вида a+b√5.
Мы реализуем класс матриц с операцией умножения и возведения в степень.
И класс чисел вида a+b√5 с основной арифметикой.

По формуле Бине $$F_n = \dfrac{ \frac{1+\sqrt5}{2}^n - \frac{1-\sqrt5}{2}^n }{\sqrt{5}}$$
А с матрицами Фибоначчи лезет при возведении матрицы (1 1) (1 0) в степень. А в степень можно возводить быстро.

class Mat2x2:
    __slots__ = ['a', 'b', 'c', 'd']

    def __init__(self, a, b, c, d):
        self.a = a
        self.b = b
        self.c = c
        self.d = d

    def __matmul__(x, y):
        ans = x.copy()
        ans @= y
        return ans

    def __imatmul__(x, y):
        x.a, x.b, x.c, x.d = x.a * y.a + x.b * y.c, x.a * y.b + x.b * y.d, x.c * y.a + x.d * y.c, x.c * y.b + x.d * y.d
        return x

    def __pow__(self, exp):
        cur = Mat2x2(1, 0, 0, 1)
        base = self.copy()
        while exp:
            if exp & 1:
                exp -= 1
                cur @= base
            else:
                exp >>= 1
                base @= base
        return cur

    def copy(self):
        return Mat2x2(self.a, self.b, self.c, self.d)

    def __repr__(self):
        return f'{self.__class__.__name__}({self.a}, {self.b}, {self.c}, {self.d})'


class R5:
    def __init__(self, a=0, b=0):
        self.a = a
        self.b = b

    def __repr__(self):
        return f'R5({self.a}, {self.b})'

    def __str__(self):
        if self.a and self.b:
            return f'({self.a}{self.b:+}√5)'
        elif self.b:
            return f'{self.b}√5'
        else:
            return f'{self.a}'

    def __add__(x, y):
        return R5(x.a + y.a, x.b + y.b)

    def __sub__(x, y):
        return R5(x.a - y.a, x.b - y.b)

    def __mul__(x, y):
        return R5(x.a * y.a + x.b * y.b * 5, x.a * y.b + x.b * y.a)

    def __pow__(x, power):
        if power == 0:
            return R5(1, 0)
        elif power % 2 == 1:
            return x * (x ** (power - 1))
        else:
            sq = x ** (power // 2)
            return sq * sq

    def __floordiv__(x, n):
        return R5(x.a // n, x.b // n)


def fib_stupid(n):
    c, p = 0, 1
    for _ in range(n):
        c, p = c + p, c
    return c


def fib_bine(n):
    return (R5(1, 1) ** n - R5(1, -1) ** n).b // 2 ** n


def fib_matrix(n, *, fib_mat=Mat2x2(0, 1, 1, 1)):
    if n == 0:
        return 0
    fib_pow = fib_mat ** (n - 1)
    return fib_pow.d


# Проверка корректности
for i in range(0, 100):
    assert fib_bine(i) == fib_stupid(i) == fib_matrix(i)

print(fib_matrix(100))

Если "глубокое" погружение то и про __iadd__ пару слов бы сказали. И как такое может работать с неизменяемыми объектами.

Sign up to leave a comment.

Articles