Python Modulo на практике: как использовать оператор %

Оглавление

Python поддерживает широкий спектр арифметических операторов, которые вы можете использовать при работе с числами в вашем коде. Одним из таких операторов является оператор по модулю (%), который возвращает остаток от деления двух чисел.

В этом уроке вы узнаете:

  • Как работает по модулю в математике
  • Как использовать оператор Python по модулю с различными числовыми типами
  • Как Python вычисляет результаты операции по модулю
  • Как переопределить .__mod__() в своих классах, чтобы использовать их с оператором по модулю
  • Как использовать оператор Python по модулю для решения реальных задач

Оператор Python modulo иногда можно упустить из виду. Но хорошее понимание этого оператора даст вам бесценный инструмент в вашем арсенале инструментов Python.

Модуль в математике

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

Классическим примером вычисления по модулю в модульной арифметике являются двенадцатичасовые часы. Двенадцатичасовые часы имеют фиксированный набор значений от 1 до 12. При отсчете по двенадцатичасовым часам вы производите отсчет по модулю 12, а затем возвращаетесь к 1. Двенадцатичасовые часы можно классифицировать как “по модулю 12”, иногда сокращая до “по модулю 2". 12.”

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

Например, предположим, вы хотите определить, сколько будет времени через девять часов после 8:00 утра. На двенадцатичасовых часах вы не можете просто прибавить 9 к 8, потому что получится 17. Вам нужно взять результат, 17, и использовать mod, чтобы получить его эквивалентное значение в двенадцатичасовом контексте:

8 o'clock + 9 = 17 o'clock
17 mod 12 = 5

17 mod 12 возвращает 5. Это означает, что через девять часов после 8:00 утра будет 5:00 вечера. Вы определили это, взяв число 17 и применив его к контексту mod 12.

Теперь, если вдуматься, 17 и 5 эквивалентны в контексте mod 12. Если бы вы посмотрели на часовую стрелку в 5:00 и 17:00, она была бы в одном и том же положении. В модульной арифметике есть уравнение, описывающее эту взаимосвязь:

a ≡ b (mod n)

Это уравнение гласит: “a и b равны по модулю n”. Это означает, что a и b эквивалентны в mod n, поскольку они имеют одинаковый остаток при делении на n. В приведенном выше уравнении n является модулем как для a, так и для b. Используя приведенные ранее значения 17 и 5, уравнение будет выглядеть следующим образом:

17 ≡ 5 (mod 12)

Это означает, что “17 и 5 равны по модулю 12.” 17 и 5 имеют одинаковый остаток, 5, если разделить на 12. Таким образом, в mod 12 числа 17 и 5 эквивалентны.

Вы можете подтвердить это с помощью деления:

17 / 12 = 1 R 5
5 / 12 = 0 R 5

Обе операции имеют одинаковый остаток, 5, поэтому они эквивалентны по модулю 12.

Это может показаться слишком сложной математикой для оператора Python, но наличие этих знаний подготовит вас к использованию оператора по модулю в примерах, приведенных далее в этом руководстве. В следующем разделе вы рассмотрите основы использования оператора Python modulo с числовыми типами int и float.

Основы работы с операторами по модулю в Python

Оператор вычисления по модулю, как и другие арифметические операторы, может использоваться с числовыми типами int и float. Как вы увидите позже, его также можно использовать с другими типами, такими как math.fmod(), decimal.Decimal, и вашими собственными классами.

Оператор по модулю С int

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

>>> 15 % 4
3

>>> 17 % 12
5

>>> 240 % 13
6

>>> 10 % 16
10

Будьте осторожны! Точно так же, как с оператором деления (/), Python вернет значение ZeroDivisionError, если вы попытаетесь использовать оператор умножения по модулю с делителем 0:

>>> 22 % 0
ZeroDivisionError: integer division or modulo by zero

Далее вы познакомитесь с использованием оператора по модулю с float.

Оператор по модулю С float

Аналогично int, оператор вычисления по модулю, используемый с float, вернет остаток от деления, но в виде значения float:

>>> 12.5 % 5.5
1.5

>>> 17.0 % 12.0
5.0

Альтернативой использованию float с оператором по модулю является использование math.fmod() для выполнения операций по модулю над float значениями:

>>> import math
>>> math.fmod(12.5, 5.5)
1.5

>>> math.fmod(8.5, 2.5)
1.0

Официальные документы по Python предлагают использовать math.fmod() вместо оператора Python по модулю при работе со значениями float из-за способа, которым math.fmod() вычисляет результат операции по модулю. Если вы используете отрицательный операнд, то можете увидеть разные результаты между math.fmod(x, y) и x % y. Более подробно использование оператора по модулю с отрицательными операндами будет рассмотрено в следующем разделе.

Как и другие арифметические операторы, оператор по модулю и math.fmod() могут столкнуться с проблемами округления и точности при работе с арифметикой с плавающей запятой:

>>> 13.3 % 1.1
0.09999999999999964

>>> import math
>>> math.fmod(13.3, 1.1)
0.09999999999999964

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

Оператор по модулю с отрицательным операндом

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

