3

Is it possible to use a datetime slice as a dictionary key?

Example:

st = datetime.datetime(2010, 12, 31, 0, 0)
en = datetime.datetime(2011, 12, 28, 0, 0)
b = [] 
b[slice(st,en)] = 'some_random_value_could_be_anything' # Error!

One of two error's occur:

In the single dictionary case:

TypeError: slice indices must be integers or None or have an __index__ method

In the nested dictionary case:

TypeError: unhashable type
pyCthon
  • 11,746
  • 20
  • 73
  • 135
  • 2
    What are you trying to achieve with slicing datetimes? – wenzul Dec 02 '14 at 17:03
  • This will not be efficient. Because for slicing you need an integer index value (absolute orderable) for your list. It has to be the smallest value, in your case minutes. For one year you will get over 500k entries. Tell us more about the problem itself. – wenzul Dec 02 '14 at 17:14
  • 2
    @wenzul: actually, you can use arbitrary objects for `slice()`. As long as you pass these into an `__getitem__` method that understands `datetime` objects this is fine. `slice()` objects *themselves* do not care about the types. This lets you do `some_datetime_sequence[starting_datetime:ending_datetime]` and Python will handle that as `some_datetime_sequence.__getitem__(slice(starting_datetime, ending_datetime))`. – Martijn Pieters Dec 02 '14 at 17:17
  • What are you trying to achieve with assigning `100`? Shouldn't that be an array as well? – k-nut Dec 02 '14 at 17:18
  • Yes that's right, you can create a `slice` object. But you cannot feed a `list` magican with it. :) You can access it, and now the list want to get an absolute index with... \_\_index\_\_. That's all the magic. – wenzul Dec 02 '14 at 17:20
  • Can also be read here https://docs.python.org/2/reference/datamodel.html#object.__index__ – wenzul Dec 02 '14 at 17:38
  • 1
    Fun question. Maybe you wanna take a look to this: http://stackoverflow.com/questions/2936863/python-implementing-slicing-in-getitem? I'm playing with this myself **:-)** – Savir Dec 02 '14 at 17:48
  • @wenzul ease of fixing broken code without a major re-factor, not concerned about efficency – pyCthon Dec 02 '14 at 18:13
  • @k-nut the 100 is an arbitrary value to store – pyCthon Dec 02 '14 at 18:13

2 Answers2

3

