Welcome to the Treehouse Community

Want to collaborate on code errors? Have bugs you need feedback on? Looking for an extra set of eyes on your latest project? Get support with fellow developers, designers, and programmers of all backgrounds and skill levels here with the Treehouse Community! While you're at it, check out some resources Treehouse students have shared here.

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and join thousands of Treehouse students and alumni in the community today.

Start your free trial

Python

My address book class with search function (Regular Expressions in Python Extra Credit)

Hi all,

I'm a huge fan of Python, and also really like regular expressions and what you can do with them.

Here's my code for an AddressBook class, including a search function:

import re
import sys


class Contact(object):
    """Contact to be created by running regex on a text file/string"""
    def __init__(self, match):
        for key, value in match.groupdict().items():
            setattr(self, key, value)

    def __str__(self):
        return '\n'.join(
            sorted(
                ["\t{}: {}".format(
                                   key, val
                                   ) for key, val in self.__dict__.items()]))


class AddressBook(object):
    """Address Book containing multiple Contact objects."""

    def __init__(self, filename):
        self.names_file = open(filename, encoding="utf-8")
        self.data = self.names_file.read()
        self.names_file.close()
        self.contacts = [Contact(match) for match in line.finditer(self.data)]

    def search(self):
        results = []
        result_count = 0
        search_term = input("\nEnter a term to search the address book with"
                            + " or 'q' to quit:\n")

        if not search_term:
            return self.search()
        elif search_term.lower() == 'q':
            exit()

        for contact in self.contacts:
            # if search_term in contact.__dict__.values():
            for val in contact.__dict__.values():
                if val and re.search("(" + search_term + ")", val):
                    result_string = "Result:\n{}".format(str(contact))
                    if result_string not in results:
                        results.append(result_string)
                        result_count += 1
                else:
                    continue

        if results:
            print('\nThere {} {} result{}:\n\n{}'.format(
                'is' if result_count == 1 else 'are',
                result_count,
                's' if result_count > 1 else '',
                '\n\n'.join(results)))
        else:
            print("Sorry, there are no results for that search term.")

        return self.search()


if (len(sys.argv) > 1):
    address_book = AddressBook(sys.argv[1])
else:
    address_book = AddressBook(
        input("What's the name of the file to get the contacts from?\n"))

address_book.search()

It's a bit messy... but let me know what you think!

Hey, thanks for posting this up. Helps me out. I'm wondering what this line does exactly. Is this creating a list of dictionaries--Contact(object) by sending match through again and again?

self.contacts = [Contact(match) for match in line.finditer(self.data)]

Glad you asked... I did this a while ago and it helped to go back and look at it. I then realised I didn't post the entire script.

Not sure why I didn't include the actual regex part... maybe because it was already developed before the end of the course.

line = re.compile(r'''
^(?P<name>(?P<name_last>[-\w ]*),\s(?P<name_first>[-\w ]+))\t  # Last and first names
(?P<email>[-\w\d.+]+@[-\w\d.]+)\t  # Email
(?P<phone>\(?\d{3}\)?-?\s?\d{3}-\d{4})?\t  # Phone
(?P<job>[\w\s]+,\s[\w .]+)\t?  # Job and Company
(?P<twitter>@[\w\d]+)?$  # Twitter
''', re.X|re.M)

So line is a precompiled regex. finditer will use that regex and produce an iterable containing the matches in the string argument as match objects. In this case, I'm passing the string in self.data that was read from the file.

The list comprehension is then taking each match it finds and creating an instance of the Contact class. It's not creating a list of dictionaries, though the Contact class uses the groupdict method of the match object to create a dictionary containing key/value pairs of the named groups in the match (name, email, etc).

If you haven't already completed the Python Comprehensions workshop, I'd highly recommend it. Comprehensions are one of my favourite features of the Python language.

Thanks for the quick response Iain! Funny thing, I actually had the python comprehensions video open in another tab while I read your comment. I think I'm understanding it better now: search.contacts is a list containing instances of Contact class; which is accessed by iterating through dict (which I'll have to look up info on).

Do you mind also explaining what you're verifying with the re.search on this line and it's formatting here (specifically the lack of 'r' and the '(' ')':

if val and re.search("(" + search_term + ")", val):

#Why not something like:
if val.lower() == search_term.lower():

The r before a string makes it a raw string, and just makes it easier to write (you don't have to escape as much).

I've used string concatenation to insert the value of the variable into the regex search pattern.

The reason to use re.search instead of just an equality check is that re.search looks for matches anywhere in the string. The equality conditional would need both strings in their entirety to be equal.

I maybe could have used if search_term.lower() in val.lower(), but since this was a course on regex, it seemed appropriate to do it the way I did!

There's also a way to use format() on a regex pattern to replace the variables in it. You use sets of double curly braces.

Great explanations, I now understand!

1 Answer

Kenneth Love
STAFF
Kenneth Love
Treehouse Guest Teacher

Yeah, a bit messy, but a great start. I'd recommend breaking things up into some smaller packages, use more functions/methods, etc. You don't need the else: continue, that'll be the default behavior anyway. But, again, great start. Keep going!