恭喜你来到继《函数》后本书理论部分第二座大山:类。保守估计本章认真学完需要4~6个小时,请学习之前先吃饱喝好,以免中途低血糖。

那么就开始吧。

第9章:类

面向对象变成是最有效的软件编写方法之一。在面向对象编程中,你编写表示现实世界中的事物和情景的类,并给予这些类来创建对象。编写类时,你定义一大类对象都有通用行为。基于类创建对象时,每个对象都自动具备这种通用行为,然后可根据需要赋予每个对象独特的个性。使用面向对象编程可模拟现实情境,其逼真程度达到了令人惊讶的地步。

根据类来创建对象称之为实例化,这让你能够使用类的实例。在本章中,你将编写一些类并创建其实例。你将指定可在实例中存储什么信息,定义可对这些实例执行哪些操作。你还将编写一些类来扩展既有类的功能,让相似的类能够高效地共享代码。你将把自己编写的类存储在模块中,并在自己的程序文件中导入其他程序员编写的类。
9.1 创建和使用类
使用类几乎可以模拟任何东西。下面来编写一个表示小狗的简单类Dog,它表示的不是特定的小狗,而是任何小狗。对于大多数宠物狗,我们都知道些什么呢?它们都有名字和年龄。我们还知道,大多数小狗还会蹲下和打滚。由于大多数小狗都具备上述两项信息(名字和年龄)和两种行为(蹲下和打滚),我们的dog类将包含它们。这个类让Python知道如何创建表示小狗的对象。编写这个类后,我们将使用它来创建表示特定小狗的实例。
9.1.1 创建Dog类
根据Dog类创建的每个实例都将存储名字和年龄,我们赋予了每条小狗蹲下(sit())和打滚(roll_over())的能力:
class Dog:
    """一次模拟小狗的简单尝试"""
    def __init__(self,name,age):
        """初始化属性name和age"""
        self.name = name
        self.age = age

    def sit(self):
        """模拟小狗收到命令时蹲下"""
        print(f"{self.name} is now sitting.")

    def roll_over(self):
        """模拟小狗收到命令时打滚"""
        print(f"{self.name} rolled over!")

(胭惜雨:需要特别说明__init__里面的_是两个,不是一个,否则会报错)
这里需要注意的地方很多,但也不用担心,本章充斥着这样的结构,你有大把的机会熟悉它。class 处定义了一个名为Dog的类。根据约定,在Python中,首字母大写的名称指的是类。这个类定义中没有圆括号,因为要从空白创建这个类。紧随其后编写了一个文档字符串,对这个类的功能做了描述。

方法_init_()
类中的函数称之为方法。你在前面学到的有关函数的一切都适用于方法,就目前而言,唯一重要的差别是调用方法的方式。_init_()是一种特殊方法,每当你根据Dog类创建新实例时,Python都将自动运行它。在这个方法的名称中,开头和末尾各有两个下划线,这是一种约定,避免Python默认方法与普通方法发生名称冲突。务必确保_init_()的两边都有两个下划线,否则当你使用类来创建实例时,将不会自动调用这个方法,进而引发难以发现的错误。

我们将方法_init_()定义包含三个形参:self、name和age。在这个方法的定义中,形参self必不可少,而且必须位于其他形参的前面。为何必须在方法定义中包含self呢?因为Python调用这个方法来创建Dog示例时,将自动传入实参self。每个与实例相关联的方法调用都自动传递实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。创建Dog实例时,Python将调用Dog类的方法_init_()。我们将通过实参向Dog()传递名字和年龄,self会自动传递,因此不需要传递它。每当根据Dog类创建实例时,都只需给最后两个形参(name和age)提供值。

self.name处定义的两个变量都有前缀self。以self为前缀的变量可供类中的所有方法使用, 可以通过类的任何实例来访问。self.name = name获取与形参name相关联的值,并将其赋给变量name,然后该变量被关联到当前创建的实例。self.age = age的作用与此类似。像这样可通过实例访问的变量称之为属性。

