26  OOP: Book Catalog

26.1 Iteration 1

26.1.1 Book Class

class Book:
    def __init__(self, title, last, first):
        self._title = title    #A
        self._last  = last     #A
        self._first = first    #A
        
    @property                            #B
    def title(self): return self._title  #B
                                         #B
    @property                            #B
    def last(self): return self._last    #B
                                         #B
    @property                            #B
    def first(self): return self._first  #B

    def __str__(self):
        return (f"{{TITLE: '{self._title}', LAST: '{self._last}', "  #C
                f"FIRST: '{self._first}'}}")                         #C

26.1.2 Catalogue Class

class Catalogue:

    @staticmethod
    def _equal_ignore_case(target_str, other_str):
        if len(target_str) == 0:   
            return True
        else:
            return target_str.casefold() == other_str.casefold()    

    def __init__(self):
        self._booklist = []    
        
    def add(self, title, last, first):
        book = Book(title, last, first)
        self._booklist.append(book)
    
    def _is_match(self, book, target):
        return (     Catalogue._equal_ignore_case(target.title, 
                                                  book.title)
                 and Catalogue._equal_ignore_case(target.last,  
                                                  book.last)
                 and Catalogue._equal_ignore_case(target.first, 
                                                  book.first)
        )
    
    def find(self, target):
        return [book for book in self._booklist
                    if self._is_match(book, target)
        ]

26.1.3 Usage

def fill(catalogue):    #A
    catalogue.add("Life of Pi", "Martel", "Yann")
    catalogue.add("The Call of the Wild", "London", "Jack")

    catalogue.add("To Kill a Mockingbird", "Lee", "Harper")
    catalogue.add("Little Women", "Alcott", "Louisa")

    catalogue.add("The Adventures of Sherlock Holmes", "Doyle", "Conan")
    catalogue.add("And Then There Were None", "Christie", "Agatha")

    catalogue.add("Carrie", "King", "Stephen")
    catalogue.add("It: A Novel", "King", "Stephen")
    catalogue.add("Frankenstein", "Shelley", "Mary")

    catalogue.add("2001: A Space Odyssey", "Clarke", "Arthur")
    catalogue.add("Ender's Game", "Card", "Orson")

def search(catalogue, target):    #B
    print()
    print("Find ", end="")
    print(target)
    
    matches = catalogue.find(target)
    
    if len(matches) == 0:
        print("No matches.")
    else:
        print("Matches:")
        
        for book in matches:
            print("  ", end="")
            print(book)
            
def test(catalogue):    #C
    target = Book("Life of Pi", "Martel", "Yann")
    search(catalogue, target)

    target = Book("", "King", "")
    search(catalogue, target)
    
    target = Book("1984", "Orwell", "George")
    search(catalogue, target)

    target = Book("", "", "")    #D
    search(catalogue, target)


catalogue = Catalogue()    
fill(catalogue)    
test(catalogue)    
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[23], line 49
     45     search(catalogue, target)
     48 catalogue = Catalogue()    
---> 49 fill(catalogue)    
     50 test(catalogue)    

Cell In[23], line 2, in fill(catalogue)
      1 def fill(catalogue):    #A
----> 2     catalogue.add("Life of Pi", "Martel", "Yann")
      3     catalogue.add("The Call of the Wild", "London", "Jack")
      5     catalogue.add("To Kill a Mockingbird", "Lee", "Harper")

TypeError: Catalogue.add() takes 2 positional arguments but 4 were given

26.2 Iteration 2

26.2.1 Genre(Enum)

from enum import Enum
 
class Genre(Enum):    #A
    UNSPECIFIED = 0
    ADVENTURE   = 1
    CLASSICS    = 2
    DETECTIVE   = 3
    FANTASY     = 4
    HISTORIC    = 5
    HORROR      = 6
    ROMANCE     = 7
    SCIFI       = 8
    
    def __str__(self): return self.name.lower()
Genre.ADVENTURE
<Genre.ADVENTURE: 1>
print(Genre.ADVENTURE)
print(Genre(1))
adventure
adventure
# Iterate through all defined genres
for genre in Genre:
    print(genre)        
unspecified
adventure
classics
detective
fantasy
historic
horror
romance
scifi

26.2.2 Attributes

class Attributes:
    
    @staticmethod
    def _equal_ignore_case(target_str, other_str):    #D
        if len(target_str) == 0: 
            return True
        else:
            return target_str.casefold() == other_str.casefold()
        
    def __init__(self, title, last, first, year, genre):
        self._title = title
        self._last  = last
        self._first = first
        self._year  = year     #C
        self._genre = genre    #C

    @property
    def title(self): return self._title
        
    @property
    def last(self): return self._last
    
    @property
    def first(self): return self._first

    @property
    def year(self):  return self._year
    
    @property
    def genre(self): return self._genre
    
    def is_match(self, target_attrs):    #B
        return (     
                Attributes._equal_ignore_case(target_attrs.title, 
                                              self._title)
            and Attributes._equal_ignore_case(target_attrs.last,  
                                              self._last)
            and Attributes._equal_ignore_case(target_attrs.first, 
                                              self._first)
            and (    (target_attrs.year  == 0)
                  or (target_attrs.year  == self._year))
            and (    (target_attrs.genre == Genre.UNSPECIFIED)
                  or (target_attrs.genre == self._genre))
        )
    def __str__(self):
        return (f"{{TITLE: '{self._title}', LAST: '{self._last}', "
                f"FIRST: '{self._first}', YEAR: {self._year}, "    #E
                f"GENRE: {self._genre}}}")    #E

26.2.3 Book & Catalogue

class Book:
    def __init__(self, attributes):
        self._attributes = attributes

    @property
    def attributes(self): return self._attributes
class Catalogue:
    def __init__(self):
        self._booklist = []
    
    def add(self, attrs):
        self._booklist.append(Book(attrs))     #A
    
    def find(self, target_attrs):
        return [book for book in self._booklist
                    if book.attributes.is_match(target_attrs)    #B
        ]

26.2.4 Usage

catalogue = Catalogue()

catalogue.add(Attributes("The Call of the Wild",
                         "London", "Jack",
                         1903, Genre.ADVENTURE))
 
catalogue.add(Attributes("To Kill a Mockingbird", 
                         "Lee", "Harper",
                         1960, Genre.CLASSICS))

target_attrs = Attributes("Life of Pi", "Martel", "Yann", 
                          2003, Genre.ADVENTURE)

search(catalogue, target_attrs)

Find {TITLE: 'Life of Pi', LAST: 'Martel', FIRST: 'Yann', YEAR: 2003, GENRE: adventure}
No matches.