Calculadora de Expansão Binomial

Sep 11 2020

Então, escrevi um programa com a documentação do micropython da minha calculadora fx cg50 para calcular vários itens, cada um deles:

  1. Pascal Triangle Entry
  2. Nível do triângulo Pascal / linha inteira
  3. Expansão Binomial
  4. Binomial Term Finder
  5. Primeiro número de termos
  6. Combinações

se você olhar o código abaixo, vai perceber, não usei nenhum módulo e reinventei a roda em algumas coisas. Isso porque a linguagem python e a biblioteca padrão do micropython são muito limitadas, então eu tive que fazer isso.

Eu gostaria de alguns conselhos sobre como otimizar e compactar meu programa e outras dicas e truques para melhorar a forma como uma tarefa é realizada.

def float_integer(num):
    """
    returns an integer if the float given, is a whole number.
    otherwise returns the same value as the argument num.

    Ex:
        4.0 ---> 4
        3.5 ---> 3.5
    """
    if num == int(num):
        return int(num)
    return num


def seperate_to_pairs(iterator):
    """
    changes it so that each item in the list pairs with its neighbor items.
    Ex:
        [1, 2, 1]       ---> [[1, 2], [2, 1]]
        [1, 2, 3, 1]    ---> [[1, 2], [2, 3], [3, 1]]
        [1, 2, 3, 2, 1] ---> [[1, 2], [2, 3], [3, 2], [2, 1]]
    """
    return [iterator[i:i+2] for i in range(0, len(iterator)-1)]


def factorial(n, endpoint=1):
    """
    acquires the factorial of n
    Ex:
        5 ---> 120
    """
    res = 1
    for i in range(endpoint, n+1):
        res *= i
    return res


