Python OOP --action() function
Question: I'm new to Python OOP and trying to create a OOP program to manage a library.
This code is from a book.

This code is working as expected but I need to understand how the `action()`
calls the corresponding function when I select a particular option, e.g.: when
I select `1` the `show_notes` function is called even though we don't call it.

`Menu.py`

    
    
    import sys
    from notebook import Notebook, Note
    
    class Menu:
        '''Display a menu and respond to choices when run.'''
        def __init__(self):
            self.notebook = Notebook()
            self.choices = {
                    "1": self.show_notes,
                    "2": self.search_notes,
                    "3": self.add_note,
                    "4": self.modify_note,
                    "5": self.quit
                    }
    
        def display_menu(self):
            print("""
    Notebook Menu
    
    1. Show all Notes
    2. Search Notes
    3. Add Note
    4. Modify Note
    5. Quit
    """)
    
        def run(self):
            '''Display the menu and respond to choices.'''
            while True:
                self.display_menu()
                choice = input("Enter an option: ")
                action = self.choices.get(choice)
                if action:
                    action()
                else:
                    print("{0} is not a valid choice".format(choice))
    
        def show_notes(self, notes=None):
            if not notes:
                notes = self.notebook.notes
            for note in notes:
                print("{0}: {1}\n{2}".format(
                    note.id, note.tags, note.memo))
    
        def search_notes(self):
            filter = input("Search for: ")
            notes = self.notebook.search(filter)
            self.show_notes(notes)
    
        def add_note(self):
            memo = input("Enter a memo: ")
            self.notebook.new_note(memo)
            print("Your note has been added.")
    
        def modify_note(self):
            id = input("Enter a note id: ")
            memo = input("Enter a memo: ")
            tags = input("Enter tags: ")
            if memo:
                self.notebook.modify_memo(id, memo)
            if tags:
                self.notebook.modify_tags(id, tags)
    
        def quit(self):
            print("Thank you for using your notebook today.")
            sys.exit(0)
    
    if __name__ == "__main__":
        Menu().run()
    

`notebook.py`

    
    
    import datetime
    
    # Store the next available id for all new notes
    last_id = 0
    
    class Note:
        '''Represent a note in the notebook. Match against a
        string in searches and store tags for each note.'''
    
    
        def __init__(self, memo, tags=''):
            '''initialize a note with memo and optional
            space-separated tags. Automatically set the note's
            creation date and a unique id'''
            self.memo = memo
            self.tags = tags
            self.creation_date = datetime.date.today()
            global last_id
            last_id += 1
            self.id = last_id
    
        def match(self, filter):
            '''Determine if this note matches the filter
            text. Return True if it matches, False otherwise.
    
            Search is case sensitive and matches both text and
            tags.'''
            return filter in self.memo or filter in self.tags
    
    class Notebook:
    
        '''Represent a collection of notes that can be tagged,
        modified, and searched.'''
    
        def __init__(self):
            '''Initialize a notebook with an empty list.'''
            self.notes = []
    
        def new_note(self, memo, tags=''):
            '''Create a new note and add it to the list.'''
            self.notes.append(Note(memo, tags))
    
        def _find_note(self, note_id):
            '''Locate the note with the given id.'''
            for note in self.notes:
                if str(note.id) == str(note_id):
                    return note
            return None
    
        def modify_memo(self, note_id, memo):
            '''Find the note with the given id and change its
            memo to the given value.'''
            note = self._find_note(note_id)
            if note:
                note.memo = memo
                return True
            return False
    
        def modify_tags(self, note_id, tags):
            '''Find the note with the given id and change its
            tags to the given value.'''
            note = self._find_note(note_id)
            if note:
                note.tags = tags
                return True
            return False
    
        def search(self, filter):
            '''Find all notes that match the given filter
            string.'''
            return [note for note in self.notes if
                    note.match(filter)]
    


Answer: Functions and methods are themselves objects. So the dict contains objects
which we then call.

So self.choices is a dictionary with "1", "2", etc as keys. The values are
method objects. When you get the value from the dictionary, you get that
object (called a "callable") and assign it to `action`. Then you call that
object with `action()`.

The key bit of code is this:

    
    
     self.choices = {
                "1": self.show_notes,
                "2": self.search_notes,
                "3": self.add_note,
                "4": self.modify_note,
                "5": self.quit
                }
    

`self.choices["1"]` evaluates to the value of `self.show_notes`, which is then
assigned to `action`, which you then call with `action()`.

(As an aside: The menu in the example code is hardcoded, but in fact could be
autogenerated, if the methods had docstrings. `"\n".join("%s: %s" % (key,
action.__doc__) for key, action in sorted(self.choices.iteritems()))` To
properly handle a menu with >=10 items, you'd want to make the keys into
integers, or expand the one-liner.)

