Занимательная математика, очаровательный python. Эпизод 3: Финальный аккорд

Пришло время закончить наш модуль. Если вы не писали код, то взять его можно здесь. В заключительной статье мы приведем наш код в порядок и более глубоко познакомимся с декораторами. Если вы ничего о них не знаете, то крайне желательно почитать это и это. Теперь можно приступать!
Давайте первым делом внимательно рассмотрим наш код и поймём чего хотим. В принципе, у нас небольшой модуль, который помещается в 200 строчек кода и можно было бы ничего не менять, но, чисто теоретически, есть вероятность, что мы будем расширять функционал и дописывать ещё функции. Кроме того, хотелось бы сделать всё как-то более компактно. Именно для этого мы и разделим все наши функции на несколько типов. Первый тип — факториалы —
factorial, superfactorial, hyperfactorial, doublefactorial, primorial
. Эти функции достаточно просты и их мы трогать практически не будем. Вторая группа функций — обратные факториалы — antifactorial, antisuperfactorial, antihyperfactorial, antidoublefactorial
и antiprimorial
. Если очень внимательно на них посмотреть, то можно увидеть, что в каждой из них мы делаем практически одно и тоже. Мы последовательно делим данное число на что-то. Третья группа — факторионы — isFactorion, isSuperfactorion, isHyperfactorion, isDoublefactorion, isPrimorion, isKFactorion
подвергнется самому большому изменению. Опять же, сейчас эти функции достаточно просты, но мы создадим ещё одну, четвертую группу функций — k-факторионы, куда войдут isKFactorion, isKSuperfactorion, isKHyperfactorion, isKDoublefactorion, isKPrimorion
, через которую мы и выразим впоследствии обычные факторионы (которые являются частным случаем k-факторионов). Наконец, мы создадим ещё одну группу функций, шестую, которая будет проверять, является ли число факториалом или суперфакториалом etc. Оставшиеся функции isPrime
и sequence
составят последнюю группу.
Может показаться, что всё это очень сложно и громоздко, но на деле наш модуль увеличится совсем незначительно. Сначала давайте посмотрим на все наши функции (кроме sequence
) и найдём общее. Как несложно догадаться, все функции работают только с типом int
, следовательно, первый декоратор, который мы напишем, будет проверять корректность данных. Вместо того, чтобы каждую функцию начинать и заканчивать строками
1 2 3 |
if type(x) == int and x >= 0: ... raise TypeError, "The given value must be positive integer, not %s"%(type(x) if type(x) != int else "negative") |
мы вынесем этот код в декоратор. Давайте удалим нашу функцию-декоратор constant
, она больше не понадобится, и импортируем wraps
:
1 |
from functools import wraps |
Зачем нужна wraps
я надеюсь вы в курсе из прочитанного на хабре 🙂
Теперь собственно о декораторах. Сначала мы объявляем функцию-декоратор, затем с помощью @wraps
мы декорируем внутреннюю функцию, в которой и происходит проверка. После чего возвращаем внутреннюю функцию. Выглядит это так:
1 2 3 4 5 6 7 8 |
def isInteger(function): @wraps(function) def wrapper(x): if type(x) == int and x >= 0: return function(x) else: raise TypeError, "The given value must be positive integer, not %s"%(type(x) if type(x) != int else "negative") return wrapper |
Теперь нам нужно удалить все инструкции if else
из наших функций и декорировать их, приведя к такому виду:
1 2 3 4 |
@isInteger def factorial(x): result = lambda x: result(x-1) * x if x > 0 else 1 return result |
И всё у нас было бы замечательно и круто, но как только мы попытаемся выполнить функцию isKFactorion
, мы получим ошибку. Дело в том, что функция wrapper
принимает только один аргумент, а функция isKFactorion
пытается передать ей два. Более того, опять же, чисто теоретически, давайте представим что когда-нибудь в будущем у нас появятся функции, которые будут иметь большее число аргументов типа int
, к примеру, пять. Выход напрашивается — использовать *args
. Соответственно, меняется и логика wrapper, теперь мы должны проверять не один аргумент x
, а все. Следует также помнить, что декоратор можно использовать только один раз, то есть если мы попытаемся во время исполнения функции isKFactorion
обратиться во второй раз к функции isInteger
, то получим в ответ None
. К счастью, нам не придется усложнять и писать много кода, мы просто чуть-чуть его подправим.
1 2 3 4 5 6 7 8 |
def isInteger(function): @wraps(function) def wrapper(*args): if all(type(each) == int and each >= 0 for each in args): return function(*args) else: raise TypeError, "The given value must be positive integer, got %s"%(str(args) + "instead" if any(type(each) != int for each in args) else "negative integer instead") return wrapper |
Всю магию здесь берут на себя функции all
и any
. Логика простая: если все значения в списке переданных аргументов выполняют условия (возвращают True
), то вернуть функцию со всеми аргументами, иначе возбудить TypeError
, в котором либо просто вернуть аргументы, если хотя бы один из них не выполняет часть условия (возвращает False
), иначе объявить его отрицательным. Я не стал усложнять, но, конечно, с точки зрения полезности, лучше бы вместо простого перечисления аргументов в этом исключении, было бы вернуть их типы. Возможно, когда-нибудь, я и займусь этим, но сейчас мне показалось вполне достаточным то, что я написал.
Теперь, если мы попробуем выполнить isKFactorion
, никаких неожиданных ошибок не будет. На этом заканчиваем с isInteger
. Мы должны задекорировать все функции, кроме sequence
. Перед написанием следующего, самого масштабного декоратора, давайте обратим внимание как раз на sequence
. В данный момент если мы попробуем выполнить такой код
1 |
sequence(isPrime, 0, 100) |
функция вернет нам список, состоящий из False
и True
. Давайте изменим её так, чтобы она возвращала значения. То есть, вместо True
возвращала бы число, которое является простым, а значения False
просто бы игнорировались. Кроме того, хотелось бы, чтобы можно было получать как прямые, так и обратные последовательности. Всё это достигается незначительным расширением кода.
1 2 3 4 5 6 7 8 9 10 |
def sequence(function, valueFrom = 0, valueTo = 1, reverse = False): result = [] seq = [function(x) for x in range(valueFrom, valueTo+1)] if False in seq: for index, value in enumerate(seq): if value: result.append(index+valueFrom) else: result = seq return result if not reverse else result[::-1] |
Всё довольно просто. Функция sequence принимает 4 параметра — саму функцию, первое и последнее значения (как и раньше) и параметр reverse
, который по умолчанию мы ставим в False
. Далее мы делаем то же, что и раньше, а затем проверяем, есть ли значение False
в нашей секвенции. Если есть, то мы проходим по последовательности и выбираем индексы тех значений, которые возвращают True
. Так как мы допускаем, что последовательность может начинаться не с 0, то к индексу мы должны прибавить valueFrom
. Если же False
нет в нашей последовательности, то мы просто присваиваем переменной result
полученную последовательность и переходим к последнему шагу. На последнем шаге функция возвращает result
если reverse = False
, как стоит по умолчанию, либо же вернет result[::-1]
, то есть обратную последовательность. Теперь мы сможем, допустим, получить последовательность простых чисел от 0 до 100 просто выполнив sequence(isPrime, valueTo = 100)
.
Эта функция нам понадобится, так как мы переходим к написанию самого большого и сложного декоратора. Он будет отвечать за целый десяток функций — все anti-
и все функции типа isFactorial
, которые мы ещё не определили. Для начала, функция, которую я назвал manager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
def manager(argument = None, entity = None): def decorator(function): @wraps(function) def wrapper(x): if x == 1: return [0, 1] if not entity else True else: for each in ([z for z in range(2, sys.getrecursionlimit()) if z % 2 == 0] if argument == "double" and x % 2 == 0 else [z for z in range(2, sys.getrecursionlimit()) if z % 2 != 0] if argument == "double" and x % 2 != 0 else sequence(isPrime, 2, sys.getrecursionlimit()) if argument == "prime" else range(2, sys.getrecursionlimit()+1)): x = float(x)/ (factorial(each) if argument == "super" else superfactorial(each) if argument == "hyper" else float(each)) if x != 1.0 and x > 1.0: continue elif x != 1.0 and x < 1.0: if entity: return False else: raise ValueError "The given value is not correct" else: if argument == "prime": values = [each] for var in range(1, sys.getrecursionlimit()): if not isPrime(each+var): values.append(each+var) continue else: return values break break else: return True if entity else each break return function(x) return wrapper return decorator |
Теперь описание всего этого кошмара)). Наша функция-декоратор принимает два аргумента — argument
, который будет принимать значения None, super, hyper, double
и prime
в зависимости от функции, которую мы будем декорировать. Второй аргумент entity
принимает значения True
и False
и он будет отвечать за функции типа isFactorial, isHyperfactorial
etc. Внутри функции manager
мы определим функцию decorator
, которая и будет принимать в качестве аргумента ту функцию, которую нужно декорировать. Дальше как обычно @wraps
и wrapper
, в которой и будет вся магия. Первым делом мы обработаем единицу. Для всех обратных функций, единица соответствует паре значений 0 и 1. Поэтому просто вернем [0, 1] если значение равно 1, при любом аргументе и только если entity
равно False
. Таким образом wrapper
”понимает”, что декорируются обратные функции. Если же entity = True
, а значит мы декорируем функции alla isFactorial
, то необходимо вернуть True
, так как 1 является и факториалом и суперфакториалом etc. Далее мы должны пройти по последовательности каких-то значений, последовательно деля данное число на каждый элемент этой последовательности. Здесь нам на помощь придёт инлайновая запись. Мы можем непосредственно в момент выполнения функции определить последовательность вот такой записью seq if conditions else seq2 if condions2 else seq3 if conditions3 else seq4
, где seq
— последовательность, conditions
— условия. Соответственно и в нашей функции определяются следующие последовательности [z for z in range(2, sys.getrecursionlimit()) if z % 2 == 0]
и [z for z in range(2, sys.getrecursionlimit()) if z % 2 != 0]
для чётных и нечётных значений соответственно для функции antidoublefactorial
; sequence(isPrime, 2, sys.getrecursionlimit())
для простых чисел для функции antiprimorial
; и, наконец, range(2, sys.getrecursionlimit()+1)
для всех остальных функций.
Следующий шаг — деление. Опять же эта задача легко решается с помощью инлайновой записи. Мы делим либо на факториал, либо на суперфакториал каждого числа, если функция superfactorial
или hyperfactorial
соответственно, либо же просто делим на число в остальных случаях (так как последовательности заданы). Дальше уже знакомая проверка. Если получившееся число больше 1.0 и не равно ей, то продолжаем, иначе если наша функция isFactorial, isHyperfactorial
etc., то вернуть False
, если же мы ищем обратную функцию, то вернуть ValueError
. Если же значение равно 1.0, то дальше если аргументом является prime
, то мы просто делаем всё то же самое, что делали с функцией antiprimorial
— код полностью идентичен коду функции antiprimorial
, начиная с инструкции values = [each]
и до break
. И финальным аккордом мы возвращаем True
, если entity = True
, то есть если мы хотели знать, является ли число факториалом/суперфакториалом/гиперфакториалом/двойным факториалом или праймориалом, или возвращаем число — значение обратного факториала/суперфакториала/гиперфакториала/двойного факториала или праймориала. Далее последовательно возвращаем function(x), wrapper
и decorator
.
Теперь всё, что нам нужно — либо переписать, либо заново создать десять функций в таком виде (на примере antifactorial
и isSuperfactorial
)
1 2 3 4 5 6 7 8 9 |
@isInteger @manager() def antifactorial(x): return x @isInteger @manager("super", True) def isSuperfactorial(x): return x |
Значительная часть кода исчезла! Теперь всё делает наш декоратор, функция же только возвращает значение. Поскольку мы установили значения аргументов по умолчанию None
, мы можем вызвать @manager()
без аргументов. Также хочу обратить внимание, что сначала мы декорируем все функции посредством @isInteger
, то есть сначала мы проверяем валидность данных и только потом отправляем их @manager
.
Теперь пришло время заняться факторионами. Как я уже писал выше, мы будем определять факторионы как частный случай k-факторионов, следовательно, сначала мы напишем декоратор для определения пяти типов k-факторионов: k-суперфакторионов, k-гиперфакторионов, k-двойных факторионов, k-прайморионов и собственно k-факторионов. Как и предыдущий раз, мы воспользуемся декоратором, который будет принимать единственный аргумент argument
. Этот декоратор совсем элементарен.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
def factorions(argument = None): def decorator(function): @wraps(function) def wrapper(x, k): if k == 0: raise ValueError, "0-factorions cannot be defined!" else: factorion = 0 number = tuple(str(x)) for each in number: factorion += (factorial(int(each)) if not argument else superfactorial(int(each)) if argument == "super" else hyperfactorial(int(each)) if argument == "hyper" else doublefactorial(int(each)) if argument == "double" else primorial(int(each))) if x == factorion * k: return True else: return False return wrapper return decorator |
Здесь всё довольно просто и не требует какого-то сверхкомментирования. Теперь осталось только переписать и создать все k-функции. Я не буду этого делать, в этом уж совсем нет никакой сложности.
Последнее, что нам осталось — выразить факторион как частный случай k-факториона, при k = 1. Это достигается одной строчкой кода:
1 2 3 |
@isInteger def isFactorion(x): return isKFactorion(x, 1) |
Вот и всё! Остальные функции также переписываются аналогичным образом. Единственные функции, которые мы не тронули — isPrime
и primorial
мы никак не будем модифицировать, только обернем их нашим декоратором @isInteger
. На этом всё! Исходный код модуля, который я назвал extendedmath
можно взять здесь и модифицировать как кому удобно, например создать строки документации. В будущем мы обязательно ещё вернёмся к нему и создадим ещё не одну занимательно-математическую функцию!
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: