python >= operator on classes - python

I have a question regarding python '>=' behaviour.
I have an old TimeStamp class, which holds (hour, minute) tuple and offers some methods such as __eq__, __gt__, __lt__.
I am refactoring it to also account for day and second, and to store the data as total seconds. Here I implemented __eq__, __gt__, __lt__ as well.
However, further in code I am using >= operator for this class and while the old class version is working properly, with the new one I am getting
TypeError: unorderable types: TimeStamp() >= TimeStamp() error.
Code is below:
class TimeStamp(tuple): # OLD, WORKING VERSION
"""TimeStamp, hh:mm tuple supporting comparison and addition"""
__slots__ = ()
def __new__(cls, *args):
if len(args) == 1: # tuple entrance
hour, minute = args[0]
elif len(args) == 2: # hour, minute entrance
hour, minute = args[0], args[1]
else:
raise TypeError('wrong input to TimeStamp')
div, minute = divmod(minute, 60)
hour += div
_, hour = divmod(hour, 24)
return tuple.__new__(cls, (hour, minute))
#property
def abs_min(self):
return self.hour * 60 + self.minute
def __gt__(self, rhs):
return self.abs_min > rhs.abs_min
def __lt__(self, rhs):
return self.abs_min < rhs.abs_min
def __eq__(self, rhs):
return self.abs_min == rhs.abs_min
New version:
class TimeStamp:
def __init__(self, *args):
for argument in args:
if not isinstance(argument, int):
raise TypeError("Can only build TimeStamp from ints, not: " + str(argument))
if len(args) == 1: # init by abs
self.abs = args[0] # put the ELEMENT, not the tuple itself
elif len(args) == 2: # init by hour:minute
hour, minute = args
self.abs = hour * 60 * 60 + minute * 60
elif len(args) == 4: #init by day:hour:minute:second
day, hour, minute, second = args
self.abs = day * 24 * 60 * 60 + hour * 60 * 60 + minute * 60 + second
else:
raise TypeError("wrong data for TimeStamp: " + str(args))
def __eq__(self, other):
if isinstance(other, TimeStamp):
return self.abs == other.abs
else:
raise TypeError("wrong argument for comparison: " + str(other))
def __gt__(self, other):
if isinstance(other, TimeStamp):
return self.abs > other.abs
else:
raise TypeError("wrong argument for comparison: " + str(other))
def __lt__(self, other):
if isinstance(other, TimeStamp):
return self.abs < other.abs
else:
raise TypeError("wrong argument for comparison: " + str(other))
Now for the comparison part:
if args[1] >= self.start:
>>TypeError: unorderable types: TimeStamp() >= TimeStamp()
I have found two fixes: first, to replace my comparison line with
if args[1] > self.start or args[1] == self.start:
or an alternative, to add
def __ge__(self, other):
if isinstance(other, TimeStamp):
return self.abs >= other.abs
else:
raise TypeError("wrong argument for comparison: " + str(other))
to my new class. However, the old one did work with neither of those fixes. It looks to me as if Python stopped deducting that ((a>b) or (a==b)) implies (a>=b). But why did it work before? Does it have something to do with me subclassing tuple?
PS. don't be scared by my __init__ code, which I included for completeness. It's supposed to be overload-like, but I might be doing it in a non-pythonic way (still learning)

The old one worked because it inherited from tuple, and tuple provides __ge__. Your new version does not inherit from tuple, so it doesn't have a __ge__ method.
Even in your old version, your __gt__ and __lt__ methods were never being called when using >= (as you can verify by putting print inside those methods). The underlying tuple.__ge__ was being called instead. However, for your case, the effect is the same, so you didn't notice. That is, given that the "minutes" number is always less than 60, comparing (hours, minutes) tuples in the usual way is equivalent to comparing 60*hours + minutes. So I don't think you really need to define the comparison methods at all if you inherit from tuple.

Related

multistep comparison test python

I wanna implement a class overloading and conclude if an event with a given point of time for example 12:59:50 happens before another event so the output is true or false, just a simple comparison test. I implemented it, as you can see, but, i'm pretty much sure this is not the most pythonic or better to say, objected oriented approach to carry out the tasks. I'm new to python so is there any improvement out there ?
Thanks
def __lt__(self, other):
if self.hour < other.hour:
return True
elif (self.hour == other.hour) and (self.minute < other.minute):
return True
elif (self.hour == other.hour) and (self.minute == other.minute) and (self.second < other.second):
return True
else:
return False
Tuples (and other sequences) already perform the type of lexicographic comparison you are implementing:
def __lt__(self, other):
return (self.hour, self.minute, self.second) < (other.hour, other.minute, other.second)
The operator module can clean that up a little:
from operator import attrgetter
def __lt__(self, other):
hms = attrgetter("hour", "minute", "second")
return hms(self) < hms(other)

How to define an indexing method within a class (__getitem__ attempted)

I am new to oop in python.
Below is a class for a mathod that is similar to range() except that it is inclusive for the range boundary.
I am trying to create an indexing method inside the class so that when a specific index is called the element with that index is returned. I read that __getitem__ can perform indexing yet I have not been successful in implementing it correctly. If there is a more efficient way not necessarily using __getitem__ please advise.
Please take a look at the code below, this is a simple code aimed at learning how create classes.
the method starting at def __getitem__(self,index) is the one that does not work and this corresponds to calling the index at the end o[4] which is what I would like to achieve.
class inclusive_range:
def __init__(self, *args):
numargs = len(args)
if numargs < 1: raise TypeError('requires at least one argument')
elif numargs == 1:
self.stop = args[0]
self.start = 0
self.step = 1
elif numargs == 2:
(self.start,self.stop) = args
self.step = 1
elif numargs == 3:
(self.start,self.stop,self.step) = args
else:
raise TypeError('three arguments required at most, got {}'.format(numargs))
def __iter__(self): # this makes the function (the method) and iterable function
i = self.start
while i <= self.stop:
yield i
i += self.step
def __getitem__(self,index):
return self[index]
print(self[index])
def main():
o = inclusive_range(5, 10, 1)
for i in o: print(i, end=' ')
o[2]
if __name__ == "__main__": main()
Thank you
You can just calculate the number based on self.start, the index and the step size. For your object to be a proper sequence you also need a length, which comes in handy when testing for the boundaries:
def __len__(self):
start, stop, step = self.start, self.stop, self.step
if step < 0:
lo, hi = stop, start
else:
lo, hi = start, stop
return ((hi - lo) // abs(step)) + 1
def __getitem__(self, i):
length = len(self)
if i < 0:
i += length
if 0 <= i < length:
return self.start + i * self.step
raise IndexError('Index out of range: {}'.format(i))
The above is based on my own translation of the range() source code to Python, with a small adjustment to account for the end being inclusive.
I'd cache the __len__ result in __init__, to avoid having to re-calculate it each time you want to know the length.

Extend date class

I have issue with extending date class in python.
Since date is static class, however, I haven't get it working as it supposed to be. I tried to modify code base on datetime, since this class extend from date. However, failed.
Question is:
How to modify code to make it working like:
ed = ExtendDate(2011,1,1,week=3, quarter=3)
print ed.week # 3
How to deal with __new__, and __init__ in python static class (in theory) ?
How to extend date class (in general) ?
Thanks.
class ExtendDate(date):
"""Extend to have week and quarter property"""
#
# def __init__(self, year, month, day, week=None, quarter=None):
# pass
def __init__(self, year, month, day, week=None, quarter=None):
print 0
super(ExtendDate, self).__init__(year,month,day)
print 1
self._week = week
self._quarter = quarter
#staticmethod
def __new__(cls, year, month, day, week=None, quarter=None):
cls._week = 1
super(ExtendDate, cls).__new__(year, month, day)
def __cmp__(self, other):
if self.cmp_year(other) in [-1,0,1]:
return self.cmp_year(other)
if hasattr(self, 'quarter'):
return self.cmp_quarter(other)
if hasattr(self, 'week'):
return self.cmp_week(other)
# TODO: test - what if it's just a normal date object ?
pass
def cmp_quarter(self, other):
if self.quarter < other.quarter:
return -1
elif self.quarter == other.quarter:
return 0
elif self.quarter > other.quarter:
return 1
def cmp_week(self, other):
if self.week < other.week:
return -1
elif self.week == other.week:
return 0
elif self.week > other.week:
return 1
def cmp_year(self, other):
if self.year < other.year:
return -1
elif self.year == other.year:
return 0
elif self.year > other.year:
return 1
def __repr__(self):
return 'year:' + str(self.year) + ' ' + \
'quarter:' + str(self.quarter) + ' ' + \
'month:' + str(self.month) + ' '+ \
'week:' + str(self.week) + ' '+ \
'day:' + str(self.day) + ' '
week = property(lambda self: 0)
quarter = property(lambda self: 0)
#week.setter
def week(self, value):
self._week = value
#quarter.setter
def quarter(self, value):
self._quarter = value
__new__ isn't supposed to be decorated with #staticmethod (it's a class method implicitly, no decorator should be used). And it's supposed to return the instance created, where your code is modifying cls (which it shouldn't) and not returning the result of the super __new__ call (which it should). Also, if you're providing a __new__ (to make the class logically immutable), you shouldn't provide a __init__.
Really, the code you'd want would be something like:
class ExtendDate(date):
"""Extend to have week and quarter property"""
__slots__ = '_week', '_quarter' # Follow date's lead and limit additional attributes
def __new__(cls, year, month, day, week=None, quarter=None):
self = super(ExtendDate, cls).__new__(cls, year, month, day)
self._week = week
self._quarter = quarter
return self
Presumably, you'd also want functional getters, e.g.
#property
def week(self):
return self._week
# ... etc. ...
As #ShadowRanger answsered, I changed some lines to make it working.
The major change is __new__(cls, year, month, day):
from datetime import date
class ExtendDate(date):
"""Extend to have week and quarter property"""
__slots__ = '_week', '_quarter'
def __new__(cls, year, month, day, week=None, quarter=None):
# self = super(ExtendDate, cls).__new__(year, month, day)
self = date.__new__(cls, year, month, day)
self._week = week
self._quarter = quarter
return self
def __cmp__(self, other):
if self.cmp_year(other) in [-1,0,1]:
return self.cmp_year(other)
if hasattr(self, 'quarter'):
return self.cmp_quarter(other)
if hasattr(self, 'week'):
return self.cmp_week(other)
# TODO: test - what if it's just a normal date object ?
pass
def cmp_quarter(self, other):
if self.quarter < other.quarter:
return -1
elif self.quarter == other.quarter:
return 0
elif self.quarter > other.quarter:
return 1
def cmp_week(self, other):
if self.week < other.week:
return -1
elif self.week == other.week:
return 0
elif self.week > other.week:
return 1
def cmp_year(self, other):
if self.year < other.year:
return -1
elif self.year == other.year:
return 0
elif self.year > other.year:
return 1
def __repr__(self):
return 'year:' + str(self.year) + ' ' + \
'quarter:' + str(self.quarter) + ' ' + \
'month:' + str(self.month) + ' '+ \
'week:' + str(self.week) + ' '+ \
'day:' + str(self.day) + ' '
#property
def week(self):
return self._week
#property
def quarter(self):
return self._quarter
ed = ExtendDate(2011,1,2,3,4)
print type(ed)
print ed._week