Dog类还定义了另外两种方法:sit()和roll_over()。这些方法执行时不需要额外的信息,因此它们只有一条形参self。我们随后将创建的实例能够访问这些方法,换句话说,它们都会蹲下和打滚。当前sit()和roll_over()所做的有限,只是打印一条消息,指出小狗正在蹲下或打滚。但可以扩展这些方法一模拟实际情况:如果这个类包含在一个计算机游戏中,这些方法将包含创建小狗蹲下和打滚动画效果的代码;如果这个类是用于控制机器狗的,这些方法将让机器狗做出蹲下和打滚的动作。
9.1.2 根据类创建实例
可将类视为有关如何创建实例的说明。Dog类是一系列说明,让Python知道如何创建表示特定小狗的实例。

下面来创建一个表示特定小狗的实例:
class Dog:
    """一次模拟小狗的简单尝试"""
    def __init__(self,name,age):
        """初始化属性name和age"""
        self.name = name
        self.age = age

    def sit(self):
        """模拟小狗收到命令时蹲下"""
        print(f"{self.name} is now sitting.")

    def roll_over(self):
        """模拟小狗收到命令时打滚"""
        print(f"{self.name} rolled over!")

my_dog = Dog('willie',6)
print(f"我的狗名字叫{my_dog.name}")
print(f"我的狗今年{my_dog.age}岁了。")

这里使用的是前一个示例中编写的Dog类。在my_dog = Dog('willie',6)处,让Python创建一条名为“Willie”、年龄为6的小狗。遇到这行代码时,Python使用实参‘Willie’和6调用Dog类的方法__init__()。方法__init__()创建一条表示特定小狗的实例,并使用提供的值来设置属性name和age。接下来,Python返回一个表示小狗的实例,而我们将这个实例赋给了变量my_dog。在这里,命名约定很有用:通常可认为首字母大写的名称(如Dog)指的是类,而小写的名称(如my_dog)指的是根据类创建的实例。

1.访问属性
要访问实例的属性,可使用句点表示法。my_dog.name代码可访问my_dog的属性name的值name。

句点表示法在Python中很常用,这种语法演示了Python如何获悉属性的值。在这里,Python先找到实例my_dog,再查找与该实例相关联的属性name。在Dog类中引用这个属性时,使用的是self.name。

输出的是有关my_dog的摘要:
我的狗名字叫willie
我的狗今年6岁了。

2.调用方法
根据Dog类创建实例后,就能使用句点表示法来调用Dog类中定义的任何方法了。下面来让小狗蹲下和打滚:
class Dog:
    """一次模拟小狗的简单尝试"""
    def __init__(self,name,age):
        """初始化属性name和age"""
        self.name = name
        self.age = age

    def sit(self):
        """模拟小狗收到命令时蹲下"""
        print(f"{self.name} is now sitting.")

    def roll_over(self):
        """模拟小狗收到命令时打滚"""
        print(f"{self.name} rolled over!")

my_dog = Dog('willie',6)
my_dog.sit()
my_dog.roll_over()

要调用方法,可指定实例的名称(这里是my_dog)和要调用的方法,并用句点分隔。遇到代码my_dog.sit()时,Python在类Dog中查找方法sit()并运行其代码。Python以同样的方式解读代码my_dog.roll_over()。

Willie按我们的命令做了:
willie is now sitting.
willie rolled over!

这种语法很有用。如果给属性和方法指定了合适的描述性名称,如name、age、sit()和roll_over(),即便是从未见过的代码块,我们也能够轻松地推断出它是做什么的。

3.创建多个实例
可按需求根据类创建任意数量的实例。下面再创建一个名为your_dog的小狗实例:
class Dog:
    """一次模拟小狗的简单尝试"""
    def __init__(self,name,age):
        """初始化属性name和age"""
        self.name = name
        self.age = age

    def sit(self):
        """模拟小狗收到命令时蹲下"""
        print(f"{self.name} is now sitting.")

    def roll_over(self):
        """模拟小狗收到命令时打滚"""
        print(f"{self.name} rolled over!")

my_dog = Dog('willie',6)
your_dog = Dog('Lucy',3)

print(f'我的狗名字叫做{my_dog.name}')
print(f'我的狗今年已经{my_dog.age}了')
my_dog.sit()

print(f'你的狗名字叫做{your_dog.name}')
print(f'你的狗今年{your_dog.age}')
your_dog.sit()

在本例中创建了两条小狗,分别名为Willie和Lucy。每条小狗都是一个独立的实例,有自己的一组属性,能够执行相同的操作:
我的狗名字叫做willie
我的狗今年已经6了
willie is now sitting.
你的狗名字叫做Lucy
你的狗今年3
Lucy is now sitting.