Disclaimer: This answer is mainly explanatory to show one (out of many) things that Python allows you to do. Also, you should probably NOT want to do things this way (the fact that you can do things doesn't mean you should)

This said, the documentation for getitem(self, key) states:

For sequence types, the accepted keys should be integers and slice objects.

which means that any class that wants to mimic the behavior of a sequence type (such as a list) must be prepared to implement a __getitem__ where key can be a type slice. That (kind of) explains (or at least, is somehow related to) why in your dictionary you're getting a TypeError: unhashable type when you try to do b[slice(st,en)] = 'bla ': It's because it's trying to use the slice(st, en) instance as a dictionary key. slice objects are not hashable, and therefore, can't be used as dict keys. A dict type is NOT a sequence type, so trying to slice a dictionary has no meaning.

Let's say you have:

{ ... 'foo': 1, ... 'bar': 2, ... 'baz': 3, ... }

What does it mean an slice from 'foo' to 'bar'? Would you return the set of keys in the order you entered them? ('foo', 'bar', 'baz')? Python doesn't know about that. Would it be their __hash__? That's internal, meaningless when it comes so slicing.

All this said, here's a very very bad thing... but that "works":

import datetime


class DatetimeDict(dict):
    def __getitem__(self, key):
        if isinstance(key, slice):
            sliced = {}
            start_dt = key.start
            stop_dt = key.stop
            step = key.step or 1
            internal_keys = sorted(self.keys())
            if start_dt is None:
                start_index = 0
            else:
                start_index = internal_keys.index(start_dt)
            end_index = internal_keys.index(stop_dt)
            for i in range(start_index, end_index, step):
                sliced.update({internal_keys[i]: self[internal_keys[i]]})
            return sliced
        else:
            return super(DatetimeDict, self).__getitem__(key)

    def __setitem__(self, key, val):
        return super(DatetimeDict, self).__setitem__(key, val)

a = DatetimeDict()
a[datetime.datetime.strptime('2014/01/01', '%Y/%m/%d')] = 'foo',
a[datetime.datetime.strptime('2014/01/02', '%Y/%m/%d')] = 'bar',
a[datetime.datetime.strptime('2014/01/03', '%Y/%m/%d')] = 'baz',
a[datetime.datetime.strptime('2014/01/04', '%Y/%m/%d')] = 'bla',

from_dt = datetime.datetime.strptime('2014/01/02', '%Y/%m/%d')
to_dt = datetime.datetime.strptime('2014/01/04', '%Y/%m/%d')
print a[from_dt:to_dt]

This outputs:

{
    datetime.datetime(2014, 1, 2, 0, 0): ('bar',), 
    datetime.datetime(2014, 1, 3, 0, 0): ('baz',)
}

But is bad, bad bad... DatetimeDict becomes a weird construct that is a dictionary but at the same time behaves kind of like a sequence type... Bad.

EDIT (After re-reading, I'm pretty sure I misunderstood the question)

You weren't actually trying to slice a dict, where you? The day I learn to read, I'll conquer the world... :-D

If what you want is use a range of datetimes as a dict key, I'd recommend that you just put the start and end in a tuple:

>>> import datetime
>>> st = datetime.datetime.strptime('2014/01/01', '%Y/%m/%d')
>>> en = datetime.datetime.strptime('2014/01/02', '%Y/%m/%d')
>>> key = (st, en)
>>> a = {}
>>> a[key] = 'foo'
>>> print a
{(datetime.datetime(2014, 1, 1, 0, 0), datetime.datetime(2014, 1, 2, 0, 0)): 'foo'}

Weeeelllll... at least I learned about slicing and that kind of stuff... LoL...

Savir
  • 17,568
  • 15
  • 82
  • 136
  • 1
    Don't beat yourself up, the question itself is very unclear. P.S. "the fact that you *can* do things doesn't mean you *should*" - imitation is the sincerest form of flattery, so thank you for the compliment. Thank you also for teaching me something about slicing, I didn't know it either. – Mark Ransom Dec 02 '14 at 19:24
  • @BorrajaX You had it right the first time! My example may have been unclear/misleading – pyCthon Dec 02 '14 at 19:30
  • @pyCthon Ah!! Thank you thank you **:-D** Is just that I spent like... 1 hour going back and forth, testing, yadda, yadda, and then I read `Python using a datetime slice as a dictonary key`... And I thought to myself that I had the whole thing backwards... Phew!! It's a relief!! – Savir Dec 02 '14 at 19:39
2

You can create a slice object from two datetimes.

And you could try to slice a list with it. But if the start or end are objects a list resolves this objects to an integer value via calling __index__ of these objects as stated here. That's the first error you get.

The second error you get is because you cannot create a hash for a slice object. Python always generates a hash value for key objets because dictionaries are similar to a hashlist. Slicing of dictionaries is not possible you could impelement a helper function which simulates that behaviour somehow. Another apporach would be using tuples as a dictionary key so that you can avoid a slice. Tuples are hashables.

You could try your suggested approach with implementing the __index__ method for a extended datetime class.

class MyDateTime(datetime.datetime):
    def __index__(self):
        # in your case return number of minutes (in this case since 1970/1/1)
        return int((self-datetime.datetime(1970,1,1)).total_seconds()/60)

st = MyDateTime(2010, 12, 31, 0, 0)
en = MyDateTime(2011, 12, 28, 0, 0)

b = [] 
b[slice(st,en)] = [100]

In this example you will use the least common multiple which corresponds to minutes. This will be the most useful but larger steps may also work for you.

But I not recommend it to implement some datetime intervals with slice and lists. I bet you will run into other problems like performance or complexity with it.

You have still not added what's the problem you try to solve. May I am right and you want to check DateTimeIntervals against overlapping and so on. If yes, look into DateTimeInterval.

wenzul
  • 3,948
  • 2
  • 21
  • 33