Вступление
Когда программист использует несколько языков, он начинает видеть преимущества и компромиссы одних языков по сравнению с другими. К числу моих любимых языков программирования, на которых я пишу, относится Julia. Он отличается множеством превосходных функций, которых мне иногда не хватает при использовании других языков.
Поскольку я занимаюсь в основном наукой о данных, то часто пользуюсь еще одним популярным языком программирования — Python. Этот язык мне тоже нравится: в нем много замечательных возможностей. Некоторые из них, безусловно, могли бы вдохновить на совершенствование кода Julia. Однако в этой статье я хотел бы поговорить о выигрышных особенностях Julia, которые, как мне кажется, стоило бы позаимствовать языку Python. Одни из них, безусловно, улучшили бы Python, другие — просто мне нравятся, хотя не касаются напрямую направлений развития Python. К статье прилагаю свой блокнот с крутыми фичами, которые я использую в коде Julia и которые мне хотелось бы видеть в Python.
#1: Надежная система типов
Надежная система типов — это то, что наверняка отметит любой программист в Julia. Лично меня Julia не перестает впечатлять своей строгой типизацией. В этом плане Python практически не уступает Julia, хотя несколько проигрывает с точки зрения преобразования типов. Тем не менее, я считаю, что иерархия типов и базовые типы данных в Julia намного лучше проработаны, чем в Python.
Это не значит, что система типов Python не является надежной, но, безусловно, некоторые улучшения ему не помешали бы. Особенно это касается наследования числовых и итерируемых типов. Например, строка должна быть подтипом массива, потому что строка, по сути, просто массив символов. Конечно, это только один конкретный случай, но есть множество других настроек, которые, на мой взгляд, могли бы улучшить систему типов в Python. В Julia есть абстрактная типизация, которая помогает создавать иерархии типов. В Python абстрактные типы используются очень редко, из-за чего в каждом подклассе происходит обратная субинициализация.
#2: MethodError
Отсутствие MethodError (функции “ошибка метода”) — это то, что мне очень не нравится в Python. Чтобы объяснить, почему Python нуждается в MethodError и как отсутствие этой функции сбивает с толку в текущей итерации, приведу конкретный пример. Напишем одну и ту же функцию сначала на Julia, затем на Python.
function double(x::Int64)
x * 2
end
def double(x : int):
return(x * 2)
На Julia не получится передать через этот метод никакого значения, кроме целого числа. Это означает, что, к примеру, строка вернет Method Error.
double("Hello")
MethodError: no method matching double(::String)
Closest candidates are:
double(::Int64) at In[1]:1
Stacktrace:
[1] top-level scope
@ In[3]:1
[2] eval
@ ./boot.jl:360 [inlined]
[3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1116
Теперь попробуем сделать то же самое в моем Python REPL:
>>> double("Hello")
'HelloHello'
Будет ли это ожидаемым поведением функции double
? Конечно, никто не подумает, что она относится к строке, однако примем во внимание, что используемый нами тип нельзя применить с оператором *
.
d = {"H" : [5, 10, 15]}
>>> double(d)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in double
TypeError: unsupported operand type(s) for *: 'dict' and 'int'
>>>
Представим, что я не писал эту функцию, а импортировал какое-то программное обеспечение и теперь столкнулся с ошибкой. Несмотря на указание, что эта функция предназначена для работы исключительно с данным типом в качестве аргумента, возникает проблема: придется обращаться к документации или гуглить TypeError, чтобы понять, откуда взялась эта ошибка. Даже программируя 1-строчную функцию, мы знаем, что нет способа выяснить, где именно в функции возникает ошибка. Сравните с выводом Method Error в Julia:
MethodError: no method matching double(::String)
Closest candidates are:
double(::Int64) at In[1]:1
Stacktrace:
[1] top-level scope
@ In[3]:1
[2] eval
@ ./boot.jl:360 [inlined]
[3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
@ Base ./loading.jl:1116
Первое, что мы получаем: функция double
не принимает строку в качестве аргумента. Затем следует исправление — double
нужно использовать с целым числом, как мы и указали. Мне кажется, что для специфицирования типов параметров недостаточно метаданных документации. Я считаю, что гораздо труднее диагностировать проблему, связанную с указанием неподходящего метода. Даже если вы неверно используете метод в Julia, вывод может дать вам адекватные подсказки в отношении выбора правильного метода во многих случаях. Вот пример с использованием метода insert!()
:
insert!(5)
MethodError: no method matching insert!(::Int64)
Closest candidates are:
insert!(::Vector{T}, ::Integer, ::Any) where T at array.jl:1317
insert!(::BitVector, ::Integer, ::Any) at bitarray.jl:887
Первая строка списка ближайших претендентов сообщает все, что нужно знать об этом методе. В функциональном стиле модифицирующий тип — абстрактный вектор — всегда идет первым, затем целое число, затем любое другое. Поскольку мы можем предположить, что время значения в векторе может быть практически любым, то наверняка это ::Any
(Любое значение). Учитывая, что индексы массива всегда являются целыми числами, это, скорее всего, целое число.
#3: Итерируемые операторы
Работа с базовыми математическими операциями над итерируемыми объектами в Python значительно сложнее. В большинстве случаев для этого требуется импортировать NumPy. Однако, как и язык R, Julia поддерживает поэлементные операторы, которые обеспечивают:
- простой способ выполнения арифметических операций как с одномерными, так и с многомерными массивами;
- быстрое определение того, что арифметические вычисления выполняются над двумя итерируемыми объектами.
И то, и другое — важные вещи, поэтому погружаться в цикл каждый раз, когда нужно выполнить эту арифметику, конечно, не самое лучшее решение. Хотя я знаю, что существуют функции для поэлементного умножения, операторы, на мой взгляд, более эффективно справляются с этой задачей.
#4: Множественная диспетчеризация (конечно)
Для ясности замечу: я не утверждаю, что каждый язык программирования нуждается в множественной диспетчеризации. Однако мой опыт программирования на Julia показал: множественная диспетчеризация — это то, от чего я никогда не отказался бы в данном языке. Это просто гибкий способ программирования, очень удобный, и это то, что мне в нем действительно нравится. Он позволяет устанавливать более прочные отношения между типами и методами с помощью параметрического полиморфизма.
В конце концов, что делает язык программирования уникальным? Конечно же, его парадигмы и способы взаимодействия методов с языковыми единицами. Безусловно, у Python в этом плане есть преимущества. Он может похвастаться как своими классами, так и функциями наследования. Думаю, что объектно-ориентированное программирование — это отличный способ создать прочные связи между типами и соответствующими функциями. При этом Python вовсе не является чисто объектно-ориентированным языком программирования. Если бы это было так, вряд ли он приобрел бы такую популярность в науке о данных.
Однако связь метода с типом в Python носит преимущественно декларативный характер: методы определяются вне области видимости класса, что создает довольно большой разрыв между типами и их функциями. Это можно объяснить отсутствием функции MethodError, о чем шла речь ранее. Единственное, что связывает тип с функцией, — это арифметика внутри функции и, возможно, приведение типов аргументов. Однако, как уже говорилось, преобразование аргументов не делает ничего, кроме как служит документацией для кода.
#5: Расширение базы
Еще одна возможность, которая меня восхищает в Julia, — это расширение методов базового языка. Многие функции базового языка Julia используются во всей экосистеме. Хотя это создает немало проблем при документировании программного обеспечения — иногда приходится документировать каждый образец вызова базовых функций, которых обычно бывает уйма — многие вещи, как мне кажется, упрощаются.
Например, мне надо отфильтровать Data Frame. База Julia поставляется с функцией filter!()
, которая используется в массивах и других базовых структурах данных для удаления определенных значений. Чтобы сделать это в DataFrame, нужно выполнить, по сути, тот же самый вызов — и все это благодаря возможности расширения функций в Julia. Я считаю это замечательной особенностью Julia, создающей большую согласованность между пакетами.
Тем не менее, подобные вещи, по крайней мере, частично зависят от множественной диспетчеризации. Рассмотрим пример использования операторов Python. Мы без проблем можем сложить два целых числа:
5 + 5
Но мы также можем складывать и другие числа, такие как булевы и плавающие:
5.5 + False
Мало того, мы можем использовать операторы со строками. Это означает, что данный метод применим ко всем типам — так же, как и множественная диспетчеризация. Возможно, с моей стороны не совсем корректно предполагать, что эти операторы действуют так же, как методы. Но мы же видим здесь реализацию, которая показывает, что подобный полиморфизм в некотором роде уже существует в Python.
Заключение
Мне нравятся и Python, и Julia, хотя возможности последнего кажутся более привлекательными. Тем не менее, оба языка могли бы стать лучше, позаимствовав кое-что друг у друга. Вряд ли большинство возможностей Julia найдут применение в Python, хотя одна функция — MethodError — ему точно не помешала бы.
Безусловно, можно предъявить немало претензий и к Julia (способам обработки типизации и множественной диспетчеризации), и к Python (к той же обработке типов). Нет предела совершенству.
Читайте также:
- Стоит ли учить Julia?
- Функциональные возможности систем типов Julia и Rust
- Сможет ли Julia занять место рядом с Python
Читайте нас в Telegram, VK и Дзен
Перевод статьи Emmett Boudreau: 5 Spectacular Features From Julia I Wish Were In Python