Как оказалось, способ, которым компьютеры определяют результат операции по модулю с отрицательным операндом, оставляет неясность относительно того, должен ли остаток принимать знак делимого (делимое число) или знак делителя (число, на которое делится делимое). Разные языки программирования справляются с этим по-разному.

Например, в JavaScript остаток будет иметь знак делимого:

8 % -3 = 2

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

8 % -3 = -1

Здесь вы можете видеть, что остаток, -1, принимает знак делителя, -3.

Возможно, вам интересно, почему остаток в JavaScript равен 2, а в Python равен -1. Это связано с тем, как разные языки определяют результат операции по модулю. Языки, в которых остаток имеет знак делимого, используют следующее уравнение для определения остатка:

r = a - (n * trunc(a/n))

В этом уравнении есть три переменные:

  1. r это остаток.
  2. a это дивиденд.
  3. n является делителем.

trunc() в этом уравнении используется усеченное деление, которое всегда округляет отрицательное число до нуля. Для получения более подробной информации смотрите приведенные ниже шаги вычисления по модулю с использованием 8 в качестве делимого и -3 в качестве делителя:

r = 8 - (-3 * trunc(8/-3))
r = 8 - (-3 * trunc(-2.666666666667))
r = 8 - (-3 * -2) # Rounded toward 0
r = 8 - 6
r = 2

Здесь вы можете увидеть, как такой язык, как JavaScript, получает остаток 2. В Python и других языках, в которых остаток принимает знак делителя, используется следующее уравнение:

r = a - (n * floor(a/n))

floor() в этом уравнении используется деление на этажи. При положительных числах деление на этажи даст тот же результат, что и при усеченном делении. Но при отрицательном числе при делении на этажи результат округляется в меньшую сторону, удаляясь от нуля:

r = 8 - (-3 * floor(8/-3))
r = 8 - (-3 * floor(-2.666666666667))
r = 8 - (-3 * -3) # Rounded away from 0
r = 8 - 9
r = -1

Здесь вы можете видеть, что результат равен -1.

Теперь, когда вы понимаете, откуда берется разница в остатке, вам может быть интересно, почему это имеет значение, если вы используете только Python. Что ж, как оказалось, не все операции по модулю в Python одинаковы. В то время как значение по модулю, используемое с типами int и float, будет принимать знак делителя, другие типы - нет.

Вы можете увидеть пример этого, сравнив результаты 8.0 % -3.0 и math.fmod(8.0, -3.0):

>>> 8.0 % -3
-1.0

>>> import math
>>> math.fmod(8.0, -3.0)
2.0

math.fmod() принимает знак делимого с помощью усеченного деления, тогда как float использует знак делителя. Позже в этом руководстве вы увидите другой тип Python, который использует знак дивиденда, decimal.Decimal.

Оператор по модулю и divmod()

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

Ниже приведен пример использования divmod() с 37 и 5:

>>> divmod(37, 5)
(7, 2)

>>> 37 // 5
7

>>> 37 % 5
2

Вы можете видеть, что divmod(37, 5) возвращает кортеж (7, 2). 7 является результатом разделения уровней 37 и 5. 2 является результатом 37 по модулю 5.

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

>>> divmod(37, -5)
(-8, -3)

>>> 37 // -5
-8

>>> 37 % -5
-3 # Result has the sign of the divisor

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

Приоритет оператора по модулю

