Writing a Method in Class Book

As you saw in Chapter 7, Using Methods, there are two ways to call a method. One way is to access the method through the class, and the other is to use object-oriented syntax. These two calls are equivalent:

 >>>​​ ​​str.capitalize(​​'browning'​​)
 'Browning'
 >>>​​ ​​'browning'​​.capitalize()
 'Browning'

We’d like to be able to write similar code involving class Book. For example, we might want to be able to ask how many authors a Book has:

 >>>​​ ​​Book.num_authors(ruby_book)
 3
 >>>​​ ​​ruby_book.num_authors()
 3

To get this to work, we’ll define a method called num_authors inside Book. Here it is:

 class​ Book:
 """Information about a book."""
 
 def​ num_authors(self) -> int:
 """Return the number of authors of this book.
  """
 
 return​ len(self.authors)

Book method num_authors looks just like a function except that it has a parameter called self, which refers to a Book. Assuming this class is defined in the file book.py, we can import it, create a Book object, and call num_authors in two different ways:

 >>>​​ ​​import​​ ​​book
 >>>​​ ​​ruby_book​​ ​​=​​ ​​book.Book()
 >>>​​ ​​ruby_book.title​​ ​​=​​ ​​'Programming Ruby'
 >>>​​ ​​ruby_book.authors​​ ​​=​​ ​​[​​'Thomas'​​,​​ ​​'Fowler'​​,​​ ​​'Hunt'​​]
 >>>​​ ​​book.Book.num_authors(ruby_book)
 3
 >>>​​ ​​ruby_book.num_authors()
 3

Let’s take a close look at the first call on method num_authors:

 >>>​​ ​​book.Book.num_authors(ruby_book)

The book part says to look in the imported module. In that module is class Book. Inside Book is method num_authors. The argument to the call, ruby_book, is passed to parameter self.

Python treats the second call on num_authors exactly as it did the first; the first call is equivalent to this one:

 >>>​​ ​​ruby_book.num_authors()

The second version is much more common because it lists the object first; we think of that version as asking the book how many authors it has. Thinking of method calls this way can really help develop an object-oriented mentality.

In the ruby_book example, we assigned the title and list of authors after the Book object was created. That approach isn’t scalable; we don’t want to have to type those extra assignment statements every time we create a Book. Instead, we’ll write a method that does this for us as we create the Book. This is a special method and is called __init__. We’ll also include the publisher, ISBN, and price as parameters of __init__:

 from​ typing ​import​ List, Any
 
 class​ Book:
 """Information about a book, including title, list of authors,
  publisher, ISBN, and price.
  """
 
 def​ __init__(self, title: str, authors: List[str], publisher: str,
  isbn: str, price: float) -> None:
 """Create a new book entitled title, written by the people in authors,
  published by publisher, with ISBN isbn and costing price dollars.
 
  >>> python_book = Book( ​​\
  'Practical Programming', ​​\
  ['Campbell', 'Gries', 'Montojo'], ​​\
  'Pragmatic Bookshelf', ​​\
  '978-1-6805026-8-8', ​​\
  25.0)
  >>> python_book.title
  'Practical Programming'
  >>> python_book.authors
  ['Campbell', 'Gries', 'Montojo']
  >>> python_book.publisher
  'Pragmatic Bookshelf'
  >>> python_book.ISBN
  '978-1-6805026-8-8'
  >>> python_book.price
  25.0
  """
 
  self.title = title
 # Copy the authors list in case the caller modifies that list later.
  self.authors = authors[:]
  self.publisher = publisher
  self.ISBN = isbn
  self.price = price
 
 def​ num_authors(self) -> int:
 """Return the number of authors of this book.
 
  >>> python_book = Book( ​​\
  'Practical Programming', ​​\
  ['Campbell', 'Gries', 'Montojo'], ​​\
  'Pragmatic Bookshelf', ​​\
  '978-1-6805026-8-8', ​​\
  25.0)
  >>> python_book.num_authors()
  3
  """
 
 return​ len(self.authors)

Notice that we can include doctests for methods just as we do for functions. Notice also that we do not specify the type of the first parameter of a method, since its type is always the class in which it is defined.

This module contains a single (complicated) statement: the class definition. When Python executes this module, it creates a class object and assigns it to variable Book:

images/oop/book2.png

Method __init__ is called whenever a Book object is created. Its purpose is to initialize the new object; this method is sometimes called a constructor. Here are the steps that Python follows when creating an object:

  1. It creates an object at a particular memory address.
  2. It calls method __init__, passing in the new object into the parameter self.
  3. It produces that object’s memory address.

Let’s try it out in the shell:

 >>>​​ ​​import​​ ​​book
 >>>​​ ​​python_book​​ ​​=​​ ​​book.Book(
 ...​​ ​​'Practical Programming'​​,
 ...​​ ​​[​​'Campbell'​​,​​ ​​'Gries'​​,​​ ​​'Montojo'​​],
 ...​​ ​​'Pragmatic Bookshelf'​​,
 ...​​ ​​'978-1-6805026-8-8'​​,
 ...​​ ​​25.0)
 >>>​​ ​​python_book.title
 'Practical Programming'
 >>>​​ ​​python_book.authors
 ['Campbell', 'Gries', 'Montojo']
 >>>​​ ​​python_book.publisher
 'Pragmatic Bookshelf'
 >>>​​ ​​python_book.ISBN
 '978-1-6805026-8-8'
 >>>​​ ​​python_book.price
 25.0

The following image shows the memory model that results from this code:

images/oop/book6.png

Let’s trace method call python_book.num_authors(). (As a reminder, this is equivalent to Book.num_authors(python_book).) Python first finds the object that python_book refers to and calls its method num_authors. There are no explicit arguments, so Python only passes in the Book object that python_book refers to, assigning that object to the self parameter:

images/oop/book7.png

The return statement, return len(self.authors), is then executed. The expression, len(self.authors), is a function call. Python evaluates the argument, self.authors, by finding the object that self refers to and then, in that object, finds instance variable authors. This is a list, and the length of that list is the value that Python returns, as shown here:

images/oop/book8.png

With constructors, methods, and instance variables in hand, we can now create classes that look and work like those that come with Python itself.