4
\$\begingroup\$

I decided to reinvent the basic calculator functions using only:

  • the set of positive integers

  • greater than, less than, and equality properties

  • increment and decrement

(in other words, the counting on fingers method)

def is_positive_int(x):
    if type(x) == int and x >= 0:
        return True
    else:
        return False


def add(x,y):
    if is_positive_int(x) and is_positive_int(y):
        for i in range(y):
            x += 1
        return x
    else: 
        return False

def multiply(x,y):
    if is_positive_int(y) and is_positive_int(y):
        m = 0
        for i in range(y):
            m = add(m,x)
        return m
    else: 
        return False

def power(x,y):
    """ x to the y power """
    if is_positive_int(x) and is_positive_int(y):
        p = 1
        for i in range(y):
            p = multiply(p,x)
        return p
    else: 
        return False

def subtract(x,y):
    """ x - y """
    if is_positive_int(x) and is_positive_int(y) and x > y:
        for i in range(y):
            x -= 1
        return x
    else: 
        return False

def divide(x,y):
    """ x / y """
    if is_positive_int(x) and is_positive_int(y):
        floor = 0 
        remainder = 0
        for i in range(x):
            remainder += 1
            if remainder == y:
                floor += 1
                remainder = 0
        return {'floor':floor, 'modulus':remainder}
    else: 
        return False

def floor(x,y):
    """ a // b"""
    return divide(x,y)['floor']


def modulus(x,y):
    """ x % y"""
    return divide(x,y)['modulus']
\$\endgroup\$
  • \$\begingroup\$ Instead of if type(x) == int you could've use isinstance(x, int). It's not that isinstance is good, mind you — it's just less bad than checking equality of types. \$\endgroup\$ – Grajdeanu Alex. Aug 18 '16 at 10:13
10
\$\begingroup\$

Let's say you have this situation:

if ...:
    ...
    return ...
else:
    return ...

What if the if condition is True? Some statements will be executed, and then the function will return. In other words, if the condition is True, execution never leaves the if block. Therefore, you really don't need the else. Just put the return directly under the if:

if ...:
    ...
    return ...
return ...

It is generally a bad idea to use the pattern type(...) == .... Usually, we use isinstance(..., ...). That way, subclasses are allowed. Of course, you might keep it if you don't want subclasses to be allowed, but I don't see a reason to restrict the user. I'm assuming you want only integers because only integers can be passed to range, but subclasses of integers can indeed also be passed. For example, booleans are integers. range(False, True) is perfectly valid. Also, True + False + False is valid, so it won't mess up your arithmetic. That makes your is_positive_int function look like this:

def is_positive_int(x):
    if isinstance(x, int) and x >= 0:
        return True
    return False

But what do you see? If a condition is True, we return True. If it is False, we return False. In other words, we are really just returning the condition, so why not do it like this:

def is_positive_int(x):
    return isinstance(x, int) and x >= 0

I see a very similar pattern in all of your functions:

if is_positive_int(x) and is_positive_int(y):
    ...
else:
    return False

First of all, we can remember what I mentioned earlier about not leaving the if block unless the condition is False. Therefore, I would actually switch the if and else:

if not is_positive_int(x) or not is_positive_int(y):
    return False
...

We remove a level of nesting that way. The problem is that we use the same code in a bunch of different functions, so we can make a decorator. A decorator is a function which, when given another function (a) to wrap, returns a wrapper function (b) for a. b overrides a and does its special thing, but it usually calls a later. This is my idea:

def guarantee_positive(function):
    # Each function takes two arguments
    def wrapper(x, y):
        if is_positive_int(x) and is_positive_int(y):
            # Deal with it normally
            return function(x, y)
        return False
    # Now wrapper(x, y) will be called instead of function(x, y)
    return wrapper

To use it, add @guarantee_positive before each function definition. For example:

@guarantee_positive
def divide(x, y):
    floor = 0
    remainder = 0
    for i in range(x):
        remainder += 1
        if remainder == y:
            floor += 1
            remainder = 0
    return {'floor': floor, 'modulus': remainder}

That makes for a much cleaner function. Of course, subtract has an extra condition, but I'll get to that later.


With all that about function decorators, why do you need them both to be positive? I can see why you might want y to be positive: because range doesn't work the same with negative numbers, but you aren't using i most of the time, so just use the absolute value of y. I'll give an example:

def multiply(x, y):
    m = 0
    function = add if y > 0 else subtract
    for _ in range(abs(y)):
        m = function(m, x)
    return m

It doesn't require a decorator, and it is capable of more. You can now use it with any combination of negative and positive:

-3,  3 -> -9
-3, -3 ->  9
 3, -3 -> -9
 3,  3 ->  9

Your add function would be like this:

def add(x, y):
    # Add 1 if y is positive; subtract 1 if it is negative
    y_add = (1 if y >= 0 else -1)
    for _ in range(abs(y)):
        x += y_add
    return x

