Перейти к содержанию

Операторы, выражения и более dunder методы

Слово «dunder» — это сленговое сокращение от «double underscore» (двойное подчёркивание). В контексте языка программирования Python оно обозначает специальные методы, название которых начинается и заканчивается двумя символами подчёркивания, например __init__, __add__, __str__ и т.д.

Mojo включает в себя множество операторов для манипулирования значениями различных типов. Как правило, операторы эквивалентны тем, которые используются в Python, хотя многие операторы также работают с дополнительными типами Mojo, такими как SIMD-векторы. Кроме того, Mojo позволяет вам определять поведение большинства из этих операторов для ваших собственных пользовательских типов, реализуя специальные методы dunder.

Этот документ содержит следующие три раздела:

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

Операторы и выражения

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

Приоритет операторов и ассоциативность

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

Операторы Описание Ассоциативность (группировка)
() Выражение, заключенное в круглые скобки Слева направо
x[index], x[index:index] Подписывание, нарезка Слева направо
** Возведение в степень Справа налево
+x, -x, ~x Положительный, отрицательный, побитовый НЕ Справа налево
*, @, /, //, % Умножение, матричное умножение, деление, минимальное деление, остаток Слева направо
+, – Сложение и вычитание Слева направо
<<, >> Сдвиги Слева направо
& Побитовое И Слева направо
^ Побитовое исключающее значение Слева направо
| Побитовый ИЛИ Слева направо
in, not in, is, is not, <, <=, >, >=, !=, == Сравнения, тесты на членство, идентификационные тесты Слева направо
not x Логическое значение НЕ Справа налево
x and y Логическое И Слева направо
x or y Логическое ИЛИ Слева направо
if-else Условное выражение Справа налево
:= Выражение присваивания (оператор walrus) Справа налево

Mojo поддерживает те же операторы, что и Python (плюс несколько расширений), и они имеют те же уровни приоритета. Например, значение следующего арифметического выражения равно 40:

5 + 4 * 3 ** 2 - 1

Это эквивалентно следующему выражению, заключенному в круглые скобки, для явного управления порядком вычисления:

(5 + (4 * (3 ** 2))) - 1

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

3 * 4 / 2 / 2

Это эквивалентно следующему выражению, заключенному в круглые скобки, для явного управления порядком вычисления:

((3 * 4) / 2) / 2

Принимая во внимание, что в приведенном ниже примере операторы возведения в степень являются правоассоциативными, что приводит к значению 264.144:

4 ** 3 ** 2

Это эквивалентно следующему выражению, заключенному в круглые скобки, для явного управления порядком вычисления:

4 ** (3 ** 2)

Mojo также использует символ курсора (^) в качестве символа переноса. В выражениях, где его использование может быть неоднозначным, Mojo обрабатывает символ как побитовый оператор XOR. Например, x^+1 обрабатывается как (x)^(+1).

Арифметические и побитовые операторы

Числовые типы описывает различные числовые типы, предоставляемые стандартной библиотекой Mojo. Арифметические и побитовые операторы немного отличаются в зависимости от типов предоставляемых значений.

Int and UInt values

Типы Int и UInt представляют собой целые числа со знаком и без знака, размер которых соответствует разрядности процессора, обычно 64 или 32 бита.

Типы Int и UInt поддерживают все арифметические операции, кроме матричного умножения (@), а также все побитовые операторы и операторы сдвига. Если оба операнда двоичного оператора имеют значения Int, то результатом будет Int, если оба операнда имеют значения UInt, то результатом будет UInt, а если один операнд имеет значение Int, а другой - значение UInt, то результатом будет Int. Единственным исключением для этих типов является значение true division, /, которое всегда возвращает значение типа Float64.

var a_int: Int = -7
var b_int: Int = 4
sum_int = a_int + b_int  # Результатом является тип Int
print("Int sum:", sum_int)

var i_uint: UInt = 9
var j_uint: UInt = 8
sum_uint = i_uint + j_uint  # Результатом является тип UInt
print("UInt sum:", sum_uint)

sum_mixed = a_int + Int(i_uint)  # Результатом является тип Int
print("Mixed sum:", sum_mixed)

quotient_int = a_int / b_int  # Результатом является тип Float64
print("Int quotient:", quotient_int)
quotient_uint = i_uint / j_uint  # Результатом является тип Float64
print("UInt quotient:", quotient_uint)
Int sum: -3
UInt sum: 17
Mixed sum: 2
Int quotient: -1.75
UInt quotient: 1.125

Значения SIMD

Стандартная библиотека Mojo определяет тип SIMD для представления массива значений фиксированного размера, который может помещаться в регистр процессора. Это позволяет использовать преимущества одной команды и нескольких операций с данными в аппаратном обеспечении для эффективной параллельной обработки нескольких значений. Значения SIMD числового типа поддерживают все арифметические операции, за исключением матричного умножения (@), хотя операторы сдвига влево (<<) и вправо (>>) поддерживают только целочисленные типы. Кроме того, значения SIMD целого или логического типа поддерживают все побитовые операторы. Значения SIMD применяют операторы поэлементно, как показано в следующем примере:

simd1 = SIMD[DType.int32, 4](2, 3, 4, 5)
simd2 = SIMD[DType.int32, 4](-1, 2, -3, 4)
simd3 = simd1 * simd2
print(simd3)
[-2, 6, -12, 20]

Скалярные значения - это просто псевдонимы для одноэлементных SIMD-векторов, поэтому Float16 - это просто псевдоним для SIMD[DType.float16, 1]. Следовательно, скалярные значения поддерживают один и тот же набор арифметических и побитовых операторов.

var f1: Float16 = 2.5
var f2: Float16 = -4.0
var f3 = f1 * f2  # Неявно тип Float16
print(f3)
-10.0

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

var i8: Int8 = 8
var f64: Float64 = 64.0
result = i8 * f64
error: invalid call to '__mul__': failed to infer parameter 'type' of parent struct 'SIMD'
    result = i8 * f64
             ~~~^~~~~
Если вам нужно выполнить арифметическую или побитовую операцию над двумя SIMD-значениями разных типов, вы можете явно преобразовать значение в нужный тип, либо вызвав его метод cast(), либо передав его в качестве аргумента конструктору целевого типа.

