0

I'm trying to do the following thing: parse all the files in my music folder and if they are among my favorites, copy them to a separate folder.

In order to achieve this, I'm parsing an xml with my loved tracks and use the info to create a custom dictionary structured like this

lovedSongs[artist][title] = 1

When I'm parsing the files from my folder, I read the song title and artist from the id3 tags and, in theory, I try to see if lovedSongs[tagArtist][tagTitle] is 1

for filename in filelist:     
        if filename[-4:] == ".mp3":
            try:
                id3r = id3reader.Reader(filename)
            except UnicodeDecodeError:
                artist = "None"
                title = "None"
            except IOError:
                artist = "None"
                title = "None"
            else:
                artist = ensureutf8andstring(id3r.getValue('performer'))
                title = ensureutf8andstring(id3r.getValue('title'))

            if artist != "None" or title != "None":
                if lovedSongs[artist][title] == 1:
                    print artist + ' - ' + title

When I try to run the .py file, I get a dictionary Key Error. I found a potential answere here: How to make a python dictionary that returns key for keys missing from the dictionary instead of raising KeyError? and applied it like below, but I still got the Key Error

class SongDict(dict):
    def __init__(self, default=None):
        self.default = default

    def __getitem__(self, key):
        if not self.has_key(key):
            self[key] = self.default()
        return dict.__getitem__(self, key)

    def __missing__(self, key):
        return 0

What would be the best solution? Use try/except perhaps instead of the simple if?

P.S. I think I should mention that I'm a newbie to python, with only a couple of hours of 'experience' and this is my way of trying to learn the language.

Community
  • 1
  • 1
BBog
  • 3,425
  • 5
  • 30
  • 63

2 Answers2

2

It sounds like what you actually want is a simple set

for filename in filelist:     
    if filename.endswith(".mp3"):
        try:
            id3r = id3reader.Reader(filename)
        except (UnicodeDecodeError, IOError):
            pass
        else:
            artist = ensureutf8andstring(id3r.getValue('performer'))
            title = ensureutf8andstring(id3r.getValue('title'))

            if artist != "None" or title != "None":
                if (artist, title) in lovedSongs:
                    print artist + ' - ' + title
Antimony
  • 33,711
  • 9
  • 88
  • 96
  • I've printed my lovedSongs dict and I confirmed this entry `u'Kipling': {u'Heroin': 1}`. Then I tried a simple test `artist = 'Kipling' title = 'Heroin' if (artist, title) in lovedSongs: print artist + ' - ' + title` and nothing happened – BBog Nov 24 '12 at 19:55
  • That's because the code I posted assumes the use of a `set`. IF you post the dict creation code, I can convert it for you too. – Antimony Nov 24 '12 at 21:38
  • I did post it, from the beginning. It's the SongDict class. – BBog Nov 25 '12 at 07:01
  • You don't need any classes. Just do `lovedSongs = set()` and then for each song, `lovedSongs.add((artist, title))` – Antimony Nov 25 '12 at 14:34
1

One thing you could try is using a defaultdict, with the argument being a defaultdict(dict). Anytime you try to reference a value that doesn't exist, it will not throw an error and will instead return the default null value of what you specify:

In [7]: from collections import defaultdict

In [8]: lovedsongs = defaultdict(lambda: defaultdict(dict))

In [9]: lovedsongs['OAR']['Blackrock'] = 1

In [10]: lovedsongs['Timati']['Moj Put'] = 1

In [11]: lovedsongs
Out[11]: defaultdict(<function <lambda> at 0x24e5ed8>, {'Timati': defaultdict(<type 'dict'>, {'Moj Put': 1}), 'OAR': defaultdict(<type 'dict'>, {'Blackrock': 1})})

In [12]: lovedsongs['Not']['Found']
Out[12]: {}

In [13]: if lovedsongs['Not']['Found'] == 1:
   ....:     print 'Found it'
   ....:
   ....:

In [14]: if lovedsongs['OAR']['Blackrock'] == 1:
   ....:     print 'Found it'
   ....:
   ....:
Found it

The defaultdict(lambda: defaultdict(dict)) syntax is likely better understood by looking at how a single-level (i.e. 'more regular') defaultdict works:

In [1]: from collections import defaultdict

In [2]: d = defaultdict(int)

In [3]: d['one'] = 1

In [4]: d['two'] = 2

In [5]: d
Out[5]: defaultdict(<type 'int'>, {'two': 2, 'one': 1})

In [6]: d['one']
Out[6]: 1

In [7]: d['three']
Out[7]: 0

We declared the defaultdict with an argument of int - this means that if a value is not found, instead of throwing an error, we will just get 0 back (since 0 is the null value of an integer). If we used str, we would get '' back, and if we used dict, we'd get a dictionary back. However the tricky part in this situation is that you have a nested dictionary (defaultdict(dict)), which will run into the same problem if you try to reference a key in the nested dictionary that does not exist:

In [13]: d = defaultdict(dict)

In [14]: d['three']['more']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)

/path/<ipython console> in <module>()

KeyError: 'more'

So in order to get our nested dictionary to become a defaultdict as well, we have to do a bit of trickery: since the argument to defaultdict is something that needs to be callable (but not yet called -> see int vs. int()), we use a lambda function to somewhat simulate this behavior. At a very simple level, think of the lambda as preventing the defaultdict(dict) from being called until it is needed. Now you will have a dictionary (with a nested dictionary) that will handle missing keys two-levels deep without throwing an error. Hope that explains a bit.

RocketDonkey
  • 33,047
  • 7
  • 75
  • 83
  • It took me a while to understand your answer since it was a little cryptic for my level of knowledge, but I'm actually grateful for that, deciphering it helped me learn new things about python :D So twice I say to you: thanks! – BBog Nov 24 '12 at 20:08
  • @BBog No problem at all. That's a good point - I'll drop in a little more explanation because I do agree that it is a bit cryptic. Glad it worked out! – RocketDonkey Nov 24 '12 at 20:10
  • @BBog Let me know if that explanation helps/hurts :) – RocketDonkey Nov 24 '12 at 20:18
  • Yes, I really appreciate the fact you took the time to add more info. When you look at it now, it's actually really simple. Even more, it's what I initially wanted to do (a dictionary inside a dictionary) but I had no idea how. I just wish I could give you another upvote for the edit – BBog Nov 24 '12 at 20:37
  • @BBog Awesome, glad it makes sense (as for the up vote, thanks for the first one!) – RocketDonkey Nov 24 '12 at 20:41
  • 1
    There really is no reason to use a dictionary here at all, using a set like the other answer proposes is much more sensible (after all we are only interested whether the `(artist, title)` tuple exists and not the value it points to) – Voo Nov 24 '12 at 21:40
  • 1
    @Voo Agree that there are different ways to represent the data (I'm sure we all have a few different ways we'd tackle this in particular :) ), but since the OP mentioned he was curious about the actual dictionary error, I was hoping to explain it a bit and provide a way to get around it in his current context. – RocketDonkey Nov 24 '12 at 21:59