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

Неизбежно, начиная с языка программирования, мы все неизбежно столкнемся с проблемами в обучении. Эти сбои могут быть глупыми ошибками, поиском решения ошибки всю ночь или просто незнанием некоторого синтаксиса, который может значительно улучшить ваш код. Как Data Scientist, большая часть моего опыта связана с объектно-ориентированным или итеративным программированием, это, безусловно, имело место, когда я впервые познакомился с языком Julia около шести лет назад. Тем не менее, я многому научился, и образовательный опыт дал мне много ценной информации, которую я теперь счастлив иметь!

Еще одна замечательная вещь, которая приходит вместе с этим опытом на моем любимом языке, заключается в том, что я могу преодолевать такие же барьеры для входа и умного программирования, делясь этой информацией! Julia — удивительный язык программирования, особенно когда речь идет о науке о данных! Тем не менее, его определенно стоит изучить, поэтому сегодня я хотел бы, чтобы ваше путешествие прошло немного гладко, продемонстрировав некоторые из моих любимых функций, которые я хотел бы твердо удержать при входе в язык и в моем скромном начале с язык.

удивительный синтаксис понимания

Как программисты, мы, вероятно, знаем об общих отношениях любви и ненависти, которые мы часто разделяем с циклами for. Хотя циклы for — отличный способ сделать работу легко читаемой и относительно эффективной, они могут стать серьезным препятствием для производительности, особенно в однопоточных языках программирования. Есть несколько различных способов избежать этого, некоторые примеры включают функцию map как в Julia, так и в Python и лямбда-выражение в Python/анонимные функции в Julia.

Недостатком обоих этих методов является то, что они часто могут запутать синтаксис. Более того, начинающие программисты могут не сразу понять подобные синтаксические уловки. Во многих отношениях, когда мы выбираем эти методы, у нас есть большой потенциал пострадать в метрике удобочитаемости. В то время как в других языках часто есть довольно крутые понимания, которые могут помочь с такими проблемами, понимания в Джулии находятся на совершенно другом уровне. С Julia мы можем очень легко создавать интерпретируемые многострочные включения. Существует множество различных причин для использования многострочного понимания, а не традиционного цикла for.

Во-первых, понимание происходит быстрее — не просто немного быстрее, а намного быстрее. Во-вторых, включения обеспечивают возврат, то есть больше не нужно создавать структуру вне цикла, а затем добавлять к ней в каждом цикле. Самым большим недостатком понятий в большинстве случаев является их удобочитаемость. Однако, поскольку синтаксис понимания Джулии работает с begin и end , мы можем очень легко написать понимание, которое так же удобочитаемо, как и традиционный цикл for.

mycol = []
for x in 1:5
  f = y -> y += 2
  push!(mycol, f(x))
end
mycol = [begin
              f = y -> y += 2
              f(x)::Int64
         end for x in 1:5]

анонимные функции

Еще одна вещь, в которой я хотел бы разобраться, начиная с языка программирования Julia, — это анонимные функции. Анонимные функции — это то же, что лямбда для Python для Джулии, и они создаются с использованием логического оператора «право» ->. Есть несколько разных причин, о которых было бы здорово узнать сразу. Во-первых, анонимные функции используются повсеместно в документации Julia, что, безусловно, довольно запутанно, если вы понятия не имеете, что делает этот оператор. Во-вторых, пригодятся анонимные функции… как всегда. Они также могут помочь сэкономить ресурсы и, как правило, ускорить работу, так как не нужно хранить тип функции, а только ее аргументы.

Их действительно легко создать. В левой части оператора мы предоставляем аргументы для нашей функции. Это может быть либо один аргумент, либо кортеж, и мы также можем аннотировать его. С правой стороны мы предоставляем саму функцию — это действительно относительно просто.

julia> f = x::Int64 -> x + 1
#3 (generic function with 1 method)

julia> f(5)
6


julia> f = (x::Int64, y::Int64) -> x + y
#1 (generic function with 1 method)

julia> f(5, 5)
10

Кроме того, мы также можем использовать синтаксис begin end в этом контексте:

julia> f = x::Int64 -> begin
           x + 1
       end
#5 (generic function with 1 method)

julia> f(5)
6

перенаправление стандартного ввода-вывода

Одна вещь, о которой я действительно узнал недавно, это то, насколько легко можно перенаправить стандартный ввод, вывод и ошибку Джулии во время оценки определенного фрагмента кода или приложения. Это делается с помощью redirect_stdout, redirect_stderr, redirect_stdin и redirect_stdio, которые можно использовать для одновременного перенаправления нескольких аргументов с ключевыми словами. Это может быть невероятно удобно, так много разных примеров можно придумать; redirect_stderr можно использовать для выдачи стандартного вывода ошибок в файл журнала, а не просто выгружать их в терминал, redirect_stdin можно использовать для захвата и записи ввода — даже, например, в HTTP.Stream .

Существенным недостатком этого — по крайней мере, насколько я его использовал — является то, что типы, с которыми это возможно, весьма ограничены. В частности, не похоже, что вы можете сделать это с чем-либо, что находится в памяти. Это настоящий облом, потому что это означает, что вы не можете, например, запустить функцию и записать выходные данные stdout в IOBuffer, чтобы поместить куда-то еще — по крайней мере, не используя этот метод и эту технику. В конкретном приложении, для которого я изначально интересовался этим, это то, что мне было нужно — так что это было большим разочарованием, однако это, конечно, не умаляет широкого диапазона возможностей, когда дело доходит до написания этих различных типов ввода. и вывод в файлы или логи!

параметры подтипа

Если есть что-то, чего мне действительно не хватало в Julia в течение первых двух лет использования языка, так это возможности подтипа параметров. Этот заголовок может немного ввести в заблуждение, так как может показаться, что я говорю о подтипировании параметров конструкторов, но на самом деле я имею в виду методы. В Julia вы, конечно, можете указать параметры определенного типа, чтобы создать разные методы для одного и того же типа с разными параметрами (который становится другим типом из-за параметров)… Например, эта функция работает только специально для Vector с Int64 в качестве параметра. :

julia> myfunc(v::Vector{Int64}) = begin

       end
myfunc (generic function with 1 method)

julia> myfunc(["hello"])
ERROR: MethodError: no method matching myfunc(::Vector{String})
Closest candidates are:
  myfunc(::Vector{Int64}) at REPL[6]:1
Stacktrace:

Однако мы также можем использовать полиморфизм, чтобы применить это к определенным типам векторов на основе подтипов того, что содержится в параметре. Например, Vector{<:Number} :

julia> myfunc(v::Vector{<:Number}) = begin

       end
myfunc (generic function with 2 methods)

julia> myfunc([5.5, 4.4])

julia> myfunc([5.5 + 2.2im])

julia> myfunc([true])

julia>

Должно быть легко увидеть, где это пригодится, но я все же предоставлю пример использования. Скажем, мы пытаемся построить разные векторы, и кто-то предоставляет Vector{String} в качестве X. Очевидно, что мы собираемся обрабатывать это совершенно иначе, чем числовое пространство. А если бы они предоставили Vector{Date} ? Ну, мы можем невероятно легко облегчить все эти несоответствия типов, несмотря на то, что эти типы являются типами элементов — и мы также можем обозначать такие вещи подтипом, поэтому нам нужно написать как можно меньше методов…

Я люблю Юлию…

наборы

Это, вероятно, немного более очевидно для тех, кто раньше использовал языки высокого уровня; наборы — это тип данных, который содержит уникальные значения данного итерируемого объекта. Конечно, когда я переходил на Julia, я был хорошо знаком с множествами как с математической точки зрения, так и с типами данных, однако я помещаю их в этот список, потому что с моей работой в Julia я действительно начал использовать такие множества намного больше. Таким образом, я просто хотел бы поделиться тем, как легко создать набор внутри Джулии, а затем рассказать о некоторых вариантах использования наборов. Чтобы создать Set, мы просто применяем Set к любому Vector .

julia> Set(["hi", "hi", "hello"])
Set{String} with 2 elements:
  "hi"
  "hello"

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

julia> s = ["hi", "hello", "hi"]
3-element Vector{String}:
 "hi"
 "hello"
 "hi"

julia> length(Set(s)) == length(s)
false

DataFrame -> Диктовка

DataFrames — отличная структура данных, пакет DataFrames.jl предоставляет довольно надежную инфраструктуру для работы с табличными данными в памяти в Julia. Однако одна довольно распространенная вещь, которую кто-то может захотеть сделать с DataFrame, также не имеет очевидной привязки, и это превращение DataFrame в Dict . Вы могли бы ожидать, что это будет простая сделка типа Dict(DataFrame), но на самом деле нам нужно вытащить каждый столбец в виде набора пар, чтобы достичь этой цели.

julia> df = DataFrame(:a => [5, 10, 15], :b => [5, 10, 15])
3×2 DataFrame
 Row │ a      b     
     │ Int64  Int64 
─────┼──────────────
   1 │     5      5
   2 │    10     10
   3 │    15     15

Это даже становится немного более запутанным; вызов eachcol вернет DataFrameColumn при нормальных обстоятельствах:

julia> eachcol(df)
3×2 DataFrameColumns
 Row │ a      b     
     │ Int64  Int64 
─────┼──────────────
   1 │     5      5
   2 │    10     10
   3 │    15     15

Однако, если бы мы использовали eachcol внутри понимания, вместо этого мы получили бы Vector из Vector :

julia> [col for col in eachcol(df)]
2-element Vector{Vector{Int64}}:
 [5, 10, 15]
 [5, 10, 15]

Однако, если мы вызовем метод pairs для нашего DataFrameColumn, то мы получим соответствующий возврат группы pairs, который мы можем, наконец, предоставить конструктору Dict, чтобы превратить наш DataFrame в Dict:

julia> Dict(pairs(eachcol(df)))
Dict{Symbol, AbstractVector} with 2 entries:
  :a => [5, 10, 15]
  :b => [5, 10, 15]

Это не так уж плохо, но определенно довольно легко увидеть, как это может быть немного сложно для тех, кто может быть не знаком с этими методами, поэтому я хотел поделиться этим!

метод самоанализа

Еще одна действительно крутая вещь в Джулии, которую на самом деле знает гораздо меньше людей, — это метод самоанализа. Интроспекцию метода можно использовать для получения информации о различных методах функции. Это открывает окно возможностей для всевозможных ловких трюков. Мы могли бы анализировать эти параметры внутри функции, вызывать каждую версию данной функции, а также все, что между ними. Мы также можем получить больше информации о различных функциях, в том числе о тех, которые предоставляются в качестве аргумента, что, безусловно, является полезной функцией. Интроспекция методов также невероятно проста, так как все, что нам действительно нужно сделать, это вызвать метод methods:

julia> function example_function(i::Int64)

       end
example_function (generic function with 1 method)

julia> methods(example_function)
# 1 method for generic function "example_function":
[1] example_function(i::Int64) in Main at REPL[27]:1

julia> methods(example_function)[1]
example_function(i::Int64) in Main at REPL[27]:1

julia> methods(example_function)[1].sig
Tuple{typeof(example_function), Int64}

julia> methods(example_function)[1].sig.parameters
svec(typeof(example_function), Int64)

julia> methods(example_function)[1].sig.parameters[1]
typeof(example_function) (singleton type of function example_function, subtype of Function)

julia> methods(example_function)[1].sig.parameters[2]
Int64

Нет

Еще одна действительно крутая функция в Джулии — это функция Not. Функция Not позволяет нам создать то, что называется InvertedIndex. В основном это означает, что мы можем проиндексировать данный Vector и выбрать любые элементы, которые мы хотим исключить.

julia> x = [1, 2, 3, 4, 5]
5-element Vector{Int64}:
 1
 2
 3
 4
 5
julia> x[Not(1:2)]
3-element Vector{Int64}:
 3
 4
 5

неоднозначные поля плохо

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

Неоднозначная типизация по существу означает, что тип чего-либо может быть любым и должен быть выведен или понят компилятором. Не судите об этом неправильно; использование одного неоднозначного поля внутри структуры, потому что другого варианта практически нет, не обязательно является здесь проблемой. Проблема в первую очередь возникает, когда это используется как обычное, а не как исключение. В идеале неоднозначные поля должны использоваться как можно реже. Если тип вашего поля должен поддерживать различные типы, лучшая техника для работы с такими вещами — предоставить параметр, который даст тип этого поля. Это сделает так, что все объекты этого типа, типа, содержащего параметр, по-прежнему будут иметь поля одного и того же типа, что делает такие вещи гораздо более предсказуемыми.

mutable struct BadFields
    n::Number
end
mutable struct BetterFields{T <: Number}
    n::T
end

найти функции

Последняя особенность Julia, с которой мне хотелось бы встретиться раньше, — это функции find; по крайней мере, так я люблю их называть. Эти функции обычно используются в алгоритмах и будут абсолютно необходимы, например, если вы хотите разобрать String . Поиск чего угодно по некоторому полю или значению внутри него будет выполняться с помощью функций поиска. Это findall, findlast, findfirst, findnext и findprev. Различия между ними должны быть очевидны, так что давайте быстро перейдем к их использованию.

Эти функции принимают Function в качестве своего первого позиционного аргумента, что означает, что вы также можете написать их с использованием синтаксиса do, а затем взять коллекцию в качестве второго аргумента. Если вы используете findnext или findprev, вам также потребуется указать индекс — Int64, который будет третьим и последним позиционным аргументом этих методов. Следует отметить, что все они возвращают единственное значение, за исключением findall, которое возвращает Vector{Int64}, содержащее разные индексы… Если только мы не предоставим String в качестве второго позиционного аргумента вместо Vector. Это превратит наш Int64 в UnitRange во всех этих случаях. Этот диапазон, конечно, представляет индекс, в котором это было найдено. При использовании этого на String мы также можем заменить первый позиционный аргумент на String :

julia> findall("emmy", "hello, I am emmy")
1-element Vector{UnitRange{Int64}}:
 13:16
julia> findfirst(x -> x == 5, 1:10)
5

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



Если это еще не очевидно из того, о чем я веду блог и как я обычно говорю о Джулии, этот язык стал моим любимым языком программирования. Изучение нового языка программирования почти всегда вызывает раздражение, особенно когда вы не привыкли к этому языку, однако, как и в случае с Джулией, продолжение образования и использование языка может быть невероятно полезным. Я много узнал не только о Джулии, но и о программировании и языках программирования в целом. Как на всю жизнь, я всегда ценю новую информацию, и я благодарен, что смог узнать такую ​​полезную и — потрясающую — информацию. Без сомнения, я продолжу довольно часто использовать большинство техник из этого списка в своих проектах! Большое спасибо за чтение!