When I use a method in division why does it count as a method and not as the returned values type(in this case an integer)?

I have a homework assignment to make a Time class. We have to overload the addition operator, and while setting it up, I am using the classes "total_minutes" method which returns the total number of minutes in the time. I am trying to divide it by 60 to get the hours (and %60 for minutes), but I get an error "TypeError: unsupported operand type(s) for /: 'method' and 'int'" The way I am typing it is this:
total = self.total_minutes
new_hr = total/60
I don't understand why total is a method. If self.total_minutes returns an integer, then shouldn't total be the value that is returned by self.total_minutes (an integer)? I'll post the whole code, maybe there is something else I am missing, but I think that's the area where I need to type something differently because the total_minutes method works on its own.
class Time:
def __init__(self, init_hr = 12, init_min = 0, init_ampm = "AM"):
if init_hr < 1 or init_hr > 12:
raise Exception("Error: Invalid hour for Time object")
if init_min < 0 or init_min > 59:
raise Exception("Error: Invalid minute for Time object")
init_ampm = init_ampm.upper()
if init_ampm != "AM" and init_ampm != "PM":
raise Exception("Error: Invalid am/pm flag for Time object")
self.hr = init_hr
self.min = init_min
self.ampm = init_ampm
# IMPLEMENT THE REMAINING METHODS OF THE Time CLASS BELOW!!
def hour(self):
return self.hr
def minute(self):
return self.min
def am_pm(self):
return self.ampm
def total_minutes(self):
if self.ampm == "AM" and self.hr <= 11:
return (self.hr*60 + self.min)
elif self.ampm == "AM" and self.hr == 12:
return self.min
elif self.ampm == "PM" and self.hr <= 11:
return (self.hr*60 + self.min + 720)
else:
return (int(self.hr*60 + self.min))
def __str__(self):
return ("%d:%02d%s"%(self.hr, self.min, self.ampm))
def __repr__(self):
return str(self)
def __add__(self, mins):
total = self.total_minutes
new_hr = total/60
if new_hr > 12:
new_ampm = "PM"
new_hr == new_hr - 12
else:
new_ampm = "AM"
new_min = total%60
return Time(new_hr, new_min, new_ampm)
Well you are setting total EQUAL to the method here and not actually calling the method. To call the method do self.total_minutes()
total_minutes is a method because that's how you defined it. In order to run a method and get its return value, you must invoke it:
total = self.total_minutes()
The parentheses cause it to be invoked, giving the return value, which is presumably an integer.
You need to use total = self.total_minutes() rather than what you currently have. What you currently have looks for a variable or a method within the this object. If you call self.total_minutes(), it executes the total_minutes() function and assigns that return value to the total variable. Without the parenthesis, it is setting total equal to the method itself, not what the method returns. Whenever you call a function, you must use parenthesis even if you are not passing any parameters in the parenthesis.

Enforcing type restriction in list abstraction using python