即使给第二条小狗指定同样的名字和年龄,Python依然会根据Dog类创建另一个实例。你可按需求根据一个类创建任意数量的实例,条件是将每个实例都存储在不同的变量里,或者占用列表和字典的不同位置。
9.2使用类和实例
可使用类来模拟现实世界中的很有情景。类编写好后,你的大部分时间将花在根据类创建的实例上。你需要执行的一个重要任务是修改实例的属性。可以直接修改实例的属性,也可以编写方法以特定的方式进行修改。

9.2.1 girl类
下面来编一些一个表示女友的类。它存储了有关女友的信息,还有一个汇总这些信息的方法:
class Girl:
    """一次模拟女友的简单尝试"""

    def __init__(self,name,age,nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())

在init处,定义了方法__init__()。与前面的Dog类中一样,这个方法的第一个形参为self。该方法还包含另外三个形参:name,age,nationality。方法__init__()接受这些形参的值,并将它们赋给根据这个类创建的实例的属性。创建新的Girl时,需要指定其名字、年龄和国籍。

在def get_girl(self)处,定义了一个名为get_girl的方法。它使用属性name,age,nationality创建一个对女友进行描述的字符串,让我们无需分别打印每个属性的值。为在这个方法中访问属性的值,使用了self.name、self.age和self.nationality。在my_a_girl = Girl("jun Hyo-Seong",32,"Korea")处,根据Girl类创建了一个实例,并将其赋给变量my_a_girl。接下来,调用方法get_girl(),指出我们拥有一个什么样的女友:
我女朋友叫Jun Hyo-Seong,今年32岁,是Korea人。

为了让这个类更有趣,下面给它增加一个随时间变化的属性,用于存储女友的胸围。
9.2.2 给属性指定默认值
创建实例时,有些属性无需通过形参来定义,可在方法__init__()中为其指定默认值。

下面来添加一个名为cup的属性,其初始值总是为A。我们还添加了一个名为read_cup()的方法,用于读取女友的胸围:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = "A"

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}罩杯的胸。")
my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())
my_a_girl.read_cup()

现在,当Python调用方法__init__()时,将像前一个示例一样以属性的方式存储姓名、年龄和国籍。接下来,Python将创建一个名为read_cup的属性,并将其初始值设置为“A”。定义一个名为read_cup()的方法,让你能够轻松地获悉女友的罩杯。

我女朋友叫Jun Hyo-Seong,今年32岁,是Korea人。
她有一对A罩杯的胸。

实际上Jun Hyo-Seong的胸并不是A,因此需要一种方式来修改该属性的值。
9.2.3 修改属性的值
我们能以三种方式修改属性的值:直接通过实例进行修改,通过方法进行设置,以及通过方法进行递增(增加特定的值)。下面依次介绍这些方式。

1.直接修改属性的值
要修改属性的值,最简单的方式就是通过实例直接访问它。下面的代码直接将罩杯设置为D:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = "A"

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}罩杯的胸。")
my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())

my_a_girl.cup = "D"
my_a_girl.read_cup()

在my_a_girl.cup = "D"处,使用句点表示法直接访问并设置女友的属性cup。这行代码让Python在实例my_a_girl中找到属性cup,并将其值设置为D:
我女朋友叫Jun Hyo-Seong,今年32岁,是Korea人。
她有一对D罩杯的胸。

有时候需要像这样直接访问属性,但其他时候需要编写对属性进行更新的方法。

2.通过方法修改属性的值
如果有方法能替你更新属性,将大有裨益。这样就无需直接访问属性,而可将值传递给方法,由它在内部进行更新。

下面的示例演示了一个update_cup()的方法:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = "A"

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}罩杯的胸。")

    def update_cup(self,cup):
        """将罩杯设置为指定的值"""
        self.cup  = cup

my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())

my_a_girl.update_cup('D')
my_a_girl.read_cup()

对Girl类所做的唯一修改是在def update_cup(self,cup)处添加了方法。这个方法接受一个罩杯值,并将其赋给self.cup。在my_a_girl.update_cup('D')处,调用update_cup(),并向它提供了实参“D”(该实参对应方法定义中的形参new_cup)。它将罩杯设置为D,而方法my_a_girl.read_cup()打印该参数:
我女朋友叫Jun Hyo-Seong,今年32岁,是Korea人。
她有一对D罩杯的胸。

可对方法update_cup()进行扩展,使其在修改罩杯时做些额外的工作。下面添加一些逻辑,禁止任何人将罩杯size进行修改:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = "D"

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}罩杯的胸。")

    def update_cup(self,new_cup):
        """
        将罩杯设置为指定的值。
        禁止将罩杯大小进行修改。
        """
        if new_cup == "D":
            self.cup = new_cup
        else:
            print("别乱动!")

my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())

my_a_girl.update_cup('D')
my_a_girl.read_cup()

现在,update_cup()在修改属性前检查指定的杯数是否为“D”。如果新指定的罩杯不等于“D”,就发出警告。

3.通过方法对属性的值进行
有时候需要将属性值递增特定的量,而不是将其设置为全新的值。下面的方法让我们能够传递这个增量,并相应地增大罩杯数:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

my_a_girl = Girl("jun Hyo-Seong",32,"Korea")
print(my_a_girl.get_girl())

my_a_girl.update_cup(32)
my_a_girl.read_cup()

my_a_girl.now_new_cup(2)
my_a_girl.read_cup()

新增一个def now_new_cup(self,now_cup)的方法,并将其加入self.cup中。在my_a_girl = Girl("jun Hyo-Seong",32,"Korea")中,创建一个女友。在my_a_girl.update_cup(32)将罩杯设置为32。在my_a_girl.now_new_cup中传入2,以增加胸部长大的2:
我女朋友叫Jun Hyo-Seong,今年32岁,是Korea人。
她有一对32D罩杯的胸。
她有一对34D罩杯的胸。

你可以轻松地修改这个额方法,以禁止增量为负值,从而防止有人利用它来回调罩杯大小。

注意:你可以使用上面的方法来控制用户修改属性值的方式,但能够访问程序的人都可以通过直接访问属性来将罩杯修改为任何值。要确保安全,除了进行类似于前面的基本检查外,还需特别注意细节。
9.3 继承
编写类时,并非总是要从空白开始。如果要编写的类是另一个现成类的特殊版本,可使用继承。一个类继承另一个类,将自动获得另一个类的所有的属性和方法。原有的类称为父类,从而新类称为子类。子类继承了父类的所有属性和方法,同时还可以定义自己的属性和方法。

9.3.1 子类的方法__init__() (胭惜雨:再次强调,是两个_)
在既有类的基础上编写新类时,通常要调用父类的方法__init__() 。这将初始化在父类__init__() 方法中定义的所有类型,从而让子类包含这些属性。

例如,下面来创建一个新女友。因为都是女友,因此可在前面创建的Girl类的基础上创建新类New_k_girl。这样就只需要为新女友特有的属性和行为编写代码。

