Новые фичи в Python 3.9

Автор оригинала: James Briggs
  • Перевод
Обзор лучших функций, включенных в последнюю итерацию Python.

image

Пришло время, выход новой версии Python неизбежен. Сейчас она в бета-версии (3.9.0b3), но скоро мы увидим полную версию Python 3.9.

Некоторые из новейших функций невероятно интересные, и будет восхитительно видеть их использование после релиза. Мы рассмотрим следующее:

  • Операторы объединения словарей
  • Тайп хинтинг
  • Два новых строковых метода
  • Новый Python Parser — это очень круто

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

Объединение словарей


Одна из новых и уже моих любимых фич с синтаксисом. Если у нас есть два словаря a и b, которые нам нужно объединить, мы теперь используем операторы объединения.

У нас есть оператор слияния “|”:

a = {1: 'a', 2: 'b', 3: 'c'}
b = {4: 'd', 5: 'e'}
c = a | b
print(c)

[Out]: {1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e'}

И оператор обновления “|=”, который обновляет исходный словарь:

a = {1: 'a', 2: 'b', 3: 'c'}
b = {4: 'd', 5: 'e'}
a |= b
print(a)

[Out]: {1: 'a', 2: 'b', 3: 'c', 4: 'd', 5: 'e'}

Если наши словари имеют общий ключ, будет использована пара ключ-значение из второго словаря:

a = {1: 'a', 2: 'b', 3: 'c', 6: 'in both'}
b = {4: 'd', 5: 'e', 6: 'but different'}
print(a | b)

[Out]: {1: 'a', 2: 'b', 3: 'c', 6: 'but different', 4: 'd', 5: 'e'}

Обновление словаря с помощью итераций


Еще одно интересное поведение оператора “|=” — возможность обновлять словарь новыми парами ключ-значение, используя итеративный объект — например, список или генератор:

a = {'a': 'one', 'b': 'two'}
b = ((i, i**2) for i in range(3))
a |= b
print(a)

[Out]: {'a': 'one', 'b': 'two', 0: 0, 1: 1, 2: 4}

Если мы попробуем повторить то же самое со стандартным оператором объединения “|” мы получим TypeError, поскольку он будет разрешать только объединения между типами dict.

image

Тайп хинтинг


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

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

image

В нашей функции add_int мы явно хотим сложить два одинаковых числа(по какой-то загадочной неопределенной причине). Но наш редактор этого не знает, и совершенно нормально соединять две строки используя “+”, поэтому никакого предупреждения мы не увидим.

Теперь мы можем указать ожидаемый тип как int. Используя это, наш редактор сразу обнаруживает проблему.

Мы также можем получить сведения об ожидаемых типах, например:

image

Тайп хинтинг может использоваться везде — и благодаря новому синтаксису он теперь выглядит намного чище:

image

Строковые методы


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

"Hello world".removeprefix("He")

[Out]: "llo world"

"Hello world".removesuffix("ld")

[Out]: "Hello wor"

Новый парсер


Это скорее скрытое изменение, но оно может стать одним из наиболее значительных изменений для будущего развития Python.

В настоящее время Python использует грамматику, основанную преимущественно на LL(1), которая, в свою очередь, может быть проанализирована синтаксическим анализатором LL(1), который анализирует код сверху вниз, слева направо, с возможностью просмотра только одного токена.

Я почти не представляю, как это работает, но я могу рассказать вам про несколько актуальных проблем в Python из-за использования этого метода:

  • Python содержит грамматику non-LL(1); из-за этого некоторые части текущей грамматики используют обходные пути, создавая ненужную сложность.
  • LL(1) создает ограничения в синтаксисе Python (без возможных обходных путей). Эта проблема подчеркивает, что следующий код просто не может быть реализован с использованием текущего синтаксического анализатора (вызывает ошибку SyntaxError):

    with (open("a_really_long_foo") as foo,
          open("a_really_long_bar") as bar):
        pass
    

  • Из-за левой рекурсии (порядка выполнения слева направо) некоторые функции при парсинге могут сломать парсер и загнать его в бесконечную рекурсию. То есть конкретный рекурсивный синтаксис может вызвать бесконечный цикл в дереве синтаксического анализа, Гвидо ван Россум, создатель Python, объясняет это здесь.

Все эти факторы (и многие другие, которые я просто не могу понять) оказывают одно большое влияние на Python; они ограничивают развитие языка.

Новый синтаксический анализатор, основанный на PEG, даст разработчикам Python значительно больше гибкости — и мы начнем это замечать начиная с Python 3.10.

Это то, что мы увидим в будущем Python 3.9. Если вы нетерпеливы, самая последняя бета-версия — 3.9.0b3 — доступна здесь.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя платные онлайн-курсы SkillFactory:



Читать еще


SkillFactory
Школа Computer Science. Скидка 10% по коду HABR

Комментарии 36

    +3
    Что-то такое ощущение, что Пайтон как язык уже давно законсервировался и особо не развивается, по сравнению с остальными.
      0
      Сейчас Python очень активно развивается, большей частью в сторону решения своих главных проблем с производительностью и GIL. Например, уже сейчас стало очень просто расставить типизацию в ключевых местах и получить компилированый код, свободный от GIL:

      from numba import int32, deferred_type, optional
      from numba.experimental import jitclass
      
      node_type = deferred_type()
      
      spec = OrderedDict()
      spec['data'] = int32
      spec['next'] = optional(node_type)
      
      @jitclass(spec)
      class LinkedNode(object):
          def __init__(self, data, next):
              self.data = data
              self.next = next
      
          def prepend(self, data):
              return LinkedNode(data, self)
      
      node_type.define(LinkedNode.class_type.instance_type)
      

      С учетом активно развивающейся концепции аннотации типов и усовершенствованием парсера синтаксиса, мы скором получим динамический язык, который путем расстановки типов в ключевых местах (и их автоматического вывода) можно ускорить практически до скорости языка C.
        +10

        А можно подробнее, как частичная типизация помогает убрать лок в интерпретаторе?

          +23
          Допустим у нас в python интерпретаторе есть python функция, для которой мы знаем
          1. Типы и структуру входных и выходных параметров.
          2. Типы переменных в самой функции строго зависят от входных параметров или являются элементарными типами.

          Тогда для такой функции мы можем выйти из python интепретатора, освободить GIL, скомпилировать код в нэйтив для данной платформы, выполнить его и вернуться обратно в интепретатор. Самый удобный пакет сейчас для такого это numba, которая основана на LLVM.

          Самое главное, что вся эта внутренняя работа абослютно незаметна для программиста. Все что нужно, это просто расставить типы. Уже как несколько лет такой подход идеально работает для математических вычислений в python, основанных на numpy array. Сам numpy array представляет собой python обертку над линейными областями памяти, поэтому для таких данных код генерируется очень хороший.

          Для примера, рассмотрим следующую задачу.

          Постановка задачи
          Даны два набора из N точек в трехмерном пространстве. Необходимо посчитать матрицу попарных регуляризированных обратных расстояний. В нашем пример N = 5000.

          def main():
              """ For the given points set calculate RBF matrix
              base on regularized inverse distances.
              
              Input data:
              -----------
              
              p = [[p0_x, p0_y, p0_y],
                   [p0_x, p0_y, p0_y],
                   ...
                   [pN_x, pN_y, pN_y]]
              
              q = [[q0_x, q1_x, ..., qN_x],
                   [q0_y, q1_x, ..., qN_x],
                   [q0_z, q1_z, ..., qN_z]]
                   
              Output data:
              -----------
              
              R = [[f(p0, q0), f(p0, q1), ..., f(p0, qN)],
                   [f(p1, q0), f(p1, q1), ..., f(p1, qN)],
                   ...
                   [f(pN, q0), f(pN, q1), ..., f(pN, qN)]],
              where f(p, q) = 1 / (1 + |q - p|)
              """
          
              N = 5000
          
              p = np.random.rand(N, 3)
              q = np.random.rand(3, N)
          


          Чистый код на Python
          def get_R_py(p, q):
              R = np.empty((p.shape[0], q.shape[1]))
          
              for i in range(p.shape[0]):
                  for j in range(q.shape[1]):
                      rx = p[i, 0] - q[0, j]
                      ry = p[i, 1] - q[1, j]
                      rz = p[i, 2] - q[2, j]
                      R[i, j] = 1 / (1 + math.sqrt(rx * rx + ry * ry + rz * rz))
          
              return R
          


          Время работы: 100.479 c.

          Это очень долго. Теперь начинается магия!

          Последовательная реализация с аннотацией типов
          from numba import float64, jit
          
          @jit(float64[:, :](float64[:, :], float64[:, :]), nopython=True, nogil=True)
          def get_R_numba_sp(p, q):
              R = np.empty((p.shape[0], q.shape[1]))
          
              for i in range(p.shape[0]):
                  for j in range(q.shape[1]):
                      rx = p[i, 0] - q[0, j]
                      ry = p[i, 1] - q[1, j]
                      rz = p[i, 2] - q[2, j]
                      R[i, j] = 1 / (1 + math.sqrt(rx * rx + ry * ry + rz * rz))
          
              return R
          

          Время работы: 0.154 с, ускорее примерно в 700 раз. И все, что мы для этого сделали, по большому счету, только проставили типы.

          Параллельная реализация с аннотацией типов
          Ну ладно, а как на счет параллельности и GIL? Почти без изменений:

          @jit(float64[:, :](float64[:, :], float64[:, :]), nopython=True, nogil=True, parallel=True)
          def get_R_numba_mp(p, q):
              R = np.empty((p.shape[0], q.shape[1]))
          
              for i in prange(p.shape[0]):
                  for j in range(q.shape[1]):
                      rx = p[i, 0] - q[0, j]
                      ry = p[i, 1] - q[1, j]
                      rz = p[i, 2] - q[2, j]
          
                      R[i, j] = 1 / (1 + math.sqrt(rx * rx + ry * ry + rz * rz))
          
              return R
          

          Мы добавили а) parallel=True в аннотацию и б) во внешнем цикле range заменили на prange.

          Время работы: 0.47 с, ускорение по сравнения с последовательной версией примерно в 3.3 раза, что неплохо для моего четырехядерного лаптопа.

          И что дальше?
          Уже как несколько лет это работает идеально для математики и numpy array. Но разработчики numba пошли гораздо дальше. Очень важный момент, что такой же аннотацией типов код можно скомплировать и для GPU. Кроме того, стали поддерживаться почти все python типы, включая типизированные List, Dict, а также спецификации для классов, что я показывал выше. А в планах тесная интеграция с python type annotations.

          Сейчас репозиторий на GitHub очень активный, около 170 контрибьюторов. И мне нравится, как развивается этот проект.
            +6

            Так, вроде, трюки с компилированием в нэйтив для числодрлбилок еще во 2м питоне были. Да, интересно, что всё больше сахара появляется, но сам интерпретатор как лочился, так и лочится. Конечно, приятно, что меньше возни руками для перехода в скомпилированый код.


            Ну и в примере вы не просто и не столько типы проставили, а получили возможность в native скомпилировать, подключив numba, что, в принципе, типов могло и не требовать.
            Естественно, что jit выиграет у интерпретатора на вычислительной задаче — это уже лет 15 не новость. С гораздо меньшим удобством замену range на prange и руками можно было бы сделать, тут, бесспорно, авторы молодцы.
            Но, опять же, из самого питона GIL никуда не делся, вы просто «незаметно» вышли из него, но если вам понадобится что-то в интерпретируемом коде — вы снова залочитесь в одном потоке.

              +2
              С одной стороны вы правы. Под капотом все тоже компилирование в нейтив. Но, с точки зрения программиста, ситуация меняется. Если раньше нужно было писать брутальный C модуль или использовать cython с его несколько извращенным синтаксисом, то теперь с numba достаточно подключения библиотеки и аннотации типов. И формально программист не выходит за пределы логики python.

              И я вижу, как это дальше развивается. Numba тесно интегрируется с аннотацией типов python и поддерживает все больше и больше чистых python структур, а не только numpy array. Все те функции python, для которых возможно полное выведение типов, будут скомпилированы в native и свободны от GIL, при этом будут бесшовно сшиваться с python интерпретаторами.

              Это похоже на концепцию языка julia, где авторы создали динамический язык, способный к автоматическому выведению типов. Правда в julia они начали с того, что обрезали все те возможности языка, для которых такое выведение типов невозможно.

              Python идет примерно по той же дороге, выделяя то подмножество языка, для которого это возможно, и шаг за шагом расширяет это подмножество.
              +3
              Время работы: 0.154 с, ускорее примерно в 700 раз. И все, что мы для этого сделали, по большому счету, только проставили типы.
              Время работы: 0.47 с, ускорение по сравнения с последовательной версией примерно в 3.3 раза, что неплохо для моего четырехядерного лаптопа.

              Получается вроде бы наооборот замедление в 3 раза. или там 0,047?

                +2
                Да, там 0.047. Спасибо за правку!
                +1

                А почему эти аннотации не могут тайпхинты юзать? Выглядят они жутко, если честно.

                  +1
                  Пока не могут, так как эти аннотации пришли в python из необходимости существенно ускорить математические вычисления, основанные на numpy array. Не все типы python пока еще поддерживаются. Да и тайпхинты пока все еще в процессе развития.

                  Но в ближайших планах как раз и перейти на тайпхинты с поддержкой все большего количество чистых пайтоновских структур данных.
                  0
                  Я за последними разработками в нумбе не следил давно, поэтому такой вопрос: уже можно, или планируется ли, чтобы такого рода код (ниже) тоже мог быстро работать? Пусть даже с аннотациями, но чтобы по возможности не сильно потерять в выразительности. И уж точно чтобы не хранить все данные как простые массивы чисел.
                  class Point:
                      ... x, y ...
                      def dist_to(self, p): ...
                  
                  class Rect:
                      ... p1, p2 # of type Point ...
                  
                  def process(rects): # n-dim array of Rects
                      for r in rects:
                          ...
                          r.p1.dist_to(r.p2)
                          ...
                  

                  Бонусом: чтобы при этом можно было (почти) не меняя кода принимать не array-of-structs, а struct-of-arrays для производительности.
                    +1
                    Я бы попробовал это реализовать двумя путями.

                    Если оставаться чисто в парадигме вложенных классов, я бы это сделал через @jitclass, как в jitclass.py и в binarytree.py.

                    Другой вариант вместо python классов использовать Numpy Strutured Array и их поддержку в Numba, structures.py.
                0
                Т.е. аннотации в numba пока и нет? Поэтому вместо «self.data: int32» приходится писать перед классом дополнительные «подсказки»?
              +5
              Добавили бы coll.map(f) заместо map(f, coll)
                +14
                Что-то очень странное написано автором про тайпхинты. В примерах с int — это и до 3.9 прекрасно работало. А вот про то, что в 3.9 можно будет использовать встроенные типы для аннотации словарей, списков и пр. вместо импорта из typing почему-то ни слова, хотя пример есть.
                  +4
                  Возможно, автор оригинальной статьи только недавно узнал про тайпинги и решил, что это новая фича.
                  Дело в том, что это перевод статьи с Towards Data Science. У них есть неплохие статьи про Data Science, но статьи по питону у них обычно откровенно слабые.
                  Оно и понятно. Датасаентисты обычно питон не очень хорошо знают. Им это на самом деле не нужно.
                  +10
                  Тайп хинтинг

                  По Вашему описанию непонятно. Тайп хинтинг был начиная с 3.5 (как указано в статье), но его синтаксис был был таким же (в 3.6 добавились хинты переменных). Как я понял отсюда (What's new in Python 3.9) добавлены built-in generic types: PEP 585.


                  Т.е. если раньше для описания типа словаря нам нужно было использовать Dict из модуля typing (напр. Dict[str, int]), то теперь можно использовать обычный dict (напр. dict[str, int]).


                  А если сравнивать два скриншота, приведенные вами:


                  Заголовок спойлера


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

                  Во втором примере у Вас менее строгие ограничения на тип, вы не указали аргументы Generic'а. И можно будет любой dict в функцию передать. Что, в прочем, можно было сделать и раньше питона 3.9.


                  P.S. Я всегда буду обновлять комментарии перед отправкой, я буду всегда буду...

                    +1
                    Эта проблема подчеркивает, что следующий код просто не может быть реализован с использованием текущего синтаксического анализатора (вызывает ошибку SyntaxError)

                    Вот как раз это никогда не было большой проблемой, просто токенайзер для "with" кривой, и простейщий lookahead для скобки как в тех же выражениях (и кортежах) был бы достаточен.
                    К чему они привязали issue12782 к PEG-based parser мне неведомо, ибо тот lookahead без необходимости backslash-ить существует с версий 2.5 или 2.6 если мне не изменяет память.


                    Сравните:


                    -with (open("a_really_long_foo") as foo,
                    -      open("a_really_long_bar") as bar):
                    +if   (open("a_really_long_foo") == foo,
                    +     open("a_really_long_bar") == bar):
                         pass

                    Т.е. алгоритм простой и в случае with и простейшим lookahead (открытая скобка) должен включаться "режим" auto-escape для NL, пока не найдена парная закрывающая скобка, точно также как это уже делается в доброй сотне других случаев.


                    Почему надо было ждать 3.9 чтобы это пофиксить я кроме как ленью долгоиграющей стратегией или идеей фикс "пересадить всех на новые версии" объяснить не могу.
                    Здесь оно и не сильно надо как бы (многострочный with можно тупо пробэкслэшить), но проблема в том, что они вообщем часто забывают, что многие системы либо еще очень долго, либо совсем не обновятся до 3.9… Т.е. абсолютно всё (даже подобные простейшие вещи) можно использовать только в проектах под >= 3.9.

                      +1

                      Нафига removeprefix если есть re.sub?

                        0
                        сахар?
                        регулярки сложные по своей сути
                          0

                          Я люблю сахар, но тут явно проще использовать re, чем искать/вспоминать, есть ли такой специфический метод.


                          Так что странноватые нововведения. Неужели больше нет сахарных идей?

                            0

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

                          +1

                          Написать, скомпилировать, запустить регулярное выражение или сделать все в нативном коде?


                          Второе явно быстрее и читаемее.

                          –4

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


                          Развитие — это процесс, при котором идёт в большей степени улучшение. Пока что у нас развитие, но не потеряется ли первоначальный смысл языка в погоне за скоростью и возможностью удовлетворить всех и вся?

                            +4
                            Ну так олдфаги могут и на актуальной версии питона писать в стиле 2.х. Хочется тебе попроще, не используй новые фишки.
                            Но что плохого в ускорении? Понадобилось мне в django проекте много математики, я за пару часов перевел проект на python 3, нагуглил про numba и время выполнения ресурсоемкой функции у меня упало с 10 секунд до 0.1 секунд.
                              0

                              Ничего нет плохого в ускорении, пока оно не идёт во вред другим плюсам языка, например понятности. Это извечный спор о том, оправдывает ли цель средства. Или то, что лучшее враг хорошего. У нас огромное количество примеров в любой сфере нашей жизни, что как только прекращается позитивное развитие, начинается деградация. C++ долгое время активно развивался, добавляя конструкции, позволяющие писать всё и вся. Тем не менее всё чаще возникают темы касательно того, а не пора ли его чистить от переизбытка хлама и делать более дружелюбным. Бешеная скорость и обширные возможности совсем не делают людей счастливыми. Вот я и боюсь, не превратят ли python в подобие C++. Мои опасения совсем не означают, что я олдфаг и противник всего нового. Python 3 во многих вещах мне нравится больше 2-го. Просто не надо делать из питона язык на все случаи жизни. У нас таких полно, на любой вкус.

                            +3
                            Со словарями, как мне кажется, неоднозначно. Оператор слияния выглядит полезным. Единственное почему не "+"? Наверняка есть объяснения.
                            А вот "=| " мне вообще не понятно зачем вводить. Есть же a.update(b) — как по мне более явный синтаксис
                              +2

                              Слава Си и Перла не даёт покоя, наверное.

                                +1

                                Видимо, по аналогии с множествами. Для них как раз оператор |= переопределён для вливания второго множества в первое.

                                  +4
                                  почитал соответствующий pep. И ещё больше убедился что это очень спорное и не особо нужное нововведение, которое вводит больше неоднозначности.
                                  оператор "+" тоже обсуждался и даже был в первом варианте, но многие были против него из за отсутствия коммутативности у объеденения словарей (т.е d1 + d2 ≠ d2 + d1). Якобы это будет смущать программистов. А вот различное действие оператора "|" в зависимости от типа операндов ни кого смущать не будет. К тому же для списков свойство коммутативности сложения тоже не соблюдается и ничего — пользуемся.
                                  В общем линтер похоже пополнится новыми правилами.
                                    +2

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

                                      +1
                                      Ну так всё равно при сложении списков и строк не соблюдается правило коммутативности сложения(от перестановки слагаемых значение не должно меняться). И, если уж мы научились использовать "+" для сложения и конкатации, думаю могли бы научиться и словари объединять с его помощью, помня что это не совсем сложение.
                                      Но вообще я думаю что никакой оператор вводить не надо было. Есть и так куча способов объеденения словарей. Самый явный из них, на мой взгляд, это {**d1, **d2}.
                                        +1

                                        Да не, все верно — тут не только про коммутативность, тут про потерю данных. Ведь выражение вида a | b для boolean при константном a=true теряет данные в b (по-научному это называется «фиктивная переменная»). То же и со словарями. А «плюс» никогда данные не теряет. Так что все логично.

                                  0

                                  По поводу объединения словарей: я давно использую собственную функцию для этих целей, а именно


                                  def merge_dicts(d1, d2, merge_value_func=None):
                                      d_result = d1.copy()
                                  
                                      for key, value in d2.items():
                                          if key in d1:
                                              if merge_value_func is None:
                                                  d_result[key] = value
                                              else:
                                                  d_result[key] = merge_value_func(d_result[key], value)
                                          else:
                                              d_result[key] = value
                                  
                                      return d_result

                                  Здесь имеется дополнительный параметр merge_value_func. Он позволяет скомбинировать в новом словаре значения для одних и тех же ключей в исходных словарях, например:


                                  d1 = { 1 : 'a' }
                                  d2 = { 1 : 'b' }
                                  d = merge_dicts(d1, d2, lambda s1, s2: s1 + s2)

                                  Результат:


                                  {1: 'ab'}
                                    0

                                    в чём новость тайп хинтинга?

                                      0

                                      Спасибо за обзор!
                                      "Обзор на лучших функций" — на лучшие функции.
                                      Изменения на 90% — сахар. Тайп хинтинг точно так же юзаем в 3.7, если честно, не совсем понял, что изменилось

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое