写给程序员的Python教程笔记8——使用类定义新类型

第8章 使用类定义新类型

        对于许多问题,内置的类型以及Python标准库提供的内容是完全足够的。但有时候,它们并不能完全满足需求,此时就可以使用类来创建自定义类型。

        Python中的所有对象都有一个类型,而当使用type()内置函数来查看对象的类型时,输出的结果会显示对象的类型:

>>> type(5)
<class 'int'>
>>> type('python')
<class 'str'>
>>> type([1, 2, 3])
<class 'list'>
>>> type(x*x for x in [2, 4, 6])
<class 'generator'>

       用于定义一个或多个对象的结构和行为,每个对象被称为类的一个实例(instance)。总体来说,Python中的对象从创建或实例化直到被销毁,这段时间内对象都有固定的类型。对象的类可控制对象的初始化,并定义该对象具有哪些属性(attribute)和方法(method)。

        类是面向对象编程(object-oriented Programming,OOP)的重要机制,虽然OOP有助于使复杂问题更易于处理,但它常常使得解决简单问题有些复杂。Python有一个很棒的优点,它高度支持面向对象,但是Python也不强制用户使用类,除非你真的需要它们。这使得Python与Java和C#完全不同。

8.1 定义类

        类定义由Class关键字引入,后跟类名。

        按照惯例,Python中的新类名称使用驼峰命名法(camel case),有时称为Pascal命名法(Pascal case)—— 每个单词都以大写字母开头,而不是以下划线开头。

        在下面的例子中,将模拟两个机场之间的飞机航班,这些代码放在airtravel.py中:

"""模拟飞机航班。"""


class Flight:
    pass

        类声明引入了一个新的代码块,最简单的类可能至少需要包含一个空操作的pass语句。

        可以在程序中的任何地方使用class声明,它将一个类定义绑定到一个类名上。当执行airtravel模块中的顶层代码时,程序就会定义类。

        将这个新类导入到REPL中:

>>> from airtravel import Flight

        上面导入的是类对象。在Python中,一切皆对象,类也不例外。

>>> Flight
<class 'airtravel.Flight'>

        调用类的构造函数来创建一个新的对象,将其赋值给f:

>>> f = Flight()
>>> type(f)
<class 'airtravel.Flight'>

        f的字面量类型就是这个类。

8.2 实例方法

        实例方法,是可以在类的实例对象(例如f)上调用的函数。

        实例方法必须接收这个方法调用实例的引用作为第一个形参,按照惯例,我们总是将这个参数称为self。        

class Flight:
    def number(self):
        return 'SN060'

        刷新REPL:

>>> from airtravel import Flight
>>> f = Flight()
>>> f.number()
'SN060'

        请注意,当调用方法时,不会在参数列表中将实例f作为实际参数self传入。这里因为标准的方法调用形式是点符号:

>>> f.number()
'SN060'

        以上是下面调用形式的简单语法糖:

>>> Flight.number(f)
'SN060'

        如果使用后者,它可以按预期工作,然而在现实开发中几乎永远不会看到这种形式。

8.3 实例初始化方法

        如果有了初始化方法,当调用构造函数时,创建新对象的过程中就要调用该初始化方法。

        初始化方法必须命名为__init__(),由用于Python运行机制的双下划线包裹。像所有其他实例方法一样,__init__()的第一个参数必须是self。        

class Flight:

    def __init__(self, number):
        self._number = number

    def number(self):
        return self._number

        初始化方法不应该返回任何东西——它只是修改自引用的对象。

        如果您有Java、C#或C++的开发经验,那么你可能很容易认为__init__()就是构造函数。这样说不太准确,在Python中,__init__()的作用是在调用__init__()的时候配置(configure)一个已经存在的对象。self参数与Java、C#或C++中的this类似。在Python中,实际构造函数是由Python运行时系统提供的,它所做的一件事是检查实例初始化方法是否存在,并在存在时调用它。

        在初始化方法中,为一个新创建的名为_number的属性(attitude)赋值。选择具有下划线前缀的_number有两个原因。

        首先,它避免了与同名方法的命名冲突。

        其次,还有一个被广泛遵循的约定,如果不想让客户端操作对象的实现细节,那该对象应该以下划线作为前缀。

        传递给构造函数的任何实参都将被转发给初始化方法。

>>> from airtravel import Flight
>>> f = Flight('SN060')
>>> f.number()
'SN060'

        也可以直接访问它的实现细节:

>>> f._number
'SN060'

        然而,在生产级的代码中,我们并不推荐这样做,这会使得调试和早期测试难以进行。

没有访问修饰符

        Python中没有其它语言的公有(public)、私有(private)和受保护(protected)等访问修饰符,采用“一切皆公有”的方式。

        Pythonistas中流行的文化是“我们都是成年人”。实际上,即使在曾经使用过后庞大而复杂的Python系统中,前置下划线的约定也被证明是足以保护变量的。人们知道不能直接使用这些属性,事实上,他们也倾向于不使用这些属性。像许多研究说的那样,缺乏访问修饰符在理论上比实践中有更大的问题。

8.4 校验与不变式

        在对象的初始化方法中建立所谓的类不变式(class invariant)是一个很好的习惯。

        不变式是关于类对象的正确性,应该在对象的整个生命周期中保持。

        对于航班来说,不变式就是,以大写的双字母航空公司代码开始,后面跟着3-4位数的航线号。

        在__init__()方法中建立类不变式,如果它无法通过校验,则会抛出异常:

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

        在开发过程中,在REPL中进行随机测试是非常有效的技术:

>>> from airtravel import Flight
>>> f = Flight('SN060')
>>> f = Flight('060')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 8, in __init__
    raise ValueError("No airline code in '{}'".format(number))
ValueError: No airline code in '060'
>>>
>>> f = Flight('sn060')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 10, in __init__
    raise ValueError("Invalid airline code '{}'".format(number))
ValueError: Invalid airline code 'sn060'
>>>
>>> f = Flight('snabcd')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 10, in __init__
    raise ValueError("Invalid airline code '{}'".format(number))
ValueError: Invalid airline code 'snabcd'
>>>
>>> f = Flight('SN12345')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 12, in __init__
    raise ValueError("Invalid route number '{}'".format(number))
ValueError: Invalid route number 'SN12345'

8.5 增加第二个类

        让航班接受座位预订。要知道座位的布局,首先要知道飞机的类型。下面来编写第二个类模拟不同种类的飞机:

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

        初始化方法为飞机创建了4个属性:注册号、型号名称、座位行数和每行座位数。

        下面添加一个seating_plan()方法,返回一个表示可用的行和每行座位的二维元组,该元组包含一个范围对象和一个座位字母串:

def seating_plan(self):
        return (range(1, self._num_rows + 1), "ABCDEFGHJK"[:self._num_seats_per_row])

        试着使用座次表一构建一架飞机座位:

>>> from airtravel import Aircraft
>>> a = Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6)
>>> a.registration()
'G-EUPT'
>>> a.model()
'Airbus A319'
>>> a.seating_plan()
(range(1, 23), 'ABCDEF')

8.6 协同类

        得墨忒耳定律(Law of Demeter)是一个面向对象的设计原则,该原则主要的意思是:不应该调用从其他调用获得的对象上的方法。换句话说:只和你直接的朋友交谈。

        修改Flight类,使其可以接收一个飞机对象,将按照得墨忒耳定律添加一个方法报告飞机型号。这个方法将代表客户端委托给Aircraft,而不是让客户端“直达”Flight然后直接询问Aircraft对象:

class Flight:
    """一个特定飞机的航班。"""

    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()

        还为该类添加了一个docstring。它与函数和模块中的docstrings类似,必须是该类语句体的第一个非注释行。

        现在可以用一架特定的飞机来构建一个航班:

>>> from airtravel import *
>>> f = Flight("BA758",Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
>>> f.aircraft_model()
'Airbus A319'

        请注意,我们构造了Aircraft对象,并直接将其传递给Flight构造函数,而不需要中间命名引用。

8.7 禅之刻

        复杂优于混乱

        现在,许多运动部件被结合在一个盒子里,它是一个很好的工具。

        aircraft_model()方法是一个很好的“复杂优于混乱”的例子:        

    def aircraft_model(self):
        return self._aircraft.model()

        Flight类更为复杂——它包含额外的代码来深入挖掘飞机的引用以便找到飞机的型号。然而,Flight的所有客户端现在都可以降低复杂度:它们都不需要知道Aircraft类,大大简化了系统。

8.8 定座位

         使用以下代码片段在Flight.__init__()中初始化座次表:

rows, seats = self._aircraft.seating_plan()
self._seating = [None] + [{letter: None for letter in seats} for _ in rows]

        在第一行,检索飞机的座次表,并使用元组拆包将行和座位标识符分别放入局部变量rows和seats中。在第二行,创建了一个座位分配列表。列表的第一个元素是被弃用的包含None的单个元素的列表。其他元素是由遍历对象的列表推导构成的字典,它是从前一行的_aircraft检索到的行号的区间。

        实际上我们并不关心行号,因为我们知道它将与最终列表中的列表索引相匹配,所以我们通过使用虚拟的划线变量来忽略它。

        列表推导的项目表达式部分本身就是推导,具体地说是字典推导!它将遍历每行的字母,并创建从单个字符的字符串到None的映射以表示一个空座位。

        使用列表推导而不是乘法运算来进行列表复制,是因为我们希望为每一行创建一个不同的字典对象。记住,重复是浅复制。

        在REPL中测试这段代码:

>>> from airtravel import *
>>> f = Flight("BA758",Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))

        由于一切都是“公有的”,所以可以在开发过程中访问实现细节。很显然,在开发过程中我们故意违背了惯例,前置的下划线会提醒我们什么是公有、什么是私有:

>>> f._seating
[None, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}, {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}]

        上述是准确的,但不是特别优雅。使用pretty-print再试一次:

>>> from pprint import pprint as pp
>>> pp(f._seating)
[None,
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}]

为乘客分配座位

        现在,将给Flight添加为乘客分配座位的行为。乘客将只是一个字符串名称:

def allocate_seat(seat, passenger):
        """为乘客分配一个座位。
        
        Args:
            seat:一个座位标识符,如'12C'或者'21F'
            passenger:乘客的名字。
            
        Raise:
            ValueError如果座位不可用。
        """
        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

        这个代码的绝大部分是对座位标识符的校验,它包含了一些有趣的片段,如下所示。

  • 第2行:方法是函数,所以也应该有docstrings。
  • 第13行:通过使用负索引来获得座位字母,并将它转成座位字符串。
  • 第14行:使用in成员资格测试运算符在seat_letters中测试该座位字母是否有效。
  • 第17行:使用字符串切片来提取行号,以获取除最后一个字符以外的所有字符。
  • 第19行:尝试使用int()构造函数将行号子字符串转换为整数。如果失败,程序捕获ValueError,并从处理程序中抛出一个新的ValueError,并带有一个适当的消息负载。
  • 第23行:可以通过对作为区间的rows对象使用in运算符来验证行号。可以这样做,因为range()对象支持容器协议。
  • 第26行:使用None进行身份验证来检查请求的座位是否空闲。如果它被占用,抛出一个ValueError。
  • 第29行:如果代码运行到这里,一切都好,那么,就可以分配座位了。

        此代码还包含一个错误,我们很快就会发现它!

        在REPL中测试这个座位分配器:

>>> from airtravel import *
>>> f.allocate_seat('12A', 'Guido van Rossum')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Flight.allocate_seat() takes 2 positional arguments but 3 were given

        在早期的面向对象的Python职业生涯中,你很可能会经常看到类似TypeError的消息。出现这个问题是因为我们忘记在allocate_seat()方法的定义中包含self参数:

def allocate_seat(self, seat, passenger):
    # ...

         一旦解决该问题,我们可以重新试一下:

>>> from airtravel import *
>>> f = Flight("BA758",Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
>>> f.allocate_seat('12A', 'Guido van Rossum')
>>> f.allocate_seat('12A', 'Rasmus Lerdorf')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 56, in allocate_seat
    raise ValueError("Seat {} already occupied".format(seat))
ValueError: Seat 12A already occupied
>>> f.allocate_seat('15F', 'Bjarne Stroustrup')
>>> f.allocate_seat('15E', 'Anders Hejlsberg')
>>> f.allocate_seat('E27', 'Yukihiro Matsumoto')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 44, in allocate_seat
    raise ValueError("Invalid seat letter {}".format(letter))
ValueError: Invalid seat letter 7
>>> f.allocate_seat('1C', 'John McCarthy')
>>> f.allocate_seat('1D', 'Richard Hickey')
>>> f.allocate_seat('DD', 'Larry Wall')
Traceback (most recent call last):
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 48, in allocate_seat
    row = int(row_text)
          ^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'D'

During handling of the above exception, another exception occurred:

        在处理以上异常的时候,又出现了另外一个异常:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 50, in allocate_seat
    raise ValueError("invalid seat row {}".format(row_text))
ValueError: invalid seat row D
>>> from pprint import pprint as pp
>>> pp(f._seating)
[None,
 {'A': None,
  'B': None,
  'C': 'John McCarthy',
  'D': 'Richard Hickey',
  'E': None,
  'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': 'Guido van Rossum',
  'B': None,
  'C': None,
  'D': None,
  'E': None,
  'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None,
  'B': None,
  'C': None,
  'D': None,
  'E': 'Anders Hejlsberg',
  'F': 'Bjarne Stroustrup'},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}]

        那个荷兰人在第12排十分寂寞,所以我们想让他坐到第15排,靠着那些丹麦人。为此,我们需要一个relocate_passenger()方法。

8.9 以实现细节命名方法

        首先,进行一次小小的重构,并将座位标识符解析和校验逻辑提取到一个单独的方法_parse_seat()中。这里使用了一个前置的下划线,因为这个方法是一个实现细节:

class Flight:
    
    # ...

    def _parse_seat(self, seat):
        """将座位标识符解析为有效的行和字母。
        
        Args:
            seat: 一个座位标识符,如12F。

        Returns:
            一个元组,包含一个整数和一个字符,分别代表行和座位。
        """
        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 letter {}".format(row_text))

        if row not in row_numbers:
            raise ValueError("Invalid row number {}".format(row))
        
        return row, letter

        新的_parse_seat()方法返回一个具有整数行号和座位字母字符串的元组。这使得allocate_seat()更简单:

    def allocate_seat(self, seat, passenger):
        """为乘客分配一个座位。
        
        Args:
            seat:一个座位标识符,如'12C'或者'21F'
            passenger:乘客的名字。
            
        Raise:
            ValueError如果座位不可用。
        """
        
        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

        请注意,对_parse_seat()的调用也需要使用self前缀进行显式限定。

8.9.1 实现relocate_passenger()

        现在,已经为实现relocate_passenger()方法奠定了基础:

class Flight:
    
    # ...

    def relocate_passenger(self, from_seat, to_seat):
        """将乘客重新安排到另一个座位。
        Args:
            from_seat:乘客当前所在座位的座位标识符。
            to_seat:新的座位标识符。
        """
        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(form_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(form_seat))

        self._seating[to_row][to_letter] = self._seating[from_row][from_letter]
        self._seating[from_row][from_letter] = None

        以上代码将解析并校验from_seat和to_seat参数,然后将乘客移至新位置。

        每次都需要重新创建Flight对象,所以需要添加一个模块级别的函数,这比较方便:

def make_flight():
    f = Flight("BA758",Aircraft("G-EUPT", "Airbus A319", num_rows=22, num_seats_per_row=6))
    f.allocate_seat('12A', 'Guido van Rossum')
    f.allocate_seat('15F', 'Bjarne Stroustrup')
    f.allocate_seat('15E', 'Anders Hejlsberg')
    f.allocate_seat('1C', 'John McCarthy')
    f.allocate_seat('1D', 'Richard Hickey')
    return f

        在Python中,同一个模块混合了相关的函数和类是很正常的:

>>> from airtravel import make_flight
>>> f = make_flight()
>>> f
<airtravel.Flight object at 0x0000021D42E5FED0>

        当只导入了一个函数make_flight时,你可能会发现能使用Flight类。这是很正常的,这也是Python动态类型系统的一个强大的方面,它有利于代码之间的松耦合。

        让我们继续,把Guido和欧洲人同伴一起安排在第15排;

>>> f.relocate_passenger('12A', '15D')
>>> from pprint import pprint as pp
>>> pp(f._seating)
[None,
 {'A': None,
  'B': None,
  'C': 'John McCarthy',
  'D': 'Richard Hickey',
  'E': None,
  'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None,
  'B': None,
  'C': None,
  'D': 'Guido van Rossum',
  'E': 'Anders Hejlsberg',
  'F': 'Bjarne Stroustrup'},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None},
 {'A': None, 'B': None, 'C': None, 'D': None, 'E': None, 'F': None}]

8.9.2 计数可用座位

        在Flight类下编写num_available_seats()方法,该方法使用两个嵌套的生成器表达式。外部表达式会过滤所有None行来排除虚拟的第一行。外部表达式中每个项目的值是每行中“None”值个数的总和。内部表达式用于迭代字典的值,并且每找到一个None就将对应的值加1:        

    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 )

        请注意,我们如何将外部表达式分成3行以提高可读性:

>>> from airtravel import make_flight
>>> f = make_flight()
>>> f.num_available_seats()
127

        快速检查显示新计算是正确的:

>>> 6 * 22 - 5
127

8.10 有时你可能只需要函数对象

        如何在不使用类的情况下编写优雅的面向对象的代码?有这样一个需求:按字母顺序为乘客提供登机牌。然而,在Flight中实现登机牌的细节似乎不太好。可以继续创建一个BoardingCardPrinter类,虽然这可能有点过度设计了。请记住,函数也是对象,对于很多情况来说,函数是完全足够的。如果没有足够的理由,不要强迫自己使用类。

        将采用面向对象的设计原则“告诉,不要去询问”,不是让卡片打印机查询航班上所有乘客的详细信息,而是让Flight告诉卡片输出函数(比较简单)该怎么做。

        首先是卡片打印机,这只是一个模块级别的函数:

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()

        在这里,介绍Python的一个特性,使用行连续的反斜杠字符\可以把一条长语句分成几行。这里将它和相邻字符串的隐式字符串连接,用以产生一个没有换行符的长字符串。

        测量这个输出行的长度,在它周围加上一些横线和边框,并使用在新行分隔符上调用的join()方法将这些行连接在一起。然后输出整个卡片的内容内容,最后是一个空白行。卡片打印机不知道任何关于Flight和Aircraft的信息——它是松耦合的。很容易想象一个具有相同接口的HTML卡片打印机。

为Flight创建乘机证

        给Flight类添加一个新的方法make_boarding_cards(),它接收card_printer:

class Flight:
    
    # ...

    def make_boarding_cards(self, card_printer):
        for passenger, seat in sorted(self._passenger_seats()):
            card_printer(passenger, seat, self.number(), self.aircraft_model())

        以上代码告诉card_printer输出每个乘客的信息,并对从_passenger_seats()实现细节方法(注意前置下划线)中获得的乘客座位元组列表进行排序。这种方法实际上是一个生成器函数,该函数搜索所有乘客的座位,并输出找到的乘客名字和座位号码:

    def _passenger_seats(self):
        """一个可迭代的乘客座位分配序列。"""
        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))

        在REPL中运行该程序,新的登机牌打印系统起作用了:

>>> from airtravel import console_card_printer, make_flight
>>> f = make_flight()
>>> f.make_boarding_cards(console_card_printer)
+----------------------------------------------------------------------+
|                                                                      |
| Name: Anders Hejlsberg Flight: BA758 Seat: 15E Aircraft: Airbus A319 |
|                                                                      |
+----------------------------------------------------------------------+

+-----------------------------------------------------------------------+
|                                                                       |
| Name: Bjarne Stroustrup Flight: BA758 Seat: 15F Aircraft: Airbus A319 |
|                                                                       |
+-----------------------------------------------------------------------+

+----------------------------------------------------------------------+
|                                                                      |
| Name: Guido van Rossum Flight: BA758 Seat: 12A Aircraft: Airbus A319 |
|                                                                      |
+----------------------------------------------------------------------+

+------------------------------------------------------------------+
|                                                                  |
| Name: John McCarthy Flight: BA758 Seat: 1C Aircraft: Airbus A319 |
|                                                                  |
+------------------------------------------------------------------+

+-------------------------------------------------------------------+
|                                                                   |
| Name: Richard Hickey Flight: BA758 Seat: 1D Aircraft: Airbus A319 |
|                                                                   |
+-------------------------------------------------------------------+

8.11 多态与鸭子类型

        多态性(polymorphism)是一种编程语言特性,它允许通过统一的接口使用不同类型的对象。多态的概念适用于函数和更复杂的对象。

        Python中的多态性是通过鸭子类型(duck typing)实现的。鸭子类型也称作“鸭子测试”,这归因于美国诗人詹姆斯·惠特孔·莱里。

        当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。

        鸭子类型是Python对象系统的基石,在这种情况下,一个对象对特定用途的适应性只能在运行时确定。它与静态类型语言不同,静态类型语言的编译器会确定对象是否可用。特别是,鸭子类型意味着对象的适用性不是基于继承层次结构、基类或对象在使用时具有的属性之外的任何东西。

        这与Java等语言形成了鲜明的对比,Java等语言依赖于名义上的子类型化(sub-typing),主要通过从基类和接口继承来实现。我们稍后将在Python的上下文中进一步讨论继承。

重构Aircraft

        让我们重新回到Aircraft类:

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), "ABCDEFGHJK"[:self._num_seats_per_row])

        这个类推设计有点不妥,使用它进行实例化的对象由飞机型号匹配的座位配置决定。为了方便练习,可以假定每个飞机型号的座位安排是固定的。

        更好、更简单的做法也许是:完全摆脱Aircraft类,并为已固定座位的每种特定型号的飞机创建不同的类。以下是空客A319座位的代码:

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"

        以下是波音777座位的代码:

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):
        # 简单起见忽略了复杂的座位安排
        return range(1, 56), "ABCDEGHJK"

        除了具有相同接口(除了初始化方法之外,现在这个方法的参数也比较少)之外,这两个飞机类没有明确的关系,与原来的Aircraft类也没有明确的关系,因此,可以使用这些新的的类型来代替彼此。

        把make_flight()方法改成make_flights(),就可以这样使用它们:

def make_flights():
    f = Flight("BA758",AirbusA319("G-EUPT"))
    f.allocate_seat('12A', 'Guido van Rossum')
    f.allocate_seat('15F', 'Bjarne Stroustrup')
    f.allocate_seat('15E', 'Anders Hejlsberg')
    f.allocate_seat('1C', 'John McCarthy')
    f.allocate_seat('1D', 'Richard Hickey')
    
    g = Flight("BA758",Boeing777("F-GSPS"))
    g.allocate_seat('55K', 'Larry wall')
    g.allocate_seat('33G', 'Yukihiro Matsumoto')
    g.allocate_seat('4B', 'Brian Kernighan')
    g.allocate_seat('4A', 'Dennis Ritchie')
    return f, g

        在Flight中可以很好地使用这两个不同类型的飞机,因为它们都是“鸭子类型”:

>>> from airtravel import *
>>> f, g = make_flights()
>>> f.aircraft_model()
'Airbus A319'
>>> g.aircraft_model()
'Boeing 777'
>>> f.num_available_seats()
127
>>> g.num_available_seats()
491
>>> g.relocate_passenger('55K', '13G')
>>> g.make_boarding_cards(console_card_printer)
+-------------------------------------------------------------------+
|                                                                   |
| Name: Brian Kernighan Flight: BA758 Seat: 4B Aircraft: Boeing 777 |
|                                                                   |
+-------------------------------------------------------------------+

+------------------------------------------------------------------+
|                                                                  |
| Name: Dennis Ritchie Flight: BA758 Seat: 4A Aircraft: Boeing 777 |
|                                                                  |
+------------------------------------------------------------------+

+---------------------------------------------------------------+
|                                                               |
| Name: Larry wall Flight: BA758 Seat: 13G Aircraft: Boeing 777 |
|                                                               |
+---------------------------------------------------------------+

+-----------------------------------------------------------------------+
|                                                                       |
| Name: Yukihiro Matsumoto Flight: BA758 Seat: 33G Aircraft: Boeing 777 |
|                                                                       |
+-----------------------------------------------------------------------+

        在Python中,鸭子类型和多态性非常重要。实际上,它们是我们讨论的集合协议如迭代器、可迭代和序列的基础。

8.12 继承与实现共享

        继承是一种机制,一个类可以从一个基类中派生,这使得可以在子类中细化行为。在诸如Java之类的名义类型语言中,基于类推继承是实现运行时多态性的手段。正如刚刚演示的那样,Python并非如此。事实上,没有任何Python的方法调用或属性查找可以绑定到实际的对象,直到它们被调用(被称为后期绑定),这意味着可以使用任何对象来尝试多态性,如果对象适合,后期绑定会成功。

        Python中的继承有助于多态性,毕竟,派生类与基类具有相同的接口。在Python中,继承对于在类之间共享实现是非常有用的。

8.12.1 一个飞机基类

        为飞机类AirbusA319和Boeing777建立一个返回总座位数的方法。

        现在为这两个类添加一个名为num_seats()的方法来执行此操作:

    def num_seats(self):
        rows, row_seats = self.seating_plan()
        return lenJ(rows) * len(row_seats)

        该实现在两个类中是相同的,因为它可以从座次表中计算出来。

        不幸的是,现在两个类中有重复的代码,并且随着添加更多的飞机类型,代码重复会持续恶化。

        解决方案是将AirbusA319和Boeing777的通用元素提取到一个基类中,所有的飞机类型都会继承该类。重新创建Aircraft类,这次把它用作基类:

class Aircraft:

    def num_seats(self):
        rows, row_seats = self.seating_plan()
        return lenJ(rows) * len(row_seats)

        Aircraft类只包含想要继承到派生类中的方法。这个类时不可用的,因为它依赖于一个名为李seating_plan()的方法,这个方法在这个级别中是不可用的,单独使用它也会失败: 

>>> from airtravel import *
>>> base = Aircraft()
>>> base.num_seats()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\python\写给程序员的Python教程\pyfund\airtravel.py", line 126, in num_seats
    rows, row_seats = self.seating_plan()
                      ^^^^^^^^^^^^^^^^^
AttributeError: 'Aircraft' object has no attribute 'seating_plan'

        这个类是抽象的,因为它不会单独实例化。

8.12.2 继承Aircraft

        现在开始编写派生类。在Python中,这样指定继承:在类语句中的类名之后使用由括号包裹的基类名。

        以下是Airbus类:

class AirbusA319(Aircraft):
    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"

以下是Boeing类:

class Boeing777(Aircraft):
    def __init__(self, registration):
        self._registration = registration

    def registration(self):
        return self._registration

    def model(self):
        return "Boeing 777"

    def seating_plan(self):
        # 简单起见忽略了复杂的座位安排
        return range(1, 56), "ABCDEGHJK"

        在REPL中执行它们: 

>>> from airtravel import *
>>> a = AirbusA319("G-EZBT")
>>> a.num_seats()
132
>>> b = Boeing777("N717AN")
>>> b.num_seats()
495

        可以看到,这两种子类型的飞机都继承了num_seats()方法,该方法现在可以按照预期工作,因为在运行时,它可以成功解析self.seating_plan()的调用。

8.12.3 将公共功能提升到基类中

        现在,有了基础的Aircraft类,就可以通过将其他常用功能提升到该类中进行重构。例如,两个子类型的初始化方法和registration()方法是相同的:

class Aircraft:

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

    def registration(self):
        return self._registration

    def num_seats(self):
        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):
        # 简单起见忽略了复杂的座位安排
        return range(1, 56), "ABCDEGHJK"

        这些派生类只包含该机型的具体信息。所有通用功能都是通过继承从基类共享的。

        由于鸭子类型,所以在Python中继承的使用要比在其他语言中少。这通常被认为是一件好事,因为继承会导致类之间的紧耦合。

8.13 小结

  • Python中的所有类型都有一个类。
  • 类定义了对象的结构和行为。
  • 对象的类是在创建对象时确定的,并且在对象的生命周期中几乎总是固定的。
  • 类是Python中面向对象编程的关键支持。
  • 类使用class关键字进行定义,后跟使用驼峰命名法命名的类名。
  • 类的实例是通过调用类来创建的,就像它是一个函数一样。
  • 实例方法是在类中定义的函数,它应该接收一个名为self的对象实例作为第一个参数。
  • 使用instance.method()语法来调用方法,该语法是将实例作为self形参传递给方法的语法糖。
  • 可以提供一个名为__init__()的可选的特殊初始化方法,用于在创建时配置自己的对象。
  • 如果存在__init__()方法,构造函数会调用它。
  • __init__()方法还是构造函数。在调用初始化方法时,该对象已经被构建。初始化方法在它返回到构造函数的调用者之前配置新创建的对象。
  • 传递给构造函数的参数会被转发给初始化方法。
  • 实例属性只会在对其赋值时生成。
  • 实现细节的属性和方法按照约定以一个下划线为前缀。Python中没有公有、受保护的或私有访问修饰符。
  • 在开发、测试和调度过程中,从类外部访问实现细节可能非常有用。
  • 应该在类初始化方法中建立类不变式。如果无法建立不变式就抛出异常作为失败信号。
  • 方法可以具有docstrings,就像常规函数一样。
  • 类可以有docstrings。
  • 即使在一个类中,方法调用也必须通过self限定。
  • 你可以根据需要在模块中包含尽可能多的类和函数。相关类和全局函数通常以这种方式组合在一起。
  • Python中的多态性是通过鸭子类型来实现的,其中属性和方法只在使用时才被解析,这种行为称为后期绑定(late-binding)。
  • Python中的多态不需要共享基类或命名接口。
  • Python中的类继承主要用于共享实现,它不是多态的必要条件。
  • 所有的方法都是继承的,包括特殊的方法,如初始化方法。
  • 字符串支持切片,因为它们实现了序列协议。
  • 遵循得墨忒耳定律可以减少耦合。
  • 我们可以使用嵌套推导。
  • 有时使用虚拟引用(通常是下划线)忽略推导中的当前项目是很有用的。
  • 在处理从1开始的集合的时候,舍弃第0个条目通常更容易。
  • 当一个简单的函数就足够了的时候,不要强迫自己使用类。函数也是对象。
  • 复杂的推导或生成器表达式可以分成多行,这样可提高可读性。
  • 可以使用行连续字符反斜线将语句分成多行。只有在需要提高可读性时,才能使用此特性。
  • 在面向对象设计中,对象将信息告诉另一个对象比一个对象去查询另一个对象具有更好的松耦合。“告诉,不要去询问”。
  • 实际上,我们可以在运行时更改对象的类别,然而这是一个高级主题,而且这种技术很少使用。
  • 在Python中,思考销毁对象通常是无益的。更好的方法是考虑对象不可达。
  • 函数的形式参数是函数定义(definition)中列出的参数。
  • 函数的实际参数是函数调用(call)中列出的参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值