(八)Python类和对象

Python 语言在设计之初,就定位为一门面向对象的编程语言,“Python 中一切皆对象”就是对 Python 这门编程语言的完美诠释。

类和对象是 Python 的重要特征,相比其它面向对象语言,Python 很容易就可以创建出一个类和对象。同时,Python 也支持面向对象的三大特征:封装、继承和多态。

本章不仅会教你掌握 Python 类和对象的基本语法,还可以带你深入底层,了解 Python 面向对象的实现原理。

一、什么是面向对象,Python面向对象(一切皆对象)

读者肯定听过 Python 中“一切皆对象”的说法,但可能并不了解它的具体含义,只是在学习的时候听说 Python 是面向对象的编程语言,本节将向大家详细介绍 Python 面向对象的含义。

面向对象编程是在面向过程编程的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。

面向对象编程(Object-oriented Programming,简称 OOP),是一种封装代码的方法。其实,在前面章节的学习中,我们已经接触了封装,比如说,将乱七八糟的数据扔进列表中,这就是一种简单的封装,是数据层面的封装;把常用的代码块打包成一个函数,这也是一种封装,是语句层面的封装。

代码封装,其实就是隐藏实现功能的具体代码,仅留给用户使用的接口,就好像使用计算机,用户只需要使用键盘、鼠标就可以实现一些功能,而根本不需要知道其内部是如何工作的。

本节所讲的面向对象编程,也是一种封装的思想,不过显然比以上两种封装更先进,它可以更好地模拟真实世界里的事物(将其视为对象),并把描述特征的数据和代码块(函数)封装到一起。

打个比方,若在某游戏中设计一个乌龟的角色,应该如何来实现呢?使用面向对象的思想会更简单,可以分为如下两个方面进行描述:

  1. 从表面特征来描述,例如,绿色的、有 4 条腿、重 10 kg、有外壳等等。
  2. 从所具有的的行为来描述,例如,它会爬、会吃东西、会睡觉、会将头和四肢缩到壳里,等等。

如果将乌龟用代码来表示,则其表面特征可以用变量来表示,其行为特征可以通过建立各种函数来表示。参考代码如下所示:

class tortoise:
    bodyColor = "绿色"
    footNum = 4
    weight = 10
    hasShell = True

    #会爬
    def crawl(self):
        print("乌龟会爬")
    #会吃东西
    def eat(self):
        print("乌龟吃东西")
    #会睡觉
    def sleep(self):
        print("乌龟在睡觉")
    #会缩到壳里
    def protect(self):
        print("乌龟缩进了壳里")

注意,以上代码仅是为了演示面向对象的编程思想,具体细节后续会做详细介绍。

因此,从某种程序上,相比较只用变量或只用函数,使用面向对象的思想可以更好地模拟现实生活中的事物。

不仅如此,在 Python 中,所有的变量其实也都是对象,包括整形(int)、浮点型(float)、字符串(str)、列表(list)、元组(tuple)、字典(dict)和集合(set)。以字典(dict)为例,它包含多个函数供我们使用,例如使用 keys() 获取字典中所有的键,使用 values() 获取字典中所有的值,使用 item() 获取字典中所有的键值对,等等。

面向对象相关术语

在系统学习面向对象编程之前,初学者要了解有关面向对象的一些术语。当和其他人讨论代码的时候,或者尝试查找我们遇到的问题的解决方案时,知道正确的术语会很有帮助。

面向对象中,常用术语包括:

  • :可以理解是一个模板,通过它可以创建出无数个具体实例。比如,前面编写的 tortoise 表示的只是乌龟这个物种,通过它可以创建出无数个实例来代表各种不同特征的乌龟(这一过程又称为类的实例化)。
  • 对象类并不能直接使用,通过类创建出的实例(又称对象)才能使用。这有点像汽车图纸和汽车的关系,图纸本身(类)并不能为人们使用,通过图纸创建出的一辆辆车(对象)才能使用。
  • 属性类中的所有变量称为属性。例如,tortoise 这个类中,bodyColor、footNum、weight、hasShell 都是这个类拥有的属性。
  • 方法类中的所有函数通常称为方法。不过,和函数所有不同的是,类方法至少要包含一个 self 参数(后续会做详细介绍)。例如,tortoise 类中,crawl()、eat()、sleep()、protect() 都是这个类所拥有的方法,类方法无法单独使用,只能和类的对象一起使用。

二、Python class:定义类(入门必读)

前面已经提到,类仅仅充当图纸的作用,本身并不能直接拿来用,而只有根据图纸造出的实际物品(对象)才能直接使用。因此,Python 程序中类的使用顺序是这样的:

  1. 创建(定义)类,也就是制作图纸的过程;
  2. 创建类的实例对象(根据图纸造出实际的物品),通过实例对象实现特定的功能。

本节先教大家如何创建(定义)一个类,如何使用定义好的类将放到后续章节进行讲解。

Python类的定义

Python 中定义一个类使用 class 关键字实现,其基本语法格式如下:

class 类名:
    多个(≥0)类属性...
    多个(≥0)类方法...

注意,无论是类属性还是类方法,对于类来说,它们都不是必需的,可以有也可以没有。另外,Python 类中属性和方法所在的位置是任意的,即它们之间并没有固定的前后次序。

和变量名一样,类名本质上就是一个标识符,因此我们在给类起名字时,必须让其符合 Python 的语法。有读者可能会问,用 a、b、c 作为类的类名可以吗?从 Python 语法上讲,是完全没有问题的,但作为一名合格的程序员,我们必须还要考虑程序的可读性。

因此,在给类起名字时,最好使用能代表该类功能的单词,例如用“Student”作为学生类的类名;甚至如果必要,可以使用多个单词组合而成,例如初学者定义的第一个类的类名可以是“TheFirstDemo”。

注意,如果由单词构成类名,建议每个单词的首字母大写,其它字母小写

给类起好名字之后,其后要跟有冒号(:),表示告诉 Python 解释器,下面要开始设计类的内部功能了,也就是编写类属性和类方法。

其实,类属性指的就是包含在类中的变量;而类方法指的是包含类中的函数。换句话说,类属性和类方法其实分别是包含类中的变量和函数的别称。需要注意的一点是,同属一个类的所有类属性和类方法,要保持统一的缩进格式,通常统一缩进 4 个空格。

Python 变量和函数的使用,前面已经做了详细的介绍,这里不再重复赘述。

通过上面的分析,可以得出这样一个结论,即 Python 类是由类头(class 类名)和类体(统一缩进的变量和函数)构成。例如,下面程序定义一个 TheFirstDemo 类:

class TheFirstDemo:
    '''这是一个学习Python定义的第一个类'''
    # 下面定义了一个类属性
    add = 'http://c.biancheng.net'
    # 下面定义了一个say方法
    def say(self, content):
        print(content)

和函数一样,我们也可以为类定义说明文档,其要放到类头之后,类体之前的位置,如上面程序中第二行的字符串,就是 TheFirstDemo 这个类的说明文档。

另外分析上面的代码可以看到,我们创建了一个名为 TheFirstDemo 的类,其包含了一个名为 add 的类属性。注意,根据定义属性位置的不同,在各个类方法之外定义的变量称为类属性或类变量(如 add 属性),而在类方法中定义的属性称为实例属性(或实例变量),它们的区别和用法可阅读(六、Python类变量和实例变量(类属性和实例属性))。

同时,TheFirstDemo 类中还包含一个 say() 类方法,细心的读者可能已经看到,该方法包含两个参数,分别是 self 和 content。可以肯定的是,content 参数就只是一个普通参数,没有特殊含义,但 self 比较特殊,并不是普通的参数,它的作用会在后续章节中详细介绍。

更确切地说,say() 是一个实例方法,除此之外,Python 类中还可以定义类方法静态方法,这 3 种类方法的区别和具体用法,可阅读(六、Python类变量和实例变量(类属性和实例属性))。

事实上,我们完全可以创建一个没有任何类属性和类方法的类,换句话说,Python 允许创建空类,例如:

class Empty:
    pass

可以看到,如果一个类没有任何类属性和类方法,那么可以直接用 pass 关键字作为类体即可。但在实际应用中,很少会创建空类,因为空类没有任何实际意义。

三、Python __init__()类构造方法

在创建类时,我们可以手动添加一个 __init__() 方法,该方法是一个特殊的类实例方法,称为构造方法(或构造函数)

构造方法用于创建对象时使用,每当创建一个类的实例对象时,Python 解释器都会自动调用它。Python 类中,手动添加构造方法的语法格式如下:

def __init__(self,...):
    代码块

注意,此方法的方法名中,开头和结尾各有 2 个下划线,且中间不能有空格。Python 中很多这种以双下划线开头、双下划线结尾的方法,都具有特殊的意义,后续会一一为大家讲解。

另外,__init__() 方法可以包含多个参数,但必须包含一个名为 self 的参数,且必须作为第一个参数。也就是说,类的构造方法最少也要有一个 self 参数。例如,仍以 TheFirstDemo 类为例,添加构造方法的代码如下所示:

class TheFirstDemo:
    '''这是一个学习Python定义的第一个类'''
    #构造方法
    def __init__(self):
        print("调用构造方法")
    # 下面定义了一个类属性
    add = 'http://c.biancheng.net'
    # 下面定义了一个say方法
    def say(self, content):
        print(content)

注意,即便不手动为类添加任何构造方法,Python 也会自动为类添加一个仅包含 self 参数的构造方法。

仅包含 self 参数的 __init__() 构造方法,又称为类的默认构造方法。

在上面代码的后面,顶头(不缩进)直接添加如下代码:

zhangsan = TheFirstDemo()

这行代码的含义是创建一个名为 zhangsan 的 TheFirstDemo 类对象。运行代码可看到如下结果:

调用构造方法

显然,在创建 zhangsan 这个对象时,隐式调用了我们手动创建的 __init__() 构造方法。

不仅如此,在 __init__() 构造方法中,除了 self 参数外,还可以自定义一些参数,参数之间使用逗号“,”进行分割。例如,下面的代码在创建 __init__() 方法时,额外指定了 2 个参数:

class CLanguage:
    '''这是一个学习Python定义的一个类'''
    def __init__(self,name,add):
        print(name,"的网址为:",add)
#创建 add 对象,并传递参数给构造函数
add = CLanguage("C语言中文网","http://c.biancheng.net")

注意,由于创建对象时会调用类的构造方法,如果构造函数有多个参数时,需要手动传递参数,传递方式如代码中所示(后续部分会做详细讲解)。

运行以上代码,执行结果为:

C语言中文网 的网址为: http://c.biancheng.net

可以看到,虽然构造方法中有 self、name、add 3 个参数,但实际需要传参的仅有 name 和 add,也就是说,self 不需要手动传递参数。

关于 self 参数,第五部分会做详细介绍,这里只需要知道,在创建类对象时,无需给 self 传参即可。

四、Python类对象的创建和使用

通过前面章节的学习,我们已经学会如何定义一个类,但要想使用它,必须创建该类的对象。

创建类对象的过程,又称为类的实例化。

1、Python类的实例化

对已定义好的类进行实例化,其语法格式如下:

类名(参数)

定义类时,如果没有手动添加 __init__() 构造方法,又或者添加的 __init__() 中仅有一个 self 参数,则创建类对象时的参数可以省略不写。

例如,如下代码创建了名为 CLanguage 的类,并对其进行了实例化:

class CLanguage :
    # 下面定义了2个类变量
    name = "C语言中文网"
    add = "http://c.biancheng.net"
    def __init__(self,name,add):
        #下面定义 2 个实例变量
        self.name = name
        self.add = add
        print(name,"网址为:",add)
    # 下面定义了一个say实例方法
    def say(self, content):
        print(content)
# 将该CLanguage对象赋给clanguage变量
clanguage = CLanguage("C语言中文网","http://c.biancheng.net")

在上面的程序中,由于构造方法除 self 参数外,还包含 2 个参数,且这 2 个参数没有设置默认参数,因此在实例化类对象时,需要传入相应的 name 值和 add 值(self 参数是特殊参数,不需要手动传值,Python 会自动传给它值)。

类变量和实例变量,简单地理解,定义在各个类方法之外(包含在类中)的变量为类变量(或者类属性),定义在类方法中的变量为实例变量(或者实例属性),二者的具体区别和用法可阅读第六部分、Python类变量和实例变量

2、Python类对象的使用

定义的类只有进行实例化,也就是使用该类创建对象之后,才能得到利用。总的来说,实例化后的类对象可以执行以下操作:

  • 访问或修改类对象具有的实例变量,甚至可以添加新的实例变量或者删除已有的实例变量;
  • 调用类对象的方法,包括调用现有的方法,以及给类对象动态添加方法。

(1)类对象访问变量或方法

使用已创建好的类对象访问类中实例变量的语法格式如下:

类对象名.变量名

