Overload __init__() for a subclass of Enum - python

I'm trying to overload the __init__() method of a subclass of an enum. Strangely, the pattern that work with a normal class doesn't work anymore with Enum.
The following show the desired pattern working with a normal class:
class Integer:
def __init__(self, a):
"""Accepts only int"""
assert isinstance(a, int)
self.a = a
def __repr__(self):
return str(self.a)
class RobustInteger(Integer):
def __init__(self, a):
"""Accepts int or str"""
if isinstance(a, str):
super().__init__(int(a))
else:
super().__init__(a)
print(Integer(1))
# 1
print(RobustInteger(1))
# 1
print(RobustInteger('1'))
# 1
The same pattern then breaks if used with an Enum:
from enum import Enum
from datetime import date
class WeekDay(Enum):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
def __init__(self, value):
"""Accepts int or date"""
if isinstance(value, date):
super().__init__(date.weekday())
else:
super().__init__(value)
assert WeekDay(0) == WeekDay.MONDAY
assert WeekDay(date(2019, 4, 3)) == WeekDay.MONDAY
# ---------------------------------------------------------------------------
# TypeError Traceback (most recent call last)
# /path/to/my/test/file.py in <module>()
# 27
# 28
# ---> 29 class WeekDay(Enum):
# 30 MONDAY = 0
# 31 TUESDAY = 1
# /path/to/my/virtualenv/lib/python3.6/enum.py in __new__(metacls, cls, bases, classdict)
# 208 enum_member._name_ = member_name
# 209 enum_member.__objclass__ = enum_class
# --> 210 enum_member.__init__(*args)
# 211 # If another member with the same value was already defined, the
# 212 # new member becomes an alias to the existing one.
# /path/to/my/test/file.py in __init__(self, value)
# 40 super().__init__(date.weekday())
# 41 else:
# ---> 42 super().__init__(value)
# 43
# 44
# TypeError: object.__init__() takes no parameters

You have to overload the _missing_ hook. All instances of WeekDay are created when the class is first defined; WeekDay(date(...)) is an indexing operation rather than a creation operation, and __new__ is initially looking for pre-existing values bound to the integers 0 to 6. Failing that, it calls _missing_, in which you can convert the date object into such an integer.
class WeekDay(Enum):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
#classmethod
def _missing_(cls, value):
if isinstance(value, date):
return cls(value.weekday())
return super()._missing_(value)
A few examples:
>>> WeekDay(date(2019,3,7))
<WeekDay.THURSDAY: 3>
>>> assert WeekDay(date(2019, 4, 1)) == WeekDay.MONDAY
>>> assert WeekDay(date(2019, 4, 3)) == WeekDay.MONDAY
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AssertionError
(Note: _missing_ is not available prior to Python 3.6.)
Prior to 3.6, it seems you can override EnumMeta.__call__ to make the same check, but I'm not sure if this will have unintended side effects. (Reasoning about __call__ always makes my head spin a little.)
# Silently convert an instance of datatime.date to a day-of-week
# integer for lookup.
class WeekDayMeta(EnumMeta):
def __call__(cls, value, *args, **kwargs):
if isinstance(value, date):
value = value.weekday())
return super().__call__(value, *args, **kwargs)
class WeekDay(Enum, metaclass=WeekDayMeta):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6

There is a much better answer, but I post this anyway as it might be helpful for understanding the issue.
The docs gives this hint:
EnumMeta creates them all while it is creating the Enum class itself,
and then puts a custom new() in place to ensure that no new ones
are ever instantiated by returning only the existing member instances.
So we have to wait with redefining __new__ until the class is created. With some ugly patching this passes the test:
from enum import Enum
from datetime import date
class WeekDay(Enum):
MONDAY = 0
TUESDAY = 1
WEDNESDAY = 2
THURSDAY = 3
FRIDAY = 4
SATURDAY = 5
SUNDAY = 6
wnew = WeekDay.__new__
def _new(cls, value):
if isinstance(value, date):
return wnew(cls, value.weekday()) # not date.weekday()
else:
return wnew(cls, value)
WeekDay.__new__ = _new
assert WeekDay(0) == WeekDay.MONDAY
assert WeekDay(date(2019, 3, 4)) == WeekDay.MONDAY # not 2019,4,3