Below is the list abstraction in functional paradigm, that encapsulates any type of data in its representation.
empty_rlist = None
#Representation - start
#Constructor
def rlist(first, rest):
return(first, rest)
#Selector
def first(s):
return s[0]
def rest(s):
return s[1]
#Representation - end
#Constructor and Selector constitutes ADT(above) that supports below invariant:
#If a recursive list s is constructed from a first element f and a recursive list r, then
# • first(s) returns f, and
# • rest(s) returns r, which is a recursive list.
#Usage(interface) - start
def create_list(first, rest):
return rlist(first, rest)
def len_rlist(s):
"""Compute the length of the recursive list s"""
def compute_length(s, length):
if s is empty_rlist:
return length
else:
return compute_length(rest(s), length + 1)
return compute_length(s, 0)
def getitem_rlist(s, i):
"""Return the element at index i of recursive list s"""
if i == 1:
return first(s)
else:
return getitem_rlist(rest(s), i-1)
def count(s, value):
"""Count the occurence of value in the list s """
def count_occurence(s, value, count):
if s == empty_rlist:
return count
else:
if first(s) == value:
return count_occurence(rest(s), value, count + 1)
else:
return count_occurence(rest(s), value, count)
return count_occurence(s, value, 0)
#Usage - end
Lst = empty_rlist
Lst = create_list(4, Lst)
Lst = create_list(3, Lst)
Lst = create_list(1, Lst)
Lst = create_list(1, Lst)
print(count(Lst, 1))
In the above code, interfaces that are provided to users of this abstraction are create_list / len_rlist / getitem_rlist / count.
Questions:
How to enforce that the object passed to parameter(s) of interfaces len_rlist / getitem_rlist / count is nothing but the object provided by create_list interface?
How to enforce above list abstraction store same type data?
Note: Practically it is required to enforce these rules from syntax perspective.
Because python is dynamicaly typed language you can't check type before executing. But in reality sometimes need check input parameters, return values. I use next solutions for this tasks:
def accepts(*types):
"""Check input types"""
#print types
def check_accepts(f):
assert len(types) == f.func_code.co_argcount
def new_f(*args, **kwds):
for (a, t) in zip(args, types):
assert isinstance(a, t), \
"arg %r does not match %s" % (a,t)
return f(*args, **kwds)
new_f.func_name = f.func_name
return new_f
return check_accepts
def returns(rtype):
"""Check returns type"""
def check_returns(f):
def new_f(*args, **kwds):
result = f(*args, **kwds)
assert isinstance(result, rtype), \
"return value %r does not match %s" % (result,rtype)
return result
new_f.func_name = f.func_name
return new_f
return check_returns
if __name__ == '__main__':
import types
#returns(types.NoneType) #Check None as result
#accepts(int, (int,float)) #First param int; second int or float
def func(arg1, arg2):
#return str(arg1 * arg2)
pass
func(1, 2)
In order to enforce the type, you will have to provide the type as a parameter somewhere in your constructor. Consider building a parameterized type constructor. Here is an example.
>>> def list_spec_for(type_):
... empty_rlist = type_()
... def rlist(first, rest):
... return (type_(first), rest)
... return empty_rlist, rlist
>>> empty_rlist, rlist = list_spec_for(int)
>>> empty_rlist
0
>>> rlist(1, empty_rlist)
(1, 0)
>>> rlist("1", empty_rlist)
(1, 0)
>>> rlist("one", empty_rlist)
ValueError: invalid literal for int() with base 10: 'one'
If accepting "1" is not OK for your purpose, you can of course add an isinstance check to the definition of rlist.
Python is not a strongly typed language. More exactly, it is a dynamic typed. That means that variables contains values that do have a type, but the language itself will never forbids to put a value of a different type in a variable.
a = 1 # a contains an integer value
a = "abc" # a now contains a string value
But, you have the isinstance and type functions that could help to achieve this requirement : you could affect a type to your recursive list and only allow to bind together an element and a recursive list of compatible types.
The full spec could be :
a rlist stores the type of the element it accepts
a rlist can be constructed by adding a first element for which isinstance(elt, typ) is true, and typ is the accepted typ of the rest part
an initial list can be constructed by giving it explicetly a type, or by using the type of its first element
Implementation:
class rlist:
def __init__(self, first, rest=None, typ=None):
self.first = first
self.rest = rest
if rest is None: # initial creation
self.typ = type(first) if typ is None else typ
else:
if not isinstance(rest, rlist):
raise TypeError(str(rest) + " not a rlist"
self.typ = rest.typ
if not isinstance(first, self.typ):
raise TypeError(str(first) + "not a " + str(typ))
# other methods ...
But when you need strong typing, you should wonder if Python is really the appropriate language - Java is strongly typed and natively supports all that. The Python way is more I accept this and just hope it'll fit, programmer should know what he does

Categories

Resources