使用类对象调用类中方法的语法格式如下:

对象名.方法名(参数)

注意,对象名和变量名以及方法名之间用点 "." 连接。

例如,下面代码演示了如何通过 clanguage 对象调用类中的实例变量和方法:

clanguage = CLanguage("C语言中文网","http://c.biancheng.net")
#输出name和add实例变量的值
print(clanguage.name,clanguage.add)
#修改实例变量的值
clanguage.name="Python教程"
clanguage.add="http://c.biancheng.net/python"
#调用clanguage的say()方法
clanguage.say("人生苦短,我用Python")
#再次输出name和add的值
print(clanguage.name,clanguage.add)

程序运行结果为:

C语言中文网 网址为: http://c.biancheng.net
C语言中文网 http://c.biancheng.net
人生苦短,我用Python
Python教程 http://c.biancheng.net/python

(2)给类对象动态添加/删除变量

Python 支持为已创建好的对象动态增加实例变量,方法也很简单,举个例子:

# 为clanguage对象增加一个money实例变量
clanguage.money= 159.9
print(clanguage.money)

运行结果为:

159.9

可以看到,通过直接增加一个新的实例变量并为其赋值,就成功地为 clanguage 对象添加了 money 变量。

既然能动态添加,那么是否能动态删除呢?答案是肯定的,使用 del 语句即可实现,例如:

#删除新添加的 money 实例变量
del clanguage.money
#再次尝试输出 money,此时会报错
print(clanguage.money)

运行程序会发现,结果显示 AttributeError 错误:

Traceback (most recent call last):
  File "C:/Users/mengma/Desktop/1.py", line 29, in <module>
    print(clanguage.money)
AttributeError: 'CLanguage' object has no attribute 'money'

(3)给类对象动态添加方法

注意,初学者在理解下面内容之前,需明白 self 参数的含义和作用,可阅读《Python self用法》详细了解。

Python 也允许为对象动态增加方法。以本节开头的 Clanguage 类为例,由于其内部只包含一个 say() 方法,因此该类实例化出的 clanguage 对象也只包含一个 say() 方法。但其实,我们还可以为 clanguage 对象动态添加其它方法。

需要注意的一点是,为 clanguage 对象动态增加的方法,Python 不会自动将调用者自动绑定到第一个参数(即使将第一个参数命名为 self 也没用)。例如如下代码:

# 先定义一个函数
def info(self):
    print("---info函数---", self)
# 使用info对clanguage的foo方法赋值(动态绑定方法)
clanguage.foo = info
# Python不会自动将调用者绑定到第一个参数,
# 因此程序需要手动将调用者绑定为第一个参数
clanguage.foo(clanguage)  # ①

# 使用lambda表达式为clanguage对象的bar方法赋值(动态绑定方法)
clanguage.bar = lambda self: print('--lambda表达式--', self)
clanguage.bar(clanguage) # ②

上面的第 5 行和第 11 行代码分别使用函数、lambda 表达式为 clanguage 对象动态增加了方法,但对于动态增加的方法,Python 不会自动将方法调用者绑定到它们的第一个参数,因此程序必须手动为第一个参数传入参数值,如上面程序中 ① 号、② 号代码所示。

有读者可能会问,有没有不用手动给 self 传值的方法呢?通过借助 types 模块下的 MethodType 可以实现,仍以上面的 info() 函数为例:

def info(self,content):
    print("C语言中文网地址为:%s" % content)
# 导入MethodType
from types import MethodType
clanguage.info = MethodType(info, clanguage)
# 第一个参数已经绑定了,无需传入
clanguage.info("http://c.biancheng.net")

可以看到,由于使用 MethodType 包装 info() 函数时,已经将该函数的 self 参数绑定为 clanguage,因此后续再使用 info() 函数时,就不用再给 self 参数绑定值了。

五、Python self用法详解

在定义类的过程中,无论是显式创建类的构造方法,还是向类中添加实例方法,都要求将 self 参数作为方法的第一个参数。例如,定义一个 Person 类:

​class Person:
    def __init__(self): 
        print("正在执行构造方法") 
    # 定义一个study()实例方法 
    def study(self,name): 
        print(name,"正在学Python")

那么,self 到底扮演着什么样的角色呢?本节就对 self 参数做详细的介绍。

事实上,Python 只是规定,无论是构造方法还是实例方法,最少要包含一个参数,并没有规定该参数的具体名称。之所以将其命名为 self,只是程序员之间约定俗成的一种习惯,遵守这个约定,可以使我们编写的代码具有更好的可读性(大家一看到 self,就知道它的作用)。

那么,self 参数的具体作用是什么呢?打个比方,如果把类比作造房子的图纸,那么类实例化后的对象是真正可以住的房子。根据一张图纸(类),我们可以设计出成千上万的房子(类对象),每个房子长相都是类似的(都有相同的类变量和类方法),但它们都有各自的主人,那么如何对它们进行区分呢?

当然是通过 self 参数,它就相当于每个房子的门钥匙,可以保证每个房子的主人仅能进入自己的房子(每个类对象只能调用自己的类变量和类方法)。

如果你接触过其他面向对象的编程语言(例如 C++),其实 Python 类方法中的 self 参数就相当于 C++ 中的 this 指针。

也就是说,同一个类可以产生多个对象,当某个对象调用类方法时,该对象会把自身的引用作为第一个参数自动传给该方法,换句话说,Python 会自动绑定类方法的第一个参数指向调用该方法的对象。如此,Python解释器就能知道到底要操作哪个对象的方法了。

因此,程序在调用实例方法和构造方法时,不需要手动为第一个参数传值。例如,更改前面的 Person 类,如下所示:

class Person:
    def __init__(self):
        print("正在执行构造方法")
    # 定义一个study()实例方法
    def study(self):
        print(self,"正在学Python")
zhangsan = Person()
zhangsan.study()
lisi = Person()
lisi.study()

上面代码中,study() 中的 self 代表该方法的调用者,即谁调用该方法,那么 self 就代表谁。因此,该程序的运行结果为:

正在执行构造方法
<__main__.Person object at 0x0000021ADD7D21D0> 正在学Python
正在执行构造方法
<__main__.Person object at 0x0000021ADD7D2E48> 正在学Python

另外,对于构造函数中的 self 参数,其代表的是当前正在初始化的类对象。举个例子:

class Person:
    name = "xxx"
    def __init__(self,name):
        self.name=name

zhangsan = Person("zhangsan")
print(zhangsan.name)
lisi = Person("lisi")
print(lisi.name)

运行结果为:

zhangsan
lisi

可以看到,zhangsan 在进行初始化时,调用的构造函数中 self 代表的是 zhangsan;而 lisi 在进行初始化时,调用的构造函数中 self 代表的是 lisi。

值得一提的是,除了类对象可以直接调用类方法,还有一种函数调用的方式,例如:

class Person:
    def who(self):
        print(self)
zhangsan = Person()
#第一种方式
zhangsan.who()
#第二种方式
who = zhangsan.who
who()#通过 who 变量调用zhangsan对象中的 who() 方法

运行结果为:

<__main__.Person object at 0x0000025C26F021D0>
<__main__.Person object at 0x0000025C26F021D0>

显然,无论采用哪种方法,self 所表示的都是实际调用该方法的对象。

总之,无论是类中的构造函数还是普通的类方法,实际调用它们的谁,则第一个参数 self 就代表谁。

六、Python类变量和实例变量(类属性和实例属性)

无论是类属性还是类方法,都无法像普通变量或者函数那样,在类的外部直接使用它们。我们可以将类看做一个独立的空间,则类属性其实就是在类体中定义的变量,类方法是在类体中定义的函数。

前面章节提到过,在类体中,根据变量定义的位置不同,以及定义的方式不同,类属性又可细分为以下 3 种类型:

  1. 类体中、所有函数之外:此范围定义的变量,称为类属性或类变量
  2. 类体中,所有函数内部:以“self.变量名”的方式定义的变量,称为实例属性或实例变量
  3. 类体中,所有函数内部:以“变量名=变量值”的方式定义的变量,称为局部变量

不仅如此,类方法也可细分为实例方法、静态方法和类方法,第七部分会做详细介绍。

那么,类变量、实例变量以及局部变量之间有哪些不同呢?接下来就围绕此问题做详细地讲解。

1、类变量(类属性)

类变量指的是在类中,但在各个类方法外定义的变量。举个例子:

class CLanguage :
    # 下面定义了2个类变量
    name = "C语言中文网"
    add = "http://c.biancheng.net"
    # 下面定义了一个say实例方法
    def say(self, content):
        print(content)

上面程序中,name 和 add 就属于类变量。

类变量的特点是,所有类的实例化对象都同时共享类变量,也就是说,类变量在所有实例化对象中是作为公用资源存在的。类方法的调用方式有 2 种,既可以使用类名直接调用,也可以使用类的实例化对象调用。

比如,在 CLanguage 类的外部,添加如下代码:

​#使用类名直接调用 
print(CLanguage.name) 
print(CLanguage.add) 
#修改类变量的值 
CLanguage.name = "Python教程" 
CLanguage.add = "http://c.biancheng.net/python" 
print(CLanguage.name) 
print(CLanguage.add)

程序运行结果为:

C语言中文网
http://c.biancheng.net
Python教程
http://c.biancheng.net/python

可以看到,通过类名不仅可以调用类变量,也可以修改它的值。

当然,也可以使用类对象来调用所属类中的类变量(此方式不推荐使用,原因后续会讲)。例如,在 CLanguage 类的外部,添加如下代码:

clang = CLanguage()
print(clang.name)
print(clang.add)

运行程序,结果为:

C语言中文网
http://c.biancheng.net

注意,因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。例如,在 CLanguage 类体外部,添加如下代码:

print("修改前,各类对象中类变量的值:")
clang1 = CLanguage()
print(clang1.name)
print(clang1.add)
clang2 = CLanguage()
print(clang2.name)
print(clang2.add)

print("修改后,各类对象中类变量的值:")
CLanguage.name = "Python教程"
CLanguage.add = "http://c.biancheng.net/python"
print(clang1.name)
print(clang1.add)
print(clang2.name)
print(clang2.add)

程序运行结果为:

修改前,各类对象中类变量的值:
C语言中文网
http://c.biancheng.net
C语言中文网
http://c.biancheng.net
修改后,各类对象中类变量的值:
Python教程
http://c.biancheng.net/python
Python教程
http://c.biancheng.net/python

显然,通过类名修改类变量,会作用到所有的实例化对象(例如这里的 clang1 和 clang2)。

注意,通过类对象是无法修改类变量的。通过类对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量(在讲实例变量时会进行详细介绍)。

值得一提的是,除了可以通过类名访问类变量之外,还可以动态地为类和对象添加类变量。例如,在 CLanguage 类的基础上,添加以下代码:

clang = CLanguage()
CLanguage.catalog = 13
print(clang.catalog)

运行结果为:

13

2、实例变量(实例属性)

实例变量指的是在任意类方法内部,以“self.变量名”的方式定义的变量,其特点是只作用于调用方法的对象。另外,实例变量只能通过对象名访问,无法通过类名访问。

举个例子:

class CLanguage :
    def __init__(self):
        self.name = "C语言中文网"
        self.add = "http://c.biancheng.net"
    # 下面定义了一个say实例方法
    def say(self):
        self.catalog = 13

此 CLanguage 类中,name、add 以及 catalog 都是实例变量。其中,由于 __init__() 函数在创建类对象时会自动调用,而 say() 方法需要类对象手动调用。因此,CLanguage 类的类对象都会包含 name 和 add 实例变量,而只有调用了 say() 方法的类对象,才包含 catalog 实例变量。

例如,在上面代码的基础上,添加如下语句:

clang = CLanguage()
print(clang.name)
print(clang.add)
#由于 clang 对象未调用 say() 方法,因此其没有 catalog 变量,下面这行代码会报错
#print(clang.catalog)

clang2 = CLanguage()
print(clang2.name)
print(clang2.add)
#只有调用 say(),才会拥有 catalog 实例变量
clang2.say()
print(clang2.catalog)

运行结果为:

C语言中文网
http://c.biancheng.net
C语言中文网
http://c.biancheng.net
13

前面讲过,通过类对象可以访问类变量,但无法修改类变量的值。这是因为,通过类对象修改类变量的值,不是在给“类变量赋值”,而是定义新的实例变量。例如,在 CLanguage 类体外,添加如下程序:

clang = CLanguage()
#clang访问类变量
print(clang.name)
print(clang.add)

clang.name = "Python教程"
clang.add = "http://c.biancheng.net/python"
#clang实例变量的值
print(clang.name)
print(clang.add)
#类变量的值
print(CLanguage.name)
print(CLanguage.add)

程序运行结果为:

C语言中文网
http://c.biancheng.net
Python教程
http://c.biancheng.net/python
C语言中文网
http://c.biancheng.net

