Переменное количество аргументов, аргументы по умолчанию и т. Д.

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

Как только я создал библиотеку для указания макета графического интерфейса для виджетов Qt, она выглядела примерно так:

Ui(
    class   = "EmptyForm",
    version = "4.0",
    root = QWidget(
        name        = "EmptyForm",
        geometry    = Rect(0, 0, 120, 120),
        windowTitle = "Form"
    )
)

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

QPushButton(name = "done", caption = "click me!", enabled = false)

При создании этого кода вы можете не знать, сколько атрибутов может иметь какой-либо компонент пользовательского интерфейса, и вам это тоже наплевать. Вы просто хотите вывести их, скажем, в формате JSON или XML. Здесь пригодятся аргументы с именами переменных.

Именованные аргументы

Вот пример попытки реализовать что-то подобное в Юлии:

function QPushButton(name = "nothing", caption = "button")
   println("name: " name, " caption:", caption)
end

Это работает для настройки значений аргументов по умолчанию:

julia> QPushButton()
name: nothing caption:button

Однако это не работает для именованных аргументов:

julia> QPushButton(name = "egg", caption = "click me")
ERROR: function QPushButton does not accept keyword arguments

Python OTOH интерпретирует это по-разному.

def QPushButton(name = "nothing", caption = "button"):
		print("name: ", name, " caption:", caption)

У меня нет проблем с вызовом этого с именованными аргументами:

python> QPushButton()
name:  nothing  caption: button
python> QPushButton(name = "egg")
name:  egg  caption: button

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

python> QPushButton(name = "egg", enable = False)
TypeError: QPushButton() got an unexpected keyword argument 'enable'

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

function QPushButton(name = "noname"; caption = "button", enable = true)
end

В этом случае name - это обычный аргумент со значением по умолчанию "noname", а caption и enable - именованные аргументы со значениями по умолчанию.

Вы могли бы назвать это так:

QPushButton("name", enable = false, caption = "click me")

Порядок безымянных аргументов имеет значение, но не порядок именованных аргументов.

Переменное количество аргументов

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

Вот пример функций, возвращающих аргументы в виде двух отдельных массивов в Julia.

getvalues(args...; kwargs...) = args, kwargs

Имена args и kwargs не важны. Вместо этого я мог бы написать:

getvalues(foo...; bar...) = foo, bar

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

julia> a, d = getvalues(false, 2, "three", four = 4, five="fünf")
((false, 2, "three"), Base.Iterators.Pairs{Symbol,Any,Tuple{Symbol,Symbol},NamedTuple{(:four, :five),Tuple{Int64,String}}}(:four=>4,:five=>"fünf"))

julia> collect(a)
3-element Array{Any,1}:
 false       
     2       
      "three"

julia> collect(d)
2-element Array{Pair{Symbol,Any},1}:
 :four => 4     
 :five => "fünf"

Notice how collect(d) gives us an array of pairs. In Julia pairs are formed with key => value syntax. :four and :five are symbols rather than strings. In LISP tradition Julia like LISP, Scheme and Ruby use extensively symbols. A symbol is essentially an immutable string which there are only one of, and which has to be a valid identifier in a programming language. You cannot throw in spaces e.g. Python will tend to use regular strings instead of symbols.

Давайте посмотрим на версию на Python. Вызов выглядит практически идентично:

python> a, d = getvalues(False, 2, "three", four = 4, five="fünf")

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

python> a
(False, 2, 'three')
python> type(a)
<class 'tuple'>

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

python> d
{'four': 4, 'five': 'fünf'}
python> type(d)
<class 'dict'>

Распаковка массивов в аргументы

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

В Julia нам нужно использовать ; при вызове функции таким образом, чтобы разделить аргументы и ключевые аргументы.

julia> ar, dic = getvalues(a...; d...)
((false, 2, "three"), Base.Iterators.Pairs{Symbol,Any,Tuple{Symbol,Symbol},NamedTuple{(:four, :five),Tuple{Int64,String}}}(:four=>4,:five=>"fünf"))

julia> collect(ar)
3-element Array{Any,1}:
 false       
     2       
      "three"

julia> collect(dic)
2-element Array{Pair{Symbol,Any},1}:
 :four => 4     
 :five => "fünf"

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

python> ar, dic = getvalues(*a, **d)
python> ar
(False, 2, 'three')
python> dic
{'four': 4, 'five': 'fünf'}

Составить список

Если вы не знаете, что такое понимание списка, проще просто привести пример.

julia> [x*x for x in 1:4]
4-element Array{Int64,1}:
  1
  4
  9
 16

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

python> [x*x for x in range(1,5)]
[1, 4, 9, 16]

Фактически, мы можем даже фильтровать таким же образом:

julia> [x*x for x in 1:4 if x != 3]
3-element Array{Int64,1}:
  1
  4
 16

python> [x*x for x in range(1,5) if x != 3]
[1, 4, 16]

Но дело обстоит глубже: как в Julia, так и в Python синтаксис понимания не ограничивается определением списков или массивов. Фактически в обоих случаях создается объект-генератор. Это генератор, который предоставляет значения для построения массива.

Так что я, например, используйте его в Julia, чтобы создать массив пар для инициализации словаря:

julia> Dict(Char(64 + i) => i for i in 1:5)
Dict{Char,Int64} with 5 entries:
  'C' => 3
  'D' => 4
  'A' => 1
  'E' => 5
  'B' => 2

Чтобы понять, как это работает, давайте создадим простую функцию как в Julia, так и в Python, чтобы извлечь объект, созданный пониманием.

julia> getobj(c) = c
julia> c = getobj(x*x for x in 1:4 if x != 3)
julia> collect(c)
3-element Array{Int64,1}:
  1
  4
 16

Если мы проверим тип, это станет немного сложнее, потому что Джулия поддерживает параметризованные типы:

julia> typeof(c)
Base.Generator{Base.Iterators.Filter{getfield(Main, Symbol("##47#49")),UnitRange{Int64}},getfield(Main, Symbol("##46#48"))}

В основном это говорит о том, что понимание имеет тип Generator, который является пакетом Base Джулии. Генераторы поддерживают итерационный интерфейс Джулии. Все, что вы хотите повторить, например цикл for должен реализовывать интерфейс итератора.

julia> v, state = iterate(c)
(1, 1)

julia> v, state = iterate(c, state)
(4, 2)

julia> v, state = iterate(c, state)
(16, 4)

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

python> def getobj(c): return c
python> c = getobj(x*x for x in range(1,5) if x != 3)
python> c
<generator object <genexpr> at 0x105717de0>
python> list(c)
[1, 4, 16]

За исключением того, что генераторы Python видоизменяются при его использовании. Таким образом, он исчерпывается, и вы не можете вызвать next на нем так же, как вы можете вызвать iterate в генераторе Julia:

python> next(c)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    next(c)
StopIteration

Состояние итератора не хранится снаружи в объекте состояния, как в случае с Джулией. Нам нужно воссоздать генератор:

python> c = getobj(x*x for x in range(1,5) if x != 3)
python> next(c)
1
python> next(c)
4
python> next(c)
16

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