5
\$\begingroup\$

I have a functional class WordAnalyzer that uses espeak to grab the IPA phonetics of a word & uses that information to find rhymes & print the number of syllables:

from difflib import SequenceMatcher
from subprocess import check_output

class AnalyzeWord:
    """
    Uses espeak's output to get a word's phonetics & syllables.
    """
    def __init__(self, arg):
        super(AnalyzeWord, self).__init__()
        self.arg = arg
        self.word = arg
        ipa_vowels = ['ɐ', 'i', 'ɪ', 'e', 'ɛ', 'æ', 'a', 'ə', 'ɑ', 'ɒ', 'ɔ', 'ʌ', 'ɚ', 'o', 'ʊ', 'u', 'eɪ', 'aɪ']
        self.phon = list(check_output(['espeak', \
                             '-q', '-v', 'en-us', '--ipa',\
                             self.arg]).decode('UTF-8').strip())
        for i, x in enumerate(self.phon):
            if self.phon[i] == 'ɪ' and self.phon[i-1] in ipa_vowels:
                    self.phon[i-1:i+1] = [''.join(self.phon[i-1:i+1])]
        self.syll = len([l for l in self.phon if l in ipa_vowels])
    def check_rhyme(self, word):
        word = AnalyzeWord(word)
        sm = SequenceMatcher(None, self.phon, word.phon)
        perc = int(sm.ratio() * 100)
        print("Good rhyme - " + str(perc) + "%") \
                if perc > 50 else print("Not a good rhyme - " + str(perc) + "%")

test1 = AnalyzeWord('steed')
test1.check_rhyme("speed")

I think the class itself isn't as 'pythonic' as it ought to be. In particular, I'd love some help cleaning up the self.phon declaration & the following for loop.

EDIT: If there's a better way to store every IPA vowel character / combination I'd love to hear it!

\$\endgroup\$
  • \$\begingroup\$ It would help if you could add a word where the if clause in the for loop actually triggers. \$\endgroup\$ – Graipher Feb 8 '17 at 9:58
  • 1
    \$\begingroup\$ The word "able" is a good one. Without the if in that for loop the class would indicate "able" has three vowel sounds, but really it's just 'eɪ' \$\endgroup\$ – zomgdavidbowie Feb 8 '17 at 15:01
4
\$\begingroup\$

First off, naming, one of the hardest things in Software Development. I would propose a different name than AnalyzeWord, because it is a word-that-has-been-analyzed object and not an analyzer-of-words object. Maybe something like PhoneticWord, EspeakWord or simply Word might better describe this object.

Next, you call super(), even though you don't have any base-class (except object, implicitly, since you are using Python 3.x). So you don't need it at all.

Regarding the better storing of the IPA vowels, I would first make it a global constant (it does not change between instances of the class. If you want to you could also make it a class constant, same difference. Next, I would make it a set, for which membership testing is \$\mathcal{O}(1)\$ instead of \$\mathcal{O}(n)\$ as it is for a list.

In your check_rhyme function, I would add a if not isinstance(word, AnalyzeWord) check around the casting of the other word. This way you could pass in an already analyzed word. I would also make the function return the match percentage and let the caller do the printing, if they want to. In addition, returning a float here, instead of an int gives the caller even more power. If he really wants an int, let them do the casting (or, if you use this feature, together with the printing, often enough, write another function that does just that).

For the printing itself, I would use str.format.

You should add a if __name__ == '__main__': guard to your testing code so it will not be executed when you do e.g. import ip_word_analyzeer in another script.

from difflib import SequenceMatcher
from subprocess import check_output


class Word:
    """
    Uses espeak's output to get a word's phonetics & syllables.
    """
    ipa_vowels = {'ɐ', 'i', 'ɪ', 'e', 'ɛ', 'æ', 'a', 'ə',
              'ɑ', 'ɒ', 'ɔ', 'ʌ', 'ɚ', 'o', 'ʊ', 'u', 'eɪ', 'aɪ'}

    cmd = ['espeak', '-q', '-v', 'en-us', '--ipa']

    def __init__(self, arg):
        self.arg = arg
        self.word = arg

        self.phon = list(check_output(
            Word.cmd + [self.arg]).decode('UTF-8').strip())
        for i, x in enumerate(self.phon):
            if x == 'ɪ' and self.phon[i - 1] in Word.ipa_vowels:
                self.phon[i - 1:i + 1] = [''.join(self.phon[i - 1:i + 1])]
        self.syll = len([l for l in self.phon if l in Word.ipa_vowels])

    def check_rhyme(self, word):
        if not isinstance(word, Word):
            word = Word(word)
        sm = SequenceMatcher(None, self.phon, word.phon)
        return sm.ratio() * 100


def compare_words(word1, word2):
    if not isinstance(word1, Word):
        word1 = Word(word1)

    perc = word1.check_rhyme(word2)
    if perc > 50:
        print("Good rhyme - {}%".format(perc))
    else:
        print("Not a good rhyme - {}%".format(perc))


if __name__ == '__main__':    
    test1 = Word('steed')
    compare_words(test1, 'speed')

Maybe with some more thought (and an example containing a 'ɪ' vowel) that loop could be improved as well.

If you want to go slightly further, you could make this Word class inherit from str, giving it a __str__, __add__, __len__ and so on function. Or you could implement sensible things for that, maybe like so (here I actually made a new class inheriting from Word, so I don't have to re-implement the constructor and such.:

class FancyWord(Word, str):

    def __repr__(self):
        return "".join(self.phon)

    def __str__(self):
        return self.word

    def __add__(self, other):
        return FancyWord(str(self) + str(other))

    def __len__(self):
        return self.syll

    def __iter__(self):
        return iter(self.phon)

With this you can now do these things:

>>> x = FancyWord("speed")
>>> print(x)
speed
>>> x
spˈiːd
>>> repr(x)
spˈiːd
>>> len(x)
1
>>> for phoneme in x: print(phoneme)
s
p
ˈ
i
ː
d

>>> y = FancyWord("steed")
>>> x + y
spˈiːdstiːd

>>> " ".join([x, y])
'speed steed'

The last one works, because of multiple inheritance magic, so basically the second base class str of FancyWord her. Word inheriting from str, as I suggested bove and then implementing these methods would also work.

This might help you with whatever you want to further do with this class.

\$\endgroup\$
  • \$\begingroup\$ " check_rhyme function, I would add a if not isinstance(word, AnalyzeWord) check around the casting of the other word" are you sure this is useful? \$\endgroup\$ – Caridorc Feb 8 '17 at 18:18
  • \$\begingroup\$ @Caridorc It depends on what the use case is. Is it analyzing one word and throwing a lot of str words at it to find a rhyme, but discarding them all again? Or is it building a list of analyzed words and trying to match them? In the latter case you would not want the cast at all. If it is somewhere in between, I would prefer the flexibility it provides. \$\endgroup\$ – Graipher Feb 8 '17 at 18:37

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.