显然,通过类对象是无法修改类变量的值的,本质其实是给 clang 对象新添加 name 和 add 这 2 个实例变量。

类中,实例变量和类变量可以同名,但这种情况下使用类对象将无法调用类变量,它会首选实例变量,这也是不推荐“类变量使用对象名调用”的原因。

另外,和类变量不同,通过某个对象修改实例变量的值,不会影响类的其它实例化对象,更不会影响同名的类变量。例如:

class CLanguage :
    name = "xxx"  #类变量
    add = "http://"  #类变量
    def __init__(self):
        self.name = "C语言中文网"   #实例变量
        self.add = "http://c.biancheng.net"   #实例变量
    # 下面定义了一个say实例方法
    def say(self):
        self.catalog = 13  #实例变量
clang = CLanguage()
#修改 clang 对象的实例变量
clang.name = "python教程"
clang.add = "http://c.biancheng.net/python"
print(clang.name)
print(clang.add)

clang2 = CLanguage()
print(clang2.name)
print(clang2.add)
#输出类变量的值
print(CLanguage.name)
print(CLanguage.add)

程序运行结果为:

python教程
http://c.biancheng.net/python
C语言中文网
http://c.biancheng.net
xxx
http://

不仅如此,Python 只支持为特定的对象添加实例变量。例如,在之前代码的基础上,为 clang 对象添加 money 实例变量,实现代码为:

clang.money = 30
print(clang.money)

3、局部变量

除了实例变量,类方法中还可以定义局部变量。和前者不同,局部变量直接以“变量名=值”的方式进行定义,例如:

class CLanguage :
    # 下面定义了一个say实例方法
    def count(self,money):
        sale = 0.8*money
        print("优惠后的价格为:",sale)
clang = CLanguage()
clang.count(100)

通常情况下,定义局部变量是为了所在类方法功能的实现。需要注意的一点是,局部变量只能用于所在函数中,函数执行完成后,局部变量也会被销毁。

七、Python实例方法、静态方法和类方法详解(包含区别和用法)

和类属性一样,类方法也可以进行更细致的划分,具体可分为类方法、实例方法和静态方法

和类属性的分类不同,对于初学者来说,区分这 3 种类方法是非常简单的,即采用 @classmethod 修饰的方法为类方法;采用 @staticmethod 修饰的方法为静态方法;不用任何修改的方法为实例方法。

其中 @classmethod 和 @staticmethod 都是函数装饰器,后续会对其做详细介绍。

接下来就给大家详细的介绍这 3 种类方法。

1、Python类实例方法

通常情况下,在类中定义的方法默认都是实例方法。前面章节中,我们已经定义了不只一个实例方法。不仅如此,类的构造方法理论上也属于实例方法,只不过它比较特殊。

比如,下面的类中就用到了实例方法:

class CLanguage:
    #类构造方法,也属于实例方法
    def __init__(self):
        self.name = "C语言中文网"
        self.add = "http://c.biancheng.net"
    # 下面定义了一个say实例方法
    def say(self):
        print("正在调用 say() 实例方法")

实例方法最大的特点就是,它最少也要包含一个 self 参数,用于绑定调用此方法的实例对象(Python 会自动完成绑定)。实例方法通常会用类对象直接调用,例如:

clang = CLanguage()
clang.say()

运行结果:

正在调用 say() 实例方法

当然,Python 也支持使用类名调用实例方法,但此方式需要手动给 self 参数传值。例如:

#类名调用实例方法,需手动给 self 参数传值
clang = CLanguage()
CLanguage.say(clang)

运行结果为:

正在调用 say() 实例方法

有关使用类名直接调用实例方法的更多介绍,可阅读第八部分:Python类调用实例方法一节。

2、Python类方法

Python 类方法和实例方法相似,它最少也要包含一个参数,只不过类方法中通常将其命名为 cls,Python 会自动将类本身绑定给 cls 参数(注意,绑定的不是类对象)。也就是说,我们在调用类方法时,无需显式为 cls 参数传参。

和 self 一样,cls 参数的命名也不是规定的(可以随意命名),只是 Python 程序员约定俗称的习惯而已。

和实例方法最大的不同在于,类方法需要使用@classmethod修饰符进行修饰,例如:

class CLanguage:
    #类构造方法,也属于实例方法
    def __init__(self):
        self.name = "C语言中文网"
        self.add = "http://c.biancheng.net"
    #下面定义了一个类方法
    @classmethod
    def info(cls):
        print("正在调用类方法",cls)

注意,如果没有 @classmethod,则 Python 解释器会将 fly() 方法认定为实例方法,而不是类方法。

类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。例如,在上面 CLanguage 类的基础上,在该类外部添加如下代码:

#使用类名直接调用类方法
CLanguage.info()
#使用类对象调用类方法
clang = CLanguage()
clang.info()

运行结果为:

正在调用类方法 <class '__main__.CLanguage'>
正在调用类方法 <class '__main__.CLanguage'>

3、Python类静态方法

静态方法,其实就是我们学过的函数,和函数唯一的区别是,静态方法定义在类这个空间(类命名空间)中,而函数则定义在程序所在的空间(全局命名空间)中

静态方法没有类似 self、cls 这样的特殊参数,因此 Python 解释器不会对它包含的参数做任何类或对象的绑定。也正因为如此,类的静态方法中无法调用任何类属性和类方法。

静态方法需要使@staticmethod修饰,例如:

class CLanguage:
    @staticmethod
    def info(name,add):
        print(name,add)

静态方法的调用,既可以使用类名,也可以使用类对象,例如:

#使用类名直接调用静态方法
CLanguage.info("C语言中文网","http://c.biancheng.net")
#使用类对象调用静态方法
clang = CLanguage()
clang.info("Python教程","http://c.biancheng.net/python")

运行结果为:

C语言中文网 http://c.biancheng.net
Python教程 http://c.biancheng.net/python

在实际编程中,几乎不会用到类方法和静态方法,因为我们完全可以使用函数代替它们实现想要的功能,但在一些特殊的场景中(例如工厂模式中),使用类方法和静态方法也是很不错的选择。

八、Python类调用实例方法

通过前面的学习,类方法大体分为 3 类,分别是类方法、实例方法和静态方法,其中实例方法用的是最多的。我们知道,实例方法的调用方式其实有 2 种,既可以采用类对象调用,也可以直接通过类名调用。

通常情况下,我们习惯使用类对象调用类中的实例方法。但如果想用类调用实例方法,不能像如下这样:

​class CLanguage: 
    def info(self): 
        print("我正在学 Python")
#通过类名直接调用实例方法 
CLanguage.info()

运行上面代码,程序会报出如下错误:

Traceback (most recent call last):
  File "D:\python3.6\demo.py", line 5, in <module>
    CLanguage.info()
TypeError: info() missing 1 required positional argument: 'self'

其中,最后一行报错信息提示我们,调用 info() 类方式时缺少给 self 参数传参。这意味着,和使用类对象调用实例方法不同,通过类名直接调用实例方法时,Python 并不会自动给 self 参数传值。

读者想想也应该明白,self 参数需要的是方法的实际调用者(是类对象),而这里只提供了类名,当然无法自动传值。

因此,如果想通过类名直接调用实例方法,就必须手动为 self 参数传值。例如修改上面的代码为:

class CLanguage:
    def info(self):
        print("我正在学 Python")
clang = CLanguage()
#通过类名直接调用实例方法
CLanguage.info(clang)

再次运行程序,结果为:

我正在学 Python

可以看到,通过手动将 clang 这个类对象传给了 self 参数,使得程序得以正确执行。实际上,这里调用实例方法的形式完全是等价于 clang.info()

值得一提的是,上面的报错信息只是让我们手动为 self 参数传值,但并没有规定必须传一个该类的对象,其实完全可以任意传入一个参数,例如:

class CLanguage:
    def info(self):
        print(self,"正在学 Python")
#通过类名直接调用实例方法
CLanguage.info("zhangsan")

运行结果为:

zhangsan 正在学 Python

可以看到,"zhangsan" 这个字符串传给了 info() 方法的 self 参数。显然,无论是 info() 方法中使用 self 参数调用其它类方法,还是使用 self 参数定义新的实例变量,胡乱的给 self 参数传参都将会导致程序运行崩溃。

总的来说,Python 中允许使用类名直接调用实例方法,但必须手动为该方法的第一个 self 参数传递参数,这种调用方法的方式被称为“非绑定方法”。

用类的实例对象访问类成员的方式称为绑定方法,而用类名调用类成员的方式称为非绑定方法

九、浅谈Python类命名空间  为什么说Python类是独立的命名空间?

前面已经不只一次提到,Python 类体中的代码位于独立的命名空间(称为类命名空间)中。换句话说,所有用 class 关键字修饰的代码块,都可以看做是位于独立的命名空间中。

和类命名空间相对的是全局命名空间,即整个 Python 程序默认都位于全局命名空间中。而类体则独立位于类命名空间中。

我们一开始学习类时就已经提到,类其实是由多个类属性和类方法构成,而类属性其实就是定义在类这个独立空间中的变量,而类方法其实就是定义在类空间中的函数,和定义在全局命名空间中的变量和函数相比,并没有明显的不同。

举个例子:

#全局空间定义变量
name = "C语言中文网"
add = "http://c.biancheng.net"
# 全局空间定义函数
def say ():
    print("我在学习Python--全局")

class CLanguage:
    # 定义CLanguage空间的say函数
    def say():
        print("我在学习Python--CLanguage独立空间")
    # 定义CLanguage空间的catalog变量
    name = "C语言中文网"
    add = "http://c.biancheng.net"
#调用全局的变量和函数
print(name,add)
say()

#调用类独立空间的变量和函数
print(CLanguage.name,CLanguage.add)
CLanguage.say()

运行结果为:

C语言中文网 http://c.biancheng.net
我在学习Python--全局
C语言中文网 http://c.biancheng.net
我在学习Python--CLanguage独立空间

可以看到,相比位于全局命名空间的变量和函数,位于类命名空间中的变量和函数在使用时,只需要标注 CLanguage 前缀即可。

甚至,Python 还允许直接在类命名空间中编写可执行程序(例如输出语句、分支语句等),例如:

class CLanguage:
    #直接编写可执行代码
    print('正在执行 CLanguage 类空间中的代码')
    for i in range(5):
        print(i)

运行结果为:

正在执行 CLanguage 类空间中的代码
0
1
2
3
4

显然,上面这些位于类命名空间的可执行程序,和位于全局命令空间相比,并没有什么不同。

但需要注意的一点是,当使用类对象调用类方法时,在传参方面是和外界的函数有区别的,因为 Python 会自动为第一个参数绑定方法的调用者,而位于全局空间中的函数,则必须显式为第一个参数传递参数。

十、什么是描述符,Python描述符详解

Python 中,通过使用描述符,可以让程序员在引用一个对象属性时自定义要完成的工作。

本质上看,描述符就是一个类,只不过它定义了另一个类中属性的访问方式。换句话说,一个类可以将属性管理全权委托给描述符类

描述符是 Python 中复杂属性访问的基础,它在内部被用于实现 property、方法、类方法、静态方法和 super 类型。

描述符类基于以下 3 个特殊方法,换句话说,这 3 个方法组成了描述符协议:

  • __set__(self, obj, type=None):在设置属性时将调用这一方法(本节后续用 setter 表示);
  • __get__(self, obj, value):在读取属性时将调用这一方法(本节后续用 getter 表示);
  • __delete__(self, obj):对属性调用 del 时将调用这一方法。

其中,实现了 setter 和 getter 方法的描述符类被称为数据描述符;反之,如果只实现了 getter 方法,则称为非数据描述符

实际上,在每次查找属性时,描述符协议中的方法都由类对象的特殊方法 __getattribute__() 调用(注意不要和 __getattr__() 弄混)。也就是说,每次使用类对象.属性(或者 getattr(类对象,属性值))的调用方式时,都会隐式地调用 __getattribute__(),它会按照下列顺序查找该属性:

  1. 验证该属性是否为类实例对象的数据描述符;
  2. 如果不是,就查看该属性是否能在类实例对象的 __dict__ 中找到;
  3. 最后,查看该属性是否为类实例对象的非数据描述符。

为了表达清楚,这里举个例子:

#描述符类
class revealAccess:
    def __init__(self, initval = None, name = 'var'):
        self.val = initval
        self.name = name

    def __get__(self, obj, objtype):
        print("Retrieving",self.name)
        return self.val

    def __set__(self, obj, val):
        print("updating",self.name)
        self.val = val
class myClass:
    x = revealAccess(10,'var "x"')
    y = 5
m = myClass()
print(m.x)
m.x = 20
print(m.x)
print(m.y)

运行结果为:

Retrieving var "x"
10
updating var "x"
Retrieving var "x"
20
5

从这个例子可以看到,如果一个类的某个属性有数据描述符,那么每次查找这个属性时,都会调用描述符的 __get__() 方法,并返回它的值;同样,每次在对该属性赋值时,也会调用 __set__() 方法。

注意,虽然上面例子中没有使用 __del__() 方法,但也很容易理解,当每次使用 del 类对象.属性(或者 delattr(类对象,属性))语句时,都会调用该方法。

除了使用描述符类自定义类属性被调用时做的操作外,还可以使用 property() 函数或者 @property 装饰器,它们会在后续章节做详细介绍。

十一、Python property()函数:定义属性

前面章节中,我们一直在用“类对象.属性”的方式访问类中定义的属性,其实这种做法是欠妥的,因为它破坏了类的封装原则。正常情况下,类包含的属性应该是隐藏的,只允许通过类提供的方法来间接实现对类属性的访问和操作。

因此,在不破坏类封装原则的基础上,为了能够有效操作类中的属性,类中应包含读(或写)类属性的多个 getter(或 setter)方法,这样就可以通过“类对象.方法(参数)”的方式操作属性,例如:

​class CLanguage: 
    #构造函数 
    def __init__(self,name): 
        self.name = name 
    #设置 name 属性值的函数 
    def setname(self,name): 
        self.name = name 
    #访问nema属性值的函数 
    def getname(self): 
        return self.name 
    #删除name属性值的函数 
    def delname(self): 
        self.name="xxx" 
clang = CLanguage("C语言中文网")     
#获取name属性值 
print(clang.getname()) 
#设置name属性值 
clang.setname("Python教程") 
print(clang.getname()) 
#删除name属性值 
clang.delname() 
print(clang.getname())​

运行结果为:

C语言中文网
Python教程
xxx

可能有读者觉得,这种操作类属性的方式比较麻烦,更习惯使用“类对象.属性”这种方式。

庆幸的是,Python 中提供了 property() 函数,可以实现在不破坏类封装原则的前提下,让开发者依旧使用“类对象.属性”的方式操作类中的属性。

property() 函数的基本使用格式如下:

属性名=property(fget=None, fset=None, fdel=None, doc=None)

其中,fget 参数用于指定获取该属性值的类方法,fset 参数用于指定设置该属性值的方法,fdel 参数用于指定删除该属性值的方法,最后的 doc 是一个文档字符串,用于说明此函数的作用。

注意,在使用 property() 函数时,以上 4 个参数可以仅指定第 1 个、或者前 2 个、或者前 3 个,当前也可以全部指定。也就是说,property() 函数中参数的指定并不是完全随意的。

例如,修改上面的程序,为 name 属性配置 property() 函数:

class CLanguage:
    #构造函数
    def __init__(self,n):
        self.__name = n
    #设置 name 属性值的函数
    def setname(self,n):
        self.__name = n
    #访问nema属性值的函数
    def getname(self):
        return self.__name
    #删除name属性值的函数
    def delname(self):
        self.__name="xxx"
    #为name 属性配置 property() 函数
    name = property(getname, setname, delname, '指明出处')
#调取说明文档的 2 种方式
#print(CLanguage.name.__doc__)
help(CLanguage.name)
clang = CLanguage("C语言中文网")
#调用 getname() 方法
print(clang.name)
#调用 setname() 方法
clang.name="Python教程"
print(clang.name)
#调用 delname() 方法
del clang.name
print(clang.name)

运行结果为:

Help on property:

    指明出处

C语言中文网
Python教程
xxx

注意,在此程序中,由于 getname() 方法中需要返回 name 属性,如果使用 self.name 的话,其本身又被调用 getname(),这将会先入无限死循环。为了避免这种情况的出现,程序中的 name 属性必须设置为私有属性,即使用 __name(前面有 2 个下划线)。

有关类属性和类方法的属性设置(分为共有属性、保护属性、私有属性),后续会做详细介绍。

当然,property() 函数也可以少传入几个参数。以上面的程序为例,我们可以修改 property() 函数如下所示

name = property(getname, setname)

这意味着,name 是一个可读写的属性,但不能删除,因为 property() 函数中并没有为 name 配置用于函数该属性的方法。也就是说,即便 CLanguage 类中设计有 delname() 函数,这种情况下也不能用来删除 name 属性。 

同理,还可以像如下这样使用 property() 函数:

name = property(getname)    # name 属性可读,不可写,也不能删除
name = property(getname, setname,delname)    #name属性可读、可写、也可删除,就是没有说明文档

十二、Python @property装饰器详解

既要保护类的封装特性,又要让开发者可以使用“对象.属性”的方式操作操作类属性,除了使用 property() 函数,Python 还提供了 @property 装饰器。通过 @property 装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对“()”小括号。

@property 的语法格式如下:

@property
def 方法名(self)
    代码块

例如,定义一个矩形类,并定义用 @property 修饰的方法操作类中的 area 私有属性,代码如下:

class Rect:
    def __init__(self,area):
        self.__area = area
    @property
    def area(self):
        return self.__area
rect = Rect(30)
#直接通过方法名来访问 area 方法
print("矩形的面积是:",rect.area)

运行结果为:

矩形的面积为: 30

上面程序中,使用 @property 修饰了 area() 方法,这样就使得该方法变成了 area 属性的 getter 方法。需要注意的是,如果类中只包含该方法,那么 area 属性将是一个只读属性。

也就是说,在使用 Rect 类时,无法对 area 属性重新赋值,即运行如下代码会报错:

rect.area = 90
print("修改后的面积:",rect.area)

运行结果为:

Traceback (most recent call last):
  File "C:\Users\mengma\Desktop\1.py", line 10, in <module>
    rect.area = 90
AttributeError: can't set attribute

而要想实现修改 area 属性的值,还需要为 area 属性添加 setter 方法,就需要用到 setter 装饰器,它的语法格式如下:

@方法名.setter
def 方法名(self, value):
    代码块

例如,为 Rect 类中的 area 方法添加 setter 方法,代码如下:

@area.setter
def area(self, value):
    self.__area = value

再次运行如下代码:

rect.area = 90
print("修改后的面积:",rect.area)

运行结果为:

修改后的面积: 90

这样,area 属性就有了 getter 和 setter 方法,该属性就变成了具有读写功能的属性。

除此之外,还可以使用 deleter 装饰器来删除指定属性,其语法格式为:

@方法名.deleter
def 方法名(self):
    代码块

例如,在 Rect 类中,给 area() 方法添加 deleter 方法,实现代码如下:

@area.deleter
def area(self):
    self.__area = 0

然后运行如下代码:

del rect.area
    print("删除后的area值为:",rect.area)

运行结果为:

删除后的area值为: 0

十三、Python封装机制及实现方法

不光是 Python,大多数面向对象编程语言(诸如 C++Java 等)都具备 3 个典型特征,即封装、继承和多态。其中,本节重点讲解 Python 类的封装特性,继承和多态会在后续详细讲解。

简单的理解封装(Encapsulation),即在设计类时,刻意地将一些属性和方法隐藏在类的内部,这样在使用此类时,将无法直接以“类对象.属性名”(或者“类对象.方法名(参数)”)的形式调用这些属性(或方法),而只能用未隐藏的类方法间接操作这些隐藏的属性和方法。

就好比使用电脑,我们只需要学会如何使用键盘和鼠标就可以了,不用关心内部是怎么实现的,因为那是生产和设计人员该操心的。

注意,封装绝不是将类中所有的方法都隐藏起来,一定要留一些像键盘、鼠标这样可供外界使用的类方法。

那么,类为什么要进行封装,这样做有什么好处呢?

首先,封装机制保证了类内部数据结构的完整性,因为使用类的用户无法直接看到类中的数据结构,只能使用类允许公开的数据,很好地避免了外部对内部数据的影响,提高了程序的可维护性。

除此之外,对一个类实现良好的封装,用户只能借助暴露出来的类方法来访问数据,我们只需要在这些暴露的方法中加入适当的控制逻辑,即可轻松实现用户对类中属性或方法的不合理操作。

并且,对类进行良好的封装,还可以提高代码的复用性。

Python 类如何进行封装?

和其它面向对象的编程语言(如 C++、Java)不同,Python 类中的变量和函数,不是公有的(类似 public 属性),就是私有的(类似 private),这 2 种属性的区别如下:

  • public:公有属性的类变量和类函数,在类的外部、类内部以及子类(后续讲继承特性时会做详细介绍)中,都可以正常访问;
  • private:私有属性的类变量和类函数,只能在本类内部使用,类的外部以及子类都无法使用。

但是,Python 并没有提供 public、private 这些修饰符。为了实现类的封装,Python 采取了下面的方法:

  • 默认情况下,Python 类中的变量和方法都是公有(public)的,它们的名称前都没有下划线(_);
  • 如果类中的变量和函数,其名称以双下划线“__”开头,则该变量(函数)为私有变量(私有函数),其属性等同于 private

除此之外,还可以定义以单下划线“_”开头的类属性或者类方法(例如 _name、_display(self)),这种类属性和类方法通常被视为私有属性和私有方法,虽然它们也能通过类对象正常访问,但这是一种约定俗称的用法,初学者一定要遵守。

注意,Python 类中还有以双下划线开头和结尾的类方法(例如类的构造函数__init__(self)),这些都是 Python 内部定义的,用于 Python 内部调用。我们自己定义类属性或者类方法时,不要使用这种格式。

例如,如下程序示范了 Python 的封装机制:

class CLanguage :
    def setname(self, name):
        if len(name) < 3:
            raise ValueError('名称长度必须大于3!')
        self.__name = name

    def getname(self):
        return self.__name
    #为 name 配置 setter 和 getter 方法
    name = property(getname, setname)
    def setadd(self, add):
        if add.startswith("http://"):
            self.__add = add
        else:
            raise ValueError('地址必须以 http:// 开头') 

    def getadd(self):
        return self.__add
   
    #为 add 配置 setter 和 getter 方法
    add = property(getadd, setadd)

    #定义个私有方法
    def __display(self):
        print(self.__name,self.__add)

clang = CLanguage()
clang.name = "C语言中文网"
clang.add = "http://c.biancheng.net"
print(clang.name)
print(clang.add)

程序运行结果为:

C语言中文网
http://c.biancheng.net

上面程序中,CLanguage 将 name 和 add 属性都隐藏了起来,但同时也提供了可操作它们的“窗口”,也就是各自的 setter 和 getter 方法,这些方法都是公有(public)的。

不仅如此,以 add 属性的 setadd() 方法为例,通过在该方法内部添加控制逻辑,即通过调用 startswith() 方法,控制用户输入的地址必须以“http://”开头,否则程序将会执行 raise 语句抛出 ValueError 异常。

有关 raise 的具体用法,后续章节会做详细的讲解,这里可简单理解成,如果用户输入不规范,程序将会报错。

通过此程序的运行逻辑不难看出,通过对 CLanguage 类进行良好的封装,使得用户仅能通过暴露的 setter() 和 getter() 方法操作 name 和 add 属性,而通过对 setname() 和 setadd() 方法进行适当的设计,可以避免用户对类中属性的不合理操作,从而提高了类的可维护性和安全性。

细心的读者可能还发现,CLanguage 类中还有一个 __display() 方法,由于该类方法为私有(private)方法,且该类没有提供操作该私有方法的“窗口”,因此我们无法在类的外部使用它。换句话说,如下调用 __display() 方法是不可行的:

#尝试调用私有的 display() 方法
clang.__display()

这会导致如下错误:

Traceback (most recent call last):
  File "D:\python3.6\1.py", line 33, in <module>
    clang.__display()
AttributeError: 'CLanguage' object has no attribute '__display'

那么,类似 __display() 这样的类方法,就没有办法调用了吗?下一节将会进行描述。

十四、Python封装底层实现原理详解(通俗易懂)

事实上,Python 封装特性的实现纯属“投机取巧”,之所以类对象无法直接调用以双下划线开头命名的类属性和类方法,是因为其底层实现时,Python 偷偷改变了它们的名称。

前面章节中,我们定义了一个 CLanguage 类,定义如下:

class CLanguage :
    def setname(self, name):
        if len(name) < 3:
            raise ValueError('名称长度必须大于3!')
        self.__name = name

    def getname(self):
        return self.__name
    #为 name 配置 setter 和 getter 方法
    name = property(getname, setname)
    def setadd(self, add):
        if add.startswith("http://"):
            self.__add = add
        else:
            raise ValueError('地址必须以 http:// 开头')

    def getadd(self):
        return self.__add
  
    #为 add 配置 setter 和 getter 方法
    add = property(getadd, setadd)

    #定义个私有方法
    def __display(self):
        print(self.__name,self.__add)

注意,在这个类中,有一个 __display() 方法,由于其是私有方法,且该类没有提供任何调用该方法的“接口”,因此在目前看来,此方法根本无法在类外部调用。也就是说,如下调用 __display() 方法是不可行的:

