Calculadora de expansión binomial

Sep 11 2020

Así que escribí un programa con la documentación para el micropython de mi calculadora fx cg50 para calcular varios elementos, cada uno de los cuales es:

  1. Entrada del triángulo de Pascal
  2. Nivel del triángulo de Pascal / Fila completa
  3. Expansión binomial
  4. Buscador de términos binomiales
  5. Primer número de términos
  6. Combinaciones

Si observa el código a continuación, notará que no he usado ningún módulo y reinventé la rueda en algunas cosas. Eso se debe a que el lenguaje Python y la biblioteca estándar de micropython son muy limitados, así que tuve que arreglármelas.

Me gustaría recibir algunos consejos sobre cómo optimizar y compactar mi programa, y ​​otros consejos y trucos para mejorar la forma en que se realiza una tarea.

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()
```

Respuestas

2 AJNeufeld Sep 13 2020 at 05:32

Prueba doc

Tiene cadenas de documentación en su código que ilustran las entradas de funciones y las salidas esperadas. ¿Por qué no formatearlos usando el estilo del doctestmódulo?

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

    >>> float_integer(4.0)
    4

    >>> float_integer(3.5)
    3.5
    """

Es posible que Micropython no tenga el doctestmódulo (o tal vez sí, no lo sé), pero aún puede ejecutar doctest en el mismo archivo fuente en un entorno Python completo para verificar el código y la documentación funciona como se esperaba.

No puedes indexar un iterador

Usar [interator[i:i+2] for i in range(...)]significa que la variable iteratorno es un iterador.

Un iterador se construye a partir de un objeto iterable, como una lista. Un iterador atravesará un objeto iterable exactamente una vez, y luego es inútil, pero se puede crear más de un iterador a partir de un objeto iterable. Las listas se pueden indexar directamente, que es lo que le está haciendo a la iteratorvariable.

Aún así, Python puede ser terriblemente ineficiente en la indexación, ya que tiene que hacer cálculos matemáticos y crear objetos para obtener resultados temporales como i+2en cada paso del ciclo; es mucho más eficiente utilizar 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)]

Aquí, creamos dos iteradores a partir del objeto iterable que se proporciona. Estos son iteradores son entidades independientes. Se pueden avanzar por separado y, de hecho, avanzamos el segundo iterador una posición hacia adelante. ziptoma ambos iteradores y extrae un elemento de cada uno, hasta que uno de los iteradores se queda sin elementos.

Lo anterior devuelve el mismo tipo ( List[List[T]]) que devolvió su función. Si permitimos cambiar el tipo de retorno del original, la función se puede convertir para devolver una lista de tuplas, usando:

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

O de manera equivalente, más eficiente pero quizás un poco más opaca:

    return list(zip(iter1, iter2))

Finalmente, dado que procesa la lista devuelta separate_to_pairsusando un for .. in ...bucle, en lugar de devolver una lista, podemos devolver un generador para los pares, lo que brinda la implementación más eficiente:

    return zip(iter1, iter2)

Enumerar

binomial_expandutiliza zip(range(1, len(coefficients)+1), coefficients)para obtener emparejamientos de cada coeficiente y su índice basado en uno.

Esta operación está integrada en Python (y con suerte, micropython) y está escrita enumerate.

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

El segundo argumento se omite a menudo y la enumeración comienza en cero, pero puede comenzar en cualquier valor de índice que desee proporcionando ese valor inicial.

Dado que la documentación de micropython menciona enumerate, pero su implementación no parece admitirlo , tal vez podría implementarlo condicionalmente usted mismo:

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

Una enumeratefunción adecuada no requiere que la longitud del iterable se conozca de antemano; esta es solo su aproximación, con un startargumento. Cuando se agrega una actualización a la implementación de micro python enumerate, la versión de hágalo usted mismo debe omitirse automáticamente.

Comprensión de listas

Declarar una lista y luego llamar repetidamente appenden un ciclo a menudo se hace mejor usando la comprensión de listas. En vez de:

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

utilizar

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

HÚMEDO -vs- SECO

"WET" significa "Escribe todo dos veces" y "DRY" para "Don't Repeat Yourself". Quieres que tu código sea "DRY" ...

Entrada de datos y validación de entrada

Tienes mucho código duplicado como int(input("...")). Ha definido una función para ingresar un par de valores flotantes. ¿Por qué no una función para ingresar un número entero?

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

Como beneficio adicional, puede agregar un bucle con una try ... exceptdeclaración y no bloquear el programa si el usuario ingresa accidentalmente un valor no entero. Cada persona que llama a este método obtendría la validación de entrada, sin duplicarla en todas partes.

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

Menú

Tiene una cadena de mensajes, que enumera todas las funciones y la letra correspondiente, y un diccionario, que enumera todas las funciones para llamar y la letra correspondiente. Si realiza un cambio, debe hacerlo en ambos lugares. Es fácil cometer un error y perder uno.

En su lugar, considere la posibilidad de generar automáticamente el mensaje a partir del diccionario. Quizás algo como:

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

Violaciones de PEP-0008

La Guía de estilo para Python tiene muchas reglas para ayudar a que los programas de Python tengan un formato más consistente y, por lo tanto, sean más fáciles de entender para otras personas. Estas reglas incluyen:

  • Un espacio alrededor de los operadores binarios ( n - r + 1, no n-r+1)
  • La función y los métodos deben estar en minúsculas snake_case. INPUT_a_bviola esto.