下面来创建New_k_girl类的一个简单版本,它具备Girl类的所有功能:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性"""
        super().__init__(name,age,nationality)

my_k_girl = New_k_girl("朴孝敏","32","Korea")
print(my_k_girl.get_girl())

首先是Girl类的代码。创建子类时,父类必须包含在当前文件中,且位于子类前面。在def __init__(self,name,age,nationality):处,定义了子类New_k_girl。定义子类时,必须在圆括号内指定父类的名称。方法__init__()接受创建Girl实例所需的信息。

super().__init__(name,age,nationality)处的super()是一个特殊函数,让你能够调用父类的方法。这行代码让Python调用Girl类的方法__init__(),让New_k_girl实例包含这个方法中定义的所有属性。父类也称为超类,名字super由此而来。

为测试继承能够正确地发挥作用,我们尝试创建一名女友,但提供的信息与创建之前的女友相同。在my_k_girl = New_k_girl("朴孝敏","32","Korea")处,创建New_k_girl类的一个实例,并将其赋给变量my_k_girl。这行代码调用New_k_girl类中定义的方法__init__(),后者让Python调用父类Girl中定义的方法__init__()。我们提供了实参"朴孝敏","32","Korea"。

除方法__init__()外,新女友没有其他特有的属性和方法。当前,我们只想确认新女友的基本信息:
我女朋友叫朴孝敏,今年32岁,是Korea人。

New_k_girl实例的行为与Girl实例一样,现在可以开始定义新女友特有的属性和方法了。
9.3.2 给子类定义属性和方法
让一个类继承另一个类后,就可以添加区分子类和父类所需的新属性和新方法了。

下面来添加一个新女友特有的属性,以及一个描述该属性的方法。我们将存储新女友的三围,并编写一个打印三围的方法:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = "34,24,36"

    def d_measurements(self):
        """打印一条描述女友三围的消息"""
        print(f"她的三围是{self.measurements}.")

my_k_girl = New_k_girl("朴孝敏","32","Korea")
print(my_k_girl.get_girl())
my_k_girl.d_measurements()

在self.measurements = "34,24,36"增加了新属性,并设置其初始值。根据New_k_girl类创建的所有实例都将包含该属性,但所有Girl实例都不包含它。在def d_measurements(self)处,添加了一个方法,打印新女友的信息。调用这个方法时,将看到新女友特有的描述:
我女朋友叫朴孝敏,今年32岁,是Korea人。
她的三围是34,24,36。

对于New_k_girl类的特殊程度没有任何限制。模拟新女友时,可根据所需的准确程度添加任意数量的属性和方法。如果一个属性或方法是任何女友都有的,而不是新女友特有的,就将其加入Girl类而非New_k_girl类中。这样使用Girl类的人将获得相应的功能,而New_k_girl类只包含新女友特有的属性和行为的代码。
9.3.3 重写父类的方法
对于父类的方法,只要它不符合子类模拟的实物的行为,都可以进行重写。为此,可在子类中定义一个与重写的父类方法相同的方法。这样Python将不会考虑这个父类方法,而只关注你在子类定义的相应方法。

假设Girl类有一个名为like_dog的方法,新女友不养狗,所以毫无意义。因此你可能想重写它。下面演示了一种重写方式:
class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = "34,24,36"

    def d_measurements(self):
        """打印一条描述女友三围的消息"""
        print(f"她的三围是{self.measurements}。")

    def like_dog(self):
        """新女友没有狗"""
        print(f"{self.name}暂时还没有养狗.")

现在,如果有人对新女友调用方法like_dog(),Python将忽略Girl类中的方法like_dog(),转而运行上述代码。使用继承时,可让子类保留从父类那里继承而来的精华,并剔除不需要的糟粕。
9.3.4 将实例用作属性
使用代码模拟实物时,你可能会发现自己给类添加的细节越来越多:属性和方法清单以及文件都越来越长。在这种情况下,可能需要将类的一部分提取出来,作为一个独立的类。可以将大型类拆分成多个协同工作的小类。

例如,不断给New_k_girl添加细节时,我们可能发现其中包含很多专门针对新女友的属性和方法。在这种情况下,可将这些属性和方法提取出来,放到一个名为Measurements的类中,并将Measurements实例作为New_k_girl类的属性:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

class Measurements:
    """一次模拟三围的简单尝试"""
    def __init__(self,measurements = "34,24,36"):
        """初始化三围属性"""
        self.measurements = measurements

    def new_measurements(self):
        """打印一条描述三围的消息"""
        print(f"她的三围是{self.measurements}")

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = Measurements()

    def like_dog(self):
        """新女友没有狗"""
        print(f"{self.name}暂时还没有养狗.")

my_k_girl = New_k_girl("朴孝敏","32","Korea")
print(my_k_girl.get_girl())
my_k_girl.measurements.new_measurements()

class Measurements处定义一个名为Measurements的新类,它没有继承任何类。def __init__(self,measurements = "34,24,36")处除self外,还有一个形参measurements。这个形参是可选的;如果没有给它提供值,三围将被设置为"34,24,36"。方法new_measurements()也移到了这个类中。

在New_k_girl类中,添加了一个名为self.measurements的属性。这行代码让Python创建一个新的Measurements实例(因为没有指定三围,所以为默认值)。并将该实例赋给属性self.measurements。每当方法__init__()被调用时,都将执行该操作,因此现在每个New_k_girl实例都包含一个自动创建的measurements实例。

我们创建一个新女友,并将其赋值给my_k_girl。描述三围时,需要使用my_k_girl的属性measurements:
my_k_girl.measurements.new_measurements()

这行代码让Python在实例my_k_girl中查找属性measurements,并对存储该属性中的measurements实例调用方法new_measurements()。

输出与你前面看到的相同:
我女朋友叫朴孝敏,今年32岁,是Korea人。
她的三围是34,24,36

这看似做了很多额外的工作,但是现在想多详细地描述点评都可以,且不会导致New_k_girl混乱不堪。下面再给Measurements类添加一个方法,它根据三围的数据报告身材情况:
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

class Measurements:
    """一次模拟三围的简单尝试"""
    def __init__(self,measurements = "34,24,36"):
        """初始化三围属性"""
        self.measurements = measurements

    def new_measurements(self):
        """打印一条描述三围的消息"""
        print(f"她的三围是{self.measurements}")

    def get_range(self):
        """打印一条消息,指出身材的情况"""
        if self.measurements == "34,24,36":
            print("哇,完美身材!")
        else:
            print("身材还可以更完美哦!")

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = Measurements()

    def like_dog(self):
        """新女友没有狗"""
        print(f"{self.name}暂时还没有养狗.")

my_k_girl = New_k_girl("朴孝敏","32","Korea")
print(my_k_girl.get_girl())
my_k_girl.measurements.new_measurements()
my_k_girl.measurements.get_range()

def get_range(self)处新增方法做了一些简单的分析:如果三围是"34,24,36",就打印"哇,完美身材!"。如果不然,则打印"身材还可以更完美哦!"。
我女朋友叫朴孝敏,今年32岁,是Korea人。
她的三围是34,24,36
哇,完美身材!
9.4 导入类
随着不断给类添加功能,文件可能变得很长,即便妥善地使用了继承亦如此。为遵循Python的总体理念,应让文件尽可能整洁。Python在这方面提供了帮助,允许将类存储在模块中,然后在主程序中导入所需的模块。

9.4.1 导入单个类
下面来创建一个只包含Girl类的模块。这让我们面临一个微妙的命名问题:在本章中已经有一个名为girl.py的文件,但这个模块也应命名为girl.py,因为它包含表示女友的代码。我们将这样解决这个命名问题:将Girl类存储在一个名为girl.py的模块中,该模块将覆盖前面使用的文件girl.py。从现在开始,使用该模块的程序都必须使用更具体的文件名,如my_girl.py。下面是模块girl.py,其中只包含Car类的代码:
"""一个可用于表示女友的类"""

class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

开头的代码行包含一个模块级文档字符串,对该模块的内容做了简要的描述。你应为自己创建的每个模块编写文档字符串。

下面来创建另一个文件my_girl.py,在其中导入Girl类并创建其实例:
from girl import Girl
my_new_girl = Girl("朴智妍","28","Korea")
print(my_new_girl.get_girl())

my_new_girl.cup = 32
my_new_girl.read_cup()

from girl import Girl处的import语句让Python打开模块girl并导入其中的Girl类。这样,我们就可以使用Girl类,就像它是在这个文件中定义的那样。输出与我们在之前看到的一样:
我女朋友叫朴智妍,今年28岁,是Korea人。
她有一对32D罩杯的胸。

导入类一种有效的变成方式。如果这个程序包含整个class类,它该有多长啊!通过将这个类移到一个模块中并导入该模块,依然可以使用其所有功能,但主程序文件变得整洁而易于阅读了。这还让你你能够将大部分逻辑存储在独立的文件中。确定类像你希望的那样工作后,就可以不管这些文件=,而专注于主程序的高级逻辑了。
9.4.2 在一个模块中存储多个类
虽然同一个模块中的类之间应存在某种相关性,但可根据需要在一个模块中存储任意数量的类。Measurements类和New_k_girl类都可以帮助模拟女友,下面将它们加入模块girl.py中:
"""一组用于表示女友的类"""

class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup
        
class Measurements:
    """一次模拟三围的简单尝试"""
    def __init__(self,measurements = "34,24,36"):
        """初始化三围属性"""
        self.measurements = measurements

    def new_measurements(self):
        """打印一条描述三围的消息"""
        print(f"她的三围是{self.measurements}")

    def get_range(self):
        """打印一条消息,指出身材的情况"""
        if self.measurements == "34,24,36":
            print("哇,完美身材!")
        else:
            print("身材还可以更完美哦!")

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = Measurements()

    def like_dog(self):
        """新女友没有狗"""
        print(f"{self.name}暂时还没有养狗.")

现在,可以新建一个名为my_new_k_girl类,并创建一个女友了:
from girl import New_k_girl

my_new_girl = New_k_girl("金珍熙","32","Korea")

print(my_new_girl.get_girl())
my_new_girl.measurements.new_measurements()
my_new_girl.measurements.get_range()

输出与我们在前面看到的相同,但大部分逻辑隐藏在一个模块中:
我女朋友叫金珍熙,今年32岁,是Korea人。
她的三围是34,24,36
哇,完美身材!
9.4.3 从一个模块中导入多个类
可根据需要在程序文件中导入任意数量的类。如果要在同一个程序中创建女友和新女友,就需要将Girl类和New_k_girl类都导入:
from girl import Girl,New_k_girl

my_girl = Girl("朴智妍","28","Korea")
print(my_girl.get_girl())

my_new_girl = New_k_girl("金珍熙","32","Korea")
print(my_new_girl.get_girl())

从一个模块中导入多个类时,用逗号分隔各个类。导入必要的类后,就可根据需要创建每个类的任意数量实例。

在本例中,创建了朴智妍和金珍熙两个女友:
我女朋友叫朴智妍,今年28岁,是Korea人。
我女朋友叫金珍熙,今年32岁,是Korea人。
9.4.4 导入整个模块
还可以导入整个模块,再使用句点表示法访问需要的类。这种导入方式很简单,代码也易于阅读。因为创建类实例的代码都包含模块名,所以不会与当前文件使用的任何名称发生冲突。

下面的代码导入整个girl模块,并创建朴智妍和金珍熙两个女朋友:
import girl

my_girl = girl.Girl("朴智妍","28","Korea")
print(my_girl.get_girl())

my_new_girl = girl.New_k_girl("金珍熙","32","Korea")
print(my_new_girl.get_girl())

在代码行开始处,导入了整个girl模块。接下来使用语法module_name.ClassName 访问需要的类。随后与前面一样,创建两个女友。
9.4.5 导入模块中的所有类
要导入模块中的每个类,可使用下面的语法:
from module_name import *

不推荐使用这种导入方式,原因有二。第一,如果只看文件开头的import语句,就能清楚地知道程序使用了哪些类,将大有裨益。然而这种导入方式没有明确地指出使用了模块中的哪些类。第二,这种方式还可能引发名称方面的迷惑。如果标新导入了一个与程序文件中其他东西同名的类,将引发难以诊断的错误。这里之所以介绍这种导入方式,是因为虽然不推荐使用,但你可能在别人编写的代码中见到它。

需要从一个模块中导入很多类时,最好导入整个模块,并使用module_name.ClassName语法来访问类。这样做时,虽然文件开头并没有列出用到的所有类,但你清楚地知道再程序的那些地方使用了导入的模块。这也避免导入模块中的每个类可能引发的名称冲突。
9.4.6 在一个模块中导入另一块模块
有时候,需要将类分散到多个模块中,以免模块太大或在同一个模块中存储不相关的类。将类存储在多个模块中时,你可能会发现一个模块中的类依赖于另一个模块中的类。在这种情况下,可在前一个模块中导入必要的类。

下面将Girl类存储在一个模块中,并将New_k_girl类和Measurements类存储在另一个模块中。将第二个模块命名为New_girl.py,并将Measurements类和New_girl类复制到这个模块中。

"""一组可用于表示新女友的类"""
from girl import Girl

class Measurements:
    """一次模拟三围的简单尝试"""
    def __init__(self,measurements = "34,24,36"):
        """初始化三围属性"""
        self.measurements = measurements

    def new_measurements(self):
        """打印一条描述三围的消息"""
        print(f"她的三围是{self.measurements}")

    def get_range(self):
        """打印一条消息,指出身材的情况"""
        if self.measurements == "34,24,36":
            print("哇,完美身材!")
        else:
            print("身材还可以更完美哦!")

class New_k_girl(Girl):
    """新女友的独特之处"""

    def __init__(self,name,age,nationality):
        """初始化父类的属性
        再初始化新女友特有的属性"""
        super().__init__(name,age,nationality)
        self.measurements = Measurements()

    def like_dog(self):
        """新女友没有狗"""
        print(f"{self.name}暂时还没有养狗.")

New_k_girl类需要访问其父类Girl,因此在开始处直接将Girl导入该模块中。如果忘记了这行代码,Python将在我们试图创建New_k_girl实例时引发错误。还需要更新模块girl,使其只包含Girl类。
"""一种表示女友的类"""
class Girl:
    """一次模拟女友的简单尝试"""
    def __init__(self, name, age, nationality):
        """初始化女友的属性"""
        self.name = name
        self.age = age
        self.nationality = nationality
        self.cup = 16

    def get_girl(self):
        """返回整洁的描述性信息"""
        new_girl = f"我女朋友叫{self.name},今年{self.age}岁,是{self.nationality}人。"
        return new_girl.title()

    def read_cup(self):
        """打印女友的胸围"""
        print(f"她有一对{self.cup}D罩杯的胸。")

    def update_cup(self,new_cup):
        """将罩杯设置为指定的值。"""
        self.cup = new_cup


    def now_new_cup(self,now_cup):
        """将罩杯增加到指定的杯"""
        self.cup += now_cup

现在可以分别从每个模块中导入类,以根据需要创建任何类型的女友了:
from girl import Girl
from new_k_girl import New_k_girl

my_girl = Girl("朴智妍","28","Korea")
print(my_girl.get_girl())

my_new_girl = New_k_girl("金珍熙","32","Korea")
print(my_new_girl.get_girl())

在代码行开始处,从模块girl导入了Girl类,并从模块new_k_girl导入New_k_girl类。接下来,创建了一个女友和新女友,都被正确的创建了出来:
我女朋友叫朴智妍,今年28岁,是Korea人。
我女朋友叫金珍熙,今年32岁,是Korea人。
9.4.7 使用别名
第八章说过,使用模块来组织项目代码时,别名大有裨益(胭惜雨:你是多喜欢“大有裨益”这个词啊喂)。导入时,也可为其指定别名。

例如,要在程序中创建女友时,需要反复输入girl,非常烦琐。为避免这种烦恼,可在import 语句中给New_k_girl指定一个别名:
from new_k_girl import New_k_girl as NG

现在每当创建新女友实例时,都可使用这个别名:
my_new_girl = NG("金珍熙","32","Korea")
9.4.8 自定义工作流程
如你所见,在组织大型项目的代码方面,Python提供了很多选项。熟悉所有这个选项很重要,这样你才能确定哪种项目组织方式是最佳的,并能理解别人开发的项目。

一开始应让代码结构尽可能简单。先尽可能在一个文件中完成所有的工作,确定一切都能正确运行后,再将类移到独立的模块中。如果你喜欢模块和文件的交互方式,可在项目开始时就尝试将类存储到模块中。先找出让你能够编写可行代码的方式,再尝试改进代码。
9.5 Python 标准库
Python 标准库是一组模块,我们安装的Python都包含它。你现在对函数和类的工作原理已有大致的了解。可以开始使用其他程序员编写好的模块了。可以使用标准库中的任何函数和类,只需在程序开头包含一条简单的import语句即可。下面来看看模块random,它在你模拟很多现实情况时很有用。

在这个模块中,一个有趣的函数是randint()。它将两个整数作为参数,并随机返回一个位于这两个整数(含)的整数。下面演示了如何生成一个位于1和6之间的随机数:
>>> from random import randint
>>> randint(1,6)
4

在模块random中,另一个有用的函数是choice()。它将一个列表或元组作为参数,并随机返回其中一个元素:
>>> from random import choice
>>> players = ['王大锤','叫兽','女神']
>>> first_up = choice(players)
>>> first_up
'王大锤'

创建于安全相关的应用程序时,请不要使用模块random,但该模块可以很好地用于创建众多有趣的项目。

注意:还可以从其他地方下载外部模块。第二部分的每个项目都需要使用外部模块,届时你将看到很多此类示例。
9.6 类编码风格
你必须熟悉有些与类相关的编码风格,在编写的程序较复杂时尤其如此。

类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块都应采用小写格式,并在单词之间加上下划线。

对于每个类,都应紧跟在类定义后面包含一个文档字符串。这种文档字符串简要地描述类的功能,并遵循编写函数的文档字符串时采用的格式约定。每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。

可使用空行来组织代码,但不要滥用。在类中,可使用一个空行来分隔方法;而在模块中,可使用两个空行来分隔类。

需要同时导入标准库中的模块和你编写的模块时,先编写导入标准模块的import语句,再添加一个空行,然后编写导入你自己编写的模块import语句。在包含多条import语句的程序中,这种做法让人更容易明白程序使用的各个模块都来自何处。

啦啦啦,恭喜你成功的跨过了“类”这座大山。熟练掌握函数和类,你就掌握了本书前半部分的精华。当然,还一知半解也没关系,后面大量的项目会让你逐渐熟悉的。

那么,下期再见,新年快乐。

拜拜~

胭惜雨

2021年02月10日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据