<jupyter_start><jupyter_text>Chapter 9. Classes 9.1 Introduction(1) Use classes to create custom types.<jupyter_code>type(5)
type("python")
type([1, 2, 3])
type(x * x for x in [2, 4, 6])<jupyter_output><empty_output><jupyter_text>(2) Classes define the structure and behavior of objects.(3) An object's class controls its initialization.(4) Classes are intended to make complex problems tractable but can make simple solutions overly complex. 9.2 Defining classes(1) `class` is used to define new classes.(2) By convention, class names use CamelCase.<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    pass
from airtravel import Flight
Flight
f = Flight()
type(f)<jupyter_output><empty_output><jupyter_text>9.3 Interface methods(1) Method: a function defined within a class(2) Instance method: functions which can be called on objects(3) `self`: the first argument to all instance methods<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    
    def number(self):
        return "SN060"
from airtravel import Flight
f = Flight()
f.number()<jupyter_output><empty_output><jupyter_text>9.4 Intializers(1) `__init__()`: the instance method for initializing new objects.(2) `__init__()` is an initializer, not a constructor.(3) `self` is similar to `this` in C++ or Java.(4) As we don't need to declare variables until we create them, neither do we need to declare object attributes before we create them.(5) Why `_number`?* Avoid name clash with `number()`.* By convention, implementation details start with underscore '_'.<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    
    def __init__(self, number):
        self._number = number
    
    def number(self):
        return self._number
from airtravel import Flight
f = Flight("SN061")
f.number()
f._number<jupyter_output><empty_output><jupyter_text>(6) **Everything of the class is public**."We're all consenting adults here."(7) Class invariants: truths about an object that endure for its lifetime.<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    
    def __init__(self, number):
        if not number[:2].isalpha():
            raise ValueError("No airline code in '{}'".format(number))
            
        if not number[:2].isupper():
            raise ValueError("Invalid airline code '{}".format(number))
            
        if not (number[2:].isdigit() and int(number[2:]) <= 9999):
            raise ValueError("Invalid route number '{}".format(number))
        
        self._number = number
    
    def number(self):
        return self._number
    
    def airline(self):
        return self._number[:2]
from airtravel import Flight
f = Flight("SN060")
f = Flight("060")
f = Flight("sn060")
f = Flight("SNabcd")
f = Flight("SN12345")<jupyter_output><empty_output><jupyter_text>9.5 A second class<jupyter_code># APPENDED to airtravel.py

class Aircraft:
    def __init__(self, registration, model, num_rows, num_seats_per_row):
        self._registration = registration
        self._model = model
        self._num_rows = num_rows
        self._num_seats_per_row = num_seats_per_row
        
    def registration(self):
        return self._registration
    
    def model(self):
        return self._model
    
    def seating_plan(self):
        return (range(1, self._num_rows + 1), 
                # 'I' is omited to avoid confusion with '1'.
                "ABCDEFGHJ"[:self._num_seats_per_row])
from airtravel import *
a = Aircraft('G-EUPT', 'Airbus A319', num_rows=22, num_seats_per_row=6)
a.registration()
a.model()
a.seating_plan()<jupyter_output><empty_output><jupyter_text>9.6 Collaborating classesLaw of Demeter -- the principle of least knowledge: only talk to your friends."Complex is better  than complicated.""Many moving parts  Combined in a clever box  Are now one good tool"<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    """A flight with a particular passenger aircraft."""
    
    def __init__(self, number, aircraft):
        if not number[:2].isalpha():
            raise ValueError("No airline code in '{}'".format(number))
            
        if not number[:2].isupper():
            raise ValueError("Invalid airline code '{}".format(number))
            
        if not (number[2:].isdigit() and int(number[2:]) <= 9999):
            raise ValueError("Invalid route number '{}".format(number))
        
        self._number = number
        self._aircraft = aircraft
    
    def number(self):
        return self._number
    
    def airline(self):
        return self._number[:2]
    
    def aircraft_model(self):
        return self._aircraft.model()
    