Например, чтобы исправить предыдущий пример, добавьте явное преобразование:

var i8: Int8 = 8
var f64: Float64 = 64.0
result = Float64(i8) * f64

Вот еще несколько примеров преобразования значений SIMD с использованием как конструкторов, так и метода cast():

simd4 = SIMD[DType.float32, 4](2.2, 3.3, 4.4, 5.5)
simd5 = SIMD[DType.int16, 4](-1, 2, -3, 4)
simd6 = simd4 * simd5.cast[DType.float32]()  # Convert with cast() method
print("simd6:", simd6)
simd7 = simd5 + SIMD[DType.int16, 4](simd4)  # Convert with SIMD constructor
print("simd7:", simd7)
simd6: [-2.2, 6.6, -13.200001, 22.0]
simd7: [1, 5, 1, 9]

Единственным исключением является то, что оператор возведения в степень, **, перегружен, так что вы можете указать показатель степени типа Int. Все значения в SIMD возводятся в одну и ту же степень.

base_simd = SIMD[DType.float64, 4](1.1, 2.2, 3.3, 4.4)
var power: Int = 2
pow_simd = base_simd ** power  # Result is SIMD[DType.float64, 4]
print(pow_simd)
[1.2100000000000002, 4.8400000000000007, 10.889999999999999, 19.360000000000003]

Существует три оператора, связанных с разделением:

  • /, оператор "истинного деления", выполняет деление с плавающей запятой для значений SIMD с типом DType с плавающей запятой. Для значений SIMD с целым типом DType при истинном делении частное усекается до целого результата.

    num_float16 = SIMD[DType.float16, 4](3.5, -3.5, 3.5, -3.5)
    denom_float16 = SIMD[DType.float16, 4](2.5, 2.5, -2.5, -2.5)
    
    num_int32 = SIMD[DType.int32, 4](5, -6, 7, -8)
    denom_int32 = SIMD[DType.int32, 4](2, 3, -4, -5)
    
    # Result is SIMD[DType.float16, 4]
    true_quotient_float16 = num_float16 / denom_float16
    print("True float16 division:", true_quotient_float16)
    
    # Result is SIMD[DType.int32, 4]
    true_quotient_int32 = num_int32 / denom_int32
    print("True int32 division:", true_quotient_int32)
    
    True float16 division: [1.4003906, -1.4003906, -1.4003906, 1.4003906]
    True int32 division: [2, -2, -1, 1]
    

  • //, оператор "поэтажного деления", выполняет деление и округляет результат в меньшую сторону до ближайшего целого числа. Результирующий SIMD по-прежнему имеет тот же тип, что и исходные операнды. Например:

    # Result is SIMD[DType.float16, 4]
    var floor_quotient_float16 = num_float16 // denom_float16
    print("Floor float16 division:", floor_quotient_float16)
    
    # Result is SIMD[DType.int32, 4]
    var floor_quotient_int32 = num_int32 // denom_int32
    print("Floor int32 division:", floor_quotient_int32)
    
    Floor float16 division: [1.0, -2.0, -2.0, 1.0]
    Floor int32 division: [2, -2, -2, 1]
    

  • %, оператор вычисления по модулю, возвращает остаток после деления числителя на знаменатель на целое число раз. Соотношение между операторами // и % может быть определено как `num == denom * (num // denom) + (num % denom). Например:

    # Result is SIMD[DType.float16, 4]
    var remainder_float16 = num_float16 % denom_float16
    print("Modulo float16:", remainder_float16)
    
    # Result is SIMD[DType.int32, 4]
    var remainder_int32 = num_int32 % denom_int32
    print("Modulo int32:", remainder_int32)
    
    print()
    
    # Result is SIMD[DType.float16, 4]
    var result_float16 = denom_float16 * floor_quotient_float16 + remainder_float16
    print("Result float16:", result_float16)
    
    # Result is SIMD[DType.int32, 4]
    var result_int32 = denom_int32 * floor_quotient_int32 + remainder_int32
    print("Result int32:", result_int32)
    
    Modulo float16: [1.0, 1.5, -1.5, -1.0]
    Modulo int32: [1, 0, -1, -3]
    
    Result float16: [3.5, -3.5, 3.5, -3.5]
    Result int32: [5, -6, 7, -8]
    

Значения IntLiteral и FloatLiteral

IntLiteral и FloatLiteral - это числовые значения во время компиляции. Когда они используются в контексте времени компиляции, они являются значениями произвольной точности. Когда они используются в контексте выполнения, они материализуются как значения типа Int и Float64 соответственно.

В качестве примера, следующий код вызывает ошибку во время компиляции, поскольку вычисленное значение IntLiteral слишком велико для хранения в переменной Int:

alias big_int = (1 << 65) + 123456789  # IntLiteral
var too_big_int: Int = big_int
print("Result:", too_big_int)
note: integer value 36893488147542560021 requires 67 bits to store, but the destination bit width is only 64 bits wide

Однако в следующем примере, когда берется то же самое внутрибуквенное значение, выполняется деление на внутрибуквенное значение 10 и затем результат присваивается переменной Int, компиляция и выполнение выполняются успешно, поскольку итоговое внутрибуквенное значение может поместиться в 64-разрядный Int.

alias big_int = (1 << 65) + 123456789  # IntLiteral
var not_too_big_int: Int = big_int // 10
print("Result:", not_too_big_int)
Result: 3689348814754256002
В контексте времени компиляции значения IntLiteral и FloatLiteral поддерживают все арифметические операторы, кроме возведения в степень (**), а значения IntLiteral поддерживают все побитовые операторы и операторы сдвига. В контексте выполнения материализованные буквенные значения являются значениями Int и, следовательно, поддерживают те же операторы, что и Int, а материализованные буквенные значения с плавающей точкой являются значениями Float64 и, следовательно, поддерживают те же операторы, что и Float64.

Операторы сравнения

Mojo поддерживает стандартный набор операторов сравнения: ==, !=, <, <=, >, и >=. Однако их поведение зависит от типа сравниваемых значений.

В оставшейся части этого раздела описаны операторы числового сравнения. Сравнение строк обсуждается в разделе "Операторы строк". Несколько других типов в стандартной библиотеке Mojo поддерживают различные операторы сравнения, в частности сравнения "равно" и "не равно". Обратитесь к документации по API для определения типа, чтобы определить, поддерживаются ли какие-либо операторы сравнения.