def combinations(n, r):
    """
    nCr - combination or number of ways of picking r items from n

    OR

    nCr = n!/r!(n-r)!
    Ex:
        4C2 ---> 6
        6C3 ---> 20
    """
    return (factorial(n, n-r+1) // factorial(r))


def pascal_triangle_entry(nth, rth):
    """
    acquires the entry in the pascal's triangle at the nth row and rth term
    Ex:
        4th row, 2nd term ---> 3
    """
    return combinations(nth-1, rth-1)


def pascal_triangle_level(level):
    """
    acquires an entire row in the pascal triangle designated by the level number, where 0 is [1], and 1 is [1, 1]
    Ex:
        5 ---> [1, 5, 10, 10, 5, 1]
        6 ---> [1, 6, 15, 20, 15, 6, 1]
    """
    if level == 0:
        return [1]

    layer = [1, 1]
    for _ in range(level-1):
        current_layer = []
        for pair in seperate_to_pairs(layer):
            current_layer.append(sum(pair))
        layer = [1] + current_layer + [1]
    return layer


def binomial_expand(a, b, n):
    """
    (a + bx)^n = a^n + (nC1) a^(n-1) bx + (nC2) a^(n-2) (bx)^2 + ... + (nCr) a^(n-r) (bx)^r + ... + (bx)^n

    Ex:
        a = 3, b = 2, n = 4 # example values for (3 + 2x)^4
        OUTPUT FORMAT:

        [4C0] --> 81.0
        (3.0)^4
        ...
        [nCr] --> Term_Value
        nCr_value (a)^(n-r) (b)^(r)
        ...
        [4C4] --> 16.0
        (2.0)^4

    """
    terms = []
    coefficients = pascal_triangle_level(n)[1:-1]

    for r, coefficient in zip(range(1, len(coefficients)+1), coefficients):
        term_value = binomial_term_finder(a, b, n, r, coefficient)
        terms.append("[{5}C{4}] --> {6}\n{0} ({1})^({2}) ({3})^({4})".format(coefficient, a, n-r, b, r, n, term_value))
    return "\n".join(["[{1}C0] --> {2}\n({0})^{1}".format(a, n, a**n)] + terms + ["[{1}C{1}] --> {2}\n({0})^{1}".format(b, n, b**n)])


def binomial_term_finder(a, b, n, r, coefficient=None):
    """
    calculates the coefficient of the rth term in (a + bx)^n

    if coefficient is given, it skips calculating it.
    Ex:
        a = 3, b = 2, n = 4, r = 2 # example values for (3 + 2x)^4
        ---> 216

    """
    if coefficient:
        return coefficient * a**(n - r) * b**r
    return combinations(n, r) * a**(n - r) * b**r


def first_rth_terms(a, b, n, rth):
    """
    calculates the coefficients of x for the first rth terms in (a + bx)^n
    Ex:
        a = 3, b = 2, n = 4, rth = 3 # example values for (3 + 2x)^4
        ---> [81, 216, 216]
    """
    return [binomial_term_finder(a, b, n, r) for r in range(rth)]


class BIOS:
    """
    responsible for input and output operations
    Hence called BIOS - Basic Input and Output System
    """

    prompt = "\n".join(["a: pascal tri. entry", "b: pascal tri. row", "c: binomial expand", "d: binomial term finder", "e: first rth terms", "f: combinations"])

    def __init__(self):
        self.running = True
        self.choices = {'a': self.pascal_triangle_entry, 'b': self.pascal_triangle_level, 'c': self.binomial_expand, 'd': self.binomial_term_finder, 'e': self.first_rth_terms, 'f': self.combinations}


    def stop_decorator(func):
        """
        Decorator for stopping certain functions, after they're done by asking with a prompt
        """
        def wrapper(self):
            func(self)
            command = input("Enter nothing to stop: ")
            if command == '':
                self.running = False
        return wrapper


    def INPUT_a_b(self):
        """
        input a and b for (a + bx)^n, using only one line
        """
        return float_integer(float(input("Enter a: "))), float_integer(float(input("Enter b: ")))


    @stop_decorator
    def pascal_triangle_entry(self):
        nth = int(input("Enter row number(n): "))
        rth = int(input("Enter entry number(r): "))
        print(pascal_triangle_entry(nth, rth))


    @stop_decorator
    def pascal_triangle_level(self):
        level = int(input("Enter level: "))
        print(pascal_triangle_level(level))


    def binomial_expand(self):
        a, b = self.INPUT_a_b()
        nth = int(input("Enter nth: "))
        self.running = False
        print(binomial_expand(a, b, nth))


    @stop_decorator
    def binomial_term_finder(self):
        a, b = self.INPUT_a_b()
        nth = int(input("Enter nth: "))
        rth = int(input("Enter rth: "))
        print(binomial_term_finder(a, b, nth, rth))


    @stop_decorator
    def first_rth_terms(self):
        a, b = self.INPUT_a_b()
        nth = int(input("Enter nth: "))
        rth = int(input("Enter first num terms: "))
        print("First {} terms:".format(rth))
        print(first_rth_terms(a, b, nth, rth))


    @stop_decorator
    def combinations(self):
        nth = int(input("Enter nth: "))
        rth = int(input("Enter rth: "))
        print(combinations(nth, rth))


    def main(self):
        """
        main program loop, uses a dictionary as an alternative for a switch case
        """
        while self.running:
            print(self.prompt)
            self.choices.get(input(">> "), lambda: None)()


program = BIOS()
program.main()
```

Respostas

2 AJNeufeld Sep 13 2020 at 05:32

Teste Doc

Você tem docstrings em seu código que ilustra entradas de função e saídas esperadas. Por que não formatar usando o estilo do doctestmódulo?

def float_integer(num):
    """
    Returns an ...

    >>> float_integer(4.0)
    4

    >>> float_integer(3.5)
    3.5
    """

Micropython pode não ter o doctestmódulo (ou talvez tenha, eu não sei), mas você ainda pode executar doctest no mesmo arquivo de origem em um ambiente Python completo para verificar o código e a documentação funcionam conforme o esperado.

Você não pode indexar um iterador

Usar [interator[i:i+2] for i in range(...)]significa que a variável iteratornão é um iterador.

Um iterador é construído a partir de um objeto iterável, como uma lista. Um iterador percorrerá um objeto iterável exatamente uma vez e, então, é inútil, mas mais de um iterador pode ser criado a partir de um objeto iterável. As listas são diretamente indexáveis, que é o que você está fazendo com a iteratorvariável.

Ainda assim, Python pode ser terrivelmente ineficiente na indexação, uma vez que precisa fazer matemática e criar objetos para resultados temporários, como i+2em cada etapa do loop; é muito mais eficiente usar iteradores.

def separate_to_pairs(iterable):

    iter1 = iter(iterable)   # create 1st iterator
    iter2 = iter(iterable)   # create a 2nd iterator
    next(iter2)              # advance 2nd iterator one position

    return [[a, b] for a, b in zip(iter1, iter2)]

Aqui, criamos dois iteradores a partir do objeto iterável fornecido. Esses são iteradores são entidades independentes. Eles podem ser avançados separadamente e, de fato, avançamos o segundo iterador uma posição à frente. zippega ambos os iteradores e extrai um elemento de cada um, até que um dos iteradores fique sem elementos.

O acima retorna o mesmo tipo ( List[List[T]]) que sua função retornou. Se permitirmos a alteração do tipo de retorno do original, a função pode ser convertida para retornar uma lista de tuplas, usando:

    return [(a, b) for a, b in zip(iter1, iter2)]

Ou de forma equivalente, de forma mais eficiente, mas talvez um pouco mais opaca:

    return list(zip(iter1, iter2))

Finalmente, como você processa a lista retornada separate_to_pairsusando um for .. in ...loop, em vez de retornar uma lista, podemos retornar um gerador para os pares, o que fornece a implementação mais eficiente:

    return zip(iter1, iter2)

Enumerar

binomial_expandusa zip(range(1, len(coefficients)+1), coefficients)para obter emparelhamentos de cada coeficiente e seu índice baseado em um.

Esta operação está embutida no Python (e esperançosamente no micropython) e é escrita enumerate.

for r, coefficient in enumerate(coefficients, 1):

O segundo argumento é frequentemente omitido e a enumeração começa em zero, mas você pode começar em qualquer valor de índice desejado fornecendo esse valor inicial.

Visto que a documentação do micropython menciona enumerate, mas sua implementação não parece suportá-lo, talvez você possa implementá-lo condicionalmente:

if 'enumerate' not in dir(__builtins__):
    def enumerate(iterable, start=0):
        """Approximation of enumerate"""
        return zip(range(start, len(iterable) + start), iterable)

Uma enumeratefunção adequada não exige que o comprimento do iterável seja conhecido com antecedência; esta é apenas sua aproximação, com um startargumento. Quando uma atualização para a implementação de micro python é adicionada enumerate, a versão do-it-yourself deve ser automaticamente ignorada.

Compreensão de lista

Declarar uma lista e, em seguida, chamar repetidamente appendem um loop geralmente é melhor feito usando a compreensão de lista. Ao invés de:

        current_layer = []
        for pair in seperate_to_pairs(layer):
            current_layer.append(sum(pair))

usar

        current_layer = [sum(pair) for pair in seperate_to_pairs(layer)]:

WET -vs- DRY

"WET" significa "Write Everything Twice" e "DRY" significa "Don't Repeat Yourself". Você quer que seu código seja "DRY" ...

Entrada de dados e validação de entrada

Você tem muitos códigos duplicados como int(input("...")). Você definiu uma função para inserir um par de valores flutuantes. Por que não uma função para inserir um número inteiro?

    @staticmethod
    def input_int(prompt):
        return int(input(prompt))

Como bônus, você pode adicionar um loop com uma try ... exceptinstrução e não travar o programa se o usuário acidentalmente inserir um valor não inteiro. Cada chamador desse método obteria a validação de entrada, sem duplicá-la em todos os lugares.

    @staticmethod
    def input_int(prompt):
        while True:
            try:
                return int(input(prompt))
            except ValueError:
                print("Invalid input - Please enter an integer")

Cardápio

Você tem uma string de prompt, que lista todas as funções e a letra correspondente, e um dicionário, que lista todas as funções a serem chamadas e a letra correspondente. Se você fizer uma mudança, terá que fazer a mudança nos dois lugares. É fácil cometer um erro e errar.

Em vez disso, considere a geração automática do prompt do dicionário. Talvez algo como:

prompt = "\n".join(key + ": " + method.__name__.replace('_', ' ')
                   for key, method in self.choices.items())

Violações PEP-0008

O Guia de Estilo para Python tem muitas regras para ajudar a tornar os programas Python formatados de forma mais consistente e, portanto, mais fácil para outras pessoas entenderem. Essas regras incluem:

  • Um espaço ao redor dos operadores binários ( n - r + 1não n-r+1)
  • Função e métodos devem estar em minúsculas snake_case. INPUT_a_bviola isso.