clang = CLanguage()
#尝试调用私有的 display() 方法
clang.__display()

这会导致如下错误:

Traceback (most recent call last):
  File "D:\python3.6\1.py", line 33, in <module>
    clang.__display()
AttributeError: 'CLanguage' object has no attribute '__display'

那么,是不是类似 display() 这种的私有方法,真的没有方法调用吗?如果你深入了解 Python 封装机制的底层实现原理,就可以调用它。

事实上,对于以双下划线开头命名的类属性或类方法,Python 在底层实现时,将它们的名称都偷偷改成了 "_类名__属性(方法)名" 的格式。

就以 CLanguage 类中的 __display() 为例,Python 在底层将其方法名偷偷改成了“_CLanguage__display()”。例如,在 CLanguage 类的基础上,执行如下代码:

clang = CLanguage()
#调用name的setname()方法
clang.name = "C语言中文网"
#调用add的setadd()方法
clang.add = "http://c.biancheng.net"
#直接调用隐藏的display()方法
clang._CLanguage__display()

输出结果为:

C语言中文网 http://c.biancheng.net

不仅如此,那些原本我们认为是私有的类属性(例如 __name 和 __add),其底层的名称也改成了“_类名__属性名”的这种格式。例如:

clang = CLanguage()
clang.name = "C语言中文网"
clang.add = "http://c.biancheng.net"
#直接调用 name 和 add 私有属性
print(clang._CLanguage__name,clang._CLanguage__add)

运行结果为:

C语言中文网 http://c.biancheng.net

甚至于,我们还可以通过这种方式修改 clang 对象的私有属性,例如:

clang._CLanguage__name = "Python教程"
clang._CLanguage__add = "http://c.biancheng.net/python"
print(clang._CLanguage__name,clang._CLanguage__add)

输出结果为:

Python教程 http://c.biancheng.net/python

总结

Python 类中所有的属性和方法,都是公有(public)属性,如果希望 Python 底层修改类属性或者类方法的名称,以此将它们隐藏起来,只需将它们的名称前添加双下划线(“__”)即可。

十五、Python继承机制及其使用

Python 类的封装、继承、多态 3 大特性,前面部分已经详细介绍了 Python 类的封装,本节继续讲解 Python 类的继承机制。

继承机制经常用于创建和现有类功能类似的新类,又或是新类只需要在现有类基础上添加一些成员(属性和方法),但又不想直接将现有类代码复制给新类。也就是说,通过使用继承这种机制,可以轻松实现类的重复使用

举个例子,假设现有一个 Shape 类,该类的 draw() 方法可以在屏幕上画出指定的形状,现在需要创建一个 Form 类,要求此类不但可以在屏幕上画出指定的形状,还可以计算出所画形状的面积。要创建这样的类,笨方法是将 draw() 方法直接复制到新类中,并添加计算面积的方法。实现代码如下所示:

class Shape:
    def draw(self,content):
        print("画",content)
class Form:
    def draw(self,content):
        print("画",content)
    def area(self):
        #....
        print("此图形的面积为...")

当然还有更简单的方法,就是使用类的继承机制。实现方法为:让 Form 类继承 Shape 类,这样当 Form 类对象调用 draw() 方法时,Python 解释器会先去 Form 中找以 draw 为名的方法,如果找不到,它还会自动去 Shape 类中找。如此,我们只需在 Form 类中添加计算面积的方法即可,示例代码如下:

class Shape:
    def draw(self,content):
        print("画",content)
class Form(Shape):
    def area(self):
        #....
        print("此图形的面积为...")

上面代码中,class Form(Shape) 就表示 Form 继承 Shape。

Python 中,实现继承的类称为子类,被继承的类称为父类(也可称为基类、超类)。因此在上面这个样例中,Form 是子类,Shape 是父类。

子类继承父类时,只需在定义子类时,将父类(可以是多个)放在子类之后的圆括号里即可。语法格式如下:

class 类名(父类1, 父类2, ...):
    #类定义部分

注意,如果该类没有显式指定继承自哪个类,则默认继承 object 类(object 类是 Python 中所有类的父类,即要么是直接父类,要么是间接父类)。另外,Python 的继承是多继承机制(和 C++ 一样),即一个子类可以同时拥有多个直接父类。

注意,有读者可能还听说过“派生”这个词汇,它和继承是一个意思,只是观察角度不同而已。换句话话,继承是相对子类来说的,即子类继承自父类;而派生是相对于父类来说的,即父类派生出子类。

了解了继承机制的含义和语法之后,下面代码演示了继承机制的用法:

class People:
    def say(self):
        print("我是一个人,名字是:",self.name)
class Animal:
    def display(self):
        print("人也是高级动物")
#同时继承 People 和 Animal 类
#其同时拥有 name 属性、say() 和 display() 方法
class Person(People, Animal):
    pass
zhangsan = Person()
zhangsan.name = "张三"
zhangsan.say()
zhangsan.display()

运行结果,结果为:

我是一个人,名字是: 张三
人也是高级动物

可以看到,虽然 Person 类为空类,但由于其继承自 People 和 Animal 这 2 个类,因此实际上 Person 并不空,它同时拥有这 2 个类所有的属性和方法。

没错,子类拥有父类所有的属性和方法,即便该属性或方法是私有(private)的。至于为什么,可阅读《Python封装实现原理》一节。

关于Python的多继承

事实上,大部分面向对象的编程语言,都只支持单继承,即子类有且只能有一个父类。而 Python 却支持多继承(C++也支持多继承)。
和单继承相比,多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 JavaC#PHP 等干脆取消了多继承。

使用多继承经常需要面临的问题是,多个父类中包含同名的类方法。对于这种情况,Python 的处置措施是:根据子类继承多个父类时这些父类的前后次序决定,即排在前面父类中的类方法会覆盖排在后面父类中的同名类方法。

举个例子:

class People:
    def __init__(self):
        self.name = People
    def say(self):
        print("People类",self.name)

class Animal:
    def __init__(self):
        self.name = Animal
    def say(self):
        print("Animal类",self.name)
#People中的 name 属性和 say() 会遮蔽 Animal 类中的
class Person(People, Animal):
    pass

zhangsan = Person()
zhangsan.name = "张三"
zhangsan.say()

程序运行结果为:

People类 张三

可以看到,当 Person 同时继承 People 类和 Animal 类时,People 类在前,因此如果 People 和 Animal 拥有同名的类方法,实际调用的是 People 类中的。

虽然 Python 在语法上支持多继承,但逼不得已,建议大家不要使用多继承。

十六、Python MRO方法解析顺序详解 在子类中,Python到底是如何找到父类的属性和方法的

我们知道,Python 类是支持(多)继承的,一个类的方法和属性可能定义在当前类,也可能定义在基类。针对这种情况,当调用类方法或类属性时,就需要对当前类以及它的基类进行搜索,以确定方法或属性的位置,而搜索的顺序就称为方法解析顺序。

方法解析顺序(Method Resolution Order),简称 MRO。对于只支持单继承的编程语言来说,MRO 很简单,就是从当前类开始,逐个搜索它的父类;而对于 Python,它支持多继承,MRO 相对会复杂一些。

实际上,Python 发展至今,经历了以下 3 种 MRO 算法,分别是:

  1. 从左往右,采用深度优先搜索(DFS)的算法,称为旧式类的 MRO;
  2. 自 Python 2.2 版本开始,新式类在采用深度优先搜索算法的基础上,对其做了优化;
  3. 自 Python 2.3 版本,对新式类采用了 C3 算法。由于 Python 3.x 仅支持新式类,所以该版本只使用 C3 算法。

有关旧式类和新式类的讲解,可阅读《Python super()使用注意事项》一文。

有读者可能会好奇,为什么 MRO 弃用了前两种算法,而选择最终的 C3 算法呢?原因很简单,前 2 种算法都存在一定的问题。

1、旧式类MRO算法

在使用旧式类的 MRO 算法时,以下面代码为例(程序一):

class A:
    def method(self):
      print("CommonA")
class B(A):
    pass
class C(A):
    def method(self):
      print("CommonC")
class D(B, C):
    pass
print(D().method())

通过分析可以想到,此程序中的 4 个类是一个“菱形”继承的关系,当使用 D 类对象访问 method() 方法时,根据深度优先算法,搜索顺序为 D->B->A->C->A

旧式类的 MRO 可通过使用 inspect 模块中的 getmro(类名) 函数直接获取。例如 inspect.getmro(D) 表示获取 D 类的 MRO。

因此,使用旧式类的 MRO 算法最先搜索得到的是基类 A 中的 method() 方法,即在 Python 2.x 版本中,此程序的运行结果为:

CommonA

但是,这个结果显然不是想要的,我们希望搜索到的是 C 类中的 method() 方法。

2、新式类MRO算法

为解决旧式类 MRO 算法存在的问题,Python 2.2 版本推出了新的计算新式类 MRO 的方法,它仍然采用从左至右的深度优先遍历,但是如果遍历中出现重复的类,只保留最后一个。

仍以上面程序为例,通过深度优先遍历,其搜索顺序为 D->B->A->C->A,由于此顺序中有 2 个 A,因此仅保留后一个,简化后得到最终的搜索顺序为 D->B->C->A

新式类可以直接通过 类名.__mro__ 的方式获取类的 MRO,也可以通过 类名.mro() 的形式,旧式类是没有 __mro__ 属性和 mro() 方法的。

可以看到,这种 MRO 方式已经能够解决“菱形”继承的问题,但是可能会违反单调性原则。所谓单调性原则,是指在类存在多继承时,子类不能改变基类的 MRO 搜索顺序,否则会导致程序发生异常。

例如,分析如下程序(程序二):

class X(object):
  pass
class Y(object):
  pass
class A(X,Y):
  pass
class B(Y,X):
  pass
class C(A, B):
  pass

通过进行深度遍历,得到搜索顺序为 C->A->X->object->Y->object->B->Y->object->X->object,再进行简化(相同取后者),得到 C->A->B->Y->X->object

下面来分析这样的搜索顺序是否合理,我们来看下各个类中的 MRO:

  • 对于 A,其搜索顺序为 A->X->Y->object;
  • 对于 B,其搜索顺序为 B->Y->X->object;
  • 对于 C,其搜索顺序为 C->A->B->X->Y->object。

可以看到,B 和 C 中,X、Y 的搜索顺序是相反的,也就是说,当 B 被继承时,它本身的搜索顺序发生了改变,这违反了单调性原则。

3、MRO C3

为解决 Python 2.2 中 MRO 所存在的问题,Python 2.3 采用了 C3 方法来确定方法解析顺序。多数情况下,如果某人提到 Python 中的 MRO,指的都是 C3 算法。

在 Python 2.3 及后续版本中,运行程序一,得到如下结果:

CommonC

运行程序二,会产生如下异常:

Traceback (most recent call last):
  File "C:\Users\mengma\Desktop\demo.py", line 9, in <module>
    class C(A, B):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases X, Y

由此可见,C3 可以有效解决前面 2 种算法的问题。那么,C3 算法是怎样实现的呢?

以程序一为主,C3 把各个类的 MRO 记为如下等式:

  • 类 A:L[A] = merge(A , object)
  • 类 B:L[B] = [B] + merge(L[A] , [A])
  • 类 C:L[C] = [C] + merge(L[A] , [A])
  • 类 D:L[D] = [D] + merge(L[B] , L[C] , [B] , [C])

注意,以类 A 等式为例,其中 merge 包含的 A 称为 L[A] 的头,剩余元素(这里仅有一个 object)称为尾。

这里的关键在于 merge,它的运算方式如下:

  1. 检查第一个列表的头元素(如 L[A] 的头),记作 H。
  2. 若 H 未出现在 merge 中其它列表的尾部,则将其输出,并将其从所有列表中删除,然后回到步骤 1;否则,取出下一个列表的头部记作 H,继续该步骤。

重复上述步骤,直至列表为空或者不能再找出可以输出的元素。如果是前一种情况,则算法结束;如果是后一种情况,Python 会抛出异常。

由此,可以计算出类 B 的 MRO,其计算过程为:

L[B] = [B] + merge(L[A],[A])
     = [B] + merge([A,object],[A])
     = [B,A] + merge([object])
     = [B,A,object]

同理,其他类的 MRO 也可以轻松计算得出。这里不再赘述,有兴趣的读者可自行推算。

十七、Python父类方法重写(入门必读)

前面讲过在 Python 中,子类继承了父类,那么子类就拥有了父类所有的类属性和类方法。通常情况下,子类会在此基础上,扩展一些新的类属性和类方法。

但凡事都有例外,我们可能会遇到这样一种情况,即子类从父类继承得来的类方法中,大部分是适合子类使用的,但有个别的类方法,并不能直接照搬父类的,如果不对这部分类方法进行修改,子类对象无法使用。针对这种情况,我们就需要在子类中重复父类的方法。

举个例子,鸟通常是有翅膀的,也会飞,因此我们可以像如下这样定义个和鸟相关的类:

class Bird:
    #鸟有翅膀
    def isWing(self):
        print("鸟有翅膀")
    #鸟会飞
    def fly(self):
        print("鸟会飞")

但是,对于鸵鸟来说,它虽然也属于鸟类,也有翅膀,但是它只会奔跑,并不会飞。针对这种情况,可以这样定义鸵鸟类:

class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("鸵鸟不会飞")

可以看到,因为 Ostrich 继承自 Bird,因此 Ostrich 类拥有 Bird 类的 isWing() 和 fly() 方法。其中,isWing() 方法同样适合 Ostrich,但 fly() 明显不适合,因此我们在 Ostrich 类中对 fly() 方法进行重写。

重写,有时又称覆盖,是一个意思,指的是对类中已有方法的内部实现进行修改。

在上面 2 段代码的基础上,添加如下代码并运行:

class Bird:
    #鸟有翅膀
    def isWing(self):
        print("鸟有翅膀")
    #鸟会飞
    def fly(self):
        print("鸟会飞")
class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("鸵鸟不会飞")

# 创建Ostrich对象
ostrich = Ostrich()
#调用 Ostrich 类中重写的 fly() 类方法
ostrich.fly()

运行结果为:

鸵鸟不会飞

显然,ostrich 调用的是重写之后的 fly() 类方法。

如何调用被重写的方法

事实上,如果我们在子类中重写了从父类继承来的类方法,那么当在类的外部通过子类对象调用该方法时,Python 总是会执行子类中重写的方法。

这就产生一个新的问题,即如果想调用父类中被重写的这个方法,该怎么办呢?

很简单,前面讲过,Python 中的类可以看做是一个独立空间,而类方法其实就是出于该空间中的一个函数。而如果想要全局空间中,调用类空间中的函数,只需要在调用该函数时备注类名即可。举个例子:

class Bird:
    #鸟有翅膀
    def isWing(self):
        print("鸟有翅膀")
    #鸟会飞
    def fly(self):
        print("鸟会飞")
class Ostrich(Bird):
    # 重写Bird类的fly()方法
    def fly(self):
        print("鸵鸟不会飞")

# 创建Ostrich对象
ostrich = Ostrich()
#调用 Bird 类中的 fly() 方法
Bird.fly(ostrich)

程序运行结果为:

鸟会飞

此程序中,需要大家注意的一点是,使用类名调用其类方法,Python 不会为该方法的第一个 self 参数自定绑定值,因此采用这种调用方法,需要手动为 self 参数赋值。

通过类名调用实例方法的这种方式,又被称为未绑定方法

十八、如何使用Python继承机制(子类化内置类型)提高开发效率

我们知道,Python 中内置有一个 object 类,它是所有内置类型的共同祖先,也是所有没有显式指定父类的类(包括用户自定义的)的共同祖先。因此在实际编程过程中,如果想实现与某个内置类型具有类似行为的类时,最好的方法就是将这个内置类型子类化

内置类型子类化,其实就是自定义一个新类,使其继承有类似行为的内置类,通过重定义这个新类实现指定的功能。

举个例子,如下所示创建了一个名为 newDict 的类,其中 newDictError 是自定义的异常类:

class newDictError(ValueError):
  """如果向newDict 添加重复值,则引发此异常"""

class newDict(dict):
  """不接受重复值的字典"""
  def __setitem__(self,key,value):
    if value in self.values():
      if ((key in self and self[key]!=value) or (key not in self)):
        raise newDictError("这个值已经存在,并对应不同的键")
    super().__setitem__(key,value)
demoDict = newDict()
demoDict['key']='value'
demoDict['other_key']='value2'
print(demoDict)
demoDict['other_key']='value'
print(demoDict)

运行结果为:

{'key': 'value', 'other_key': 'value2'}
Traceback (most recent call last):
  File "C:\Users\mengma\Desktop\demo.py", line 15, in <module>
    demoDict['other_key']='value'
  File "C:\Users\mengma\Desktop\demo.py", line 9, in __setitem__
    raise newDictError("这个值已经存在,并对应不同的键")
newDictError: 这个值已经存在,并对应不同的键

可以看到,newDict 是 Python 中 dict 类型的子类,所以其大部分行为都和 dict 内置类相同,唯一不同之处在于,newDict 不允许字典中多个键对应相同的值。如果用户试图添加具有相同值的新元素,则会引发 newDictError 异常,并给出提示信息。

由于目前尚未学习如何处理异常,因此这里没有 newDictError 做任何处理,异常处理会在后续章节做详细讲解。

另外,如果查看现有代码你会发现,其实很多类都是对 Python 内置类的部分实现,它们作为子类的速度更快,代码更整洁。

比如,list 类型用来管理序列,如果一个类需要在内部处理序列,那么就可以对 list 进行子类化,示例代码如下:

class myList(list):
  def __init__(self,name):
    self.name = name

  def dir(self,nesting = 0):
    offset = " " * nesting
    print("%s%s/" % (offset,self.name))

    for element in self:
      if hasattr(element , 'dir'):
        element.dir(nesting + 1)
      else:
        print("%s %s" % (offset,element))

demoList = myList('C语言中文网')
demoList.append('http://c.biancheng.net')
print(demoList.dir())

运行结果如下:

C语言中文网/
http://c.biancheng.net
None

其实,除了 Python 中常用的基本内置类型,collections 模块中还额外提供了很多有用的容器,这些容器可以满足大部分情况。

十九、Python super()函数:调用父类的构造方法

前面不止一次讲过,Python 中子类会继承父类所有的类属性和类方法。严格来说,类的构造方法其实就是实例方法,因此毫无疑问,父类的构造方法,子类同样会继承。

但我们知道,Python 是一门支持多继承的面向对象编程语言,如果子类继承的多个父类中包含同名的类实例方法,则子类对象在调用该方法时,会优先选择排在最前面的父类中的实例方法。显然,构造方法也是如此。

举个例子:

class People:
    def __init__(self,name):
        self.name = name
    def say(self):
        print("我是人,名字为:",self.name)

class Animal:
    def __init__(self,food):
        self.food = food
    def display(self):
        print("我是动物,我吃",self.food)
#People中的 name 属性和 say() 会遮蔽 Animal 类中的
class Person(People, Animal):
    pass

per = Person("zhangsan")
per.say()
#per.display()

运行结果,结果为:

我是人,名字为: zhangsan

上面程序中,Person 类同时继承 People 和 Animal,其中 People 在前。这意味着,在创建 per 对象时,其将会调用从 People 继承来的构造函数。因此我们看到,上面程序在创建 per 对象的同时,还要给 name 属性进行赋值。

但如果去掉最后一行的注释,运行此行代码,Python 解释器会报如下错误:

Traceback (most recent call last):
  File "D:\python3.6\Demo.py", line 18, in <module>
    per.display()
  File "D:\python3.6\Demo.py", line 11, in display
    print("我是动物,我吃",self.food)
AttributeError: 'Person' object has no attribute 'food'

这是因为,从 Animal 类中继承的 display() 方法中,需要用到 food 属性的值,但由于 People 类的构造方法“遮蔽”了Animal 类的构造方法,使得在创建 per 对象时,Animal 类的构造方法未得到执行,所以程序出错。

反过来也是如此,如果将第 13 行代码改为如下形式:

class Person(Animal, People)

则在创建 per 对象时,会给 food 属性传值。这意味着,per.display() 能顺序执行,但 per.say() 将会报错。

针对这种情况,正确的做法是定义 Person 类自己的构造方法(等同于重写第一个直接父类的构造方法)。但需要注意,如果在子类中定义构造方法,则必须在该方法中调用父类的构造方法。

在子类中的构造方法中,调用父类构造方法的方式有 2 种,分别是:

  1. 类可以看做一个独立空间,在类的外部调用其中的实例方法,可以向调用普通函数那样,只不过需要额外备注类名(此方式又称为未绑定方法);
  2. 使用 super() 函数。但如果涉及多继承,该函数只能调用第一个直接父类的构造方法。

也就是说,涉及到多继承时,在子类构造函数中,调用第一个父类构造方法的方式有以上 2 种,而调用其它父类构造方法的方式只能使用未绑定方法。

值得一提的是,Python 2.x 中,super() 函数的使用语法格式如下:

super(Class, obj).__init__(...)

其中,Class 值得是子类的类名,obj 通常指的就是 self。

但在 Python 3.x 中,super() 函数有一种更简单的语法格式,推荐大家使用这种格式:

super().__init__(...)

在掌握 super() 函数用法的基础上,我们可以尝试修改上面的程序:

class People:
    def __init__(self,name):
        self.name = name
    def say(self):
        print("我是人,名字为:",self.name)
class Animal:
    def __init__(self,food):
        self.food = food
    def display(self):
        print("我是动物,我吃",self.food)
class Person(People, Animal):
    #自定义构造方法
    def __init__(self,name,food):
        #调用 People 类的构造方法
        super().__init__(name)
        #super(Person,self).__init__(name) #执行效果和上一行相同
        #People.__init__(self,name)#使用未绑定方法调用 People 类构造方法
        #调用其它父类的构造方法,需手动给 self 传值
        Animal.__init__(self,food)    
per = Person("zhangsan","熟食")
per.say()
per.display()

运行结果为:

我是人,名字为: zhangsan
我是动物,我吃 熟食

可以看到,Person 类自定义的构造方法中,调用 People 类构造方法,可以使用 super() 函数,也可以使用未绑定方法。但是调用 Animal 类的构造方法,只能使用未绑定方法。

二十、Python super()使用注意事项(包含新式类和旧式类的区别)

切记:super()只能在新式类中使用!

前面已经讲解了 super() 函数的用法,值得一提的是,Python 2 中 super() 函数的用法和 Python 3 大致相同,唯一的区别在于,Python 2 中不能使用零参数形式的格式,必须提供至少一个参数。

对于想要编写跨版本兼容代码的程序员来说,还要注意一件事,即 Python 2 中的 super() 函数只适用于新式类,在旧式类中不能使用 super()。

那么,什么是旧式类和新式类呢?在早期版本的 Python 中,所有类并没有一个共同的祖先 object,如果定义一个类,但没有显式指定其祖先,那么就被解释为旧式类,例如:

class oldStyle1:
  pass
class oldStyle2:
  pass

其中,oldStyle1 和 oldStyle2 都属于旧式类。

Python 2.x 版本中,为了向后兼容保留了旧式类。该版本中的新式类必须显式继承 object 或者其他新式类:

class newStyleClass(object):
  pass
class newStyleClass(newStyleClass):
  pass

显然,以上两个类都属于新式类。

而在 Python 3.x 版本中,不再保留旧式类的概念。因此,没有继承任何其他类的类都隐式地继承自 object。

可以说,在 Python 3.x 中,显式声明某个类继承自 object 似乎是冗余的。但如果考虑跨版本兼容,那么就必须将 object 作为所有基类的祖先,因为如果不这么做的话,这些类将被解释为旧式类,最终会导致难以诊断的问题。

二十一、Python super()使用注意事项

Python 中,由于基类不会在 __init__() 中被隐式地调用,需要程序员显式调用它们。这种情况下,当程序中包含多重继承的类层次结构时,使用 super 是非常危险的,往往会在类的初始化过程中出现问题。

1、混用super与显式类调用

分析如下程序,C 类使用了 __init__() 方法调用它的基类,会造成 B 类被调用了 2 次:

class A:
    def __init__(self):
        print("A",end=" ")
        super().__init__()
class B:
    def __init__(self):
        print("B",end=" ")
        super().__init__()
class C(A,B):
    def __init__(self):
        print("C",end=" ")
        A.__init__(self)
        B.__init__(self)
print("MRO:",[x.__name__ for x in C.__mro__])
C()

运行结果为:

MRO: ['C', 'A', 'B', 'object']
C A B B

出现以上这种情况的原因在于,C 的实例调用 A.__init__(self),使得 super(A,self).__init__() 调用了 B.__init__() 方法。换句话说,super 应该被用到整个类的层次结构中。

但是,有时这种层次结构的一部分位于第三方代码中,我们无法确定外部包的这些代码中是否使用 super(),因此,当需要对某个第三方类进行子类化时,最好查看其内部代码以及 MRO 中其他类的内部代码。

2、不同种类的参数

使用 super 的另一个问题是初始化过程中的参数传递。如果没有相同的签名,一个类怎么能调用其基类的 __init__() 代码呢?这会导致下列问题:

class commonBase:
    def __init__(self):
        print("commonBase")
        super().__init__()

class base1(commonBase):
    def __init__(self):
        print("base1")
        super().__init__()

class base2(commonBase):
    def __init__(self):
        print("base2")
        super().__init__()

class myClass(base1,base2):
    def __init__(self,arg):
        print("my base")
        super().__init__(arg)
myClass(10)

运行结果为:

my base
Traceback (most recent call last):
  File "C:\Users\mengma\Desktop\demo.py", line 20, in <module>
    myClass(10)
  File "C:\Users\mengma\Desktop\demo.py", line 19, in __init__
    super().__init__(arg)
TypeError: __init__() takes 1 positional argument but 2 were given

一种解决方法是使用 *args 和 **kwargs 包装的参数和关键字参数,这样即使不使用它们,所有的构造函数也会传递所有参数,如下所示:

class commonBase:
    def __init__(self,*args,**kwargs):
        print("commonBase")
        super().__init__()

class base1(commonBase):
    def __init__(self,*args,**kwargs):
        print("base1")
        super().__init__(*args,**kwargs)

class base2(commonBase):
    def __init__(self,*args,**kwargs):
        print("base2")
        super().__init__(*args,**kwargs)

class myClass(base1,base2):
    def __init__(self,arg):
        print("my base")
        super().__init__(arg)
myClass(10)

运行结果为:

my base
base1
base2
commonBase

不过,这是一种很糟糕的解决方法,由于任何参数都可以传入,所有构造函数都可以接受任何类型的参数,这会导致代码变得脆弱。另一种解决方法是在 MyClass 中显式地使用特定类的 __init__() 调用,但这无疑会导致第一种错误。

3、总结

如果想要避免程序中出现以上的这些问题,这里给出几点建议:

  • 尽可能避免使用多继承,可以使用一些设计模式来替代它;
  • super 的使用必须一致,即在类的层次结构中,要么全部使用 super,要么全不用。混用 super 和传统调用是一种混乱的写法;
  • 如果代码需要兼容 Python 2.x,在 Python 3.x 中应该显式地继承自 object。在 Python 2.x 中,没有指定任何祖先地类都被认定为旧式类。
  • 调用父类时应提前查看类的层次结构,也就是使用类的 __mro__ 属性或者 mro() 方法查看有关类的 MRO。

二十二、Python __slots__:限制类实例动态添加属性和方法

通过学习《Python类变量和实例变量》一节,了解了如何动态的为单个实例对象添加属性,甚至如果必要的话,还可以为所有的类实例对象统一添加属性(通过给类添加属性)。

那么,Python 是否也允许动态地为类或实例对象添加方法呢?答案是肯定的。我们知道,类方法又可细分为实例方法、静态方法和类方法,Python 语言允许为类动态地添加这 3 种方法;但对于实例对象,则只允许动态地添加实例方法,不能添加类方法和静态方法。

为单个实例对象添加方法,不会影响该类的其它实例对象;而如果为类动态地添加方法,则所有的实例对象都可以使用。

举个例子:

class CLanguage:
    pass
#下面定义了一个实例方法
def info(self):
    print("正在调用实例方法")
#下面定义了一个类方法
@classmethod
def info2(cls):
    print("正在调用类方法")
#下面定义个静态方法
@staticmethod
def info3():
    print("正在调用静态方法")

#类可以动态添加以上 3 种方法,会影响所有实例对象
CLanguage.info = info
CLanguage.info2 = info2
CLanguage.info3 = info3

clang = CLanguage()
#如今,clang 具有以上 3 种方法
clang.info()
clang.info2()
clang.info3()

#类实例对象只能动态添加实例方法,不会影响其它实例对象
clang1 = CLanguage()
clang1.info = info
#必须手动为 self 传值
clang1.info(clang1)

程序输出结果为:

正在调用实例方法
正在调用类方法
正在调用静态方法
正在调用实例方法

显然,动态给类或者实例对象添加属性或方法,是非常灵活的。但与此同时,如果胡乱地使用,也会给程序带来一定的隐患,即程序中已经定义好的类,如果不做任何限制,是可以做动态的修改的。

庆幸的是,Python 提供了 __slots__ 属性,通过它可以避免用户频繁的给实例对象动态地添加属性或方法。

再次声明,__slots__ 只能限制为实例对象动态添加属性和方法,而无法限制动态地为类添加属性和方法。

__slots__ 属性值其实就是一个元组,只有其中指定的元素,才可以作为动态添加的属性或者方法的名称。举个例子:

class CLanguage:
    __slots__ = ('name','add','info')

可以看到, CLanguage 类中指定了 __slots__ 属性,这意味着,该类的实例对象仅限于动态添加 name、add、info 这 3 个属性以及 name()、add() 和 info() 这 3 个方法。

注意,对于动态添加的方法,__slots__ 限制的是其方法名,并不限制参数的个数。

比如,在 CLanguage 类的基础上,添加如下代码并运行:

def info(self,name):
    print("正在调用实例方法",self.name)
clang = CLanguage()
clang.name = "C语言中文网"
#为 clang 对象动态添加 info 实例方法
clang.info = info
clang.info(clang,"Python教程")

程序运行结果为:

正在调用实例方法 C语言中文网

还是在 CLanguage 类的基础上,添加如下代码并运行:

def info(self,name):
    print("正在调用实例方法",self.name)
clang = CLanguage()
clang.name = "C语言中文网"
clang.say = info
clang.say(clang,"Python教程")

运行程序,显示如下信息:

Traceback (most recent call last):
  File "D:\python3.6\1.py", line 9, in <module>
    clang.say = info
AttributeError: 'CLanguage' object has no attribute 'say'

显然,根据 __slots__ 属性的设置,CLanguage 类的实例对象是不能动态添加以 say 为名称的方法的。

另外本节前面提到,__slots__ 属性限制的对象是类的实例对象,而不是类,因此下面的代码是合法的:

def info(self):
    print("正在调用实例方法")

CLanguage.say = info
clang = CLanguage()
clang.say()

程序运行结果为:

正在调用实例方法

当然,还可以为类动态添加类方法和静态方法,这里不再给出具体实例,读者可自行编写代码尝试。

此外,__slots__ 属性对由该类派生出来的子类,也是不起作用的。例如如下代码:

class CLanguage:
    __slots__ = ('name','add','info')
#Clanguage 的空子类
class CLangs(CLanguage):
    pass
#定义的实例方法
def info(self):
    print("正在调用实例方法")
clang = CLangs()
#为子类对象动态添加 say() 方法
clang.say = info
clang.say(clang)

运行结果为:

正在调用实例方法

显然,__slots__ 属性只对当前所在的类起限制作用。

因此,如果子类也要限制外界为其实例对象动态地添加属性和方法,必须在子类中设置 __slots__ 属性。

注意,如果为子类也设置有 __slots__ 属性,那么子类实例对象允许动态添加的属性和方法,是子类中 __slots__ 属性和父类 __slots__ 属性的和。

二十三、Python type()函数:动态创建类

我们知道,type() 函数属于 Python 内置函数,通常用来查看某个变量的具体类型。其实,type() 函数还有一个更高级的用法,即创建一个自定义类型(也就是创建一个类)。

type() 函数的语法格式有 2 种,分别如下:

type(obj) 
type(name, bases, dict)

以上这 2 种语法格式,各参数的含义及功能分别是:

  • 第一种语法格式用来查看某个变量(类对象)的具体类型,obj 表示某个变量或者类对象。
  • 第二种语法格式用来创建类,其中 name 表示类的名称;bases 表示一个元组,其中存储的是该类的父类;dict 表示一个字典,用于表示类内定义的属性或者方法。

对于使用 type() 函数查看某个变量或类对象的类型,由于很简单,这里不再做过多解释,直接给出一个样例:

#查看 3.4 的类型
print(type(3.4))
#查看类对象的类型
class CLanguage:
    pass
clangs = CLanguage()
print(type(clangs))

输出结果为:

<class 'float'>
<class '__main__.CLanguage'>

这里重点介绍 type() 函数的另一种用法,即创建一个新类,先来分析一个样例:

#定义一个实例方法
def say(self):
    print("我要学 Python!")
#使用 type() 函数创建类
CLanguage = type("CLanguage",(object,),dict(say = say, name = "C语言中文网"))
#创建一个 CLanguage 实例对象
clangs = CLanguage()
#调用 say() 方法和 name 属性
clangs.say()
print(clangs.name)

注意,Python 元组语法规定,当 (object,) 元组中只有一个元素时,最后的逗号(,)不能省略。

可以看到,此程序中通过 type() 创建了类,其类名为 CLanguage,继承自 objects 类,且该类中还包含一个 say() 方法和一个 name 属性。

有读者可能会问,如何判断 dict 字典中添加的是方法还是属性?很简单,如果该键值对中,值为普通变量(如 "C语言中文网"),则表示为类添加了一个类属性;反之,如果值为外部定义的函数(如 say() ),则表示为类添加了一个实例方法。

运行上面的程序,其输出结果为:

我要学 Python!
C语言中文网

可以看到,使用 type() 函数创建的类,和直接使用 class 定义的类并无差别。事实上,我们在使用 class 定义类时,Python 解释器底层依然是用 type() 来创建这个类。

二十四、Python MetaClass元类详解

MetaClass元类,本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法。

不要从字面上去理解元类的含义,事实上 MetaClass 中的 Meta 这个词根,起源于希腊语词汇 meta,包含“超越”和“改变”的意思。

举个例子,根据实际场景的需要,我们要为多个类添加一个 name 属性和一个 say() 方法。显然有多种方法可以实现,但其中一种方法就是使用 MetaClass 元类。

如果在创建类时,想用 MetaClass 元类动态地修改内部的属性或者方法,则类的创建过程将变得复杂:先创建 MetaClass 元类,然后用元类去创建类,最后使用该类的实例化对象实现功能。

和前面章节创建的类不同,如果想把一个类设计成 MetaClass 元类,其必须符合以下条件:

  1. 必须显式继承自 type 类;
  2. 类中需要定义并实现 __new__() 方法,该方法一定要返回该类的一个实例对象,因为在使用元类创建类时,该 __new__() 方法会自动被执行,用来修改新建的类。

讲了这么多,读者可能对 MetaClass 元类的功能还是比较懵懂。没关系,我们先尝试定义一个 MetaClass 元类:

#定义一个元类
class FirstMetaClass(type):
    # cls代表动态修改的类
    # name代表动态修改的类名
    # bases代表被动态修改的类的所有父类
    # attr代表被动态修改的类的所有属性、方法组成的字典
    def __new__(cls, name, bases, attrs):
        # 动态为该类添加一个name属性
        attrs['name'] = "C语言中文网"
        attrs['say'] = lambda self: print("调用 say() 实例方法")
        return super().__new__(cls,name,bases,attrs)

此程序中,首先可以断定 FirstMetaClass 是一个类。其次,由于该类继承自 type 类,并且内部实现了 __new__() 方法,因此可以断定 FirstMetaCLass 是一个元类。

有关 __new__() 的具体用法,可阅读《Python __new__()方法》一节。

可以看到,在这个元类的 __new__() 方法中,手动添加了一个 name 属性和 say() 方法。这意味着,通过 FirstMetaClass 元类创建的类,会额外添加 name 属性和 say() 方法。通过如下代码,可以验证这个结论:

#定义类时,指定元类
class CLanguage(object,metaclass=FirstMetaClass):
    pass

clangs = CLanguage()
print(clangs.name)
clangs.say()

可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名),则当 Python 解释器在创建这该类时,FirstMetaClass 元类中的 __new__ 方法就会被调用,从而实现动态修改类属性或者类方法的目的。

运行上面的程序,输出结果为:

C语言中文网
调用 say() 实例方法

显然,FirstMetaClass 元类的 __new__() 方法动态地为 Clanguage 类添加了 name 属性和 say() 方法,因此,即便该类在定义时是空类,它也依然有 name 属性和 say() 方法。

对于 MetaClass 元类,它多用于创建 API,因此我们几乎不会使用到它。

二十五、Python MetaClass元类实现的底层原理 Python底层是如何实现 MetaClass元类的

要理解 MetaClass 的底层原理,首先要深入理解 Python 类型模型。本节将从以下 2 点对 Python 类型模型做详细的介绍。

1、所有的 Python 的用户定义类,都是 type 这个类的实例

事实上,类本身不过是一个名为 type 类的实例,可以通过如下代码进行验证:

class MyClass:
  pass

instance = MyClass()
print(type(instance))
print(type(MyClass))

输出结果为:

<class '__main__.MyClass'>
<class 'type'>

可以看到,instance 是 MyClass 的实例,而 MyClass 是 type 的实例。

2、用户自定义类,只不过是 type 类的 __call__ 运算符重载。

当定义完成一个类时,真正发生的情况是 Python 会调用 type 类的 __call__ 运算符。

简单来说,当定义一个类时,例如下面语句:

class MyClass:
  data = 1

Python 底层执行的是下面这段代码:

class = type(classname, superclasses, attributedict)

其中等号右边的 type(classname, superclasses, attributedict) 就是 type 的 __call__ 运算符重载,它会进一步调用下面这 2 个函数:

type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)

以上整个过程,可以通过如下代码进行论证:

class MyClass:
  data = 1
 
instance = MyClass()
print(MyClass,instance)
print(instance.data)
MyClass = type('MyClass', (), {'data': 1})
instance = MyClass()

print(MyClass,instance)
print(instance.data)

运行结果为:

<class '__main__.MyClass'> <__main__.MyClass object at 0x000001CB469F7400>
1
<class '__main__.MyClass'> <__main__.MyClass object at 0x000001CB46A50828>
1

由此可见,正常的 MyClass 定义,和手工调用 type 运算符的结果是完全一样的。

总之,正是 Python 的类创建机制,给了 metaclass 大展身手的机会,即一旦把一个类型 MyClass 设置成元类 MyMeta,那么它就不再由原生的 type 创建,而是会调用 MyMeta 的 __call__ 运算符重载:

class = type(classname, superclasses, attributedict)
# 变为了
class = MyMeta(classname, superclasses, attributedict)

3、使用 metaclass 的风险

正如上面所看到的那样,metaclass 这样“逆天”的存在,会"扭曲变形"正常的 Python 类型模型,所以,如果使用不慎,对于整个代码库造成的风险是不可估量的。

换句话说,metaclass 仅仅是给小部分 Python 开发者,在开发框架层面的 Python 库时使用的。而在应用层,metaclass 往往不是很好的选择。

建议初学者不要轻易尝试使用 mateclass。

二十六、什么是多态,Python多态及用法详解

在面向对象程序设计中,除了封装和继承特性外,多态也是一个非常重要的特性,本节就带领大家详细了解什么是多态。

我们都知道,Python 是弱类型语言,其最明显的特征是在使用变量时,无需为其指定具体的数据类型。这会导致一种情况,即同一变量可能会被先后赋值不同的类对象,例如:

class CLanguage:
    def say(self):
        print("赋值的是 CLanguage 类的实例对象")
class CPython:
    def say(self):
        print("赋值的是 CPython 类的实例对象")
a = CLanguage()
a.say()

a = CPython()
a.say()

运行结果为:

赋值的是 CLanguage 类的实例对象
赋值的是 CPython 类的实例对象

可以看到,a 可以被先后赋值为 CLanguage 类和 CPython 类的对象,但这并不是多态。类的多态特性,还要满足以下 2 个前提条件:

  1. 继承:多态一定是发生在子类和父类之间;
  2. 重写:子类重写了父类的方法。

下面程序是对上面代码的改写:

class CLanguage:
    def say(self):
        print("调用的是 Clanguage 类的say方法")
class CPython(CLanguage):
    def say(self):
        print("调用的是 CPython 类的say方法")
class CLinux(CLanguage):
    def say(self):
        print("调用的是 CLinux 类的say方法")
a = CLanguage()
a.say()

a = CPython()
a.say()

a = CLinux()
a.say()

程序执行结果为:

调用的是 Clanguage 类的say方法
调用的是 CPython 类的say方法
调用的是 CLinux 类的say方法

可以看到,CPython 和 CLinux 都继承自 CLanguage 类,且各自都重写了父类的 say() 方法。从运行结果可以看出,同一变量 a 在执行同一个 say() 方法时,由于 a 实际表示不同的类实例对象,因此 a.say() 调用的并不是同一个类中的 say() 方法,这就是多态。

但是,仅仅学到这里,读者还无法领略 Python 类使用多态特性的精髓。其实,Python 在多态的基础上,衍生出了一种更灵活的编程机制。

继续对上面的程序进行改写:

class WhoSay:
    def say(self,who):
        who.say()
class CLanguage:
    def say(self):
        print("调用的是 Clanguage 类的say方法")

class CPython(CLanguage):
    def say(self):
        print("调用的是 CPython 类的say方法")

class CLinux(CLanguage):
    def say(self):
        print("调用的是 CLinux 类的say方法")
a = WhoSay()
#调用 CLanguage 类的 say() 方法
a.say(CLanguage())
#调用 CPython 类的 say() 方法
a.say(CPython())
#调用 CLinux 类的 say() 方法
a.say(CLinux())

程序执行结果为:

调用的是 Clanguage 类的say方法
调用的是 CPython 类的say方法
调用的是 CLinux 类的say方法

此程序中,通过给 WhoSay 类中的 say() 函数添加一个 who 参数,其内部利用传入的 who 调用 say() 方法。这意味着,当调用 WhoSay 类中的 say() 方法时,我们传给 who 参数的是哪个类的实例对象,它就会调用那个类中的 say() 方法。

在其它教程中,Python 这种由多态衍生出的更灵活的编程机制,又称为“鸭子模型”或“鸭子类型”。

二十七、Python枚举类定义和使用(详解版)

一些具有特殊含义的类,其实例化对象的个数往往是固定的,比如用一个类表示月份,则该类的实例对象最多有 12 个;再比如用一个类表示季节,则该类的实例化对象最多有 4 个。

针对这种特殊的类,Python 3.4 中新增加了 Enum 枚举类。也就是说,对于这些实例化对象个数固定的类,可以用枚举类来定义。

例如,下面程序演示了如何定义一个枚举类:

from enum import Enum
class Color(Enum):
    # 为序列值指定value值
    red = 1
    green = 2
    blue = 3

如果想将一个类定义为枚举类,只需要令其继承自 enum 模块中的 Enum 类即可。例如在上面程序中,Color 类继承自 Enum 类,则证明这是一个枚举类。

在 Color 枚举类中,red、green、blue 都是该类的成员(可以理解为是类变量)。注意,枚举类的每个成员都由 2 部分组成,分别为 name 和 value,其中 name 属性值为该枚举值的变量名(如 red),value 代表该枚举值的序号(序号通常从 1 开始)。

和普通类的用法不同,枚举类不能用来实例化对象,但这并不妨碍我们访问枚举类中的成员。访问枚举类成员的方式有多种,例如以 Color 枚举类为例,在其基础上添加如下代码:

#调用枚举成员的 3 种方式
print(Color.red)
print(Color['red'])
print(Color(1))
#调取枚举成员中的 value 和 name
print(Color.red.value)
print(Color.red.name)
#遍历枚举类中所有成员的 2 种方式
for color in Color:
    print(color)

程序输出结果为:

Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue

枚举类成员之间不能比较大小,但可以用 == 或者 is 进行比较是否相等,例如:

print(Color.red == Color.green)
print(Color.red.name is Color.green.name)

输出结果为:

Flase
Flase

需要注意的是,枚举类中各个成员的值,不能在类的外部做任何修改,也就是说,下面语法的做法是错误的:

Color.red = 4

除此之外,该枚举类还提供了一个 __members__ 属性,该属性是一个包含枚举类中所有成员的字典,通过遍历该属性,也可以访问枚举类中的各个成员。例如:

for name,member in Color.__members__.items():
    print(name,"->",member)

输出结果为:

red -> Color.red
green -> Color.green
blue -> Color.blue

值得一提的是,Python 枚举类中各个成员必须保证 name 互不相同,但 value 可以相同,举个例子:

from enum import Enum

class Color(Enum):
    # 为序列值指定value值
    red = 1
    green = 1
    blue = 3
print(Color['green'])

输出结果为:

Color.red

可以看到,Color 枚举类中 red 和 green 具有相同的值(都是 1),Python 允许这种情况的发生,它会将 green 当做是 red 的别名,因此当访问 green 成员时,最终输出的是 red。

在实际编程过程中,如果想避免发生这种情况,可以借助 @unique 装饰器,这样当枚举类中出现相同值的成员时,程序会报 ValueError 错误。例如:

#引入 unique
from enum import Enum,unique
#添加 unique 装饰器
@unique
class Color(Enum):
    # 为序列值指定value值
    red = 1
    green = 1
    blue = 3
print(Color['green'])

运行程序会报错:

Traceback (most recent call last):
  File "D:\python3.6\demo.py", line 3, in <module>
    class Color(Enum):
  File "D:\python3.6\lib\enum.py", line 834, in unique
    (enumeration, alias_details))
ValueError: duplicate values found in <enum 'Color'>: green -> red

除了通过继承 Enum 类的方法创建枚举类,还可以使用 Enum() 函数创建枚举类。例如:

from enum import Enum
#创建一个枚举类
Color = Enum("Color",('red','green','blue'))

#调用枚举成员的 3 种方式
print(Color.red)
print(Color['red'])
print(Color(1))
#调取枚举成员中的 value 和 name
print(Color.red.value)
print(Color.red.name)
#遍历枚举类中所有成员的 2 种方式
for color in Color:
    print(color)

Enum() 函数可接受 2 个参数,第一个用于指定枚举类的类名,第二个参数用于指定枚举类中的多个成员。

如上所示,仅通过一行代码,即创建了一个和前面的 Color 类相同的枚举类。运行程序,其输出结果为:

Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue

二十八、[Python项目实战]利用面向对象思想实现搜索引擎

要想实现一个搜索引擎,首先要了解什么是搜索引擎。简单地理解,搜索引擎是一个系统,它可以帮助用户去互联网上搜集与其检索内容相关的信息。

通常,一个搜索引擎由搜索器、索引器、检索器以及用户接口组成,其中各个部分的含义如下:

  • 搜索器:其实就是我们常说的爬虫、它能够从互联网中搜集大量的信息,并将之传递给索引器;
  • 索引器:理解搜索器搜索到的信息,并从中抽取出索引项,存储到内部的数据库中,等待检索;
  • 检索器:根据用户查询的内容,在已经建立好的索引库中快速检索出与之相关的信息,并做相关度评价,以此进行排序。
  • 用户接口:其作用就是提供给用户输入查询内容的窗口(例如百度、谷歌的搜索框),并将检索好的内容反馈给用户。

由于爬虫知识不是本节学习的重点,这里不再做深入介绍,我们假设搜索样本就存在于本地磁盘中。为了方便,这里只提供五个用于检索的文件,各文件存放的内容分别如下:

# 1.txt
C语言中文网
# 2.txt
http://c.biancheng.net
# 3.txt
「C语言中文网」是一个在线学习编程的网站,我们发布了多套文字教程,它们都通俗易懂,深入浅出。
# 4.txt
C语言中文网成立于 2012 年初,目前已经运营了将近 7 年,我们致力于分享精品教程,帮助对编程感兴趣的读者。
# 5.txt
坚持做好一件事情,做到极致,让自己感动,让用户心动,这就是足以传世的作品!

下面,根据以上知识,我们先实现一个最基本的搜索引擎:

class SearchEngineBase:
    def __init__(self):
        pass
    #搜索器
    def add_corpus(self, file_path):  
        with open(file_path, 'rb') as fin:
            text = fin.read().decode('utf-8')
        self.process_corpus(file_path, text)
    #索引器
    def process_corpus(self, id, text):
        raise Exception('process_corpus not implemented.')
    #检索器
    def search(self, query):
        raise Exception('search not implemented.')
#用户接口
def main(search_engine):
    for file_path in ['1.txt', '2.txt', '3.txt', '4.txt', '5.txt']:
        search_engine.add_corpus(file_path)
    while True:
        query = input()
        results = search_engine.search(query)
        print('found {} result(s):'.format(len(results)))
        for result in results:
            print(result)

以上代码仅是建立了搜索引擎的一个基本框架,它可以作为基类被其他类继承,那么继承自此类的类将分别代表不同的搜索引擎,它们应该各自实现基类中的 process_corpus() 和 search() 方法。

整个代码的运行过程是这样的,首先将各个检索文件中包含的内容连同该文件所在的路径一起传递给索引器,索引器会以该文件的路径建立索引,等待用户检索。

在 SearchEngineBase 类的基础上,下面实现了一个基本可以工作的搜索引擎:

#继承SearchEngineBase类,并重写了 process_corpus 和 search 方法
class SimpleEngine(SearchEngineBase):
    def __init__(self):
        super(SimpleEngine, self).__init__()
        #建立索引时使用
        self.__id_to_texts = {}

    def process_corpus(self, id, text):
        #以文件路径为键,文件内容为值,形成键值对,存储在字典中,由此建立索引
        self.__id_to_texts[id] = text

    def search(self, query):
        results = []
        #依次检索字典中的键值对,如果文件内容中包含用户要搜索的信息,则将此文件的文件路径存储在 results 列表中
        for id, text in self.__id_to_texts.items():
            if query in text:
                results.append(id)
        return results

search_engine = SimpleEngine()
main(search_engine)

运行结果为:

C语言中文网
found 3 result(s):
1.txt
3.txt
4.txt

可以看到,用户搜索与“C语言中文网”有关的内容,最终检索到了 1.txt、3.txt和 4.txt 文件中包含与之相关的内容。由此,只需要短短十来行代码就可以实现一个基础的搜索引擎。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值