Bool-возвращающие сравнения

Эти сравнения возвращают одно логическое значение:

  • Int, UInt, IntLiteral и любой тип, который может быть неявно преобразован в Int или UInt, выполняют стандартное численное сравнение с результатом Bool.

  • Операторы равенства (== и !=) с многоэлементными SIMD-значениями возвращают Bool-результат, используя семантику сокращения. Сравнение выполняется только в том случае, если оно выполняется для всех соответствующих элементов. Например:

    simd8 = SIMD[DType.int32, 4](1, 2, 3, 2)
    simd9 = SIMD[DType.int32, 4](1, 2, 4, 2)
    print("simd8 == simd9:", simd8 == simd9)  # False (element 2 differs)
    print("simd8 != simd9:", simd8 != simd9)  # True (not all elements equal)
    
    simd8 == simd9: False
    simd8 != simd9: True
    

  • Операторы неравенств (<, <=, >, >=) с многоэлементными SIMD-значениями не поддерживаются. Эти операторы работают только со скалярными (одноэлементными) SIMD-значениями .

  • Скалярные значения являются просто псевдонимами для одноэлементных SIMD-векторов и поддерживают все операторы сравнения с результатами Bool:

    var float1: Float16 = 12.345         # SIMD[DType.float16, 1]
    var float2: Float32 = 0.5            # SIMD[DType.float32, 1]
    result = Float32(float1) > float2    # Результат Bool
    print(result)
    
    True
    

Поэлементные сравнения

Для поэлементных сравнений, которые возвращают результат SIMD[DType.bool], используйте методы сравнения: eq(), ne(), lt(), le(), gt() и ge(). Эти методы работают как при сравнении SIMD-to-SIMD, так и при сравнении SIMD-to-scalar. Ниже приведены примеры, демонстрирующие все шесть методов поэлементного сравнения:

simd8 = SIMD[DType.int32, 4](1, 2, 3, 2)
simd9 = SIMD[DType.int32, 4](1, 2, 4, 2)

print("simd8.eq(simd9):", simd8.eq(simd9))    # Equal
print("simd8.ne(simd9):", simd8.ne(simd9))    # Not equal
print("simd8.lt(simd9):", simd8.lt(simd9))    # Less than
print("simd8.le(simd9):", simd8.le(simd9))    # Less than or equal
print("simd8.gt(simd9):", simd8.gt(simd9))    # Greater than
print("simd8.ge(simd9):", simd8.ge(simd9))    # Greater than or equal

simd8.eq(simd9): [True, True, False, True]
simd8.ne(simd9): [False, False, True, False]
simd8.lt(simd9): [False, False, True, False]
simd8.le(simd9): [True, True, True, True]
simd8.gt(simd9): [False, False, False, False]
simd8.ge(simd9): [True, True, False, True]

Вы также можете использовать эти методы для SIMD- и скалярных сравнений:

simd4 = SIMD[DType.int16, 4](-1, 2, -3, 4)
simd5 = simd4.gt(2)  # SIMD[DType.bool, 4]
print("simd4.gt(2):", simd5)

simd6 = SIMD[DType.float32, 4](1.1, -2.2, 3.3, -4.4)
simd7 = simd6.gt(0.5)  # SIMD[DType.bool, 4]
print("simd6.gt(0.5):", simd7)

simd4.gt(2): [False, False, False, True]
simd6.gt(0.5): [True, False, True, False]

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

Строковые операторы

Как обсуждалось в разделе Типы, тип String представляет собой изменяемое строковое значение. В отличие от этого, тип StringLiteral представляет собой буквальную строку, которая встроена в вашу скомпилированную программу, но во время выполнения она преобразуется в строку, что позволяет изменять ее:

message = "Hello"       # type = String
alias name = " Pat"       # type = StringLiteral
greeting = " good Day!"  # type = String

# Mutate the original `message` String
message += name
message += greeting
print(message)
Hello Pat good day!
Это означает, что буквенные значения String могут быть перемешаны со строковыми значениями в любом выражении среды выполнения без необходимости преобразования между типами.

Объединение строк

Оператор + выполняет объединение строк. Тип StringLiteral поддерживает объединение строк во время компиляции.

alias last_name = "Curie"

# Compile-time StringLiteral alias
alias marie = "Marie " + last_name
print(marie)

# Compile-time concatenation before materializing to a run-time `String`
pierre = "Pierre " + last_name
print(pierre)

При объединении нескольких значений в строку использование конструктора String() с несколькими аргументами более эффективно, чем использование нескольких операторов объединения +, и может улучшить читаемость кода. Например, вместо написания этого:

result = "The point at (" + String(x) + ", " + String(y) + ")"
вы можете написать:
result = String("The point at (", x, ", ", y, ")")
Это приведет к записи базовых данных с использованием стекового буфера и приведет к выделению и memcpy в куче только один раз.

Репликация строк

Оператор * повторяет строку заданное количество раз. Например:

var str1: String = "la"
str2 = str1 * 5
print(str2)
lalalalala

StringLiteral поддерживает оператор * как для репликации строк во время компиляции, так и для репликации строк во время выполнения. В следующих примерах выполняется репликация строк во время компиляции, в результате чего получаются значения StringLiteral:

alias divider1 = "=" * 40
alias symbol = "#"
alias divider2 = symbol * 40


# Вы должны определить следующую функцию, используя `fn`, потому что alias
# не может вызвать функцию, которая потенциально может вызвать ошибку.
fn generate_divider(char: String, repeat: Int) -> String:
    return char * repeat

alias divider3 = generate_divider("~", 40)  # Evaluated at compile-time

print(divider1)
print(divider2)
print(divider3)
========================================
########################################
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Напротив, в следующих примерах выполняется репликация строк во время выполнения, что приводит к получению строковых значений:

repeat = 40
div1 = "^" * repeat
print(div1)
print("_" * repeat)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
________________________________________

Сравнение строк

Значения String и StringLiteral могут быть сопоставлены с использованием стандартного лексикографического порядка, что позволяет получить значение Bool. Например, "Zebra" рассматривается как меньшее, чем "ant", поскольку в кодировке символов заглавные буквы располагаются перед строчными.

var animal: String = "bird"

is_cat_eq = "cat" == animal
print('Is "cat" equal to "{}"?'.format(animal), is_cat_eq)

is_cat_ne = "cat" != animal
print('Is "cat" not equal to "{}"?'.format(animal), is_cat_ne)

is_bird_eq = "bird" == animal
print('Is "bird" equal to "{}"?'.format(animal), is_bird_eq)

is_cat_gt = "CAT" > animal
print('Is "CAT" greater than "{}"?'.format(animal), is_cat_gt)

is_ge_cat = animal >= "CAT"
print('Is "{}" greater than or equal to "CAT"?'.format(animal), is_ge_cat)
Is "cat" equal to "bird"? False
Is "cat" not equal to "bird"? True
Is "bird" equal to "bird"? True
Is "CAT" greater than "bird"? False
Is "bird" greater than or equal to "CAT"? True

Проверка подстрок

String, StringLiteral и StringSlice поддерживают использование оператора in для получения логического результата, указывающего, появляется ли данная подстрока в другой строке. Оператор перегружен, так что вы можете использовать любую комбинацию String и StringLiteral как для подстроки, так и для строки для тестирования.

var food: String = "peanut butter"

if "nut" in food:
    print("It contains a nut")
else:
    print("It doesn't contain a nut")
It contains a nut

Индексация и slicing строк

String, StringLiteral и StringSlice позволяют использовать индексацию для возврата одного символа. Позиции символов определяются с помощью индекса, основанного на нуле, начиная с первого символа. Вы также можете указать отрицательный индекс для обратного отсчета от конца строки, при этом последний символ будет обозначаться индексом -1. Указание индекса, выходящего за пределы строки, приводит к ошибке во время выполнения.

var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  # String type value
print(alphabet[0], alphabet[-1])

# The following would produce a run-time error
# print(alphabet[45])
A Z

Типы String и StringSlice, но не тип StringLiteral, также поддерживают срезы для возврата подстроки из исходной строки. Предоставление фрагмента в виде [start:end] возвращает подстроку, начинающуюся с индекса символа, указанного в start, и продолжающуюся до символа в конце индекса, но не включающую его. Вы можете использовать положительную или отрицательную индексацию как для начального, так и для конечного значений. Опускать начало - это то же самое, что указывать 0, а опускать конец - то же самое, что указывать длину строки.

var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" # String type value
print(alphabet[1:4])  # The 2nd through 4th characters
print(alphabet[:6])   # The first 6 characters
print(alphabet[-6:])  # The last 6 characters
BCD
ABCDEF
UVWXYZ

Вы также можете указать фрагмент со значением шага, как в [start:end:step], указывающим приращение между последующими индексами слайда. (Это также иногда называют "шагом".) Если вы укажете отрицательное значение для шага, символы будут выбраны в обратном порядке, начиная с начального, но затем с уменьшением значений индекса вплоть до конца, но не включая его.

print(alphabet[1:6:2])     # The 2nd, 4th, and 6th characters
print(alphabet[-1:-4:-1])  # The last 3 characters in reverse order
print(alphabet[::-1])      # The entire string reversed
BDF
ZYX
ZYXWVUTSRQPONMLKJIHGFEDCBA

Операторы присваивания на месте

Изменяемые типы, поддерживающие двоичную арифметику, побитовые операторы и операторы сдвига, обычно поддерживают эквивалентные операторы присваивания на месте. Это означает, что для типа, поддерживающего оператор +, следующие два оператора по существу эквивалентны:

a = a + b
a += b

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

Тип должен явно реализовывать методы присваивания на месте, поэтому вы можете столкнуться с некоторыми типами, в которых эквиваленты на месте не поддерживаются.

Выражения присваивания

Оператор присваивания("walrus"), :=, позволяет присвоить значение переменной в выражении. Указанное значение одновременно присваивается переменной и становится результатом выражения. Это часто может упростить логику выполнения условных или циклических операций. Например, рассмотрим следующий цикл запроса:

while True:
    name = input("Enter a name or 'quit' to exit: ")
    if name == "quit":
        break
    print("Hello,", name)
Enter a name or 'quit' to exit: Coco
Hello, Coco
Enter a name or 'quit' to exit: Vivienne
Hello, Vivienne
Enter a name or 'quit' to exit: quit

Используя оператор walrus, вы можете реализовать такое же поведение следующим образом:

while (name := input("Enter a name or 'quit' to exit: ")) != "quit":
    print("Hello,", name)
Enter a name or 'quit' to exit: Donna
Hello, Donna
Enter a name or 'quit' to exit: Vera
Hello, Vera
Enter a name or 'quit' to exit: quit

Слияние типов

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

Следующий код демонстрирует слияние типов на основе неявных преобразований:

list = [0.5, 1, 2]
for value in list:
    print(value)
0.5
1.0
2.0

Здесь list-литерал включает в себя как float-литералы, так и целочисленные литералы, которые реализуются как Float64 и Int соответственно. Поскольку Int может быть неявно преобразован в Float64, результатом является List[Float64].

Вот пример того, где слияние типов не удается:

a: Int = 0
b: String = "Hello"
c = a if a > 0 else b   # Error: value of type 'Int' is not compatible with
                        # value of type 'String'mojo

В этом случае Int не может быть неявно преобразован в String, а String не может быть неявно преобразована в Int, поэтому слияние типов завершается неудачей. Это правильный результат: Mojo не может узнать, какой тип вы хотите, чтобы c принял. Вы можете исправить это, добавив явное преобразование:

c = String(a) if a > 0 else b

Отдельные структуры могут определять пользовательскую логику слияния типов, определяя метод __merge_with__(). Например:

@fieldwise_init
struct MyType(Movable, Copyable):
    var val: Int

    def __bool__(self) -> Bool:
        return self.val > 0

    def __merge_with__[other_type: type_of(Int)](self) -> Int:
        return Int(self.val)

def main():
    i = 0
    m = MyType(9)
    print(i if i > 0 else m)  # prints "9"

Если какой-либо из типов в выражении определяет пользовательский параметр __merge_with__() для слияния с другим типом, этот тип имеет приоритет над любыми неявными преобразованиями. (Обратите внимание, что результирующий тип не обязательно должен быть одним из входных типов, это может быть третий тип.)

