Каждый программист, использующий Python, наверняка сталкивался с операторами, такими как сложение (+) и умножение (*). Но что происходит "под капотом" этих операторов? В этой статье мы погрузимся в детали их работы на примере базовых типов, пользовательских классов и строк.

Специальные методы: Сердце операторов

В Python операторы реализованы с помощью "специальных методов". Эти методы вызываются автоматически при использовании операторов.

Для сложения:

  • __add__(self, other)

  • __radd__(self, other)

Для умножения:

  • __mul__(self, other)

  • __rmul__(self, other)

Как Python складывает и умножает разные типы

Целые числа и числа с плавающей запятой:

При сложении int и float, например, 5 (int) и 3.5 (float), Python вызывает __add__ у объекта 5 с объектом 3.5 в качестве аргумента. Если сложение между этими типами невозможно напрямую, Python пытается использовать __radd__ у второго объекта.

Строки:

Строки в Python также поддерживают операторы + и *. С помощью + можно объединить две строки:

s1 = "Hello"
s2 = "World"
result = s1 + s2  # "HelloWorld"

А с помощью * можно "умножить" строку, повторив ее определенное количество раз:

s = "Hello"
result = s * 3  # "HelloHelloHello"

Производительность и реализация на C

За красивой и понятной высокоуровневой абстракцией Python скрывается низкоуровневая реализация на языке C. Большинство базовых операций, таких как сложение или умножение, оптимизированы и выполняются на "родном" уровне, что обеспечивает высокую скорость выполнения.

Когда вы используете базовые операторы с встроенными типами данных, такими как числа или строки, ваши операции выполняются на скорости, сравнимой со скоростью низкоуровневых языков программирования, таких как C.

Заключение

Python предлагает нам интуитивно понятные и мощные инструменты для работы с различными типами данных. Однако за этой простотой скрывается сложная архитектура, которая делает Python одновременно гибким и производительным. Благодаря низкоуровневой реализации на C, операции с данными в Python выполняются быстро, что делает этот язык отличным выбором для широкого круга задач.


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

Комментарии (24)


  1. fireSparrow
    09.10.2023 21:44
    +24

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

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


    1. vandriichuk Автор
      09.10.2023 21:44
      -8

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


      1. omaxx
        09.10.2023 21:44
        +18

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


        1. vandriichuk Автор
          09.10.2023 21:44
          -17

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


          1. alexeydg
            09.10.2023 21:44
            +12

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


          1. kuza2000
            09.10.2023 21:44
            +18

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

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

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

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


      1. kekoz
        09.10.2023 21:44
        +2

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

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


    1. Skykharkov
      09.10.2023 21:44
      +20

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

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


    1. taskevich
      09.10.2023 21:44

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


  1. DustCn
    09.10.2023 21:44
    +1

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

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


    1. lorc
      09.10.2023 21:44
      +12

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

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


      1. AWRDev
        09.10.2023 21:44
        +5

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


      1. tzlom
        09.10.2023 21:44

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


        1. lorc
          09.10.2023 21:44

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


      1. DustCn
        09.10.2023 21:44

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


  1. yeswell
    09.10.2023 21:44
    +5

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

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


  1. celen
    09.10.2023 21:44
    +1

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


  1. MihaTeam
    09.10.2023 21:44

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

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

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

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


    1. fenrir1121
      09.10.2023 21:44
      +1

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

      Насколько мне известно 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)


      1. MihaTeam
        09.10.2023 21:44

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


  1. Andrey_Solomatin
    09.10.2023 21:44

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


  1. myswordishatred
    09.10.2023 21:44

    >Глубокое погружение

    >2 мин

    У вас там всё в порядке?


  1. ShashkovS
    09.10.2023 21:44

    Чтобы на этой странице было побольше полезно, то приведу два примера как быстро считать числа Фибоначчи при помощи матриц 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))
    


  1. Irish_head
    09.10.2023 21:44

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