Функции¶
Как упоминалось в обзоре синтаксиса, Mojo поддерживает два ключевых слова для объявления функций: def и fn. Вы можете использовать любое из этих слов для любой функции, включая функцию main(), но они имеют разное поведение по умолчанию, как описано на этой странице.
Мы считаем, что и def, и fn имеют хорошие варианты использования, и не считаем, что один из них лучше другого. Решение о том, какой стиль лучше всего подходит для конкретной задачи, зависит от личного вкуса.
Функции, объявленные внутри
struct, называются "методами", но они обладают всеми теми же качествами, что и "функции", описанные здесь.
Анатомия функции¶
Оба объявления функций def и fn содержат одни и те же базовые компоненты (здесь показано с помощью функции def):
def function_name[
parameters ...
](
arguments ...
) -> return_value_type:
function_body
Функции могут иметь:
- Параметры: Функция может опционально принимать одно или несколько значений параметров времени компиляции, используемых для метапрограммирования.
- Аргументы: Функция опционально может принимать один или несколько аргументов времени выполнения.
- Возвращаемое значение: Функция опционально может возвращать значение.
- Тело функции: инструкции, которые выполняются при вызове функции. Определения функций должны содержать тело.
Все необязательные части функции могут быть опущены, поэтому минимальная функция выглядит примерно так:
def do_nothing():
pass
Если функция не принимает параметров, вы можете опустить квадратные скобки, но они всегда обязательны.
Хотя вы не можете исключить текст функции, вы можете использовать оператор pass для определения функции, которая ничего не делает.
Аргументы и параметры¶
Функции принимают два вида входных данных: аргументы и параметры. Аргументы известны из многих других языков: это значения, передаваемые в функцию во время выполнения.
def add(a: Int, b: Int) -> Int:
return a+b
С другой стороны, вы можете рассматривать параметр как переменную времени компиляции, которая становится константой времени выполнения. Например, рассмотрим следующую функцию с параметром:
def add_tensors[rank: Int](a: MyTensor[rank], b: MyTensor[rank]) -> MyTensor[rank]:
# ...
В этом случае значение rank должно быть указано таким образом, чтобы его можно было определить во время компиляции, например, с помощью литерала или выражения.
Когда вы компилируете программу, использующую этот код, компилятор создает уникальную версию функции для каждого уникального значения rank, используемого в программе, причем rank рассматривается как константа в каждой специализированной версии.
Такое использование термина "параметр", вероятно, отличается от того, к которому вы привыкли в других языках, где "параметр" и "аргумент" часто используются взаимозаменяемо. В Mojo "параметр" и "выражение параметра" относятся к значениям времени компиляции, а "аргумент" и "выражение" относятся к значениям времени выполнения.
По умолчанию и аргументы, и параметры могут быть заданы либо позицией, либо ключевым словом. Эти формы также могут быть смешаны в одном и том же вызове функции.
# positional
x = add(5, 7) # Позиционно, a=5 and b=7
# keyword
y = add(b=3, a=9)
# mixed
z = add(5, b=7) # Позиционно, a=5
Дополнительные сведения об аргументах см. в разделе Аргументы функции на этой странице. Дополнительные сведения о параметрах см. в разделе Параметризация: метапрограммирование во время компиляции.
Сравнение def и fn¶
Определение функции с помощью def и fn имеет много общего. К ним обоим предъявляются следующие требования:
-
Вы должны указать тип каждого параметра и аргумента функции.
-
Если функция не возвращает значение, вы можете либо не указывать тип возвращаемого значения, либо объявить
Noneв качестве типа возвращаемого значения.# Следующие определения функций эквивалентны def greet(name: String): print("Hello," name) def greet(name: String) -> None: print("Hello," name) -
Если функция возвращает значение, вы должны либо объявить возвращаемый тип, используя синтаксис
-> type, либо указать именованный результат в списке аргументов.# Следующие определения функций эквивалентны def incr(a: Int) -> Int: return a + 1 def incr(a: Int, out b: Int): b = a + 1Для получения дополнительной информации смотрите раздел "Возвращаемые значения" на этой странице.
Разница между def и fn заключается в обработке ошибок.
- Компилятор не позволяет функции, объявленной с помощью
fn, вызывать условие ошибки, если оно явно не содержит объявленияraises. Напротив, компилятор предполагает, что все функции, объявленные с помощьюdef, могут вызывать ошибку. Смотрите раздел "Вызывающие и не вызывающие функции" на этой странице для получения дополнительной информации.
Что касается вызывающей функции, то нет разницы между вызовом функции, объявленной с помощью def, и функцией, объявленной с помощью fn. Вы могли бы переопределить функцию def как функцию fn, не внося никаких изменений в код, вызывающий эту функцию.
Аргументы функции¶
Правила для аргументов, описанные в этом разделе, применимы как к функциям def, так и к функциям fn.
Функции с / и * в списке аргументов¶
Вместо аргументов могут быть следующие символы: косая черта (
/) и/или звездочка (*). Например:def myfunc(pos_only, /, pos_or_keyword, *, keyword_only):Аргументы перед
/могут передаваться только по позиции. Аргументы после*могут передаваться только по ключевому слову. Дополнительные сведения см. в разделе Аргументы только для позиций и только для ключевых слов Вы также можете увидеть имена аргументов с префиксом в одну или две звездочки (*):Имя аргумента с префиксом в виде одиночной звездочки, напримерdef myfunc2(*names, **attributes):*names, идентифицирует переменный аргумент, в то время как имя аргумента с префиксом в виде двойной звездочки, например**attributes, идентифицирует переменный аргумент, содержащий только ключевое слово.
Опциональные аргументы¶
Опциональный аргумент - это аргумент, который содержит значение по умолчанию, например, аргумент exp здесь:
fn my_pow(base: Int, exp: Int = 2) -> Int:
return base ** exp
fn use_defaults():
# Использует значение по умолчанию для `exp`
var z = my_pow(3)
print(z)
Однако вы не можете определить значение по умолчанию для аргумента, объявленного с помощью mut.
Любые необязательные аргументы должны отображаться после любых обязательных аргументов. Аргументы только для ключевых слов, обсуждаемые ниже, также могут быть обязательными или необязательными.
Аргументы ключевых слов¶
Вы также можете использовать аргументы ключевого слова при вызове функции. Аргументы ключевого слова задаются в формате argument_name = argument_value. Вы можете передавать аргументы ключевого слова в любом порядке:
fn my_pow(base: Int, exp: Int = 2) -> Int:
return base ** exp
fn use_keywords():
# Использует имена аргументов с ключевыми словами (в обратном порядке)
var z = my_pow(exp=3, base=2)
print(z)
Переменные аргументы¶
Переменные аргументы позволяют функции принимать переменное количество аргументов. Чтобы определить функцию, которая принимает переменные аргументы, используйте синтаксис переменных аргументов *имя_аргумента:
fn sum(*values: Int) -> Int:
var sum: Int = 0
for value in values:
sum = sum + value
return sum
Переменный аргумент values здесь является заполнителем, который принимает любое количество переданных позиционных аргументов.
Вы можете указать ноль или более аргументов перед переменным аргументом. При вызове функции все оставшиеся позиционные аргументы присваиваются переменному аргументу, поэтому любые аргументы, объявленные после переменного ргумента, могут быть указаны только с помощью ключевого слова.
Переменные аргументы можно разделить на две категории:
- Однородные переменные аргументы, в которых все передаваемые аргументы имеют один и тот же тип — например, все
Intили всеString. - Разнородные переменные аргументы, которые могут принимать набор аргументов разных типов.
В следующих разделах описывается, как работать с однородными и разнородными переменными аргументами.
Переменные параметры¶
Mojo также поддерживает переменные параметры, но с некоторыми ограничениями — подробнее смотрите в разделе Переменные параметры.
Однородные переменные аргументы¶
При определении однородного вариационного аргумента (все аргументы должны быть одного типа) используйте *argument_name: argument_type:
def greet(*names: String):
...
Внутри тела функции переменный аргумент доступен в виде повторяющегося списка для удобства использования. В настоящее время существуют некоторые различия в обработке списка в зависимости от того, являются ли аргументы типами, передаваемыми через регистр (например, Int), или типами, доступными только для памяти(например, String).
TODO¶
Мы надеемся устранить эти различия в будущем.
Типы, передаваемые через регистр, такие как Int, доступны в виде типа VariadicList. Как показано в предыдущем примере, вы можете перебирать значения, используя цикл for..in.
fn sum(*values: Int) -> Int:
var sum: Int = 0
for value in values:
sum = sum+value
return sum
Типы, доступные только для памяти, такие как String, доступны в виде VariadicListMem. При непосредственном просмотре этого списка с помощью цикла for..in в настоящее время создается ссылка на элемент, который может быть изменен с помощью списка переменных mut. Используйте шаблон привязки ref для получения изменяемой ссылки, если вы хотите изменить элементы списка:
def make_worldly(mut *strs: String):
for ref i in strs:
i += " world"
Вы также можете напрямую индексировать список целыми числами:
fn make_worldly(mut *strs: String):
for i in range(len(strs)):
strs[i] += " world"
Разнородные переменные аргументы¶
Реализация разнородных переменных аргументов(каждый тип аргумента может отличаться) несколько сложнее, чем однородных переменных аргументов. Для обработки нескольких типов аргументов функция должна быть универсальной, что требует использования трейтов и параметров. Поэтому синтаксис может показаться немного незнакомым, если вы не работали с этими функциями.
Сигнатура для функции с неоднородным переменным аргументом выглядит следующим образом:
def count_many_things[*ArgTypes: Intable](*args: *ArgTypes):
...
Список параметров [*ArgTypes: Intable] указывает, что функция принимает параметр ArgTypes, который представляет собой список типов, все из которых соответствуют трейту Intable. Звездочка в *ArgTypes указывает на то, что ArgTypes - это параметр переменного типа (список типов).
Список аргументов (*args: *ArgTypes) содержит знакомые *args для переменного аргумента, но вместо одного типа, его тип определяется как список переменных типов *ArgTypes. Звездочка в *args указывает на переменный аргумент, а звездочка в *ArgTypes указывает на параметр переменного типа.
Это означает, что каждый аргумент в args имеет соответствующий тип в ArgTypes, поэтому args[n] относится к типу ArgTypes[n].
Внутри функции args становится VariadicPack, потому что синтаксис *args: *ArgTypes создает разнородный переменный аргумент. Это означает, что каждый элемент в args может быть разного типа, который требует разного объема памяти. Чтобы выполнить итерацию по VariadicPack, компилятор должен знать тип каждого элемента(его объем памяти), поэтому вы должны использовать параметрический цикл for:
fn count_many_things[*ArgTypes: Intable](*args: *ArgTypes) -> Int:
var total = 0
@parameter
for i in range(args.__len__()):
total += Int(args[i])
return total
def main():
print(count_many_things(5, 11.7, 12))
28
Обратите внимание, что при вызове функции count_many_things() вы на самом деле не передаете список типов аргументов. Вам нужно только передать аргументы, и Mojo сам сгенерирует список ArgTypes.
Переменные аргументы ключевых слов¶
Функции Mojo также поддерживают переменные аргументы ключевых слов (**kwargs). Переменные аргументы ключевых слов позволяют пользователю передавать произвольное количество аргументов ключевых слов. Чтобы определить функцию, которая принимает аргумент ключевого слова variadic, используйте синтаксис аргумента ключевого слова variadic **kw_argument_name:
fn print_nicely(**kwargs: Int) raises:
for key in kwargs.keys():
print(key, "=", kwargs[key])
# prints:
# `a = 7`
# `y = 8`
print_nicely(a=7, y=8)
В этом примере имя аргумента kwargs является заполнителем, который принимает любое количество аргументов с ключевыми словами. Внутри тела функции вы можете получить доступ к аргументам в виде словаря ключевых слов и значений аргументов (в частности, к экземпляру OwnedKwargsDict).
В настоящее время существует несколько ограничений:
-
Ключевые слова переменных аргументов всегда неявно обрабатываются так, как если бы они были объявлены с использованием соглашения о собственном аргументе, и не могут быть объявлены иначе:
# Пока не поддерживается. fn read_var_kwargs(read **kwargs: Int): ... -
Ключевые слова переменных аргументов должны иметь одинаковый тип, и это определяет тип словаря аргументов. Например, если аргументом является
**kwargs: Float64, то словарем аргументов будетOwnedKwargsDict[Float64]. -
Тип аргумента должен соответствовать как трейту
Movable, так и трейтуCopyable. -
Распаковка словаря пока не поддерживается:
fn takes_dict(d: Dict[String, Int]): print_nicely(**d) # Пока не поддерживается. -
Параметры переменных аргументов пока не поддерживаются:
Пока не поддерживается.¶
fn var_kwparams**kwparams: Int: ...
Positional-only и keyword-only аргументы¶
При определении функции вы можете ограничить некоторые аргументы, чтобы они могли передаваться только как positional-only, или они могут передаваться только как keyword-only слова.
Чтобы определить аргументы только для позиционных аргументов, добавьте символ косой черты (/) в список аргументов. Любые аргументы перед символом / являются только позиционными: они не могут быть переданы в качестве аргументов ключевого слова. Например:
fn min(a: Int, b: Int, /) -> Int:
return a if a < b else b
Эта функция min() может быть вызвана с помощью min(1, 2), но не может быть вызвана с использованием ключевых слов, таких как min(a=1, b=2).
Существует несколько причин, по которым вы можете захотеть написать функцию с аргументами, содержащими только позиционные значения:
- Имена аргументов не имеют смысла для вызывающей стороны.
- Вы хотите иметь возможность изменять имена аргументов позже, не нарушая обратной совместимости.
Например, в функции min() имена аргументов не добавляют никакой реальной информации, и нет смысла указывать аргументы по ключевому слову.
Дополнительные сведения о позиционных аргументах см. в разделе PEP 570 - Позиционные параметры Python.
Аргументы, основанные только на ключевом слове, являются обратными аргументам, основанным только на позиции: они могут быть заданы только с помощью ключевого слова. Если функция принимает переменные аргументы, то любые аргументы, определенные после переменных аргументов, обрабатываются только как ключевые слова. Например:
fn sort(*values: Float64, ascending: Bool = True): ...
В этом примере пользователь может передать любое количество значений Float64, необязательно с последующим аргументом возрастания ключевого слова:
var a = sort(1.1, 6.5, 4.3, ascending=False)
Если функция не принимает переменные аргументы, вы можете добавить одну звездочку (*) в список аргументов, чтобы отделить аргументы, содержащие только ключевые слова:
fn kw_only_args(a1: Int, a2: Int, *, double: Bool) -> Int:
var product = a1 * a2
if double:
return product * 2
else:
return product
Аргументы, относящиеся только к ключевым словам, часто имеют значения по умолчанию, но это необязательно. Если аргумент, относящийся только к ключевым словам, не имеет значения по умолчанию, это обязательный аргумент, относящийся только к ключевым словам. Он должен быть указан, и он должен быть указан с помощью ключевого слова.
Все обязательные аргументы, относящиеся только к ключевым словам, должны отображаться в подписи перед любыми необязательными аргументами, относящимися только к ключевым словам. То есть аргументы отображаются в следующей последовательности в сигнатуре функции:
- Обязательные позиционные аргументы.
- Необязательные позиционные аргументы.
- Переменные аргументы.
- Обязательные аргументы, относящиеся только к ключевым словам.
- Необязательные аргументы только для ключевых слов.
- Аргументы с переменным числом ключевых слов.
Дополнительные сведения об аргументах только для ключевых слов см. в PEP 3102 - Аргументы только для ключевых слов.
Перегруженные функции¶
Все объявления функций должны указывать типы аргументов, поэтому, если вы хотите, чтобы функция работала с разными типами данных, вам необходимо реализовать отдельные версии функции, в каждой из которых указаны разные типы аргументов. Это называется "перегрузкой" функции.
Например, вот перегруженная функция add(), которая может принимать типы Int или String:
fn add(x: Int, y: Int) -> Int:
return x + y
fn add(x: String, y: String) -> String:
return x + y
Если вы передадите функции add() что-либо, отличное от Int или String, вы получите ошибку компилятора. То есть, если только Int или String не могут неявно преобразовать тип в свой собственный тип. Например, String содержит перегруженную версию своего конструктора (__init__()), который поддерживает неявное преобразование из значения StringLiteral. Таким образом, вы также можете передать StringLiteral функции, которая ожидает строку.
При разрешении вызова перегруженной функции компилятор Mojo пробует каждую функцию-кандидата и использует ту, которая работает (если работает только одна версия), или выбирает наиболее близкое соответствие (если он может определить близкое соответствие), или сообщает, что вызов неоднозначен (если он не может определить, какая именно).
Если компилятор не может определить, какую функцию использовать, вы можете устранить неоднозначность, явно приведя свое значение к поддерживаемому типу аргумента. Например, следующий код вызывает перегруженную функцию foo(), но обе реализации принимают аргумент, который поддерживает неявное преобразование из StringLiteral. Таким образом, вызов функции foo(string) неоднозначен и приводит к ошибке компилятора. Вы можете исправить это, приведя значение к тому типу, который вам действительно нужен:
struct MyString:
@implicit
fn __init__(out self, string: StringLiteral):
pass
fn foo(name: String):
print("String")
fn foo(name: MyString):
print("MyString")
fn call_foo():
alias string = "Hello"
# foo(string) # error: ambiguous call to 'foo' ... This call is ambiguous because two `foo` functions match it
foo(MyString(string))
Перегрузка также работает с комбинациями объявлений функций fn и def.
Разрешение на перегрузку¶
При разрешении перегруженной функции Mojo не учитывает тип возвращаемого значения или другую контекстную информацию на месте вызова — он учитывает только типы параметров и аргументов, а также то, являются ли функции методами экземпляра или статическими методами.
Логика разрешения перегрузки фильтрует кандидатов в соответствии со следующими правилами в порядке приоритета:
- Кандидаты, требующие наименьшего количества неявных преобразований (как в аргументах, так и в параметрах).
- Кандидаты без переменных аргументов.
- Кандидаты без переменных параметров.
- Кандидаты с наименьшей сигнатурой параметра.
- Кандидаты, не относящиеся к
@staticmethod(вместо кандидатов@staticmethod, если таковые имеются).
Если после применения этих правил будет найдено более одного кандидата, разрешение перегрузки завершится ошибкой. Например:
@register_passable("trivial")
struct MyInt:
"""A type that is implicitly convertible to `Int`."""
var value: Int
@implicit
fn __init__(out self, _a: Int):
self.value = _a
fn foo[x: MyInt, a: Int]():
print("foo[x: MyInt, a: Int]()")
fn foo[x: MyInt, y: MyInt]():
print("foo[x: MyInt, y: MyInt]()")
fn bar[a: Int](b: Int):
print("bar[a: Int](b: Int)")
fn bar[a: Int](*b: Int):
print("bar[a: Int](*b: Int)")
fn bar[*a: Int](b: Int):
print("bar[*a: Int](b: Int)")
fn parameter_overloads[a: Int, b: Int, x: MyInt]():
# `foo[x: myInt, a: Int]()` вызывается потому, что не требует неявных
# преобразований, в то время как `foo[x: myInt, y: myInt]()` требует их.
# `bar[a: Int](b: Int)` вызывается потому, что у него нет переменных
# аргументов или параметров.
bar[a](b)
# `bar[*a: Int](b: Int)` вызывается потому, что он имеет переменные параметры.
bar[a, a, a](b)
parameter_overloads[1, 2, MyInt(3)]()
struct MyStruct:
fn __init__(out self):
pass
fn foo(mut self):
print("calling instance method")
@staticmethod
fn foo():
print("calling static method")
fn test_static_overload():
var a = MyStruct()
# `foo(mut self)` имеет приоритет над статическим методом.
a.foo()
foo[x: MyInt, a: Int]()
bar[a: Int](b: Int)
bar[*a: Int](b: Int)
Return values¶
Типы возвращаемых значений объявляются в сигнатуре с использованием синтаксиса -> type. Значения передаются с использованием ключевого слова return, которое завершает работу функции и возвращает идентифицированное значение (если таковое имеется) вызывающей стороне.
def get_greeting() -> String:
return "Hello"
По умолчанию значение возвращается вызывающей стороне как собственное значение. Как и в случае с аргументами, возвращаемое значение может быть неявно преобразовано в возвращаемый тип named. Например, в предыдущем примере return вызывался со строковым литералом "Hello", который неявно преобразовался в строку.
Возвращающая ссылку¶
Функция также может возвращать изменяемую или неизменяемую ссылку, используя возвращаемое значение
ref. Дополнительные сведения см. в разделах Время жизни, источники и ссылки.
Именованные результаты¶
Результаты именованной функции позволяют функции возвращать значение, которое нельзя переместить или скопировать. Синтаксис именованного результата позволяет указать именованную неинициализированную переменную, которая будет возвращена вызывающей стороне, используя соглашение об использовании аргумента out:
def get_name_tag(var name: String, out name_tag: NameTag):
name_tag = NameTag(name^)
Соглашение о выводе аргументов определяет неинициализированную переменную, которую функция должна инициализировать. (Это то же самое, что и соглашение о выводе, используемое в конструкторах struct.) Аргумент out для именованного результата может отображаться в любом месте списка аргументов, но по соглашению он должен быть последним в списке.
Функция может объявлять только одно возвращаемое значение, независимо от того, объявлено ли оно с использованием аргумента out или с использованием синтаксиса standard -> type.
Функция с именованным аргументом result не обязательно должна содержать явную инструкцию return, как показано выше. Если функция завершается без return или при выполнении инструкции return без значения, вызывающей стороне возвращается значение аргумента out. Если он содержит инструкцию return со значением, это значение возвращается вызывающей стороне, как обычно.
Тот факт, что функция использует именованный результат, прозрачен для вызывающей стороны. То есть эти две подписи взаимозаменяемы для вызывающей стороны:
def get_name_tag(var name: String) -> NameTag:
...
def get_name_tag(var name: String, out name_tag: NameTag):
...
В обоих случаях вызов выглядит следующим образом:
tag = get_name_tag("Judith")
Поскольку возвращаемое значение присваивается этой специальной переменной out, его не нужно перемещать или копировать, когда оно возвращается вызывающей стороне. Это означает, что вы можете создать функцию, которая возвращает тип, который нельзя переместить или скопировать, и для инициализации которой требуется несколько шагов:
struct ImmovableObject:
var name: String
fn __init__(out self, var name: String):
self.name = name^
def create_immovable_object(var name: String, out obj: ImmovableObject):
obj = ImmovableObject(name^)
obj.name += "!"
# obj неявно возвращается
def main():
my_obj = create_immovable_object("Blob")
Напротив, следующая функция со стандартным возвращаемым значением не работает:
def create_immovable_object2(var name: String) -> ImmovableObject:
obj = ImmovableObject(name^)
obj.name += "!"
return obj^ # Error: ImmovableObject is not copyable or movable
Поскольку create_immovable_object2 использует локальную переменную для хранения объекта во время его создания, обратный вызов требует, чтобы объект был либо перемещен, либо скопирован вызываемому объекту. Это не проблема, если только что созданное значение возвращается немедленно:
def create_immovable_object3(var name: String) -> ImmovableObject:
return ImmovableObject(name^) # OK
Raising и non-raising функции¶
По умолчанию, когда функция выдает ошибку, она немедленно завершает работу, и ошибка передается вызывающей функции. Если вызывающая функция не обрабатывает ошибку, она продолжает распространяться вверх по стеку вызовов.
def raises_error():
raise Error("There was an error.")
Компилятор Mojo всегда обрабатывает функцию, объявленную с помощью def, как вызывающую функцию, даже если тело функции не содержит никакого кода, который мог бы вызвать ошибку.
Функции, объявленные с помощью fn без ключевого слова raises, не являются вызывающими функциями, то есть им запрещено передавать ошибку вызывающей функции. Если функция, не вызывающая raises, вызывает вызывающую функцию, она должна обрабатывать все возможные ошибки.
# This function will not compile
fn unhandled_error():
raises_error() # Error: can't call raising function in a non-raising context
# Explicitly handle the error
fn handle_error():
try:
raises_error()
except e:
print("Handled an error:", e)
# Explicitly propagate the error
fn propagate_error() raises:
raises_error()
Если вы пишете код, который, как ожидается, будет широко использоваться или распространяться в виде пакета, вы можете захотеть использовать функции fn для API, которые не вызывают ошибок, чтобы ограничить количество мест, где пользователям необходимо добавлять ненужный код обработки ошибок. Для некоторых программ, производительность которых крайне важна, может быть предпочтительнее избегать обработки ошибок во время выполнения.
Дополнительные сведения см. в разделах Ошибки, обработка ошибок и контекстные менеджеры.