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']
3 Answers 3
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).
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.)
-
\$\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– Daerdemandt2016年08月18日 14:04:39 +00:00Commented Aug 18, 2016 at 14:04
-
\$\begingroup\$ Found it!
functools.total_ordering
is the name. \$\endgroup\$Daerdemandt– Daerdemandt2016年10月10日 16:47:37 +00:00Commented Oct 10, 2016 at 16:47
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.
-
\$\begingroup\$
Hozier.to_church()
print Hozier.sins
sharpenKnife()
return deathlessDeath
\$\endgroup\$DasBeasto– DasBeasto2016年08月18日 19:26:31 +00:00Commented Aug 18, 2016 at 19:26
Explore related questions
See similar questions with these tags.
if type(x) == int
you could've useisinstance(x, int)
. It's not thatisinstance
is good, mind you — it's just less bad than checking equality of types. \$\endgroup\$