2
\$\begingroup\$

So i wrote a program with the documentation for my fx cg50 calculator's micropython to calculate various items, each of which are:

  1. Pascal Triangle Entry
  2. Pascal Triangle Level/Entire Row
  3. Binomial Expansion
  4. Binomial Term Finder
  5. First num of terms
  6. Combinations

if you look at the code below, you'll notice, I haven't used any modules, and reinvented the wheel on some things. That is because, micropython's python language and standard library is very limited, so I had to make do.

I would like some advice on optimizing, and compacting of my program, and other tips and tricks to improve how a task is done.

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()
```
\$\endgroup\$
2
  • \$\begingroup\$ You may want to look at making some of these functions that return lists into generators (e.g. seperate_to_pairs(), take a look at itertools.pairwise() in the itertools recipes, especially as all you a doing with it is looping through it. \$\endgroup\$ – AChampion Sep 12 '20 at 9:47
  • 2
    \$\begingroup\$ You can also generate an individual row in pascals triangle without building up from the beginning, it has a recurrence relationship of c * (n - r + 1) // r, where n is the row number, r is the element in the row and c is the recurrence value, starting at 1 \$\endgroup\$ – AChampion Sep 12 '20 at 9:50
2
\$\begingroup\$

Doc Test

You have docstrings in your code which illustrates function inputs and expected outputs. Why not format these using the style of the doctest module?

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

    >>> float_integer(4.0)
    4

    >>> float_integer(3.5)
    3.5
    """

Micropython may not have the doctest module (or maybe it does, I don’t know), but you can still run doctest on the same source file in a full Python environment to check the code and the documentation work as expected.

You Can’t Index an Iterator

Using [interator[i:i+2] for i in range(...)] means the variable iterator is not an iterator.

An iterator is constructed from an iterable object, such as a list. An iterator will traverse an iterable object exactly once, and then it is useless, but more than one iterator can be created from an iterable object. Lists are directly indexable, which is what you are doing to the iterator variable.

Still, Python can be horribly inefficient at indexing, since it has to do math and create objects for temporary results like i+2 on every step of the loop; it is much more efficient to use iterators.

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)]

Here, we create two iterators from the iterable object that is given. These are iterators are independent entities. They can be advanced separately, and indeed we advance the second iterator one position forward. zip takes both iterators, and extracts one element from each, until one of the iterators runs out of elements.

The above returns the same type (List[List[T]]) that your function returned. If we allow changing the return type from the original, the function can be converted to return a list of tuples, using:

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

Or equivalently, more efficiently but perhaps a bit more opaquely:

    return list(zip(iter1, iter2))

Finally, since you process the returned list from separate_to_pairs using a for .. in ... loop, instead of returning a list, we can return a generator for the pairs, which gives the most efficient implementation:

    return zip(iter1, iter2)

Enumerate

binomial_expand uses zip(range(1, len(coefficients)+1), coefficients) to get pairings of the each coefficient and its one-based index.

This operation is built in to Python (and hopefully micropython), and is spelt enumerate.

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

The second argument is often omitted and the enumeration starts at zero, but you can begin at any index value you desired by providing that starting value.

Since the micropython documentation mentions enumerate, but your implementation does not appear to support it, perhaps you could conditionally implementing it yourself:

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

A proper enumerate function doesn't require the length of the iterable to be known ahead of time; this is just your approximation, with a start argument. When an update to the micro python implementation adds enumerate, the do-it-yourself version should automatically be skipped.

List Comprehension

Declaring a list and then repeatedly calling append in a loop is often better done using list comprehension. Instead of:

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

use

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

WET -vs- DRY

"WET" stands for "Write Everything Twice", and "DRY" for "Don't Repeat Yourself". You want your code do be "DRY"...

Data entry & input validation

You have a lot of duplicate code like int(input("...")). You've defined a function for inputting a pair of float values. Why not a function for inputting an integer?

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

As a bonus, you could add a loop with a try ... except statement, and not crash the program if the user accidentally inputs a non-integer value. Every caller of this method would gain the input validation, without duplicating it everywhere.

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

Menu

You have a prompt string, which lists all the functions and the corresponding letter, and a dictionary, which lists all of the functions to call and the corresponding letter. If you make a change, you have to do the change in both places. It is easy to make a mistake and miss one.

Instead, consider auto-generating the prompt from the dictionary. Perhaps something like:

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

PEP-0008 Violations

The Style Guide for Python has many rule to help make Python programs formatted more consistently and thus easier for other people to understand. These rules include:

  • One space around binary operators (n - r + 1, not n-r+1)
  • Function & methods should be in lowercase snake_case. INPUT_a_b violates this.
\$\endgroup\$
4
  • \$\begingroup\$ ah yes, sadly enumerate for some reason, is not available on micropython, so i just reinvented it using zip, and i really never bothered with PEP, should probably look into it and take a read. \$\endgroup\$ – Eren Yaegar Sep 13 '20 at 7:25
  • \$\begingroup\$ Hmm, you talk about efficiency and then unpack and repack every already perfectly fine tuple that zip produces for no apparent reason. Did you mean to create lists instead, as the original did? And PEP 8 (not PEP-8 btw :-P) calls x = x*2 - 1 correct and x = x * 2 - 1 wrong. Where does it say what you're saying? \$\endgroup\$ – superb rain Sep 13 '20 at 10:39
  • \$\begingroup\$ @ErenYaegar The documentation does list enumerate... \$\endgroup\$ – superb rain Sep 13 '20 at 10:47
  • \$\begingroup\$ hmm wierd, it's included in the documentation, but it gives a name error on the micropython shell \$\endgroup\$ – Eren Yaegar Sep 13 '20 at 11:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.