Тип может объявлять несколько переопределений __merge_with__() для разных типов.

На высоком уровне логика объединения двух типов выглядит следующим образом:

  • Определяет ли какой-либо из типов метод __merge_with__() для другого типа? Если да, то возвращаемое значение определяет целевой тип.
    • Если оба типа определяют метод __merge_with__() для другого типа, оба метода должны возвращать один и тот же тип, иначе преобразование завершится ошибкой.
    • Оба типа должны быть неявно преобразуемыми в целевой тип (тип всегда неявно преобразуется в сам себя).
  • Является ли какой-либо из типов неявно преобразуемым в другой тип?
    • Если только один тип неявно преобразуется в другой тип, преобразуйте его.
    • Если оба типа преобразуются в другой тип, преобразование будет неоднозначным и завершится ошибкой.

Более подробную информацию о слиянии типов и методе __merge_with__() смотрите в предложении "Настраиваемое слияние типов" в Mojo.

Реализуйте операторы для пользовательских типов

Когда вы создаете пользовательскую структуру, Mojo позволяет вам определять поведение многих встроенных операторов для этого типа, реализуя специальные методы dunder (двойное подчеркивание). В этом разделе перечислены методы dunder, связанные с операторами, и кратко описаны требования к их реализации.

В настоящее время Mojo не поддерживает определение произвольных пользовательских операторов (например, -^-). Вы можете определить поведение только для операторов, перечисленных в следующих подразделах.

Методы определения унарного оператора

Унарный оператор вызывает соответствующий метод определения для значения, к которому он применяется. Поддерживаемые унарные операторы и соответствующие им методы приведены в таблице ниже.

Operator Dunder method
+ positive pos()
- negative neg()
~ bitwise NOT invert()

Для каждого из этих методов, которые вы решите реализовать, вы должны возвращать либо исходное значение, если оно не изменилось, либо новое значение, представляющее результат выполнения оператора. Например, вы могли бы реализовать оператор - negative для структуры MyInt следующим образом:

@fieldwise_init
struct MyInt:
    var value: Int

    def __neg__(self) -> Self:
        return Self(-self.value)

Методы определения двоичной арифметики, сдвига и побитовых операторов

Когда у вас есть двоичное выражение типа a + b, есть два возможных метода, которые могут быть вызваны.

Mojo сначала определяет, имеет ли значение левой части (a в этом примере) "обычную" версию метода для оператора +, которая принимает значение типа правой части. Если это так, то он затем вызывает этот метод для левого значения и передает правое значение в качестве аргумента.

Если Mojo не находит соответствующий "обычный" метод для значения левой части, он затем проверяет, имеет ли значение правой части "отраженную" (иногда называемую "перевернутой") версию метода оператора +, которая принимает значение левой части. Если это так, то он вызывает этот метод для значения в правой части и передает значение в левой части в качестве аргумента.

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

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

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

Operator Normal Reflected In-place
+ addition add() radd() iadd()
- subtraction sub() rsub() isub()
* multiplication mul() rmul() imul()
/ division truediv() rtruediv() itruediv()
// floor division floordiv() rfloordiv() ifloordiv()
% modulus/remainder mod() rmod() imod()
** exponentiation pow() rpow() ipow()
@ matrix multiplication matmul() rmatmul() imatmul()
<< left shift lshift() rlshift() ilshift()
>> right shift rshift() rrshift() irshift()
& bitwise AND and() rand() iand()
| bitwise OR or() ror() ior()
^ bitwise XOR xor() rxor() ixor()

В качестве примера рассмотрим реализацию поддержки всех методов + для пользовательской структуры MyInt. Здесь показана поддержка добавления двух экземпляров MyInt, а также добавления MyInt и Int. Мы можем поддержать случай использования Int в качестве правого аргумента, перегрузив определение __add__(). Но для поддержки использования Int в качестве левого аргумента нам нужно реализовать метод __radd__(), потому что встроенный тип Int не имеет метода __add__(), который поддерживает наш пользовательский тип MyInt.

@fieldwise_init
struct MyInt:
    var value: Int

    def __add__(self, rhs: MyInt) -> Self:
        return MyInt(self.value + rhs.value)

    def __add__(self, rhs: Int) -> Self:
        return MyInt(self.value + rhs)

    def __radd__(self, lhs: Int) -> Self:
        return MyInt(self.value + lhs)

    def __iadd__(mut self, rhs: MyInt) -> None:
        self.value += rhs.value

    def __iadd__(mut self, rhs: Int) -> None:
        self.value += rhs

Dunder методы оператора сравнения

Когда у вас есть выражение сравнения, такое как a < b, Mojo вызывает связанный метод для значения в левой части и передает значение в правой части в качестве аргумента. Mojo не поддерживает "отраженные" версии этих методов, потому что вы должны сравнивать только значения одного типа. Методы для сравнения должны возвращать Bool-результат, представляющий результат сравнения.

Есть две особенности, связанные с методами для сравнения. Тип, который реализует сопоставимый трейт, определяет все методы сравнения, и авторы должны реализовать, по крайней мере, методы "меньше, чем" и "равно", поскольку этот трейт предоставляет значения по умолчанию для остальных. Однако некоторые типы не имеют естественного порядка (например, комплексные числа). Для этих типов вы можете реализовать трейт Equatable, который определяет только методы сравнения "равно" и "не равно", при этом "равно" требуется реализовать с помощью соответствующих структур.

Поддерживаемые операторы сравнения и соответствующие им методы приведены в таблице ниже.

Operator Dunder method
== equal eq()
!= not equal ne()
< less than lt()
<= less than or equal le()
> greater than gt()
>= greater than or equal ge()

Трейты Comparable и Equatable не позволяют методам сравнения вызывать ошибки. Поскольку использование def для определения метода подразумевает, что он может вызвать ошибку, вы должны использовать fn для реализации методов сравнения, объявленных этими трейтами. Дополнительные сведения о различиях между определением функций с помощью def и fn.

В качестве примера рассмотрим реализацию поддержки всех методов оператора сравнения для пользовательской структуры MyInt, опираясь на реализации по умолчанию, предоставляемые сопоставимыми (и транзитивно эквивалентными) трейту.

@fieldwise_init
struct MyInt(Comparable):
    var value: Int

    fn __eq__(self, rhs: MyInt) -> Bool:
        return self.value == rhs.value

    fn __lt__(self, rhs: MyInt) -> Bool:
        return self.value < rhs.value

    # `__ne__`, `__le__`, `__gt__`, and `__ge__` have default implementations.

Dunder методы Membership оператора

Операторы in и not in зависят от типа, реализующего метод определения __contains__(). Обычно этот метод реализуется только в типах коллекций (таких как List, Dict и Set). Он должен принимать правостороннее значение в качестве аргумента и возвращать Bool, указывающий, присутствует ли это значение в коллекции или нет.

Dunder методы Subscript и slicing

Подписывание и разбиение на части обычно применяются только к последовательным типам коллекций, таким как List и String. Подписание ссылается на отдельный элемент коллекции или измерение многомерного контейнера, тогда как разбиение относится к диапазону значений. Тип поддерживает как подписывание, так и разбиение на части, реализуя метод __getitem__() для извлечения значений и метод __setitem__() для установки значений.

Индексация

В простом случае одномерной последовательности методы __getitem__() и __setitem__() должны иметь сигнатуры, аналогичные этой:

struct MySeq[type: Copyable & Movable]:
    fn __getitem__(self, idx: Int) -> type:
        # Возвращает элемент с заданным индексом
        ...
    fn __setitem__(mut self, idx: Int, value: type):
        # Присвойте элементу с заданным индексом указанное значение

Также возможна поддержка многомерных коллекций, и в этом случае вы можете реализовать методы __getitem__() и __setitem__() для приема нескольких аргументов индекса или даже переменных аргументов индекса для коллекций произвольного размера.

struct MySeq[type: Copyable & Movable]:
    # поддержка 2-х измерений
    fn __getitem__(self, x_idx: Int, y_idx: Int) -> type:
        ...
    # Поддержка произвольных размеров
    fn __getitem__(self, *indices: Int) -> type:
        ...

Slicing

Вы также обеспечиваете поддержку slicing для типа коллекции, реализуя методы __getitem__() и __setitem__(). Но для нарезки вместо того, чтобы принимать индекс Int (или индексы, в случае многомерной коллекции), вы реализуете методы для приема фрагмента (или нескольких фрагментов в случае многомерной коллекции).

struct MySeq[type: Copyable & Movable]:
    # Return a new MySeq with a subset of elements
    fn __getitem__(self, span: Slice) -> Self:
        ...

Slice содержит три поля:

  • start (Optional[Int]): Начальный индекс среза
  • end (Optional[Int]): Конечный индекс
  • step (Optional[Int]): Значение шага приращения среза.

Поскольку значения start, end и step являются необязательными при использовании slice синтаксиса, они представлены в виде значений Optional[Int] в Slice. И если они присутствуют, значения индекса могут быть отрицательными, представляющими относительную позицию от конца последовательности. Для удобства Slice предоставляет метод indexes(), который принимает значение длины и возвращает 3-кратный набор "нормализованных" значений начала, конца и шага для заданной длины, все они представлены в виде неотрицательных значений. Затем вы можете использовать эти нормализованные значения для определения соответствующих элементов вашей коллекции, на которые ссылаются.

struct MySeq[type: Copyable & Movable]:
    var size: Int

    # Возвращает новый MySeq с подмножеством элементов
    fn __getitem__(self, span: Slice) -> Self:
        var start: Int
        var end: Int
        var step: Int
        start, end, step = span.indices(self.size)
        ...

Пример реализации операторов для пользовательского типа

В качестве примера реализации операторов для пользовательского типа Mojo давайте создадим сложную структуру для представления одного комплексного числа, причем как действительная, так и мнимая составляющие будут храниться в виде значений Float64. Мы реализуем большинство арифметических операторов, связанные с ними операторы присваивания на месте, операторы сравнения на равенство и несколько дополнительных удобных методов для поддержки таких операций, как печать сложных значений. Мы также разрешим смешивать сложные значения и значения с плавающей точкой в арифметических выражениях для получения сложного результата.

В этом примере наша сложная структура строится поэтапно. Вы также можете найти полный пример в общедоступном репозитории на GitHub.

Обратите внимание, что стандартная библиотека Mojo реализует параметризованную структуру ComplexSIMD, которая обеспечивает поддержку базового набора арифметических операторов. Однако наш тип Complex не будет основан на структуре ComplexSIMD и не будет совместим с ней.

Реализация методов жизненного цикла

Наша сложная структура - это пример простого типа значения, состоящего из тривиальных числовых полей и не требующего специальных действий конструктора или деструктора. Это означает, что мы можем использовать декоратор @register_passable("trivial"), который объявляет, что тип может быть тривиально скопирован, перемещен и уничтожен — и не нуждается в конструкторе копирования, конструкторе перемещения или деструкторе.

На данный момент мы также будем использовать декоратор @fieldwise_init для автоматической реализации полевого инициализатора (конструктора с аргументами для каждого поля).

@fieldwise_init
@register_passable("trivial")
struct Complex:
    var re: Float64
    var im: Float64

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

c1 = Complex(-1.2, 6.5)
print("c1: Real: {}; Imaginary: {}".format(c1.re, c1.im))
c1: Real: -1.2; Imaginary: 6.5

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

@register_passable("trivial")
struct Complex():
    var re: Float64
    var im: Float64

    fn __init__(out self, re: Float64, im: Float64 = 0.0):
        self.re = re
        self.im = im

Поскольку этот конструктор также обрабатывает создание сложного экземпляра как с реальными, так и с воображаемыми компонентами, нам больше не нужен декоратор @fieldwise_init.

Теперь мы можем создать сложный экземпляр и предоставить только реальный компонент.

c2 = Complex(3.14159)
print("c2: Real: {}; Imaginary: {}".format(c2.re, c2.im))
c2: Real: 3.1415899999999999; Imaginary: 0.0

Реализация трейтов Writable и Stringable

Чтобы упростить вывод сложных значений, давайте реализуем свойство, доступное для записи. Кроме того, давайте также реализуем свойство Stringable, чтобы мы могли использовать конструктор String() для создания строкового представления сложного значения. Вы можете узнать больше об этих трейтах и связанных с ними методах в разделах Stringable, Representable и Writable трейты.