Как и для других операторов Python, для оператора по модулю существуют определенные правила, которые определяют его приоритет при вычислении выражений. Оператор умножения по модулю (%) имеет тот же уровень приоритета, что и операторы умножения (*), деления (/), и деления на число (//).

Взгляните на пример приоритета оператора по модулю ниже:

>>> 4 * 10 % 12 - 9
-5

И операторы умножения, и операции умножения по модулю имеют одинаковый уровень приоритета, поэтому Python будет оценивать их слева направо. Вот шаги для выполнения описанной выше операции:

  1. 4 * 10 оценивается, в результате чего 40 % 12 - 9.
  2. 40 % 12 оценивается, в результате чего 4 - 9.
  3. 4 - 9 оценивается, в результате чего -5.

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

>>> 4 * 10 % (12 - 9)
1

В этом примере сначала вычисляется (12 - 9), затем 4 * 10 и, наконец, 40 % 3, что равно 1.

Оператор Python по модулю на практике

Теперь, когда вы познакомились с основами Python modulo operator, вы познакомитесь с некоторыми примерами его использования для решения реальных задач программирования. Иногда бывает трудно определить, когда использовать оператор modulo в вашем коде. Приведенные ниже примеры дадут вам представление о множестве способов его использования.

Как проверить, является ли число четным или нечетным

В этом разделе вы увидите, как можно использовать оператор умножения по модулю, чтобы определить, является ли число четным или нечетным. Используя оператор modulo с модулем 2, вы можете проверить любое число, чтобы увидеть, делится ли оно равномерно на 2. Если оно делится равномерно, то это четное число.

Взгляните на is_even(), который проверяет, является ли параметр num четным:

def is_even(num):
    return num % 2 == 0

Здесь num % 2 будет равно 0, если num четно, и 1, если num нечетно. Проверка на соответствие 0 вернет Логическое значение из True или False в зависимости от того, является ли num четным или нет.

Проверка нечетных чисел очень похожа. Чтобы проверить нечетное число, вы инвертируете проверку на равенство:

def is_odd(num):
    return num % 2 != 0

Эта функция вернет True, если num % 2 не равно 0, что означает, что есть остаток, доказывающий, что num является нечетным числом. Теперь вам, возможно, интересно, можете ли вы использовать следующую функцию, чтобы определить, является ли num нечетным числом:

def is_odd(num):
    return num % 2 == 1

Ответ на этот вопрос - да и - нет. Технически, эта функция будет работать так, как Python вычисляет по модулю с целыми числами. Тем не менее, вам следует избегать сравнения результата операции по модулю с 1, поскольку не все операции по модулю в Python возвращают одинаковый остаток.

Вы можете увидеть почему в следующих примерах:

>>> -3 % 2
1

>>> 3 % -2
-1

Во втором примере остаток принимает знак отрицательного делителя и возвращает -1. В этом случае логическая проверка 3 % -2 == 1 вернет False.

Однако, если вы сравните операцию по модулю с 0, то не имеет значения, какой операнд отрицательный. Результатом всегда будет True, если это четное число:

>>> -2 % 2
0

>>> 2 % -2
0

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

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

Как запускать код с определенными интервалами в цикле

С помощью оператора Python modulo operator вы можете запускать код с определенными интервалами внутри цикла. Это делается путем выполнения операции по модулю с текущим индексом цикла и модулем. Номер модуля определяет, как часто в цикле будет выполняться код, зависящий от интервала.

Вот пример:

def split_names_into_rows(name_list, modulus=3):
    for index, name in enumerate(name_list, start=1):
        print(f"{name:-^15} ", end="")
        if index % modulus == 0:
            print()
    print()

Этот код определяет split_names_into_rows(), который принимает два параметра. name_list представляет собой список имен, которые следует разбить на строки. modulus задает модуль для операции, эффективно определяя, сколько имен должно быть в каждой строке. split_names_into_rows() перейдет на name_list и начнет новую строку после того, как достигнет значения modulus.

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

>>> names = ["Picard", "Riker", "Troi", "Crusher", "Worf", "Data", "La Forge"]
>>> split_names_into_rows(names)
----Picard----- -----Riker----- -----Troi------
----Crusher---- -----Worf------ -----Data------
---La Forge----

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

>>> split_names_into_rows(names, modulus=4)
----Picard----- -----Riker----- -----Troi------ ----Crusher----
-----Worf------ -----Data------ ---La Forge----

>>> split_names_into_rows(names, modulus=2)
----Picard----- -----Riker-----
-----Troi------ ----Crusher----
-----Worf------ -----Data------
---La Forge----

>>> split_names_into_rows(names, modulus=1)
----Picard-----
-----Riker-----
-----Troi------
----Crusher----
-----Worf------
-----Data------
---La Forge----

Теперь, когда вы увидели код в действии, вы можете разобрать, что он делает. Во-первых, он использует enumerate() для перебора name_list, присваивая текущему элементу в списке значение name, а значению счетчика - index. Вы можете видеть, что необязательный аргумент start для enumerate() имеет значение 1. Это означает, что счетчик index будет начинаться с 1 вместо 0:

for index, name in enumerate(name_list, start=1):

Далее, внутри цикла, функция вызывает print() для вывода name в текущую строку. Параметр end для print() является пустой строкой ("") поэтому он не будет выводить новую строку в конце строки. f-строка передается в print(), который использует синтаксис форматирования выходных данных , предоставляемый Python:

print(f"{name:-^15} ", end="")

Не вдаваясь в подробности, отметим, что синтаксис :-^15 подсказывает print() выполнить следующее:

  • Выведите не менее 15 символов, даже если строка короче 15 символов.
  • Выровняйте строку по центру.
  • Заполните любой пробел справа или слева от строки символом дефиса (-).

Теперь, когда название введено в строку, взгляните на основную часть split_names_into_rows():

if index % modulus == 0:
    print()

Этот код берет текущую итерацию index и, используя оператор вычисления по модулю, сравнивает ее с modulus. Если результат равен 0, то можно выполнить код, зависящий от интервала. В этом случае функция вызывает print(), чтобы добавить новую строку, с которой начинается новая строка.

Приведенный выше код - это только один пример. Использование шаблона index % modulus == 0 позволяет запускать другой код в циклах с определенными интервалами. В следующем разделе вы немного углубитесь в эту концепцию и рассмотрите циклическую итерацию.

Как создать циклическую итерацию

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

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

import turtle
import random

def draw_with_cyclic_iteration():
    colors = ["green", "cyan", "orange", "purple", "red", "yellow", "white"]

    turtle.bgcolor("gray8") # Hex: #333333
    turtle.pendown()
    turtle.pencolor(random.choice(colors)) # First color is random

    i = 0 # Initial index

    while True:
        i = (i + 1) % 6 # Update the index
        turtle.pensize(i) # Set pensize to i
        turtle.forward(225)
        turtle.right(170)

        # Pick a random color
        if i == 0:
            turtle.pencolor(random.choice(colors))

В приведенном выше коде используется бесконечный цикл для рисования повторяющейся формы звезды. После каждых шести итераций цвет пера меняется. Размер пера увеличивается с каждой итерацией, пока значение i не будет сброшено обратно на значение 0. Если вы запустите код, то у вас должно получиться что-то похожее на это:

Example of cyclic iteration using Python mod (%) Operator

Важные части этого кода выделены ниже:

import turtle
import random

def draw_with_cyclic_iteration():
    colors = ["green", "cyan", "orange", "purple", "red", "yellow", "white"]

    turtle.bgcolor("gray8") # Hex: #333333
    turtle.pendown()
    turtle.pencolor(random.choice(colors))

    i = 0 # Initial index

    while True:
        i = (i + 1) % 6 # Update the index
        turtle.pensize(i) # Set pensize to i
        turtle.forward(225)
        turtle.right(170)

        # Pick a random color
        if i == 0:
            turtle.pencolor(random.choice(colors))

При каждом прохождении цикла i обновляется на основе результатов (i + 1) % 6. Это новое значение i используется для увеличения значения .pensize с каждой итерацией. Как только i достигнет значения 5, (i + 1) % 6, оно будет равно 0, а значение i вернется к значению 0.

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

i = 0 : (0 + 1) % 6 = 1
i = 1 : (1 + 1) % 6 = 2
i = 2 : (2 + 1) % 6 = 3
i = 3 : (3 + 1) % 6 = 4
i = 4 : (4 + 1) % 6 = 5
i = 5 : (5 + 1) % 6 = 0 # Reset

Когда значение i возвращается к значению 0, .pencolor меняется на новый случайный цвет, как показано ниже:

if i == 0:
    turtle.pencolor(random.choice(colors))

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

Как пересчитать единицы измерения

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

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

def convert_inches_to_feet(total_inches):
    inches = total_inches % 12
    feet = total_inches // 12

    print(f"{total_inches} inches = {feet} feet and {inches} inches")

Вот пример используемой функции:

>>> convert_inches_to_feet(450)
450 inches = 37 feet and 6 inches

Как вы можете видеть из выходных данных, 450 % 12 возвращает 6, то есть оставшиеся дюймы, которые не были равномерно разделены на футы. Результатом 450 // 12 будет 37, то есть общее количество футов, на которое были равномерно разделены дюймы.

В следующем примере вы можете немного углубиться в это. convert_minutes_to_days() принимает целое число, total_mins, представляющее количество минут, и выводит период времени в днях, часах и минутах:

def convert_minutes_to_days(total_mins):
    days = total_mins // 1440
    extra_minutes = total_mins % 1440

    hours = extra_minutes // 60
    minutes = extra_minutes % 60

    print(f"{total_mins} = {days} days, {hours} hours, and {minutes} minutes")

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

  1. Определяет общее количество дней, делящихся поровну на total_mins // 1440, где 1440 - количество минут в сутках
  2. Вычисляет все extra_minutes, оставшиеся с total_mins % 1440
  3. Использует extra_minutes, чтобы получить равномерно делимое hours и любые дополнительные minutes

Вы можете увидеть, как это работает ниже:

>>> convert_minutes_to_days(1503)
1503 = 1 days, 1 hours, and 3 minutes

>>> convert_minutes_to_days(3456)
3456 = 2 days, 9 hours, and 36 minutes

>>> convert_minutes_to_days(35000)
35000 = 24 days, 7 hours, and 20 minutes

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

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

Ниже операторы разделения по полу и по модулю были заменены на divmod():

def convert_inches_to_feet_updated(total_inches):
    feet, inches = divmod(total_inches, 12)
    print(f"{total_inches} inches = {feet} feet and {inches} inches")

Как вы можете видеть, divmod(total_inches, 12) возвращает кортеж, который распаковывается в feet и inches.

Если вы попробуете эту обновленную функцию, то получите те же результаты, что и раньше:

>>> convert_inches_to_feet(450)
450 inches = 37 feet and 6 inches

>>> convert_inches_to_feet_updated(450)
450 inches = 37 feet and 6 inches

Вы получите тот же результат, но теперь код более лаконичен. Вы также можете обновить convert_minutes_to_days():

def convert_minutes_to_days_updated(total_mins):
    days, extra_minutes = divmod(total_mins, 1440)
    hours, minutes = divmod(extra_minutes, 60)

    print(f"{total_mins} = {days} days, {hours} hours, and {minutes} minutes")

Используя divmod(), функция становится более понятной, чем предыдущая версия, и возвращает тот же результат:

>>> convert_minutes_to_days(1503)
1503 = 1 days, 1 hours, and 3 minutes

>>> convert_minutes_to_days_updated(1503)
1503 = 1 days, 1 hours, and 3 minutes

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

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

Как определить, является ли число простым

В следующем примере вы увидите, как можно использовать оператор Python modulo для проверки того, является ли число простым числом. Простое число - это любое число, которое содержит только два множителя: 1 и само себя. Вот несколько примеров простых чисел: 2, 3, 5, 7, 23, 29, 59, 83, и 97.

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

def check_prime_number(num):
    if num < 2:
        print(f"{num} must be greater than or equal to 2 to be prime.")
        return

    factors = [(1, num)]
    i = 2

    while i * i <= num:
        if num % i == 0:
            factors.append((i, num//i))
        i += 1

    if len(factors) > 1:
        print(f"{num} is not prime. It has the following factors: {factors}")
    else:
        print(f"{num} is a prime number")

Этот код определяет check_prime_number(), который принимает параметр num и проверяет, является ли это простым числом. Если это так, то выводится сообщение о том, что num является простым числом. Если это не простое число, то выводится сообщение со всеми множителями этого числа.

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

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

>>> check_prime_number(44)
44 is not prime. It has the following factors: [(1, 44), (2, 22), (4, 11)]

>>> check_prime_number(53)
53 is a prime number

>>> check_prime_number(115)
115 is not prime. It has the following factors: [(1, 115), (5, 23)]

>>> check_prime_number(997)
997 is a prime number

Заглянув в код, вы можете увидеть, что он начинается с проверки, не меньше ли num, чем 2. Простые числа могут быть больше или равны только 2. Если num меньше, чем 2, то продолжать выполнение функции не нужно. Это будет print() сообщение и return:

if num < 2:
    print(f"{num} must be greater than or equal to 2 to be prime.")
    return

Если num больше, чем 2, то функция проверяет, является ли num простым числом. Чтобы проверить это, функция перебирает все числа между 2 и квадратным корнем из num, чтобы увидеть, делится ли какое-либо из них равномерно на num. Если одно из чисел делится равномерно, то найден множитель, и num не может быть простым числом.

Вот основная часть функции:

factors = [(1, num)]
i = 2

while i * i <= num:
    if num % i == 0:
        factors.append((i, num//i))
    i += 1

Здесь еще многое предстоит разобрать, так что давайте разберем это шаг за шагом.

Сначала создается список factors с начальными коэффициентами, (1, num). Этот список будет использоваться для хранения любых других найденных коэффициентов:

factors = [(1, num)]

Далее, начиная с 2, код увеличивает i до тех пор, пока не достигнет квадратного корня из num. На каждой итерации он сравнивает num с i, чтобы определить, делится ли он равномерно. В коде нужно только проверить квадратный корень из num включительно, потому что он не будет содержать никаких коэффициентов выше этого:

i = 2

while i * i <= num:
    if num % i == 0:
        factors.append((i, num//i))
    i += 1

Вместо того, чтобы пытаться определить квадратный корень из num, функция использует цикл while, чтобы проверить, является ли i * i <= num. До тех пор, пока i * i <= num, цикл не достигнет квадратного корня из num.

Внутри цикла while оператор вычисления по модулю проверяет, делится ли num равномерно на i:

factors = [(1, num)]
i = 2 # Start the initial index at 2

while i * i <= num:
    if num % i == 0:
        factors.append((i, num//i))
    i += 1

Если num равномерно делится на i, то i является множителем num, и к factors <<< множителю добавляется набор множителей.5>>> список.

После завершения цикла while код проверяет, были ли обнаружены какие-либо дополнительные факторы:

if len(factors) > 1:
    print(f"{num} is not prime. It has the following factors: {factors}")
else:
    print(f"{num} is a prime number")

Если в списке factors существует более одного кортежа, то num не может быть простым числом. Для простых чисел выводятся множители. Для простых чисел функция выводит сообщение о том, что num является простым числом.

Как реализовать шифры

Оператор Python modulo можно использовать для создания шифров. Шифр - это тип алгоритма для выполнения шифрования и дешифрования входных данных, обычно текстовых. В этом разделе вы рассмотрите два шифра: Шифр Цезаря и шифр Виженера.

Шифр Цезаря

Первый шифр, который вы увидите, - это Шифр Цезаря, названный в честь Юлия Цезаря, который использовал его для тайной передачи сообщений. Это шифр подстановки, который использует буквенную подстановку для шифрования текстовой строки.

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

Например, если бы сдвиг был 5, то A сместился бы на пять букв вверх и стал бы F, B стал бы G и так далее. Ниже вы можете увидеть процесс шифрования текста REALPYTHON со сдвигом на 5:

Caesar Cipher using Python mod (%) Operator

Результирующий шифр равен WJFQUDYMTS.

Расшифровка шифра выполняется путем изменения сдвига на противоположный. Как процессы шифрования, так и процессы дешифрования могут быть описаны следующими выражениями, где char_index - индекс символа в алфавите:

encrypted_char_index = (char_index + shift) % 26
decrypted_char_index = (char_index - shift) % 26

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

import string

def caesar_cipher(text, shift, decrypt=False):
    if not text.isascii() or not text.isalpha():
        raise ValueError("Text must be ASCII and contain no numbers.")

    lowercase = string.ascii_lowercase
    uppercase = string.ascii_uppercase
    result = ""

    if decrypt:
        shift = shift * -1

    for char in text:
        if char.islower():
            index = lowercase.index(char)
            result += lowercase[(index + shift) % 26]
        else:
            index = uppercase.index(char)
            result += uppercase[(index + shift) % 26]

    return result

Этот код определяет функцию с именем caesar_cipher(), которая имеет два обязательных параметра и один необязательный параметр:

  • text это текст, подлежащий шифрованию или расшифровке.
  • shift это количество позиций, на которые нужно сдвинуть каждую букву.
  • decrypt это логическое значение, которое задается, если text должно быть расшифровано.

decrypt это предусмотрено для того, чтобы можно было использовать одну и ту же функцию как для шифрования, так и для дешифрования. Эта реализация может обрабатывать только буквенные символы, поэтому функция сначала проверяет, что text является буквенным символом в кодировке ASCII:

def caesar_cipher(text, shift, decrypt=False):
    if not text.isascii() or not text.isalpha():
        raise ValueError("Text must be ASCII and contain no numbers.")

Затем функция определяет три переменные для хранения lowercase символов ASCII, uppercase символов ASCII и результатов шифрования или дешифрования:

lowercase = string.ascii_lowercase # "abcdefghijklmnopqrstuvwxyz"
uppercase = string.ascii_uppercase # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result = ""

Далее, если функция используется для расшифровки text, то она умножает shift на -1, чтобы выполнить обратный сдвиг:

if decrypt:
    shift = shift * -1

Наконец, caesar_cipher() перебирает отдельные символы в text и выполняет следующие действия для каждого из них. char:

  1. Проверьте, является ли char строчным или прописным.
  2. Получить index из char либо в lowercase, либо в uppercase Списки ASCII.
  3. Добавьте shift к этому index, чтобы определить индекс используемого символа шифрования.
  4. Используйте % 26, чтобы убедиться, что сдвиг будет перенесен обратно в начало алфавита.
  5. Добавьте зашифрованный символ к строке result.

После того, как цикл завершит итерацию по значению text, будет возвращено значение result:

for char in text:
    if char.islower():
        index = lowercase.index(char)
        result += lowercase[(index + shift) % 26]
    else:
        index = uppercase.index(char)
        result += uppercase[(index + shift) % 26]

return result

Вот еще раз полный код:

import string

def caesar_cipher(text, shift, decrypt=False):
    if not text.isascii() or not text.isalpha():
        raise ValueError("Text must be ASCII and contain no numbers.")

    lowercase = string.ascii_lowercase
    uppercase = string.ascii_uppercase
    result = ""

    if decrypt:
        shift = shift * -1

    for char in text:
        if char.islower():
            index = lowercase.index(char)
            result += lowercase[(index + shift) % 26]
        else:
            index = uppercase.index(char)
            result += uppercase[(index + shift) % 26]

    return result

Теперь запустите код в Python REPL, используя текст meetMeAtOurHideOutAtTwo со сдвигом на 10:

>>> caesar_cipher("meetMeAtOurHideOutAtTwo", 10)
woodWoKdYebRsnoYedKdDgy

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

>>> caesar_cipher("woodWoKdYebRsnoYedKdDgy", 10, decrypt=True)
meetMeAtOurHideOutAtTwo

С шифром Цезаря интересно поиграть при знакомстве с криптографией. Хотя шифр Цезаря редко используется сам по себе, он является основой для более сложных шифров замещения. В следующем разделе вы познакомитесь с одним из потомков шифра Цезаря - шифром Виженера.

Шифр Виженера

Шифр Виженера представляет собой многоалфавитный шифр подстановки. Для выполнения шифрования он использует свой собственный шифр Цезаря для каждой буквы входного текста. Шифр Виженера использует ключевое слово, чтобы определить, какой шифр Цезаря следует использовать для поиска зашифрованной буквы.

Вы можете увидеть пример процесса шифрования на следующем рисунке. В этом примере вводимый текст REALPYTHON зашифрован с использованием ключевого слова MODULO:

Vigenère Cipher using Python mod (%) Operator

Для каждой буквы входного текста, REALPYTHON, используется буква из ключевого слова MODULO, чтобы определить, какой столбец шифра Цезаря следует выбрать. Если ключевое слово короче входного текста, как в случае с MODULO, то буквы ключевого слова повторяются до тех пор, пока все буквы входного текста не будут зашифрованы.

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

import string

def vigenere_cipher(text, key, decrypt=False):
    if not text.isascii() or not text.isalpha() or not text.isupper():
        raise ValueError("Text must be uppercase ASCII without numbers.")

    uppercase = string.ascii_uppercase # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    results = ""

    for i, char in enumerate(text):
        current_key = key[i % len(key)]
        char_index = uppercase.index(char)
        key_index = uppercase.index(current_key)

        if decrypt:
            index = char_index - key_index + 26
        else:
            index = char_index + key_index

        results += uppercase[index % 26]

    return results

Возможно, вы заметили, что подпись для vigenere_cipher() очень похожа на caesar_cipher() из предыдущего раздела:

def vigenere_cipher(text, key, decrypt=False):
    if not text.isascii() or not text.isalpha() or not text.isupper():
        raise ValueError("Text must be uppercase ASCII without numbers.")

    uppercase = string.ascii_uppercase
    results = ""

Основное отличие заключается в том, что вместо параметра shift vigenere_cipher() принимает параметр key, который является ключевым словом, используемым при шифровании и дешифровании. Другим отличием является добавление text.isupper(). Исходя из этой реализации, vigenere_cipher() может принимать только вводимый текст в верхнем регистре.

Например caesar_cipher(), vigenere_cipher() повторяет каждую букву входного текста, чтобы зашифровать или расшифровать его:

for i, char in enumerate(text):
    current_key = key[i % len(key)]

В приведенном выше коде вы можете увидеть, как функция впервые использует оператор по модулю:

current_key = key[i % len(key)]

Здесь значение current_key определяется на основе индекса, возвращаемого из i % len(key). Этот индекс используется для выбора буквы из строки key, например M из MODULO.

Оператор modulo позволяет использовать ключевое слово любой длины, независимо от длины text, которое необходимо зашифровать. Как только индекс i, индекс символа, который в данный момент шифруется, сравняется с длиной ключевого слова, все начнется сначала с начала ключевого слова.

Для каждой буквы входного текста несколько шагов определяют, как ее зашифровать или расшифровать:

  1. Определите char_index на основе индекса char внутри uppercase.
  2. Определите значение key_index на основе индекса current_key внутри uppercase.
  3. Используйте char_index и key_index, чтобы получить индекс для зашифрованного или расшифрованного символа.

Взгляните на эти шаги в приведенном ниже коде:

char_index = uppercase.index(char)
key_index = uppercase.index(current_key)

if decrypt:
    index = char_index - key_index + 26
else:
    index = char_index + key_index

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

После того, как index определено, вы найдете второе использование функции для оператора по модулю:

results += uppercase[index % 26]

index % 26 гарантирует, что index символа не превышает 25, таким образом, гарантируя, что он остается внутри алфавита. С помощью этого индекса зашифрованный или расшифрованный символ выбирается из uppercase и добавляется к results.

Вот снова полный код шифра Виженера:

import string

def vigenere_cipher(text, key, decrypt=False):
    if not text.isascii() or not text.isalpha() or not text.isupper():
        raise ValueError("Text must be uppercase ASCII without numbers.")

    uppercase = string.ascii_uppercase # "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    results = ""

    for i, char in enumerate(text):
        current_key = key[i % len(key)]
        char_index = uppercase.index(char)
        key_index = uppercase.index(current_key)

        if decrypt:
            index = char_index - key_index + 26
        else:
            index = char_index + key_index

        results += uppercase[index % 26]

    return results

Теперь идем дальше и запускаем его в Python REPL:

>>> vigenere_cipher(text="REALPYTHON", key="MODULO")
DSDFAMFVRH

>>> encrypted = vigenere_cipher(text="REALPYTHON", key="MODULO")
>>> print(encrypted)
DSDFAMFVRH

>>> vigenere_cipher(encrypted, "MODULO", decrypt=True)
REALPYTHON

Отлично! Теперь у вас есть работающий шифр Виженера для шифрования текстовых строк.

Расширенное использование оператора Python Modulo Operator

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

Используя оператор Python по модулю С decimal.Decimal

Ранее в этом руководстве вы видели, как можно использовать оператор по модулю с числовыми типами, такими как int и float, а также с math.fmod(). Вы также можете использовать оператор вычисления по модулю с помощью Decimal из модуля decimal. Вы используете decimal.Decimal, когда хотите дискретно контролировать точность арифметических операций с плавающей запятой.

Вот несколько примеров использования целых чисел с decimal.Decimal и оператором по модулю:

>>> import decimal
>>> decimal.Decimal(15) % decimal.Decimal(4)
Decimal('3')

>>> decimal.Decimal(240) % decimal.Decimal(13)
Decimal('6')

Вот несколько чисел с плавающей запятой, используемых с decimal.Decimal и оператором по модулю:

>>> decimal.Decimal("12.5") % decimal.Decimal("5.5")
Decimal('1.5')

>>> decimal.Decimal("13.3") % decimal.Decimal("1.1")
Decimal('0.1')

Все операции по модулю с decimal.Decimal возвращают те же результаты, что и для других числовых типов, за исключением случаев, когда один из операндов отрицательный. В отличие от int и float, но аналогично math.fmod(), decimal.Decimal использует знак делимого для результатов.

Взгляните на приведенные ниже примеры, в которых сравниваются результаты использования оператора по модулю со стандартными значениями int и float, а также с decimal.Decimal:

>>> -17 % 3
1 # Sign of the divisor

>>> decimal.Decimal(-17) % decimal.Decimal(3)
Decimal(-2) # Sign of the dividend

>>> 17 % -3
-1 # Sign of the divisor

>>> decimal.Decimal(17) % decimal.Decimal(-3)
Decimal("2") # Sign of dividend

>>> -13.3 % 1.1
1.0000000000000004 # Sign of the divisor

>>> decimal.Decimal("-13.3") % decimal.Decimal("1.1")
Decimal("-0.1") # Sign of the dividend

По сравнению с math.fmod(), decimal.Decimal будет иметь тот же знак, но точность будет другой:

>>> decimal.Decimal("-13.3") % decimal.Decimal("1.1")
Decimal("-0.1")

>>> math.fmod(-13.3, 1.1)
-0.09999999999999964

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

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

Использование оператора Python по модулю с пользовательскими классами

Модель данных Python позволяет переопределять встроенные методы в объекте Python для настройки его поведения. В этом разделе вы узнаете, как переопределить .__mod__(), чтобы вы могли использовать оператор по модулю в своих собственных классах.

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

class Student:
    def __init__(self, name):
        self.name = name
        self.study_sessions = []

    def add_study_sessions(self, sessions):
        self.study_sessions += sessions

Класс Student инициализируется параметром name и начинается с пустого списка study_sessions, который будет содержать список целых чисел, представляющих минуты, изучаемые за сеанс. Существует также .add_study_sessions(), который принимает параметр sessions, который должен быть списком учебных сессий для добавления в study_sessions.

Теперь, если вы помните из раздела преобразование единиц измерения выше, convert_minutes_to_day() использовал оператор Python modulo для преобразования total_mins в дни, часы и минуты. Теперь вы реализуете модифицированную версию этого метода, чтобы увидеть, как вы можете использовать свой пользовательский класс с оператором по модулю:

def total_study_time_in_hours(student, total_mins):
    hours = total_mins // 60
    minutes = total_mins % 60

    print(f"{student.name} studied {hours} hours and {minutes} minutes")

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

class Student:
    def __init__(self, name):
        self.name = name
        self.study_sessions = []

    def add_study_sessions(self, sessions):
        self.study_sessions += sessions

def total_study_time_in_hours(student, total_mins):
    hours = total_mins // 60
    minutes = total_mins % 60

    print(f"{student.name} studied {hours} hours and {minutes} minutes")

Если вы загрузите этот модуль в Python REPL, то вы можете использовать его следующим образом:

>>> jane = Student("Jane")
>>> jane.add_study_sessions([120, 30, 56, 260, 130, 25, 75])
>>> total_mins = sum(jane.study_sessions)
>>> total_study_time_in_hours(jane, total_mins)
Jane studied 11 hours and 36 minutes

Приведенный выше код выводит общее количество изученных часов jane. Эта версия кода работает, но требует дополнительного шага суммирования study_sessions, чтобы получить total_mins перед вызовом total_study_time_in_hours().

Вот как вы можете изменить класс Student, чтобы упростить код:

class Student:
    def __init__(self, name):
        self.name = name
        self.study_sessions = []

    def add_study_sessions(self, sessions):
        self.study_sessions += sessions

    def __mod__(self, other):
        return sum(self.study_sessions) % other

    def __floordiv__(self, other):
        return sum(self.study_sessions) // other

Переопределяя .__mod__() и .__floordiv__(), вы можете использовать экземпляр Student с оператором по модулю. Вычисление sum() из study_sessions также входит в класс Student.

С этими изменениями вы можете использовать экземпляр Student непосредственно в total_study_time_in_hours(). Поскольку total_mins больше не нужен, вы можете удалить его:

def total_study_time_in_hours(student):
    hours = student // 60
    minutes = student % 60

    print(f"{student.name} studied {hours} hours and {minutes} minutes")

Вот полный код после внесения изменений:

class Student:
    def __init__(self, name):
        self.name = name
        self.study_sessions = []

    def add_study_sessions(self, sessions):
        self.study_sessions += sessions

    def __mod__(self, other):
        return sum(self.study_sessions) % other

    def __floordiv__(self, other):
        return sum(self.study_sessions) // other

def total_study_time_in_hours(student):
    hours = student // 60
    minutes = student % 60

    print(f"{student.name} studied {hours} hours and {minutes} minutes")

Теперь, вызвав код в Python REPL, вы можете увидеть, что он гораздо более лаконичен:

>>> jane = Student("Jane")
>>> jane.add_study_sessions([120, 30, 56, 260, 130, 25, 75])
>>> total_study_time_in_hours(jane)
Jane studied 11 hours and 36 minutes

Переопределяя .__mod__(), вы позволяете своим пользовательским классам вести себя больше похоже на встроенные числовые типы Python.

Заключение

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

В этом руководстве вы узнали, как:

  • Используйте оператор по модулю с int, float, math.fmod(), divmod(), и decimal.Decimal
  • Вычислить результаты операции по модулю
  • Решайте реальные задачи с помощью оператора по модулю
  • Переопределите .__mod__() в ваших собственных классах, чтобы использовать их с оператором по модулю

Благодаря знаниям, которые вы получили в этом руководстве, теперь вы можете с большим успехом начать использовать оператор по модулю в своем собственном коде. Счастливого использования Python!

Back to Top