class Aircraft:
    def __init__(self, registration, model, num_rows, num_seats_per_row):
        self._registration = registration
        self._model = model
        self._num_rows = num_rows
        self._num_seats_per_row = num_seats_per_row
        
    def registration(self):
        return self._registration
    
    def model(self):
        return self._model
    
    def seating_plan(self):
        return (range(1, self._num_rows + 1), 
                # 'I' is omited to avoid confusion with '1'.
                "ABCDEFGHJ"[:self._num_seats_per_row])    
from airtravel import *

f = Flight("BA758", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
f.aircraft_model()<jupyter_output><empty_output><jupyter_text>9.7 Example: booking seatsA list of dictionaries.* One list entry for each row in the aircraft.* One key of the dictionary for each seat of the row.```pythonrows, seats = self._aircraft.seating_plan()self._seating = [None] + [ {letter: None for letter in seats} for _ in rows]```<jupyter_code># Don't use list repetition since it will simply duplicate the same list object.
rows, seats = (range(1, 23), 'ABCDEF')
seating = [None] + [{letter: None for letter in seats}] * len(rows)
seating
seating[1]['A'] = True
seating
from airtravel import *
f = Flight("BA758", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
f._seating
from pprint import pprint as pp
pp(f._seating)
class Flight:
    
    def allocate_seat(seat, passenger):
        """Allocate a seat to a passenger.
        
        Args:
            seat: A seat designator such as '12C' or '21F'.
            passenger: The passenger name.
            
        Raises:
            ValueError: If the seat is unavailable.
        """
        rows, seat_letters = self._aircraft.seating_plan()
        
        letter = seat[-1]
        if letter not in seat_letters:
            raise ValueError("Invalid seat letter {}".format(letter))
            
        row_text = seat[:-1]
        try:
            row = int(row_text)
        except ValueError:
            raise ValueError("Invalid seat row {}".format(row_text))
            
        if row not in rows:
            raise ValueError("Invalid row number {}".format(row))
            
        if self._seating[row][letter] is not None:
            raise ValueError("Seat {} already occupied".format(seat))
            
        self._seating[row][letter] = passenger
from airtravel import *
from pprint import pprint as pp

f = Flight("BA758", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
f.allocate_seat('12A', '<NAME>')
f.allocate_seat('12A', '<NAME>')
f.allocate_seat('15F', '<NAME>')
f.allocate_seat('15E', '<NAME>')
f.allocate_seat('E27', '<NAME>')
f.allocate_seat('1C', '<NAME>')
f.allocate_seat('1D', '<NAME>')
f.allocate_seat('DD', '<NAME>')
pp(f._seating)
# Refactor the class Flight and add a convenience function make_flight().

# SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    """A flight with a particular passenger aircraft."""
    
    def __init__(self, number, aircraft):
        if not number[:2].isalpha():
            raise ValueError("No airline code in '{}'".format(number))
            
        if not number[:2].isupper():
            raise ValueError("Invalid airline code '{}".format(number))
            
        if not (number[2:].isdigit() and int(number[2:]) <= 9999):
            raise ValueError("Invalid route number '{}".format(number))
        
        self._number = number
        self._aircraft = aircraft
        
        rows, seats = self._aircraft.seating_plan()
        self._seating = [None] + [ {letter: None for letter in seats} for _ in rows]
    
    def number(self):
        return self._number
    
    def airline(self):
        return self._number[:2]
    
    def aircraft_model(self):
        return self._aircraft.model()
        
    def _parse_seat(self, seat):
        """Parse a seat designator into a valid row and letter.
        
        Args:
            seat: A seat designator such as '12F'.
            
        Returns:
            A tuple containing an integer and a string for row and seat.
        """
        
        row_numbers, seat_letters = self._aircraft.seating_plan()
        
        letter = seat[-1]
        if letter not in seat_letters:
            raise ValueError("Invalid seat letter {}".format(letter))
            
        row_text = seat[:-1]
        try:
            row = int(row_text)
        except ValueError:
            raise ValueError("Invalid seat row {}".format(row_text))
            
        if row not in row_numbers:
            raise ValueError("Invalid row number {}".format(row))
            
        return row, letter
        
    def allocate_seat(self, seat, passenger):
        """Allocate a seat to a passenger.
        
        Args:
            seat: A seat designator such as '12C' or '21F'.
            passenger: The passenger name.
            
        Raises:
            ValueError: If the seat is unavailable.
        """
        row, letter = self._parse_seat(seat)
            
        if self._seating[row][letter] is not None:
            raise ValueError("Seat {} already occupied".format(seat))
            
        self._seating[row][letter] = passenger
        
    def relocate_passenger(self, from_seat, to_seat):
        """Relocate a passenger to a different seat.
        
        Args:
            from_seat: The existing seat designator for the 
                       passenger to be moved.
                 
            to_seat: The new seat designator.
        """
        from_row, from_letter = self._parse_seat(from_seat)
        if self._seating[from_row][from_letter] is None:
            raise ValueError("No passenger to relocate in seat {}".format(from_seat))
            
        to_row, to_letter = self._parse_seat(to_seat)
        if self._seating[to_row][to_letter] is not None:
            raise ValueError("Seat {} already occupied".format(to_seat))
            
        self._seating[to_row][to_letter] = self._seating[from_row][from_letter]
        self._seating[from_row][from_letter] = None
    
class Aircraft:
    def __init__(self, registration, model, num_rows, num_seats_per_row):
        self._registration = registration
        self._model = model
        self._num_rows = num_rows
        self._num_seats_per_row = num_seats_per_row
        
    def registration(self):
        return self._registration
    
    def model(self):
        return self._model
    
    def seating_plan(self):
        return (range(1, self._num_rows + 1), 
                # 'I' is omited to avoid confusion with '1'.
                "ABCDEFGHJ"[:self._num_seats_per_row])   
                
def make_flight():
    f = Flight("BA758", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
    f.allocate_seat('12A', '<NAME>')
    f.allocate_seat('15F', '<NAME>')
    f.allocate_seat('15E', '<NAME>')
    f.allocate_seat('1C', '<NAME>')
    f.allocate_seat('1D', '<NAME>')
    return f
from airtravel import make_flight

f = make_flight()
f
f.relocate_passenger('12A', '15D')
from pprint import pprint as pp
pp(f._seating)
# Add a new method num_available_seats() to the class Flight

class Flight:
    
    def num_available_seats(self):
        return sum(sum(1 for s in row.values() if s is None)
                   for row in self._seating if row is not None)
from airtravel import make_flight

f = make_flight()
f.num_available_seats()
22 * 6 - 5<jupyter_output><empty_output><jupyter_text>9.8 OO with function objects(1) Don't feel compelled to create classes without good reason.(2) Tell! Don't ask.* Tell objects what to do.* Don't ask for their state.<jupyter_code># SAVE AS airtravel.py

"""Model for aircraft flights"""

class Flight:
    """A flight with a particular passenger aircraft."""
    
    def __init__(self, number, aircraft):
        if not number[:2].isalpha():
            raise ValueError("No airline code in '{}'".format(number))
            
        if not number[:2].isupper():
            raise ValueError("Invalid airline code '{}".format(number))
            
        if not (number[2:].isdigit() and int(number[2:]) <= 9999):
            raise ValueError("Invalid route number '{}".format(number))
        
        self._number = number
        self._aircraft = aircraft
        
        rows, seats = self._aircraft.seating_plan()
        self._seating = [None] + [ {letter: None for letter in seats} for _ in rows]
    
    def number(self):
        return self._number
    
    def airline(self):
        return self._number[:2]
    
    def aircraft_model(self):
        return self._aircraft.model()
        
    def _parse_seat(self, seat):
        """Parse a seat designator into a valid row and letter.
        
        Args:
            seat: A seat designator such as '12F'.
            
        Returns:
            A tuple containing an integer and a string for row and seat.
        """
        
        row_numbers, seat_letters = self._aircraft.seating_plan()
        
        letter = seat[-1]
        if letter not in seat_letters:
            raise ValueError("Invalid seat letter {}".format(letter))
            
        row_text = seat[:-1]
        try:
            row = int(row_text)
        except ValueError:
            raise ValueError("Invalid seat row {}".format(row_text))
            
        if row not in row_numbers:
            raise ValueError("Invalid row number {}".format(row))
            
        return row, letter
        
    def allocate_seat(self, seat, passenger):
        """Allocate a seat to a passenger.
        
        Args:
            seat: A seat designator such as '12C' or '21F'.
            passenger: The passenger name.
            
        Raises:
            ValueError: If the seat is unavailable.
        """
        row, letter = self._parse_seat(seat)
            
        if self._seating[row][letter] is not None:
            raise ValueError("Seat {} already occupied".format(seat))
            
        self._seating[row][letter] = passenger
        
    def relocate_passenger(self, from_seat, to_seat):
        """Relocate a passenger to a different seat.
        
        Args:
            from_seat: The existing seat designator for the 
                       passenger to be moved.
                 
            to_seat: The new seat designator.
        """
        from_row, from_letter = self._parse_seat(from_seat)
        if self._seating[from_row][from_letter] is None:
            raise ValueError("No passenger to relocate in seat {}".format(from_seat))
            
        to_row, to_letter = self._parse_seat(to_seat)
        if self._seating[to_row][to_letter] is not None:
            raise ValueError("Seat {} already occupied".format(to_seat))
            
        self._seating[to_row][to_letter] = self._seating[from_row][from_letter]
        self._seating[from_row][from_letter] = None
        
    def num_available_seats(self):
        return sum(sum(1 for s in row.values() if s is None) 
                   for row in self._seating if row is not None)
    
    def make_boarding_cards(self, card_printer):
        for passenger, seat in sorted(self._passenger_seats()):
            card_printer(passenger, seat, self.number(), self.aircraft_model())
            
    def _passenger_seats(self):
        """An iterable series of passenger seating allocations."""
        row_numbers, seat_letters = self._aircraft.seating_plan()
        for row in row_numbers:
            for letter in seat_letters:
                passenger = self._seating[row][letter]
                if passenger is not None:
                    yield (passenger, "{}{}".format(row, letter))
    
class Aircraft:
    def __init__(self, registration, model, num_rows, num_seats_per_row):
        self._registration = registration
        self._model = model
        self._num_rows = num_rows
        self._num_seats_per_row = num_seats_per_row
        
    def registration(self):
        return self._registration
    
    def model(self):
        return self._model
    
    def seating_plan(self):
        return (range(1, self._num_rows + 1), 
                # 'I' is omited to avoid confusion with '1'.
                "ABCDEFGHJ"[:self._num_seats_per_row])   
                
def make_flight():
    f = Flight("BA758", Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
    f.allocate_seat('12A', '<NAME>')
    f.allocate_seat('15F', '<NAME>')
    f.allocate_seat('15E', '<NAME>')
    f.allocate_seat('1C', '<NAME>')
    f.allocate_seat('1D', '<NAME>')
    return f
    
def console_card_printer(passenger, seat, flight_number, aircraft):
    output = "| Name: {0}"     \
             "  Flight: {1}"   \
             "  Seat: {2}"     \
             "  Aircraft: {3}" \
             " |".format(passenger, flight_number, seat, aircraft)
    banner = '+' + '-' * (len(output) - 2) + '+'
    border = '|' + ' ' * (len(output) - 2) + '|'         
    lines = [banner, border, output, border, banner]
    card = '\n'.join(lines)
    print(card)
    print()
from airtravel import (make_flight, console_card_printer)

f = make_flight()
f.make_boarding_cards(console_card_printer)<jupyter_output>+-------------------------------------------------------------------------+
|                                                                         |
| Name: <NAME>  Flight: BA758  Seat: 15E  Aircraft: Airbus A319 |
|                                                                         |
+-------------------------------------------------------------------------+

+--------------------------------------------------------------------------+
|                                                                          |
| Name: <NAME>  Flight: BA758  Seat: 15F  Aircraft: Airbus A319 |
|                                                                          |
+--------------------------------------------------------------------------+

+-------------------------------------------------------------------------+
|                                                                         |
| Name: <NAME>  Flight: BA758  Seat: 12A  Aircraft: Airbus A319 |
|    [...]<jupyter_text>9.9 Polymorphism and duck typing 9.9.1 PolymorphismUsing objects of **different types** through a **common interface**. 9.9.2 Duck typing"When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."                                                                    --<NAME>                                                                    (1) Polymorphism in Python is supported by duck typing. (2) An object's fitness for purpose is determined at the **time of use**. not at the compile time.<jupyter_code>class AirbusA319:
    def __init__(self, registration):
        self._registration = registration
        
    def registration(self):
        return self._registration
    
    def model(self):
        return "Airbus A319"
    
    def seating_plan(self):
        return range(1, 23), "ABCDEF"

class Boeing777:
    def __init__(self, registration):
        self._registration = registration
        
    def registration(self):
        return self._registration
    
    def model(self):
        return "Boeing 777"
    
    def seating_plan(self):
        # For simplicity's sake, we ignore complex 
        # seating arrangement for first-class.
        return range(1, 56), "ABCDEFGHJK"
    
def make_flights():
    f = Flight("BA758", AirbusA319("G-EUPT"))
    f.allocate_seat('12A', '<NAME>')
    f.allocate_seat('15F', '<NAME>')
    f.allocate_seat('15E', '<NAME>')
    f.allocate_seat('1C', '<NAME>')
    f.allocate_seat('1D', '<NAME>')
    
    g = Flight("AF72", Boeing777("F-GSPS"))
    g.allocate_seat('55K', '<NAME>')
    g.allocate_seat('33G', '<NAME>')
    g.allocate_seat('4B', '<NAME>')
    g.allocate_seat('4A', '<NAME>')
    
    return f, g
from airtravel import *

f, g = make_flights()
f.aircraft_model()
g.aircraft_model()
f.num_available_seats()
g.num_available_seats()<jupyter_output><empty_output><jupyter_text>9.10 Inheritance and implementation sharing 9.10.1 Inheritance(1) A sub-class can derive from a base-class, inheriting its behavior and making behavior specific to the sub-class.(2) Python use late binding. 9.10.2 In Python inheritance is most useful for sharing implementation.<jupyter_code>class Aircraft:

    def __init__(self, registration):
        self._registration = registration
        
    def registration(self):
        return self._registration

    def num_seats(self):
        # seating_plan() is not defined in the class Aircraft, 
        # so the class Aircraft can't be used alone.
        rows, row_seats = self.seating_plan()
        return len(rows) * len(row_seats)
                
class AirbusA319(Aircraft):
    
    def model(self):
        return "Airbus A319"
    
    def seating_plan(self):
        return range(1, 23), "ABCDEF"

class Boeing777(Aircraft):
    
    def model(self):
        return "Boeing 777"
    
    def seating_plan(self):
        # For simplicity's sake, we ignore complex 
        # seating arrangement for first-class.
        return range(1, 56), "ABCDEFGHJK"
from airtravel import *

a = AirbusA319("G-EZBT")
a.num_seats()
b = Boeing777("N717AN")
b.num_seats()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1239077560580128, 'fraction_numerical': 0.01567426357985767, 'mean_word_length': 2.2048210161662816, 'pattern_counts': {'":': 0, '<': 79, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 20, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '11', 'dir': 'jupyter-structured-clean-dedup', 'id': '345367', 'license': 'Apache-2.0', 'n_tokens_mistral': 6762, 'n_tokens_neox': 6351, 'n_words': 1845, 'path': 'python/pluralsight-python-fundamental/ch09-Classes.ipynb', 'repo_name': 'renweizhukov/learning-ml', 'size': '22488'}