Related

What dunder method to use in Python to return an attribute of the object?

I am trying to create a "CumulativeMovingAverage" class. That's what I did:
class CumulativeMovingAverage():
cma = None
n = 0
def add(self, *args):
if self.cma is None:
self.cma = args[0]
else:
self.cma = (args[0] + self.n*self.cma) / (self.n+1)
self.n += 1
return None
def __call__(self):
return self.cma
It works like this:
a = CumulativeMovingAverage()
a.add(2)
a.add(4)
a.cma ==> 3
a() ==> 3
I would like to overwrite a dunder method such that
a ==> 3
and also
b = a + 100
b ==> 103
That is, without having to call a with parenthesis. Is it possible? What dunder should I overwrite?

IntEnum returning AttributeError: can't set attribute

That is an unsettling problem. For the function:
def influencePositive(q1, q2):
if(q1.magnitude.greaterZero()):
q2.derivative.value = DValue.add(q2.derivative.value, 1)
The following unit test runs without issues:
def test_i_plus_active(self):
q1 = Quantity(Magnitude(MValue.PLUS), None)
q2 = Quantity(None, Derivative(DValue.ZERO))
r.influencePositive(q1, q2)
self.assertEqual(q2.derivative.value, DValue.PLUS)
But for this other function:
def exogenous(q1, q2, value):
if type(value) == int:
q2.derivative.value = DValue.add(q1.derivative.value, value)
The following unittest breaks:
def test_ex_increase_minus(self):
q1 = Quantity(None, DValue.MINUS)
q2 = Quantity(None, DValue.MINUS)
r.exogenous(q1, q2, 1)
self.assertEqual(q2.derivative.value, DValue.ZERO)
It raises a Atribute error: AttributeError: can't set attribute. That is the whole traceback:
Traceback (most recent call last):
File "C:/Users/Victor Zuanazzi/Documents/Artificial Intelligence/Knowledge Representation/Practicals/Qualitative_Reaoning/relationFunction_test.py", line 121, in test_ex_increase_minus
r.exogenous(q1, q2, 1)
File "C:\Users\Victor Zuanazzi\Documents\Artificial Intelligence\Knowledge Representation\Practicals\Qualitative_Reaoning\relationFunctions.py", line 31, in exogenous
q2.derivative.value = DValue.add(q1.derivative.value, value)
File "C:\ProgramData\Anaconda3\lib\types.py", line 146, in __set__
raise AttributeError("can't set attribute")
AttributeError: can't set attribute
Here is some background knowledge to understand the above code.
DValue is an IntEnum:
from enum import IntEnum
class DValue(IntEnum):
MINUS = -1
ZERO = 0
PLUS = 1
#staticmethod
def add(dvalue, delta):
return max(min(dvalue + delta, DValue.PLUS), DValue.MINUS)
We use that to set the class Derivative:
class Derivative:
def __init__(self, value):
#if value is type int, it is converted to Enum.
if value is int:
value = DValue(value)
self.value = value
Quantity is a class that has a magnitude and derivative values set to it's instances:
from magnitude import Magnitude, MValue
from derivative import Derivative, DValue
class Quantity:
def __init__(self, magnitude, derivative):
self.magnitude = magnitude
self.derivative = derivative
I don't see why influencePositive() works just fine and exogenous() breaks. They both call DValue.add() in the same way.
Here is your issue:
In the case of your first test, the derivative is a Derivative object, whose .value attribute can be reassigned. In your second test, the derivative is a DValue (IntEnum) object, whose .value attribute cannot be reassigned.
In [4]: d = Derivative(DValue.ZERO)
In [5]: d.value
Out[5]: <DValue.ZERO: 0>
In [6]: d.value = 1
In [7]: d.value
Out[7]: 1
In [8]: d = DValue.MINUS
In [9]: d.value
Out[9]: -1
In [10]: d.value = 1
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
<ipython-input-10-3c0164b4d46d> in <module>()
----> 1 d.value = 1
/home/ethan/.local/lib/python2.7/site-packages/enum/__init__.pyc in __set__(self, instance, value)
54
55 def __set__(self, instance, value):
---> 56 raise AttributeError("can't set attribute")
57
58 def __delete__(self, instance):
AttributeError: can't set attribute
So I think your second test should set up q2 like this maybe:
q2 = Quantity(None, Derivative(DValue.MINUS))

Python: Custom class to work with nested defaultdict

Hello I have these two classes
class BaseCounter(object):
def __init__(self):
print ("BaseCounter init = ")
self._counter = 0
def increment(self, count=1):
self._counter += count
def items(self):
return self._counter
class DictCounter(object):
def __init__(self, dict_class):
self._counter = defaultdict(lambda: dict_class)
def increment(self, key, value, *args, **kwargs):
print (key, value, args, kwargs)
self._counter[key].increment(value, *args, **kwargs)
def items(self):
result = []
for key, counter in self._counter.items():
result.append((key, counter.items()))
return result
and I am trying to create something like this:
y = DictCounter(DictCounter(DictCounter(BaseCounter())))
y.increment(10,1,2,3)
y.increment(10,1,2,3)
y.increment(10,1,3,3)
y.increment(10,2,2,3)
which leads to
10 1 2 12
10 1 3 12
10 2 2 12
10 2 3 12
but I was expecting
10 1 2 6
10 1 3 3
10 2 2 3
it should simulate, which is working correctly
defaultdict(defaultdict(defaultdict(int))) "with counter at the end"
but I am confused with the behavior (I think there will be problem with shallow copy or something with references)
Any idea?
As Martijn Pieters said. The problem was referencing to the same object (dict_class) for every new key. So instead of this:
class DictCounter(object):
def __init__(self, dict_class):
self._counter = defaultdict(lambda: dict_class)
....
DictCounter(DictCounter(DictCounter(BaseCounter())))
do this:
class DictCounter(object):
def __init__(self, dict_class):
self._counter = defaultdict(dict_class)
....
DictCounter(lambda: DictCounter(lambda: DictCounter(lambda: BaseCounter())))
I was trying to describe it a little bit more at my blog.

How to properly overload the __add__ method?

I am required to write a class involving dates. I am supposed to overload the + operator to allow days being added to dates. To explain how it works: A Date object is represented as (2016, 4, 15) in the format (year, month, date). Adding integer 10 to this should yield (2016, 4, 25). The Date class has values self.year, self.month, self.day.
My problem is that the code is supposed to work in the form Date + 10 as well as 10 + Date. Also Date - 1 should work in the sense of adding a negative number of days. Date(2016, 4, 25) - 1 returns Date(2016, 4, 24).
My code works perfectly in the form of Date + 10 but not in the form 10 + D or D - 1.
def __add__(self,value):
if type(self) != int and type(self) != Date or (type(value) != int and type(value) != Date):
raise TypeError
if type(self) == Date:
day = self.day
month = self.month
year = self.year
value = value
if type(value) != int:
raise TypeError
days_to_add = value
while days_to_add > 0:
day+=1
if day == Date.days_in(year,month):
month+=1
if month > 12:
day = 0
month = 1
year+=1
day = 0
days_to_add -=1
return(Date(year,month,day))
These are the errors I get
TypeError: unsupported operand type(s) for +: 'int' and 'Date'
TypeError: unsupported operand type(s) for -: 'Date' and 'int'
__radd__ handles right side addition so you need to implement that as well.
I am seeing some flaws in your implementation so I recommend you using datetime module (especially datetime.timedelta class) to at least handle basic date arithmetic correctly:
import datetime
class Date(object):
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def as_date(self):
return datetime.date(self.year, self.month, self.day)
def __add__(self, other):
if isinstance(other, int):
date = self.as_date() + datetime.timedelta(days=other)
return Date(date.year, date.month, date.day)
else:
raise ValueError("int value is required")
def __radd__(self, other):
return self.__add__(other)
def __sub__(self, other):
return self.__add__(-other)
def __rsub__(self, other):
raise RuntimeError("Doesn't make sense.")
def __repr__(self):
return str(self.as_date())
Demo:
>>> date = Date(2015, 10, 23)
>>> print date + 10 # __add__ is called
2015-11-02
>>> print 20 + date # __radd__ is called
2015-11-12
>>> print date - 25 # __sub__ is called
2015-09-28
>>> print 25 - date # __rsub__ is called
RuntimeError: Doesn't make sense

Get next enumerator constant/property

Lets's say I have an enumerator, is it possible to get the property that follows? So if I had today=Days.Sunday would I be able to do something like tomorrow=today.next()?
example:
class Days(Enum):
Sunday = 'S'
Monday = 'M'
...
Saturday = 'Sa'
I know I could use tuples (like below) to do something like tomorrow=today[1], but I was hoping there was something built in or more elegant.
class Days(Enum):
Sunday = ('S','Monday')
Monday = ('M','Tuesday')
...
Saturday = ('Sa','Sunday')
Absolutely.
Just add the desired functionality to your Days class:
class Days(Enum):
Sunday = 'S'
Monday = 'M'
Tuesday = 'T'
Wednesday = 'W'
Thursday = 'Th'
Friday = 'F'
Saturday = 'Sa'
def next(self):
cls = self.__class__
members = list(cls)
index = members.index(self) + 1
if index >= len(members):
index = 0
return members[index]
and in use:
today = Days.Wednesday
print(today.next())
# Days.Thursday
While the above is probably easier to understand, it is possible to do the work once in __init__ by adding a next attribute to each member (and previous while we're at it).
class Days(Enum):
#
Sunday = 'S'
Monday = 'M'
Tuesday = 'T'
Wednesday = 'W'
Thursday = 'Th'
Friday = 'F'
Saturday = 'Sa'
#
def __init__(self, value):
if len(self.__class__):
# make links
all = list(self.__class__)
first, previous = all[0], all[-1]
previous.next = self
self.previous = previous
self.next = first
and in use:
>>> Days.Tuesday.next
<Days.Wednesday: 'W'>
>>> Days.Tuesday.previous
<Days.Monday: 'M'>
>>> Days.Saturday.next
<Days.Sunday: 'S'>
>>> Days.Saturday.previous
<Days.Friday: 'F'>
NB Using the this method of attributes means we no longer need the ()s after next/previous.
You can create a dictionary to lookup the next day like so:
In [10]: class Days(Enum):
Sun = 'Su'
Mon = 'M'
Tue = 'Tu'
Wed = 'W'
Thu = 'Th'
Fri = 'F'
Sat = 'Sa'
In [11]: days = list(Days)
In [12]: nxt = dict((day, days[(i+1) % len(days)]) for i, day in enumerate(days))
Quick test:
In [13]: nxt[Days.Tue]
Out[13]: <Days.Wed: 'W'>
In [14]: nxt[Days.Sat]
Out[14]: <Days.Sun: 'Su'>
#ENUM CLASS
#colors
import enum
class Color(enum.Enum):
turquoise = 1
indigo = 2
magenta = 3
cyan = 4
teal = 5
azure = 6
rose = 7
amber = 8
vermillon = 9
plum = 10
russet = 11
slate = 12
def __iter__(self):
self.idx_current = self.value
return self
def __next__(self):
if (self.idx_current > 12):
return None
self.idx_current = self.idx_current + 1
return Color (self.idx_current - 1)
#CLIENT CODE
#iterate colors starting from cyan
it = iter (Color.cyan)
while True:
#print (v.get_id())
c = next (it)
if c is None:
break
print(c)
#OUTPUT
Color.cyan
Color.teal
Color.azure
Color.rose
Color.amber
Color.vermillon
Color.plum
Color.russet
Color.slate
For me that seems like the most elegant solution without additional functions
day = Days.Sunday
day = Days((day.value + 1) % len(Days) + 1) # next day cycled

Categories