@register_passable("trivial")
struct Complex(
    Writable,
    Stringable,
):
    # ...

    fn __str__(self) -> String:
        return String.write(self)

    fn write_to(self, mut writer: Some[Writer]):
        writer.write("(", self.re)
        if self.im < 0:
            writer.write(" - ", -self.im)
        else:
            writer.write(" + ", self.im)
        writer.write("i)")

Свойство Writable не позволяет методу write_to() выдавать ошибку, а свойство Stringable не позволяет методу __str__() выдавать ошибку. Поскольку определение метода с помощью def подразумевает, что это может привести к ошибке, вместо этого мы должны определить эти методы с помощью fn. Смотрите раздел Функции для получения дополнительной информации о различиях между определением функций с помощью def и fn.

Теперь мы можем напечатать комплексное значение напрямую, и мы можем явно сгенерировать строковое представление, передав комплексное значение в String(), которая создает новую строку из всех переданных ей аргументов.

c3 = Complex(3.14159, -2.71828)
print("c3 =", c3)

var msg = String("The value is: ", c3)
print(msg)
c3 = (3.1415899999999999 - 2.71828i)
The value is: (3.1415899999999999 - 2.71828i)

Реализация базовой индексации

Индексация обычно поддерживается только типами коллекций. Но в качестве примера давайте реализуем поддержку доступа к реальному компоненту с индексом 0 и к воображаемому компоненту с индексом 1. В этом примере мы не будем реализовывать нарезку или присвоение переменных.

    # ...
    def __getitem__(self, idx: Int) -> Float64:
        if idx == 0:
            return self.re
        elif idx == 1:
            return self.im
        else:
            raise "index out of bounds"

    def __setitem__(mut self, idx: Int, value: Float64) -> None:
        if idx == 0:
            self.re = value
        elif idx == 1:
            self.im = value
        else:
            raise "index out of bounds"

Теперь давайте попробуем получить и задать действительную и мнимую составляющие комплексного значения с помощью индексации.

c2 = Complex(3.14159)
print("c2[0]: {}; c2[1]: {}".format(c2[0], c2[1]))
c2[0] = 2.71828
c2[1] = 42
print("c2[0] = 2.71828; c2[1] = 42; c2:", c2)

c2[0]: 3.1415899999999999; c2[1]: 0.0
c2[0] = 2.71828; c2[1] = 42; c2: (2.71828 + 42.0i)

Реализовывать арифметические операторы

Теперь давайте реализуем dunder методы, которые позволяют нам выполнять арифметические операции с комплексными значениями. (Более подробное объяснение формул для этих операторов приведено на странице Википедии, посвященной комплексным числам).

Реализуем базовые операторы для комплексных значений.

Унарный оператор + просто возвращает исходное значение, тогда как унарный оператор - возвращает новое комплексное значение с отрицательными действительной и мнимой составляющими.

    # ...
    def __pos__(self) -> Self:
        return self

    def __neg__(self) -> Self:
        return Self(-self.re, -self.im)

Давайте проверим это, распечатав результат применения каждого оператора.

c1 = Complex(-1.2, 6.5)
print("+c1:", +c1)
print("-c1:", -c1)

+c1: (-1.2 + 6.5i)
-c1: (1.2 - 6.5i)

Далее мы реализуем основные бинарные операторы: +, -, * и /. Деление комплексных чисел немного сложнее, поэтому мы также определим вспомогательный метод norm() для вычисления евклидовой нормы сложного экземпляра, который также может быть полезен для других типов анализа со сложными числами. цифры.

Для всех этих dunder методов левосторонним операндом является self, а правосторонний операнд передается в качестве аргумента. Мы возвращаем новое комплексное значение, представляющее результат.

from math import sqrt

# ...

    def __add__(self, rhs: Self) -> Self:
        return Self(self.re + rhs.re, self.im + rhs.im)

    def __sub__(self, rhs: Self) -> Self:
        return Self(self.re - rhs.re, self.im - rhs.im)

    def __mul__(self, rhs: Self) -> Self:
        return Self(
            self.re * rhs.re - self.im * rhs.im,
            self.re * rhs.im + self.im * rhs.re
        )

    def __truediv__(self, rhs: Self) -> Self:
        denom = rhs.squared_norm()
        return Self(
            (self.re * rhs.re + self.im * rhs.im) / denom,
            (self.im * rhs.re - self.re * rhs.im) / denom
        )

    def squared_norm(self) -> Float64:
        return self.re * self.re + self.im * self.im

    def norm(self) -> Float64:
        return sqrt(self.squared_norm())

Теперь мы можем их опробовать.

c1 = Complex(-1.2, 6.5)
c3 = Complex(3.14159, -2.71828)
print("c1 + c3 =", c1 + c3)
print("c1 - c3 =", c1 - c3)
print("c1 * c3 =", c1 * c3)
print("c1 / c3 =", c1 / c3)
c1 + c3 = (1.9415899999999999 + 3.78172i)
c1 - c3 = (-4.3415900000000001 + 9.21828i)
c1 * c3 = (13.898912000000001 + 23.682270999999997i)
c1 / c3 = (-1.2422030701265261 + 0.99419218883955773i)

Реализуйте перегруженные арифметические операторы для значений Float64

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

Для случая, когда у нас есть complex1 + float1, мы можем просто создать перегруженное определение __add__(). Но как насчет случая float1 + complex1? По умолчанию, когда Mojo встречает оператор +, он пытается вызвать метод __add__() левого операнда, но встроенный тип Float64 не реализует поддержку сложения со сложным значением. Это пример, в котором нам нужно реализовать метод __radd__() для сложного типа. Когда Mojo не может найти метод __add__(self, rhs: Complex) -> Complex, определенный в Float64, он использует метод __radd__(self, lhs: Float64) -> Complex, определенный в Complex.