Again, it's shorter than the original, and it does more.

There are some functions where it isn't quite that easy. For example, power() would be difficult to implement a negative y (x doesn't need to be positive even with the current implementation), but most of the others don't need the decorator. I won't tell you all of them, but I have given examples of the more common hangups.


Back to the decorator, I think it is better to raise an exception when something is wrong than to return False. After all, they are called exceptions for a reason. Normally, the function does one thing, but there are exceptions (or there should be).


I see a lot of for i in range(y) when i isn't used. The common way of signifying that i is just a place holder is using _ as the variable name.


It looks like you cheated with divide() :) You don't actually have division, you have divmod. I think a name similar to that would be more appropriate. another thing is that you return a dictionary. I prefer something closer to how the built-in function does it:

return floor, remainder

That way, you can do:

floor, remainder = divide(...)

Instead of the equivalent:

result = divide(...)
floor, remainder = result['float'], result['modulus']

Either one should have a doc string that explains what the return value is. If I called divide(...) when it had a doc string of just x / y, I would assume that I would get x / y, not a dictionary with the floored result and the remainder. Your floor and modulus functions would then be:

def floor(x, y):
    """a // b"""
    return divide(x, y)[0]

def modulus(x, y):
    """x % y"""
    return divide(x, y)[1]

Except that divide will hopefully have a more descriptive name.

I did notice that floor is inside of divide(), but I am assuming that is just a typo.


You seem to be following PEP 8 fairly well, but I noticed that you don't have any whitespace between the key-value pairs in your dictionary. PEP 8 recommends that you put a space between the colon and the value. I don't see it in the PEP, but I think you should put a space between a comma and the following object. For example:

def divide(x, y):

not

def divide(x,y):

Without the space, it feels smooshed, like you're golfing (code golfing, not with a golf club).

\$\endgroup\$
6
\$\begingroup\$

Why not go a step further and implement this as a class, this way you learn something about magic methods as well :)

class Fingers(int):
    """A class implementing counting with fingers instead of the more effective built-in int methods"""

    def __init__(self, x):
        if not isinstance(x, int):
            raise TypeError("Input must be 'int' (or subclass)")
        self.x = x

    def __pos__(self):
        return self

    def __neg__(self):
        return Fingers(-self.x)

    def __abs__(self):
        return Fingers(abs(self.x))

    def __add__(self, other):
        """a + b, a and b may be negative"""
        y_add = (1 if other >= 0 else -1)
        x = self.x
        for _ in range(abs(other)):
            x += y_add
        return Fingers(x)

    def __sub__(self, other):
        return self + (-other)

    def __mul__(self, other):
        """a * b, a and b may be negative"""
        m = Fingers(0)
        for _ in range(abs(other)):
            m = m + self
        return m

    def __pow__(self, exponent):
        """a**b, b must be positive"""
        if exponent < 0:
            raise NotImplementedError
        p = Fingers(1)
        for _ in range(y):
            p = p * self
        return p

    def __divmod__(self, other):
        floor = Fingers(0)
        remainder = Fingers(0)
        for _ in range(self):
            remainder = remainder + Fingers(1)
            if remainder == other:
                floor = floor + Fingers(1)
                remainder = Fingers(0)
        return floor, remainder

    def __floordiv__(self, other):
        """a//b"""
        return self.__divmod__(other)[0]

    def __mod__(self, other):
        """a%b"""
        return self.__divmod__(other)[1]

if __name__ == "__main__":
    x = Fingers(3)
    y = Fingers(2)
    assert x + y == Fingers(5)
    assert x * y == Fingers(6)
    assert x**y == Fingers(9)
    assert divmod(x, y) == (Fingers(1), Fingers(1))
    assert x // y == Fingers(1)
    assert x % y == Fingers(1)

(I did not implement __iadd__ etc, so x += 3 will not work. They are trivial to implement, though.)

\$\endgroup\$
  • \$\begingroup\$ There was a decorator or maybe a base class that allowed you to implement only some operators; reflected and in-place would be filled in automagically, but I can't find it now. \$\endgroup\$ – Daerdemandt Aug 18 '16 at 14:04
  • \$\begingroup\$ Found it! functools.total_ordering is the name. \$\endgroup\$ – Daerdemandt Oct 10 '16 at 16:47
0
\$\begingroup\$

It has already been said that you should've probably implemented this as a class. There's even something in the docs about that.

However, there's also one concept you could've made use of: Church numerals.

You could make your numbers have something like .to_church() method, so that CustomNumeral(3).to_church(f) would return something similar to lambda x: f(f(f(x))), it allows for very expressive illustration for how addition naturally arises from incrementing and multiplication - from addition.

\$\endgroup\$
  • \$\begingroup\$ Hozier.to_church() print Hozier.sins sharpenKnife() return deathlessDeath \$\endgroup\$ – DasBeasto Aug 18 '16 at 19:26

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.