Таким образом, мы можем поддерживать арифметические операции над комплексными значениями и значениями Float64, реализуя следующие восемь методов.

    # ...
    def __add__(self, rhs: Float64) -> Self:
        return Self(self.re + rhs, self.im)

    def __radd__(self, lhs: Float64) -> Self:
        return Self(self.re + lhs, self.im)

    def __sub__(self, rhs: Float64) -> Self:
        return Self(self.re - rhs, self.im)

    def __rsub__(self, lhs: Float64) -> Self:
        return Self(lhs - self.re, -self.im)

    def __mul__(self, rhs: Float64) -> Self:
        return Self(self.re * rhs, self.im * rhs)

    def __rmul__(self, lhs: Float64) -> Self:
        return Self(lhs * self.re, lhs * self.im)

    def __truediv__(self, rhs: Float64) -> Self:
        return Self(self.re / rhs, self.im / rhs)

    def __rtruediv__(self, lhs: Float64) -> Self:
        denom = self.squared_norm()
        return Self(
            (lhs * self.re) / denom,
            (-lhs * self.im) / denom
        )

Давайте посмотрим на них в действии.

c1 = Complex(-1.2, 6.5)
f1 = 2.5
print("c1 + f1 =", c1 + f1)
print("f1 + c1 =", f1 + c1)
print("c1 - f1 =", c1 - f1)
print("f1 - c1 =", f1 - c1)
print("c1 * f1 =", c1 * f1)
print("f1 * c1 =", f1 * c1)
print("c1 / f1 =", c1 / f1)
print("f1 / c1 =", f1 / c1)
c1 + f1 = (1.3 + 6.5i)
f1 + c1 = (1.3 + 6.5i)
c1 - f1 = (-3.7000000000000002 + 6.5i)
f1 - c1 = (3.7000000000000002 - 6.5i)
c1 * f1 = (-3.0 + 16.25i)
f1 * c1 = (-3.0 + 16.25i)
c1 / f1 = (-0.47999999999999998 + 2.6000000000000001i)
f1 / c1 = (-0.068665598535133904 - 0.37193865873197529i)

Реализуйте операторы присваивания на месте

Теперь давайте реализуем поддержку операторов присваивания на месте: +=, -=, *=, и /=. Они изменяют исходное значение, поэтому нам нужно пометить self как аргумент mut и обновить поля re и im вместо того, чтобы возвращать новый экземпляр Complex. И еще раз, мы перегрузим определения, чтобы поддерживать как Complex, так и Float64 операнды.

    # ...
    def __iadd__(mut self, rhs: Self) -> None:
        self.re += rhs.re
        self.im += rhs.im

    def __iadd__(mut self, rhs: Float64) -> None:
        self.re += rhs

    def __isub__(mut self, rhs: Self) -> None:
        self.re -= rhs.re
        self.im -= rhs.im

    def __isub__(mut self, rhs: Float64) -> None:
        self.re -= rhs

    def __imul__(mut self, rhs: Self) -> None:
        new_re = self.re * rhs.re - self.im * rhs.im
        new_im = self.re * rhs.im + self.im * rhs.re
        self.re = new_re
        self.im = new_im

    def __imul__(mut self, rhs: Float64) -> None:
        self.re *= rhs
        self.im *= rhs

    def __itruediv__(mut self, rhs: Self) -> None:
        denom = rhs.squared_norm()
        new_re = (self.re * rhs.re + self.im * rhs.im) / denom
        new_im = (self.im * rhs.re - self.re * rhs.im) / denom
        self.re = new_re
        self.im = new_im

    def __itruediv__(mut self, rhs: Float64) -> None:
        self.re /= rhs
        self.im /= rhs

А теперь попробуем их опробовать.

c4 = Complex(-1, -1)
print("c4 =", c4)
c4 += Complex(0.5, -0.5)
print("c4 += Complex(0.5, -0.5) =>", c4)
c4 += 2.75
print("c4 += 2.75 =>", c4)
c4 -= Complex(0.25, 1.5)
print("c4 -= Complex(0.25, 1.5) =>", c4)
c4 -= 3
print("c4 -= 3 =>", c4)
c4 *= Complex(-3.0, 2.0)
print("c4 *= Complex(-3.0, 2.0) =>", c4)
c4 *= 0.75
print("c4 *= 0.75 =>", c4)
c4 /= Complex(1.25, 2.0)
print("c4 /= Complex(1.25, 2.0) =>", c4)
c4 /= 2.0
print("c4 /= 2.0 =>", c4)
c4 = (-1.0 - 1.0i)
c4 += Complex(0.5, -0.5) => (-0.5 - 1.5i)
c4 += 2.75 => (2.25 - 1.5i)
c4 -= Complex(0.25, 1.5) => (2.0 - 3.0i)
c4 -= 3 => (-1.0 - 3.0i)
c4 *= Complex(-3.0, 2.0) => (9.0 + 7.0i)
c4 *= 0.75 => (6.75 + 5.25i)
c4 /= Complex(1.25, 2.0) => (3.404494382022472 - 1.247191011235955i)
c4 /= 2.0 => (1.702247191011236 - 0.6235955056179775i)

Реализовать операторы равенства

Поле комплексных чисел не является упорядоченным полем, поэтому для нас не имеет смысла реализовывать сопоставимый трейт и операторы >, >=, < и <=. Однако мы можем реализовать сопоставимый трейт и операторы == и !=. (Конечно, это связано с тем же ограничением, что и сравнение чисел с плавающей запятой на равенство, из-за ограниченной точности представления чисел с плавающей запятой при выполнении арифметических операций. Но мы продолжим и реализуем операторы для полноты.)

struct Complex(
    Equatable,
    Formattable,
    Stringable,
):
    # ...
    fn __eq__(self, other: Self) -> Bool:
        return self.re == other.re and self.im == other.im

    fn __ne__(self, other: Self) -> Bool:
        return self.re != other.re or self.im != other.im

Свойство Equatable не позволяет методам __eq__() и __ne__() вызывать ошибки. Поскольку определение метода с помощью def подразумевает, что он может вызвать ошибку, мы должны вместо этого определить эти методы с помощью fn. Дополнительные сведения о различиях между определением функций с помощью def и fn.

А теперь попробуем их опробовать.

c1 = Complex(-1.2, 6.5)
c3 = Complex(3.14159, -2.71828)
c5 = Complex(-1.2, 6.5)

if c1 == c5:
    print("c1 is equal to c5")
else:
    print("c1 is not equal to c5")

if c1 != c3:
    print("c1 is not equal to c3")
else:
    print("c1 is equal to c3")
c1 is equal to c5
c1 is not equal to c3