Fork me on GitHub

Python类和对象

class:定义类

定义一个类使用class关键字实现:

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

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

类属性指的就是包含在类中的变量;类方法指的是包含类中的函数。换句话说,类属性和类方法其实分别是包含类中的变量和函数的别称。

Python 类是由类头(class类名)和类体(统一缩进的变量和函数)构成。

1
2
3
4
5
6
7
class TheFirstDemo:
'''这是一个学习Python定义的第一个类'''
# 下面定义了一个类属性
add = 'hello'
# 下面定义了一个say方法
def say(self, content):
print(content)

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

另外分析上面的代码可以看到,我们创建了一个名为TheFirstDemo的类,其包含了一个名为add的类属性。注意,根据定义属性位置的不同,在各个类方法之外定义的变量称为类属性或类变量(如add属性)。

同时,TheFirstDemo类中还包含一个say()类方法,该方法包含两个参数,分别是selfcontentcontent参数就只是一个普通参数,没有特殊含义,但self比较特殊,并不是普通的参数。

更确切地说,say()是一个实例方法,除此之外,Python 类中还可以定义类方法和静态方法。

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

1
2
class Empty:
pass

可以看到,如果一个类没有任何类属性和类方法,那么可以直接用pass关键字作为类体即可。

init()类构造方法

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

构造方法用于创建对象时使用,每当创建一个类的实例对象时,Python 解释器都会自动调用它。

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

另外,__init__()方法可以包含多个参数,但必须包含一个名为self的参数,且必须作为第一个参数。也就是说,类的构造方法最少也要有一个self参数。

1
2
3
4
5
6
7
8
9
10
class TheFirstDemo:
'''这是一个学习Python定义的第一个类'''
#构造方法
def __init__(self):
print("调用构造方法")
# 下面定义了一个类属性
add = 'test'
# 下面定义了一个say方法
def say(self, content):
print(content)

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

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

1
zhangsan = TheFirstDemo()

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

1
调用构造方法

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

__init__()构造方法中,除了self参数外,还可以自定义一些参数,参数之间使用逗号进行分割。

1
2
3
4
5
6
class CLanguage:
'''这是一个学习Python定义的一个类'''
def __init__(self, name, add):
print(name, "的英文名为:", add)
#创建 add 对象,并传递参数给构造函数
add = CLanguage("小明","xiaoming")

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

类对象的创建和使用

类的实例化

对已定义好的类进行实例化:

1
类名(参数)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CLanguage :
# 下面定义了2个类变量
name = "小明"
add = "xiaoming"
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("小明","xiaoming")

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

类变量和实例变量,简单地理解,定义在各个类方法之外(包含在类中)的变量为类变量(或者类属性),定义在类方法中的变量为实例变量(或者实例属性)。

类对象的使用

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

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

类对象访问变量或方法

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

1
类对象名.变量名

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

1
对象名.方法名(参数)
1
2
3
4
5
6
7
8
9
#输出name和add实例变量的值
print(clanguage.name,clanguage.add)
#修改实例变量的值
clanguage.name="小红"
clanguage.add="xiaohong"
#调用clanguage的say()方法
clanguage.say("人生苦短,我用Python")
#再次输出name和add的值
print(clanguage.name,clanguage.add)

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

Python 支持为已创建好的对象动态增加实例变量:

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

动态删除使用del语句即可实现:

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

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

1
2
3
4
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'

给类对象动态添加方法

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

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

1
2
3
4
5
6
7
8
9
10
11
# 先定义一个函数
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) # ②

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

有没有不用手动给self传值的方法呢?通过借助types模块下的MethodType可以实现:

1
2
3
4
5
6
7
def info(self,content):
print("小明的英文名为:%s" % content)
# 导入MethodType
from types import MethodType
clanguage.info = MethodType(info, clanguage)
# 第一个参数已经绑定了,无需传入
clanguage.info("xiaoming")

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

self用法

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

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

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

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

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

其实 Python 类方法中的self参数就相当于 C++ 中的this指针。

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

因此,程序在调用实例方法和构造方法时,不需要手动为第一个参数传值。

1
2
3
4
5
6
7
8
9
10
class Person:
def __init__(self):
print("正在执行构造方法")
# 定义一个study()实例方法
def study(self):
print(self, "正在学Python")
zhangsan = Person()
zhangsan.study()
lisi = Person()
lisi.study()

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

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

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

1
2
3
4
5
6
7
8
class Person:
name = "xxx"
def __init__(self,name):
self.name=name
zhangsan = Person("zhangsan")
print(zhangsan.name) # zhangsan
lisi = Person("lisi")
print(lisi.name) # lisi

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

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

1
2
3
4
5
6
7
8
9
class Person:
def who(self):
print(self)
zhangsan = Person()
#第一种方式
zhangsan.who()
#第二种方式
who = zhangsan.who
who()#通过 who 变量调用zhangsan对象中的 who() 方法

运行结果为:

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

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

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

类属性和实例属性

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

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

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

类方法也可细分为实例方法、静态方法和类方法。

类变量(类属性)

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

1
2
3
4
5
6
7
class CLanguage :
# 下面定义了2个类变量
name = "小明"
add = "xiaoming"
# 下面定义了一个say实例方法
def say(self, content):
print(content)

上面程序中,nameadd就属于类变量。

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

1
2
3
4
5
6
7
8
#使用类名直接调用
print(CLanguage.name) # 小明
print(CLanguage.add) # xiaoming
#修改类变量的值
CLanguage.name = "小红"
CLanguage.add = "xiaohong"
print(CLanguage.name) # 小红
print(CLanguage.add) # xiaohong

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

当然,也可以使用类对象来调用所属类中的类变量。

1
2
3
clang = CLanguage()
print(clang.name)
print(clang.add)

注意,因为类变量为所有实例化对象共有,通过类名修改类变量的值,会影响所有的实例化对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print("修改前,各类对象中类变量的值:")
clang1 = CLanguage()
print(clang1.name) # 小明
print(clang1.add) # xiaoming
clang2 = CLanguage()
print(clang2.name) # 小明
print(clang2.add) # xiaoming
print("修改后,各类对象中类变量的值:")
CLanguage.name = "小红"
CLanguage.add = "xiaohong"
print(clang1.name) # 小红
print(clang1.add) # xiaohong
print(clang2.name) # 小红
print(clang2.add) # xiaohong

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

注意,通过类对象是无法修改类变量的。通过类对象对类变量赋值,其本质将不再是修改类变量的值,而是在给该对象定义新的实例变量。

值得一提的是,除了可以通过类名访问类变量之外,还可以动态地为类和对象添加类变量。

1
2
3
clang = CLanguage()
CLanguage.catalog = 13
print(clang.catalog) # 13

实例变量(实例属性)

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

1
2
3
4
5
6
7
class CLanguage :
def __init__(self):
self.name = "小明"
self.add = "xiaoming"
# 下面定义了一个say实例方法
def say(self):
self.catalog = 13

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

1
2
3
4
5
6
7
8
9
10
11
clang = CLanguage()
print(clang.name) # 小明
print(clang.add) # xiaoming
#由于 clang 对象未调用 say() 方法,因此其没有 catalog 变量,下面这行代码会报错
#print(clang.catalog)
clang2 = CLanguage()
print(clang2.name) # 小明
print(clang2.add) # xiaoming
#只有调用 say(),才会拥有 catalog 实例变量
clang2.say()
print(clang2.catalog) # 13

通过类对象可以访问类变量,但无法修改类变量的值。这是因为,通过类对象修改类变量的值,不是在给“类变量赋值”,而是定义新的实例变量。

1
2
3
4
5
6
7
8
9
10
11
12
clang = CLanguage()
#clang访问类变量
print(clang.name) # 小明
print(clang.add) # xiaoming
clang.name = "小红"
clang.add = "xiaohong"
#clang实例变量的值
print(clang.name) # 小红
print(clang.add) # xiaohong
#类变量的值
print(CLanguage.name) # 小明
print(CLanguage.add) # xiaoming

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class CLanguage :
name = "小明" #类变量
add = "xiaoming" #类变量
def __init__(self):
self.name = "小红" #实例变量
self.add = "xiaohong" #实例变量
# 下面定义了一个say实例方法
def say(self):
self.catalog = 13 #实例变量
clang = CLanguage()
#修改 clang 对象的实例变量
clang.name = "小李"
clang.add = "xiaoli"
print(clang.name) # 小李
print(clang.add) # xiaoli
clang2 = CLanguage()
print(clang2.name) # 小红
print(clang2.add) # xiaohong
#输出类变量的值
print(CLanguage.name) # 小明
print(CLanguage.add) # xiaoming

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

1
2
clang.money = 30
print(clang.money)

局部变量

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

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

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

实例方法、静态方法和类方法

类方法可分为类方法、实例方法和静态方法。采用@classmethod修饰的方法为类方法;采用@staticmethod修饰的方法为静态方法;不用任何修饰的方法为实例方法。

类实例方法

通常情况下,在类中定义的方法默认都是实例方法。类的构造方法理论上也属于实例方法,只不过它比较特殊。

1
2
3
4
5
6
7
8
class CLanguage:
#类构造方法,也属于实例方法
def __init__(self):
self.name = "小明"
self.add = "xiaoming"
# 下面定义了一个say实例方法
def say(self):
print("正在调用 say() 实例方法")

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

1
2
clang = CLanguage()
clang.say() # 正在调用 say() 实例方法

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

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

类方法

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

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

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

1
2
3
4
5
6
7
8
9
class CLanguage:
#类构造方法,也属于实例方法
def __init__(self):
self.name = "小明"
self.add = "xiaoming"
#下面定义了一个类方法
@classmethod
def info(cls):
print("正在调用类方法", cls)

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

类方法推荐使用类名直接调用,当然也可以使用实例对象来调用(不推荐)。

1
2
3
4
5
#使用类名直接调用类方法
CLanguage.info()
#使用类对象调用类方法
clang = CLanguage()
clang.info()

类静态方法

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

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

静态方法需要使用@staticmethod修饰:

1
2
3
4
class CLanguage:
@staticmethod
def info(name, add):
print(name, add)

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

1
2
3
4
5
#使用类名直接调用静态方法
CLanguage.info("小明", "xiaoming")
#使用类对象调用静态方法
clang = CLanguage()
clang.info("小红", "xiaohong")

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

类调用实例方法

实例方法的调用方式有 2 种,既可以采用类对象调用,也可以直接通过类名调用。

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

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

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

1
2
3
4
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参数传值。

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

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

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

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

运行结果为:

1
zhangsan 正在学 Python

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

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

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

描述符

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

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

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

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

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

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

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

  • 验证该属性是否为类实例对象的数据描述符;
  • 如果不是,就查看该属性是否能在类实例对象的__dict__中找到;
  • 最后,查看该属性是否为类实例对象的非数据描述符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#描述符类
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)

运行结果为:

1
2
3
4
5
6
Retrieving var "x"
10
updating var "x"
Retrieving var "x"
20
5

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

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

property()函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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("小明")
#获取name属性值
print(clang.getname()) # 小明
#设置name属性值
clang.setname("Python教程") # Python教程
print(clang.getname())
#删除name属性值
clang.delname()
print(clang.getname()) # xxx

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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("小明")
#调用 getname() 方法
print(clang.name)
#调用 setname() 方法
clang.name="Python教程"
print(clang.name)
#调用 delname() 方法
del clang.name
print(clang.name)

运行结果为:

1
2
3
4
5
6
7
Help on property:

指明出处

小明
Python教程
xxx

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

当然,property()函数也可以少传入几个参数。

1
name = property(getname, setname)

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

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

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

封装

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

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

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

Python 类如何进行封装

Python 类中的变量和函数,不是公有的(类似public属性),就是私有的(类似private)。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CLanguage :
def setname(self, name):
if len(name) < 1:
raise ValueError('名称长度必须大于1!')
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 = "百度"
clang.add = "http://www.baidu.com"
print(clang.name) # 百度
print(clang.add) # http://www.baidu.com

上面程序中,CLanguagenameadd属性都隐藏了起来,但同时也提供了可操作它们的“窗口”,也就是各自的settergetter方法,这些方法都是公有的。

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

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

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

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

这会导致如下错误:

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

继承

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

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

1
2
3
4
5
6
7
8
9
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类中添加计算面积的方法即可:

1
2
3
4
5
6
7
class Shape:
def draw(self,content):
print("画",content)
class Form(Shape):
def area(self):
#....
print("此图形的面积为...")

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

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

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

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

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

“派生”和继承是一个意思,只是观察角度不同而已。换句话说,继承是相对子类来说的,即子类继承自父类;而派生是相对于父类来说的,即父类派生出子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

运行结果为:

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

可以看到,虽然 Person 类为空类,但由于其继承自 People 和 Animal 这 2 个类,因此实际上 Person 并不空,它同时拥有这 2 个类所有的属性和方法。
没错,子类拥有父类所有的属性和方法,即便该属性或方法是私有(private)的。

关于Python的多继承

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

程序运行结果为:

1
People类 张三

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

父类方法重写

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

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

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

1
2
3
4
5
6
7
class Bird:
#鸟有翅膀
def isWing(self):
print("鸟有翅膀")
#鸟会飞
def fly(self):
print("鸟会飞")

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

运行结果为:

1
鸵鸟不会飞

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

如何调用被重写的方法

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

程序运行结果为:

1
鸟会飞

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

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

super()函数

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

运行结果,结果为:

1
我是人,名字为: zhangsan

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

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

1
2
3
4
5
6
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 行代码改为如下形式:

1
class Person(Animal, People)

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

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

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

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

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

在 Python 3.x 中,super()函数的语法格式:

1
super().__init__(...)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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() # 我是人,名字为: zhangsan
per.display() # 我是动物,我吃 熟食

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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__属性值其实就是一个元组,只有其中指定的元素,才可以作为动态添加的属性或者方法的名称。举个例子:

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

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

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

1
2
3
4
5
6
7
def info(self,name):
print("正在调用实例方法",self.name)
clang = CLanguage()
clang.name = "小明"
#为 clang 对象动态添加 info 实例方法
clang.info = info
clang.info(clang,"Python教程")

程序运行结果为:

1
正在调用实例方法 小明

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

1
2
3
4
5
6
def info(self,name):
print("正在调用实例方法",self.name)
clang = CLanguage()
clang.name = "小明"
clang.say = info
clang.say(clang,"Python教程")

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

1
2
3
4
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__属性限制的对象是类的实例对象,而不是类,因此下面的代码是合法的:

1
2
3
4
5
def info(self):
print("正在调用实例方法")
CLanguage.say = info
clang = CLanguage()
clang.say()

程序运行结果为:

1
正在调用实例方法

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

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

1
2
3
4
5
6
7
8
9
10
11
12
class CLanguage:
__slots__ = ('name','add','info')
#Clanguage 的空子类
class CLangs(CLanguage):
pass
#定义的实例方法
def info(self):
print("正在调用实例方法")
clang = CLangs()
#为子类对象动态添加 say() 方法
clang.say = info
clang.say(clang)

运行结果为:

1
正在调用实例方法

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

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

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

type()函数:动态创建类

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

type() 函数的语法格式有 2 种:

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

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

  • 第一种语法格式用来查看某个变量(类对象)的具体类型,obj 表示某个变量或者类对象。
  • 第二种语法格式用来创建类,其中 name 表示类的名称;bases 表示一个元组,其中存储的是该类的父类;dict 表示一个字典,用于表示类内定义的属性或者方法。
1
2
3
4
5
6
7
#查看 3.4 的类型
print(type(3.4))
#查看类对象的类型
class CLanguage:
pass
clangs = CLanguage()
print(type(clangs))

输出结果为:

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

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

1
2
3
4
5
6
7
8
9
10
#定义一个实例方法
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属性。

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

1
2
我要学 Python!
C语言中文网

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

MetaClass元类

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

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

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

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

如果想把一个类设计成MetaClass元类,其必须符合以下条件:

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

我们先尝试定义一个MetaClass元类:

1
2
3
4
5
6
7
8
9
10
11
#定义一个元类
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__()方法中,手动添加了一个 name 属性和 say() 方法。这意味着,通过 FirstMetaClass 元类创建的类,会额外添加 name 属性和 say() 方法。通过如下代码,可以验证这个结论:

1
2
3
4
5
6
#定义类时,指定元类
class CLanguage(object,metaclass=FirstMetaClass):
pass
clangs = CLanguage()
print(clangs.name)
clangs.say()

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

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

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

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

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

多态

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

1
2
3
4
5
6
7
8
9
10
class CLanguage:
def say(self):
print("赋值的是 CLanguage 类的实例对象")
class CPython:
def say(self):
print("赋值的是 CPython 类的实例对象")
a = CLanguage()
a.say()
a = CPython()
a.say()

运行结果为:

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

程序执行结果为:

1
2
3
调用的是 Clanguage 类的say方法
调用的是 CPython 类的say方法
调用的是 CLinux 类的say方法

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

其实,Python 在多态的基础上,衍生出了一种更灵活的编程机制。继续对上面的程序进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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())

程序执行结果为:

1
2
3
调用的是 Clanguage 类的say方法
调用的是 CPython 类的say方法
调用的是 CLinux 类的say方法

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

枚举类

一些具有特殊含义的类,其实例化对象的个数往往是固定的,比如用一个类表示月份,则该类的实例对象最多有 12 个。对于这些实例化对象个数固定的类,可以用枚举类来定义。

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

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

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

和普通类的用法不同,枚举类不能用来实例化对象,但这并不妨碍我们访问枚举类中的成员。访问枚举类成员的方式有多种:

1
2
3
4
5
6
7
8
9
10
#调用枚举成员的 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)

程序输出结果为:

1
2
3
4
5
6
7
8
Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue

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

1
2
print(Color.red == Color.green) # Flase
print(Color.red.name is Color.green.name) # Flase

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

1
Color.red = 4

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

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

输出结果为:

1
2
3
red -> Color.red
green -> Color.green
blue -> Color.blue

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

1
2
3
4
5
6
7
from enum import Enum
class Color(Enum):
# 为序列值指定value值
red = 1
green = 1
blue = 3
print(Color['green'])

输出结果为:

1
Color.red

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

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

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

运行程序会报错:

1
2
3
4
5
6
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()函数创建枚举类。

1
2
3
4
5
6
7
8
9
10
11
12
13
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类相同的枚举类。运行程序,其输出结果为:

1
2
3
4
5
6
7
8
Color.red
Color.red
Color.red
1
red
Color.red
Color.green
Color.blue

导入模块

import的用法主要有以下两种:
import 模块名1 [as 别名1], 模块名2 [as 别名2],…:使用这种语法格式的import语句,会导入指定模块中的所有成员(包括变量、函数、类等)。不仅如此,当需要使用模块中的成员时,需用该模块名(或别名)作为前缀,否则 Python 解释器会报错。
from 模块名 import 成员名1 [as 别名1],成员名2 [as 别名2],…:使用这种语法格式的import语句,只会导入模块中指定的成员,而不是全部成员。同时,当程序中使用该成员时,无需附加任何前缀,直接使用成员名(或别名)即可。

注意,用[]括起来的部分,可以使用,也可以省略。

其中,第二种import语句也可以导入指定模块中的所有成员,即使用form模块名import *,但此方式不推荐使用。

import 模块名 as 别名

下面程序使用导入整个模块的最简单语法来导入指定模块:

1
2
3
4
# 导入sys整个模块
import sys
# 使用sys模块名作为前缀来访问模块中的成员
print(sys.argv[0])

上面第 2 行代码使用最简单的方式导入了sys模块,因此在程序中使用sys模块内的成员时,必须添加模块名作为前缀。

运行上面程序,可以看到如下输出结果(sys模块下的argv变量用于获取运行 Python 程序的命令行参数,其中argv[0]用于获取当前 Python 程序的存储路径):

1
C:\Users\mengma\Desktop\hello.py

导入整个模块时,也可以为模块指定别名。例如如下程序:

1
2
3
4
# 导入sys整个模块,并指定别名为s
import sys as s
# 使用s模块别名作为前缀来访问模块中的成员
print(s.argv[0])

第 2 行代码在导入sys模块时才指定了别名s,因此在程序中使用sys模块内的成员时,必须添加模块别名s作为前缀。运行该程序,可以看到如下输出结果:

1
C:\Users\mengma\Desktop\hello.py

也可以一次导入多个模块,多个模块之间用逗号隔开。

1
2
3
4
5
6
# 导入sys、os两个模块
import sys,os
# 使用模块名作为前缀来访问模块中的成员
print(sys.argv[0])
# os模块的sep变量代表平台上的路径分隔符
print(os.sep)

上面第 2 行代码一次导入了 sys 和 os 两个模块,因此程序要使用 sys、os 两个模块内的成员,只要分别使用 sys、os 模块名作为前缀即可。在 Windows 平台上运行该程序,可以看到如下输出结果(os 模块的 sep 变量代表平台上的路径分隔符):

1
2
C:\Users\mengma\Desktop\hello.py
\

在导入多个模块的同时,也可以为模块指定别名:

1
2
3
4
5
# 导入sys、os两个模块,并为sys指定别名s,为os指定别名o
import sys as s,os as o
# 使用模块别名作为前缀来访问模块中的成员
print(s.argv[0])
print(o.sep)

上面第 2 行代码一次导入了sys 和 os 两个模块,并分别为它们指定别名为 s、o,因此程序可以通过 s、o 两个前缀来使用 sys、os 两个模块内的成员。在 Windows 平台上运行该程序,可以看到如下输出结果:

1
2
C:\Users\mengma\Desktop\hello.py
\

from 模块名 import 成员名 as 别名

1
2
3
4
# 导入sys模块的argv成员
from sys import argv
# 使用导入成员的语法,直接使用成员名访问
print(argv[0])

第 2 行代码导入了sys模块中的argv成员,这样即可在程序中直接使用 argv 成员,无须使用任何前缀。

导入模块成员时,也可以为成员指定别名,例如如下程序:

1
2
3
4
# 导入sys模块的argv成员,并为其指定别名v
from sys import argv as v
# 使用导入成员(并指定别名)的语法,直接使用成员的别名访问
print(v[0])

第 2 行代码导入了sys模块中的argv成员,并为该成员指定别名v,这样即可在程序中通过别名v使用argv成员,无须使用任何前缀。

form...import导入模块成员时,支持一次导入多个成员:

1
2
3
4
5
# 导入sys模块的argv,winver成员
from sys import argv, winver
# 使用导入成员的语法,直接使用成员名访问
print(argv[0])
print(winver)

上面第 2 行代码导入了sys模块中的argv、 winver成员,这样即可在程序中直接使用argv、winver两个成员,无须使用任何前缀。运行该程序,可以看到如下输出结果(syswinver成员记录了该 Python 的版本号):

1
2
C:\Users\mengma\Desktop\hello.py
3.11

一次导入多个模块成员时,也可指定别名,同样使用as关键字为成员指定别名:

1
2
3
4
5
# 导入sys模块的argv,winver成员,并为其指定别名v、wv
from sys import argv as v, winver as wv
# 使用导入成员(并指定别名)的语法,直接使用成员的别名访问
print(v[0])
print(wv)

上面第 2 行代码导入了 sys 模块中的 argv、winver 成员,并分别为它们指定了别名 v、wv,这样即可在程序中通过 v 和 wv 两个别名使用 argv、winver 成员,无须使用任何前缀。

不推荐使用 from import 导入模块所有成员

在使用from...import语法时,可以一次导入指定模块内的所有成员(此方式不推荐):

1
2
3
4
5
#导入sys 棋块内的所有成员
from sys import *
#使用导入成员的语法,直接使用成员的别名访问
print(argv[0])
print(winver)

上面代码一次导入了sys模块中的所有成员,这样程序即可通过成员名来使用该模块内的所有成员。该程序的输出结果和前面程序的输出结果完全相同。

需要说明的是,一般不推荐使用“from 模块 import”这种语法导入指定模块内的所有成员,因为它存在潜在的风险。比如同时导入module1module2内的所有成员,假如这两个模块内都有一个foo()函数,那么当在程序中执行如下代码时:

1
foo()

上面调用的这个foo()函数到底是module1模块中的还是module2模块中的?因此,这种导入指定模块内所有成员的用法是有风险的。

但如果换成如下两种导入方式:

1
2
import module1
import module2 as m2

接下来要分别调用这两个模块中的foo()函数就非常清晰。

1
2
3
4
5
6
7
8
9
#使用模块module1 的模块名作为前缀调用foo()函数
module1.foo()
#使用module2 的模块别名作为前缀调用foo()函数
m2.foo()
或者使用 from...import 语句也是可以的:
#导入module1 中的foo 成员,并指定其别名为foo1
from module1 import foo as fool
#导入module2 中的foo 成员,并指定其别名为foo2
from module2 import foo as foo2

此时通过别名将 module1 和 module2 两个模块中的 foo 函数很好地进行了区分,接下来分别调用两个模块中 foo() 函数就很清晰:

1
2
foo1() #调用module1 中的foo()函数
foo2() #调用module2 中的foo()函数

自定义模块

Python 模块就是 Python 程序,换句话说,只要是 Python 程序,都可以作为模块导入。例如,下面定义了一个简单的模块(编写在 demo.py 文件中):

1
2
3
4
5
6
7
8
9
10
11
name = "小明"
add = "xiaoming"
print(name,add)
def say():
print("人生苦短,我学Python!")
class CLanguage:
def __init__(self,name,add):
self.name = name
self.add = add
def say(self):
print(self.name,self.add)

可以看到,我们在 demo.py 文件中放置了变量(name 和 add)、函数( say() )以及一个 Clanguage 类,该文件就可以作为一个模板。

但通常情况下,为了检验模板中代码的正确性,我们往往需要为其设计一段测试代码,例如:

1
2
3
4
5
6
7
say()
clangs = CLanguage("小红","xiaohong")
clangs.say()
运行 demo.py 文件,其执行结果为:
小明 xiaoming
人生苦短,我学Python!
小红 xiaohong

通过观察模板中程序的执行结果可以断定,模板文件中包含的函数以及类,是可以正常工作的。

在此基础上,我们可以新建一个 test.py 文件,并在该文件中使用 demo.py 模板文件,即使用 import 语句导入 demo.py:

1
import demo

注意,虽然 demo 模板文件的全称为 demo.py,但在使用 import 语句导入时,只需要使用该模板文件的名称即可。

此时,如果直接运行 test.py 文件,其执行结果为:

1
2
3
小明 xiaoming
人生苦短,我学Python!
小红 xiaohong

可以看到,当执行 test.py 文件时,它同样会执行 demo.py 中用来测试的程序,这显然不是我们想要的效果。正常的效果应该是,只有直接运行模板文件时,测试代码才会被执行;反之,如果是其它程序以引入的方式执行模板文件,则测试代码不应该被执行。

要实现这个效果,可以借助 Python 内置的__name__变量。当直接运行一个模块时,name变量的值为__main__;而将模块被导入其他程序中并运行该程序时,处于模块中的__name__变量的值就变成了模块名。因此,如果希望测试函数只有在直接运行模块文件时才执行,则可在调用测试函数时增加判断,即只有当__name__ =='__main__'时才调用测试函数。

因此,我们可以修改 demo.py 模板文件中的测试代码为:

1
2
3
4
if __name__ == '__main__':
say()
clangs = CLanguage("小明","xiaoming")
clangs.say()

显然,这里执行的仅是模板文件中的输出语句,测试代码并未执行。

自定义模块编写说明文档

我们知道,在定义函数或者类时,可以为其添加说明文档,以方便用户清楚的知道该函数或者类的功能。自定义模块也不例外。

为自定义模块添加说明文档,和函数或类的添加方法相同,即只需在模块开头的位置定义一个字符串即可。例如,为 demo.py 模板文件添加一个说明文档:

1
2
3
4
5
6
7
'''
demo 模块中包含以下内容:
name 字符串变量:初始值为“Python教程”
add 字符串变量:初始值为“http://c.biancheng.net/python”
say() 函数
CLanguage类:包含 name 和 add 属性和 say() 方法。
'''

在此基础上,我们可以通过模板的 doc 属性,来访问模板的说明文档。例如,在 test.py 文件中添加如下代码:

1
2
import demo
print(demo.__doc__)

程序运行结果为:
Python教程 http://c.biancheng.net/python

1
2
3
4
5
demo 模块中包含以下内容:
name 字符串变量:初始值为“Python教程”
add 字符串变量:初始值为“http://c.biancheng.net/python”
say() 函数
CLanguage类:包含 name 和 add 属性和 say() 方法。

导入模块的3种方式

即自定义 Python 模板后,在其它文件中用 import(或 from…import) 语句引入该文件时,Python 解释器同时如下错误:

1
ModuleNotFoundError: No module named '模块名'

意思是 Python 找不到这个模块名,这是什么原因导致的呢?要想解决这个问题,读者要先搞清楚 Python 解释器查找模块文件的过程。

通常情况下,当使用 import 语句导入模块后,Python 会按照以下顺序查找指定的模块文件:
在当前目录,即当前执行的程序文件所在目录下查找;
到 PYTHONPATH(环境变量)下的每个目录中查找;
到 Python 默认的安装目录下查找。

以上所有涉及到的目录,都保存在标准模块 sys 的 sys.path 变量中,通过此变量我们可以看到指定程序文件支持查找的所有目录。换句话说,如果要导入的模块没有存储在 sys.path 显示的目录中,那么导入该模块并运行程序时,Python 解释器就会抛出 ModuleNotFoundError(未找到模块)异常。

解决“Python找不到指定模块”的方法有 3 种,分别是:

  • 向 sys.path 中临时添加模块文件存储位置的完整路径;
  • 将模块放在 sys.path 变量中已包含的模块加载路径中;
  • 设置 path 系统环境变量。
1
2
3
4
5
6
#hello.py
def say ():
print("Hello,World!")
#say.py
import hello
hello.say()

显然,hello.py 文件和 say.py 文件并不在同一目录,此时运行 say.py 文件,其运行结果为:

1
2
3
4
 Traceback (most recent call last):
File "C:\Users\mengma\Desktop\say.py", line 1, in <module>
import hello
ModuleNotFoundError: No module named 'hello'

可以看到,Python 解释器抛出了 ModuleNotFoundError 异常。接下来,分别用以上 3 种方法解决这个问题。

导入模块方式一:临时添加模块完整路径

模块文件的存储位置,可以临时添加到 sys.path 变量中,即向 sys.path 中添加 D:\python_module(hello.py 所在目录),在 say.py 中的开头位置添加如下代码:

1
2
import sys
sys.path.append('D:\\python_module')

注意:在添加完整路径中,路径中的 ‘' 需要使用 \ 进行转义,否则会导致语法错误。再次运行 say.py 文件,运行结果如下:

1
Hello,World!

可以看到,程序成功运行。在此基础上,我们在 say.py 文件中输出 sys.path 变量的值,会得到以下结果:

1
['C:\\Users\\mengma\\Desktop', 'D:\\python3.6\\Lib\\idlelib', 'D:\\python3.6\\python36.zip', 'D:\\python3.6\\DLLs', 'D:\\python3.6\\lib', 'D:\\python3.6', 'C:\\Users\\mengma\\AppData\\Roaming\\Python\\Python36\\site-packages', 'D:\\python3.6\\lib\\site-packages', 'D:\\python3.6\\lib\\site-packages\\win32', 'D:\\python3.6\\lib\\site-packages\\win32\\lib', 'D:\\python3.6\\lib\\site-packages\\Pythonwin', 'D:\\python_module']

该输出信息中,红色部分就是临时添加进去的存储路径。需要注意的是,通过该方法添加的目录,只能在执行当前文件的窗口中有效,窗口关闭后即失效。

导入模块方式二:将模块保存到指定位置

如果要安装某些通用性模块,比如复数功能支持的模块、矩阵计算支持的模块、图形界面支持的模块等,这些都属于对 Python 本身进行扩展的模块,这种模块应该直接安装在 Python 内部,以便被所有程序共享,此时就可借助于 Python 默认的模块加载路径。

Python 程序默认的模块加载路径保存在 sys.path 变量中,因此,我们可以在 say.py 程序文件中先看看 sys.path 中保存的默认加载路径,向 say.py 文件中输出 sys.path 的值,如下所示:

1
['C:\\Users\\mengma\\Desktop', 'D:\\python3.6\\Lib\\idlelib', 'D:\\python3.6\\python36.zip', 'D:\\python3.6\\DLLs', 'D:\\python3.6\\lib', 'D:\\python3.6', 'C:\\Users\\mengma\\AppData\\Roaming\\Python\\Python36\\site-packages', 'D:\\python3.6\\lib\\site-packages', 'D:\\python3.6\\lib\\site-packages\\win32', 'D:\\python3.6\\lib\\site-packages\\win32\\lib', 'D:\\python3.6\\lib\\site-packages\\Pythonwin']

上面的运行结果中,列出的所有路径都是 Python 默认的模块加载路径,但通常来说,我们默认将 Python 的扩展模块添加在 lib\site-packages 路径下,它专门用于存放 Python 的扩展模块和包。

所以,我们可以直接将我们已编写好的 hello.py 文件添加到 lib\site-packages 路径下,就相当于为 Python 扩展了一个 hello 模块,这样任何 Python 程序都可使用该模块。

移动工作完成之后,再次运行 say.py 文件,可以看到成功运行的结果:

1
Hello,World!

导入模块方式三:设置环境变量

PYTHONPATH 环境变量(简称 path 变量)的值是很多路径组成的集合,Python 解释器会按照 path 包含的路径进行一次搜索,直到找到指定要加载的模块。当然,如果最终依旧没有找到,则 Python 就报 ModuleNotFoundError 异常。

在 Windows 平台上设置环境变量
首先,找到桌面上的“计算机”(或者我的电脑),并点击鼠标右键,单击“属性”。此时会显示“控制面板\所有控制面板项\系统”窗口,单击该窗口左边栏中的“高级系统设置”菜单,出现“系统属性”对话框,如图 1 所示。

图 1 系统属性对话框

如图 1 所示,点击“环境变量”按钮,此时将弹出图 2 所示的对话框:

图 2 环境变量对话框

如图 2 所示,通过该对话框,就可以完成 path 环境变量的设置。需要注意的是,该对话框分为上下 2 部分,其中上面的“用户变量”部分用于设置当前用户的环境变量,下面的“系统变量”部分用于设置整个系统的环境变量。

通常情况下,建议大家设置设置用户的 path 变量即可,因为此设置仅对当前登陆系统的用户有效,而如果修改系统的 path 变量,则对所有用户有效。
对于普通用户来说,设置用户 path 变量和系统 path 变量的效果是相同的,但 Python 在使用 path 变量时,会先按照系统 path 变量的路径去查找,然后再按照用户 path 变量的路径去查找。

这里我们选择设置当前用户的 path 变量。单击用户变量中的“新建”按钮, 系统会弹出如图 3 所示的对话框。

图 3 新建PYTHONPATH环境变量

其中,在“变量名”文本框内输入 PYTHONPATH,表明将要建立名为 PYTHONPATH 的环境变量;在“变量值”文本框内输入 .;d:\python_ module。注意,这里其实包含了两条路径(以分号 ;作为分隔符):
第一条路径为一个点(.),表示当前路径,当运行 Python 程序时,Python 将可以从当前路径加载模块;
第二条路径为 d:\python_ module,当运行 Python 程序时,Python 将可以从 d:\python_ module 中加载模块。

然后点击“确定”,即成功设置 path 环境变量。此时,我们只需要将模块文件移动到和引入该模块的文件相同的目录,或者移动到 d:\python_ module 路径下,该模块就能被成功加载。
在 Linux 上设置环境变量
启动 Linux 的终端窗口,进入当前用户的 home 路径下,然后在 home 路径下输入如下命令:
ls - a

该命令将列出当前路径下所有的文件,包括隐藏文件。Linux 平台的环境变量是通过 .bash_profile 文件来设置的,使用无格式编辑器打开该文件,在该文件中添加 PYTHONPATH 环境变量。也就是为该文件增加如下一行:
#设置PYTHON PATH 环境变量
PYTHONPATH=.:/home/mengma/python_module

Linux 与 Windows 平台不一样,多个路径之间以冒号(:)作为分隔符,因此上面一行同样设置了两条路径,点(.)代表当前路径,还有一条路径是 /home/mengma/python_module(mengma 是在 Linux 系统的登录名)。

在完成了 PYTHONPATH 变量值的设置后,在 .bash_profile 文件的最后添加导出 PYTHONPATH 变量的语句。

1
2
#导出PYTHONPATH 环境变量
export PYTHONPATH

重新登录 Linux 平台,或者执行如下命令:

1
source.bash_profile

这两种方式都是为了运行该文件,使在文件中设置的 PYTHONPATH 变量值生效。

在成功设置了上面的环境变量之后,接下来只要把前面定义的模块(Python 程序)放在与当前所运行 Python 程序相同的路径中(或放在 /home/mengma/python_module 路径下),该模块就能被成功加载了。
在Mac OS X 上设置环境变量
在 Mac OS X 上设置环境变量与 Linux 大致相同(因为 Mac OS X 本身也是类 UNIX 系统)。启动 Mac OS X 的终端窗口(命令行界面),进入当前用户的 home 路径下,然后在 home 路径下输入如下命令:
ls -a

该命令将列出当前路径下所有的文件,包括隐藏文件。Mac OS X 平台的环境变量也可通过,bash_profile 文件来设置,使用无格式编辑器打开该文件,在该文件中添加 PYTHONPATH 环境变量。也就是为该文件增加如下一行:
#设置PYTHON PATH 环境变盘
PYTHONPATH=.:/Users/mengma/python_module

Mac OS X 的多个路径之间同样以冒号(:)作为分隔符,因此上面一行同样设置了两条路径:点(.)代表当前路径,还有一条路径是 /Users/mengma/python_module。

在完成了 PYTHONPATH 变量值的设置后,在 .bash_profile 文件的最后添加导出 PYTHONPATH 变量的语句。
#导出PYTHON PATH 环境变量
export PYTHONPATH

重新登录 Mac OS X 系统,或者执行如下命令:
source.bash_profile

这两种方式都是为了运行该文件,使在文件中设置的 PYTHONPATH 变量值生效。

在成功设置了上面的环境变量之后,接下来只要把前面定义的模块(Python 程序)放在与当前所运行 Python 程序相同的路径中(或放在 Users/mengma/python_module 路径下),该模块就能被成功加载了。

__all__变量

事实上,当我们向文件导入某个模块时,导入的是该模块中那些名称不以下划线(单下划线“_”或者双下划线“__”)开头的变量、函数和类。因此,如果我们不想模块文件中的某个成员被引入到其它文件中使用,可以在其名称前添加下划线。

1
2
3
4
5
6
7
8
9
10
11
12
#demo.py
def say():
print("人生苦短,我学Python!")
def CLanguage():
print("小明")
def disPython():
print("小红")
#test.py
from demo import *
say()
CLanguage()
disPython()

在此基础上,如果demo.py模块中的disPython()函数不想让其它文件引入,则只需将其名称改为_disPython()或者__disPython()。修改之后,再次执行test.py,其输出结果为:

1
2
3
4
5
6
人生苦短,我学Python!
小明:xiaoming
Traceback (most recent call last):
File "C:/Users/mengma/Desktop/2.py", line 4, in <module>
disPython()
NameError: name 'disPython' is not defined

显然,test.py文件中无法使用未引入的disPython()函数。

Python模块__all__变量

除此之外,还可以借助模块提供的__all__变量,该变量的值是一个列表,存储的是当前模块中一些成员(变量、函数或者类)的名称。通过在模块文件中设置__all__变量,当其它文件以from 模块名 import *的形式导入该模块时,该文件中只能使用__all__列表中指定的成员。
也就是说,只有以from 模块名 import *形式导入的模块,当该模块设有__all__变量时,只能导入该变量指定的成员,未指定的成员是无法导入的。

举个例子,修改 demo.py 模块文件中的代码:

1
2
3
4
5
6
7
def say():
print("人生苦短,我学Python!")
def CLanguage():
print("小明")
def disPython():
print("小红")
__all__ = ["say","CLanguage"]

可见,__all__变量只包含say()CLanguage()的函数名,不包含disPython()函数的名称。此时直接执行test.py文件,其执行结果为:

1
2
3
4
5
6
人生苦短,我学Python!
小明
Traceback (most recent call last):
File "C:/Users/mengma/Desktop/2.py", line 4, in <module>
disPython()
NameError: name 'disPython' is not defined

显然,对于 test.py 文件来说,demo.py 模块中的 disPython() 函数是未引入,这样调用是非法的。

再次声明,__all__变量仅限于在其它文件中以“from 模块名 import *”的方式引入。也就是说,如果使用以下 2 种方式引入模块,则__all__变量的设置是无效的。

  1. 以“import 模块名”的形式导入模块。通过该方式导入模块后,总可以通过模块名前缀(如果为模块指定了别名,则可以使用模快的别名作为前缀)来调用模块内的所有成员(除了以下划线开头命名的成员)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #demo.py
    def say():
    print("人生苦短,我学Python!")
    def CLanguage():
    print("小明")
    def disPython():
    print("小红")
    __all__ = ["say"]
    #test.py
    import demo
    demo.say()
    demo.CLanguage()
    demo.disPython()

    可以看到,虽然 demo.py 模块文件中设置有__all__变量,但是当以import demo的方式引入后,__all__变量将不起作用。

  2. from 模块名 import 成员的形式直接导入指定成员。使用此方式导入的模块,__all__变量即便设置,也形同虚设。

    1
    2
    3
    4
    5
    6
    from demo import say
    from demo import CLanguage
    from demo import disPython
    say()
    CLanguage()
    disPython()

实际开发中,一个大型的项目往往需要使用成百上千的 Python 模块,如果将这些模块都堆放在一起,势必不好管理。而且,使用模块可以有效避免变量名或函数名重名引发的冲突,但是如果模块名重复怎么办呢?因此,Python提出了包(Package)的概念。

简单理解,包就是文件夹,只不过在该文件夹下必须存在一个名为__init__.py的文件。

注意,这是 Python 2.x 的规定,而在 Python 3.x 中,__init__.py对包来说,并不是必须的。

每个包的目录下都必须建立一个__init__.py的模块,可以是一个空模块,可以写一些初始化代码,其作用就是告诉 Python 要将该目录当成包来处理。

注意,__init__.py不同于其他模块文件,此模块的模块名不是__init__,而是它所在的包名。例如,在settings包中的__init__.py文件,其模块名就是settings

包是一个包含多个模块的文件夹,它的本质依然是模块,因此包中也可以包含包。

Python 库:相比模块和包,库是一个更大的概念,例如在 Python 标准库中的每个库都有好多个包,而每个包中都有若干个模块。

创建包,导入包

包其实就是文件夹,更确切的说,是一个包含__init__.py文件的文件夹。因此,如果我们想手动创建一个包,只需进行以下 2 步操作:

  • 新建一个文件夹,文件夹的名称就是新建包的包名;
  • 在该文件夹中,创建一个__init__.py文件(前后各有 2 个下划线‘_’),该文件中可以不编写任何代码。当然,也可以编写一些 Python 初始化代码,则当有其它程序文件导入包时,会自动执行该文件中的代码。

例如,现在我们创建一个非常简单的包,该包的名称为my_package,可以仿照以上 2 步进行:

  • 创建一个文件夹,其名称设置为my_package
  • 在该文件夹中添加一个__init__.py文件,此文件中可以不编写任何代码。不过,这里向该文件编写如下代码:
    1
    2
    3
    4
    '''
    创建第一个 Python 包
    '''
    print('python')
    可以看到,__init__.py文件中,包含了 2 部分信息,分别是此包的说明信息和一条print输出语句。

由此,我们就成功创建好了一个 Python 包。

创建好包之后,我们就可以向包中添加模块(也可以添加包)。这里给my_package包添加 2 个模块,分别是module1.py、module2.py,各自包含的代码分别如下所示:

1
2
3
4
5
6
7
#module1.py模块文件
def display(arc):
print(arc)
#module2.py 模块文件
class CLanguage:
def display(self):
print("python")

现在,我们就创建好了一个具有如下文件结构的包:

1
2
3
4
my_package
┠── __init__.py
┠── module1.py
┗━━ module2.py

Python包的导入

包其实本质上还是模块,因此导入模块的语法同样也适用于导入包。无论导入我们自定义的包,还是导入从他处下载的第三方包,导入方法可归结为以下 3 种:

  • import 包名[.模块名 [as 别名]]
  • from 包名 import 模块名 [as 别名]
  • from 包名.模块名 import 成员名 [as 别名]

[]括起来的部分,是可选部分,即可以使用,也可以直接忽略。

注意,导入包的同时,会在包目录下生成一个含有__init__.cpython-36.pyc文件的__pycache__文件夹。

1) import 包名[.模块名 [as 别名]]

1
2
import my_package.module1
my_package.module1.display("java")

可以看到,通过此语法格式导入包中的指定模块后,在使用该模块中的成员(变量、函数、类)时,需添加“包名.模块名”为前缀。当然,如果使用as给包名.模块名”起一个别名的话,就使用直接使用这个别名作为前缀使用该模块中的方法了:

1
2
import my_package.module1 as module
module.display("python")

另外,当直接导入指定包时,程序会自动执行该包所对应文件夹下的__init__.py文件中的代码。

1
2
import my_package
my_package.module1.display("linux")

直接导入包名,并不会将包中所有模块全部导入到程序中,它的作用仅仅是导入并执行包下的__init__.py文件,因此,运行该程序,在执行__init__.py文件中代码的同时,还会抛出AttributeError异常(访问的对象不存在):

1
2
3
4
5
python
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 2, in <module>
my_package.module1.display("linux")
AttributeError: module 'my_package' has no attribute 'module1'

我们知道,包的本质就是模块,导入模块时,当前程序中会包含一个和模块名同名且类型为module的变量,导入包也是如此:

1
2
3
4
import my_package
print(my_package)
print(my_package.__doc__)
print(type(my_package))

运行结果为:

1
2
3
4
5
6
python
<module 'my_package' from 'C:\\Users\\mengma\\Desktop\\my_package\\__init__.py'>

创建第一个 Python 包

<class 'module'>

2) from 包名 import 模块名 [as 别名]

1
2
from my_package import module1
module1.display("golang")

运行结果为:

1
2
python
golang

可以看到,使用此语法格式导入包中模块后,在使用其成员时不需要带包名前缀,但需要带模块名前缀。

当然,我们也可以使用as为导入的指定模块定义别名:

1
2
from my_package import module1 as module
module.display("golang")

同样,既然包也是模块,那么这种语法格式自然也支持from 包名 import *这种写法,它和import包名 的作用一样,都只是将该包的__init__.py文件导入并执行。

3) from 包名.模块名 import 成员名 [as 别名]

此语法格式用于向程序中导入“包.模块”中的指定成员(变量、函数或类)。通过该方式导入的变量(函数、类),在使用时可以直接使用变量名(函数名、类名)调用:

1
2
from my_package.module1 import display
display("shell")

运行结果为:

1
2
python
shell

当然,也可以使用as为导入的成员起一个别名:

1
2
from my_package.module1 import display as dis
dis("shell")

该程序的运行结果和上面相同。

另外,在使用此种语法格式加载指定包的指定模块时,可以使用 * 代替成员名,表示加载该模块下的所有成员。

1
2
from my_package.module1 import *
display("python")

查看模块

查看模块成员:dir()函数

通过dir()函数,我们可以查看某指定模块包含的全部成员(包括变量、函数和类)。注意这里所指的全部成员,不仅包含可供我们调用的模块成员,还包含所有名称以双下划线“__”开头和结尾的成员,而这些“特殊”命名的成员,是为了在本模块中使用的,并不希望被其它文件调用。

1
2
import string
print(dir(string))

程序执行结果为:

1
['Formatter', 'Template', '_ChainMap', '_TemplateMetaclass', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '_re', '_string', 'ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace']

可以看到,通过dir()函数获取到的模块成员,不仅包含供外部文件使用的成员,还包含很多“特殊”(名称以 2 个下划线开头和结束)的成员,列出这些成员,对我们并没有实际意义。

1
2
import string
print([e for e in dir(string) if not e.startswith('_')])

程序执行结果为:

1
['Formatter', 'Template', 'ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace']

显然通过列表推导式,可在dir()函数输出结果的基础上,筛选出对我们有用的成员并显示出来。

查看模块成员:__all__变量

__all__变量也可以查看模块(包)内包含的所有成员。

1
2
import string
print(string.__all__)

程序执行结果为:

1
['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template']

dir()函数相比,__all__变量在查看指定模块成员时,它不会显示模块中的特殊成员,同时还会根据成员的名称进行排序显示。

不过需要注意的是,并非所有的模块都支持使用__all__变量,因此对于获取有些模块的成员,就只能使用dir()函数。

__doc__属性:查看文档

在使用dir()函数和__all__变量的基础上,虽然我们能知晓指定模块(或包)中所有可用的成员(变量、函数和类):

1
2
import string
print(string.__all__)

程序执行结果为:

1
['ascii_letters', 'ascii_lowercase', 'ascii_uppercase', 'capwords', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation', 'whitespace', 'Formatter', 'Template']

但对于以上的输出结果,对于不熟悉string模块的用户,还是不清楚这些名称分别表示的是什么意思,更不清楚各个成员有什么功能。

针对这种情况,我们可以使用help()函数来获取指定成员(甚至是该模块)的帮助信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#***__init__.py 文件中的内容***
from my_package.module1 import *
from my_package.module2 import *
#***module1.py 中的内容***
#module1.py模块文件
def display(arc):
'''
直接输出指定的参数
'''
print(arc)
#***module2.py中的内容***
#module2.py 模块文件
class CLanguage:
'''
CLanguage是一个类,其包含:
display() 方法
'''
def display(self):
print("http://c.biancheng.net/python/")

现在,我们先借助dir()函数,查看my_package包中有多少可供我们调用的成员:

1
2
import my_package
print([e for e in dir(my_package) if not e.startswith('_')])

程序输出结果为:

1
['CLanguage', 'display', 'module1', 'module2']

通过此输出结果可以得知,在my_package包中,有以上 4 个成员可供我们使用。接下来,我们使用help()函数来查看这些成员的具体含义(以 module1 为例):

1
2
import my_package
help(my_package.module1)

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
Help on module my_package.module1 in my_package:

NAME
my_package.module1 - #module1.py模块文件

FUNCTIONS
display(arc)
直接输出指定的参数

FILE
c:\users\mengma\desktop\my_package\module1.py

通过输出结果可以得知,module1实际上是一个模块文件,其包含display()函数,该函数的功能是直接输出指定的arc参数。同时,还显示出了该模块具体的存储位置。

值得一提的是,之所以我们可以使用help()函数查看具体成员的信息,是因为该成员本身就包含表示自身身份的说明文档(本质是字符串,位于该成员内部开头的位置)。无论是函数还是类,都可以使用__doc__属性获取它们的说明文档,模块也不例外。

1
2
import my_package
print(my_package.module1.display.__doc__)

其实,help()函数底层也是借助__doc__属性实现的。

如果使用help()函数或者__doc__属性,仍然无法满足我们的需求,还可以调用__file__属性,查看该模块或者包文件的具体存储位置,直接查看其源代码。

__file__属性:查看模块的源文件路径

当指定模块(或包)没有说明文档时,仅通过help()函数或者__doc__属性,无法有效帮助我们理解该模块(包)的具体功能。在这种情况下,我们可以通过__file__属性查找该模块(或包)文件所在的具体存储位置,直接查看其源代码。

1
2
import my_package
print(my_package.__file__) # C:\Users\mengma\Desktop\my_package\__init__.py

注意,因为当引入my_package包时,其实际上执行的是__init__.py文件,因此这里查看my_package包的存储路径,输出的__init__.py文件的存储路径。

1
2
import string
print(string.__file__) # D:\python3.6\lib\string.py

注意,并不是所有模块都提供__file__属性,因为并不是所有模块的实现都采用 Python 语言,有些模块采用的是其它编程语言。

当程序运行时,变量是保存数据的好方法,但变量、序列以及对象中存储的数据是暂时的,程序结束后就会丢失,如果希望程序结束后数据仍然保持,就需要将数据保存到文件中。Python 提供了内置的文件对象,以及对文件、目录进行操作的内置模块,通过这些技术可以很方便地将数据保存到文件(如文本文件等)中。

关于文件,它有两个关键属性,分别是“文件名”和“路径”。

在 Windows 上,路径书写使用反斜杠 “" 作为文件夹之间的分隔符。但在 OS X 和 Linux 上,使用正斜杠 “/“ 作为它们的路径分隔符。如果想要程序运行在所有操作系统上,在编写 Python 脚本时,就必须处理这两种情况。

好在,用os.path.join()函数来做这件事很简单。如果将单个文件和路径上的文件夹名称的字符串传递给它,os.path.join()就会返回一个文件路径的字符串,包含正确的路径分隔符。在交互式环境中输入以下代码:

1
2
3
>>> import os
>>> os.path.join('demo', 'exercise')
'demo\\exercise'

因为此程序是在 Windows 上运行的,所以os.path.join('demo', 'exercise')返回'demo\\exercise'(请注意,反斜杠有两个,因为每个反斜杠需要由另一个反斜杠字符来转义)。如果在 OS X 或 Linux 上调用这个函数,该字符串就会是'demo/exercise'

不仅如此,如果需要创建带有文件名称的文件存储路径,os.path.join() 函数同样很有用。例如,下面的例子将一个文件名列表中的名称,添加到文件夹名称的末尾:

1
2
3
4
import os
myFiles = ['accounts.txt', 'details.csv', 'invite.docx']
for filename in myFiles:
print(os.path.join('C:\\demo\\exercise', filename))

运行结果为:

1
2
3
C:\demo\exercise\accounts.txt
C:\demo\exercise\details.csv
C:\demo\exercise\invite.docx

绝对路径和相对路径

什么是当前工作目录

每个运行在计算机上的程序,都有一个“当前工作目录”(或cwd)。所有没有从根文件夹开始的文件名或路径,都假定在当前工作目录下。

在 Python 中,利用os.getcwd()函数可以取得当前工作路径的字符串,还可以利用os.chdir()改变它。

1
2
3
4
5
6
import os
os.getcwd()
'C:\\Users\\mengma\\Desktop'
os.chdir('C:\\Windows\\System32')
os.getcwd()
'C:\\Windows\\System32'

可以看到,原本当前工作路径为'C:\\Users\\mengma\\Desktop'(也就是桌面),通过os.chdir()函数,将其改成了'C:\\Windows\\System32'

需要注意的是,如果使用os.chdir()修改的工作目录不存在,Python 解释器会报错:

1
2
3
4
5
os.chdir('C:\\error')
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
os.chdir('C:\\error')
FileNotFoundError: [WinError 2] 系统找不到指定的文件。: 'C:\\error'

什么是绝对路径与相对路径

明确一个文件所在的路径,有 2 种表示方式,分别是:

  • 绝对路径:总是从根文件夹开始,Window 系统中以盘符(C:、D:)作为根文件夹,而 OS X 或者 Linux 系统中以/作为根文件夹。
  • 相对路径:指的是文件相对于当前工作目录所在的位置。例如,当前工作目录为"C:\Windows\System32",若文件demo.txt就位于这个 System32 文件夹下,则demo.txt的相对路径表示为".\demo.txt"(其中.\就表示当前所在目录)。

在使用相对路径表示某文件所在的位置时,除了经常使用.\表示当前所在目录之外,还会用到..\表示当前所在目录的父目录。

Python处理绝对路径和相对路径

Python os.path模块提供了一些函数,可以实现绝对路径和相对路径之间的转换,以及检查给定的路径是否为绝对路径,比如说:

  • 调用os.path.abspath(path)将返回 path 参数的绝对路径的字符串,这是将相对路径转换为绝对路径的简便方法。
  • 调用os.path.isabs(path),如果参数是一个绝对路径,就返回True,如果参数是一个相对路径,就返回False
  • 调用os.path.relpath(path, start)将返回从start路径到path的相对路径的字符串。如果没有提供start,就使用当前工作目录作为开始路径。
  • 调用os.path.dirname(path)将返回一个字符串,它包含path参数中最后一个斜杠之前的所有内容;调用os.path.basename(path)将返回一个字符串,它包含path参数中最后一个斜杠之后的所有内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> os.getcwd()
'C:\\Windows\\System32'
>>> os.path.abspath('.')
'C:\\Windows\\System32'
>>> os.path.abspath('.\\Scripts')
'C:\\Windows\\System32\\Scripts'
>>> os.path.isabs('.')
False
>>> os.path.isabs(os.path.abspath('.'))
True
>>> os.path.relpath('C:\\Windows', 'C:\\')
'Windows'
>>> os.path.relpath('C:\\Windows', 'C:\\spam\\eggs')
'..\\..\\Windows'
>>> path = 'C:\\Windows\\System32\\calc.exe'
>>> os.path.basename(path)
'calc.exe'
>>> os.path.dirname(path)
'C:\\Windows\\System32'

除此之外,如果同时需要一个路径的目录名称和基本名称,就可以调用os.path.split()获得这两个字符串的元组:

1
2
3
>>> path = 'C:\\Windows\\System32\\calc.exe'
>>> os.path.split(path)
('C:\\Windows\\System32', 'calc.exe')

注意,可以调用os.path.dirname()os.path.basename(),将它们的返回值放在一个元组中,从而得到同样的元组。但使用os.path.split()无疑是很好的快捷方式。

同时,如果提供的路径不存在,许多 Python 函数就会崩溃并报错,但好在os.path模块提供了以下函数用于检测给定的路径是否存在,以及它是文件还是文件夹:

  • 如果path参数所指的文件或文件夹存在,调用os.path.exists(path)将返回True,否则返回False
  • 如果path参数存在,并且是一个文件,调用os.path.isfile(path)将返回True,否则返回False
  • 如果path参数存在,并且是一个文件夹,调用os.path.isdir(path)将返回True,否则返回False
1
2
3
4
5
6
7
8
9
10
11
12
>>> os.path.exists('C:\\Windows')
True
>>> os.path.exists('C:\\some_made_up_folder')
False
>>> os.path.isdir('C:\\Windows\\System32')
True
>>> os.path.isfile('C:\\Windows\\System32')
False
>>> os.path.isdir('C:\\Windows\\System32\\calc.exe')
False
>>> os.path.isfile('C:\\Windows\\System32\\calc.exe')
True

文件基本操作

对文件的常见的操作包括创建、删除、修改权限、读取、写入等,这些操作可大致分为以下 2 类:

  • 删除、修改权限:作用于文件本身,属于系统级操作。
  • 写入、读取:是文件最常用的操作,作用于文件的内容,属于应用级操作。

其中,对文件的系统级操作可以借助 Python 中的专用模块(os、sys等),并调用模块中的指定函数来实现。例如,假设如下代码文件的同级目录中有一个文件a.txt,通过调用os模块中的remove函数,可以将该文件删除:

1
2
import os
os.remove("a.txt")

文件的应用级操作可以分为以下 3 步,每一步都需要借助对应的函数实现:

  • 打开文件:使用open()函数,该函数会返回一个文件对象;
  • 对已打开文件做读/写操作:读取文件内容可使用read()、readline()以及readlines()函数;向文件中写入内容,可以使用write()函数。
  • 关闭文件:完成对文件的读/写操作之后,最后需要关闭文件,可以使用close()函数。

一个文件,必须在打开之后才能对其进行操作,并且在操作结束之后,还应该将其关闭,这 3 步的顺序不能打乱。

open()函数:打开指定文件

如果想要操作文件,首先需要创建或者打开指定的文件,并创建一个文件对象,可以通过内置的open()函数实现。

open()函数用于创建或打开指定文件:

1
file = open(file_name [, mode='r' [ , buffering=-1 [ , encoding = None ]]])

此格式中,用[]括起来的部分为可选参数,即可以使用也可以省略。其中,各参数含义:

  • file:表示要创建的文件对象。
  • file_name:要创建或打开文件的文件名称,该名称要用引号(单引号或双引号都可以)括起来。需要注意的是,如果要打开的文件和当前执行的代码文件位于同一目录,则直接写文件名即可;否则,此参数需要指定打开文件所在的完整路径。
  • mode:可选参数,用于指定文件的打开模式。如果不写,则默认以只读(r)模式打开文件。
  • buffering:可选参数,用于指定对文件做读写操作时,是否使用缓冲区。
  • encoding:手动设定打开文件时所使用的编码格式,不同平台的ecoding参数值也不同,Windows 默认为 cp936(实际上就是 GBK 编码)。

open函数支持的文件打开模式:

模式 意义 注意事项
r 只读模式打开文件,读文件内容的指针会放在文件的开头。 操作的文件必须存在。
rb 以二进制格式、采用只读模式打开文件,读文件内容的指针位于文件的开头,一般用于非文本文件,如图片文件、音频文件等。 操作的文件必须存在。
r+ 打开文件后,既可以从头读取文件内容,也可以从开头向文件中写入新的内容,写入的新内容会覆盖文件中等长度的原有内容。 操作的文件必须存在。
rb+ 以二进制格式、采用读写模式打开文件,读写文件的指针会放在文件的开头,通常针对非文本文件(如音频文件)。 操作的文件必须存在。
w 以只写模式打开文件,若该文件存在,打开时会清空文件中原有的内容。 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
wb 以二进制格式、只写模式打开文件,一般用于非文本文件(如音频文件) 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
w+ 打开文件后,会对原有内容进行清空,并对该文件有读写权限。 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
wb+ 以二进制格式、读写模式打开文件,一般用于非文本文件 若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
a 以追加模式打开一个文件,对文件只有写入权限,如果文件已经存在,文件指针将放在文件的末尾(即新写入内容会位于已有内容之后);反之,则会创建新文件。
ab 以二进制格式打开文件,并采用追加模式,对文件只有写权限。如果该文件已存在,文件指针位于文件末尾(新写入文件会位于已有内容之后);反之,则创建新文件。
a+ 以读写模式打开文件;如果文件存在,文件指针放在文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件。
ab+ 以二进制模式打开文件,并采用追加模式,对文件具有读写权限,如果文件存在,则文件指针位于文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件。

文件打开模式,直接决定了后续可以对文件做哪些操作。例如,使用r模式打开的文件,后续编写的代码只能读取文件,而无法修改文件内容。

不同文件打开模式的功能:

1
2
3
#当前程序文件同目录下没有 a.txt 文件
file = open("a.txt")
print(file)

当以默认模式打开文件时,默认使用r权限,由于该权限要求打开的文件必须存在,因此运行此代码会报如下错误:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 1, in <module>
file = open("a.txt")
FileNotFoundError: [Errno 2] No such file or directory: 'a.txt'

现在,在程序文件同目录下,手动创建一个a.txt文件,并再次运行该程序,其运行结果为:

1
<_io.TextIOWrapper name='a.txt' mode='r' encoding='cp936'>

可以看到,当前输出结果中,输出了file文件对象的相关信息,包括打开文件的名称、打开模式、打开文件时所使用的编码格式。

使用open()打开文件时,默认采用 GBK 编码。但当要打开的文件不是 GBK 编码格式时,可以在使用open()函数时,手动指定打开文件的编码格式:

1
file = open("a.txt",encoding="utf-8")

注意,手动修改encoding参数的值,仅限于文件以文本的形式打开,也就是说,以二进制格式打开时,不能对encoding参数的值做任何修改,否则程序会抛出ValueError异常:

1
ValueError: binary mode doesn't take an encoding argument

open()是否需要缓冲区

通常情况下、建议在使用open()函数时打开缓冲区,即不需要修改buffing参数的值。

如果buffing参数的值为 0(或者False),则表示在打开指定文件时不使用缓冲区;如果buffing参数值为大于 1 的整数,该整数用于指定缓冲区的大小(单位是字节);如果buffing参数的值为负数,则代表使用默认的缓冲区大小。

目前为止计算机内存的 I/O 速度仍远远高于计算机外设(例如键盘、鼠标、硬盘等)的 I/O 速度,如果不使用缓冲区,则程序在执行 I/O 操作时,内存和外设就必须进行同步读写操作,也就是说,内存必须等待外设输入(输出)一个字节之后,才能再次输出(输入)一个字节。这意味着,内存中的程序大部分时间都处于等待状态。

而如果使用缓冲区,则程序在执行输出操作时,会先将所有数据都输出到缓冲区中,然后继续执行其它操作,缓冲区中的数据会有外设自行读取处理;同样,当程序执行输入操作时,会先等外设将数据读入缓冲区中,无需同外设做同步读写操作。

open()文件对象常用的属性

成功打开文件之后,可以调用文件对象本身拥有的属性获取当前文件的部分信息,其常见的属性为:

  • file.name:返回文件的名称;
  • file.mode:返回打开文件时,采用的文件打开模式;
  • file.encoding:返回打开文件时使用的编码格式;
  • file.closed:判断文件是否己经关闭。
1
2
3
4
5
6
7
8
9
10
# 以默认方式打开文件
f = open('my_file.txt')
# 输出文件是否已经关闭
print(f.closed) # False
# 输出访问模式
print(f.mode) # r
#输出编码格式
print(f.encoding) # cp936
# 输出文件名
print(f.name) # my_file.txt

注意,使用open()函数打开的文件对象,必须手动进行关闭,Python 垃圾回收机制无法自动回收打开文件所占用的资源。

read()函数:按字节(字符)读取文件

Python 提供了如下 3 种函数,它们都可以帮我们实现读取文件中数据的操作:

  • read()函数:逐个字节或者字符读取文件中的内容;
  • readline()函数:逐行读取文件中的内容;
  • readlines()函数:一次性读取文件中多行内容。

read()函数

对于借助open()函数,并以可读模式(包括r、r+、rb、rb+)打开的文件,可以调用read()函数逐个字节(或者逐个字符)读取文件中的内容。

如果文件是以文本模式(非二进制模式)打开的,则read()函数会逐个字符进行读取;反之,如果文件以二进制模式打开,则read()函数会逐个字节进行读取。

1
file.read([size])

其中,file表示已打开的文件对象;size作为一个可选参数,用于指定一次最多可读取的字符(字节)个数,如果省略,则默认一次性读取所有内容。

1
2
3
4
5
6
#以 utf-8 的编码格式打开指定文件
f = open("my_file.txt",encoding = "utf-8")
#输出读取到的数据
print(f.read()) # hello world!
#关闭文件
f.close()

当然,我们也可以通过使用size参数,指定read()每次可读取的最大字符(或者字节)数:

1
2
3
4
5
6
#以 utf-8 的编码格式打开指定文件
f = open("my_file.txt",encoding = "utf-8")
#输出读取到的数据
print(f.read(6)) # hello
#关闭文件
f.close()

显然,该程序中的read()函数只读取了my_file文件开头的 6 个字符。

再次强调,size表示的是一次最多可读取的字符(或字节)数,因此,即便设置的size大于文件中存储的字符(字节)数,read()函数也不会报错,它只会读取文件中所有的数据。

除此之外,对于以二进制格式打开的文件,read()函数会逐个字节读取文件中的内容。

1
2
3
4
5
6
#以二进制形式打开指定文件
f = open("my_file.txt",'rb+')
#输出读取到的数据
print(f.read()) # b'Python\xe6\x95\x99\xe7\xa8\x8b\r\n'
#关闭文件
f.close()

可以看到,输出的数据为bytes字节串。我们可以调用decode()方法,将其转换成我们认识的字符串。

另外需要注意的一点是,想使用read()函数成功读取文件内容,除了严格遵守read()的语法外,其还要求open()函数必须以可读默认(包括r、r+、rb、rb+)打开文件。举个例子,将上面程序中open()的打开模式改为w,程序会抛出io.UnsupportedOperation异常,提示文件没有读取权限:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\file.py", line 3, in <module>
print(f.read())
io.UnsupportedOperation: not readable

read()函数抛出UnicodeDecodeError异常的解决方法

在使用read()函数时,如果 Python 解释器提示UnicodeDecodeError异常,其原因在于,目标文件使用的编码格式和open()函数打开该文件时使用的编码格式不匹配。

举个例子,如果目标文件的编码格式为 GBK 编码,而我们在使用open()函数并以文本模式打开该文件时,手动指定encoding参数为 UTF-8。这种情况下,由于编码格式不匹配,当我们使用read()函数读取目标文件中的数据时,Python 解释器就会提示UnicodeDecodeError异常。

要解决这个问题,要么将open()函数中的encoding参数值修改为和目标文件相同的编码格式,要么重新生成目标文件(即将该文件的编码格式改为和open()函数中的encoding参数相同)。

除此之外,还有一种方法:先使用二进制模式读取文件,然后调用bytesdecode()方法,使用目标文件的编码格式,将读取到的字节串转换成认识的字符串。

1
2
3
4
5
6
7
8
9
10
#以二进制形式打开指定文件,该文件编码格式为 utf-8
f = open("my_file.txt",'rb+')
byt = f.read()
print(byt)
print("\n转换后:")
print(byt.decode('utf-8'))
#关闭文件
f.close()
程序执行结果为:
b'Python\xe6\x95\x99\xe7\xa8\x8b\r\n'

readline()和readlines()函数:按行读取文件

和 read() 函数不同,这 2 个函数都以“行”作为读取单位,即每次都读取目标文件中的一行。对于读取以文本格式打开的文件,读取一行很好理解;对于读取以二进制格式打开的文件,它们会以“\n”作为读取一行的标志。

readline()函数

readline()函数用于读取文件中的一行,包含最后的换行符\n

1
file.readline([size])

其中,file 为打开的文件对象;size 为可选参数,用于指定读取每一行时,一次最多读取的字符(字节)数。
和 read() 函数一样,此函数成功读取文件数据的前提是,使用 open() 函数指定打开文件的模式必须为可读模式(包括 r、rb、r+、rb+ 4 种)。

1
2
3
4
f = open("my_file.txt")
# 读取一行数据
byt = f.readline()
print(byt)

由于readline()函数在读取文件中一行的内容时,会读取最后的换行符\n,再加上print()函数输出内容时默认会换行,所以输出结果中会看到多出了一个空行。

不仅如此,在逐行读取时,还可以限制最多可以读取的字符(字节)数:

1
2
3
4
#以二进制形式打开指定文件
f = open("my_file.txt",'rb')
byt = f.readline(6)
print(byt)

和上一个例子的输出结果相比,由于这里没有完整读取一行的数据,因此不会读取到换行符。

readlines()函数

readlines()函数用于读取文件中的所有行,它和调用不指定size参数的read()函数类似,只不过该函数返回是一个字符串列表,其中每个元素为文件中的一行内容。

readline()函数一样,readlines()函数在读取每一行时,会连同行尾的换行符一块读取。

1
file.readlines()

其中,file为打开的文件对象。和read()、readline()函数一样,它要求打开文件的模式必须为可读模式(包括r、rb、r+、rb+4 种)。

1
2
3
4
5
f = open("my_file.txt",'rb')
byt = f.readlines()
print(byt)
运行结果为:
[b'Python\xbd\xcc\xb3\xcc\r\n', b'http://www.baidu.com']

write()和writelines():向文件中写入数据

Python 中的文件对象提供了write()函数,可以向文件中写入指定内容。

1
file.write(string)

其中,file表示已经打开的文件对象;string表示要写入文件的字符串(或字节串,仅适用写入二进制文件中)。

注意,在使用write()向文件中写入数据,需保证使用open()函数是以r+、w、w+、aa+的模式打开文件,否则执行write()函数会抛出io.UnsupportedOperation错误。

1
2
3
f = open("a.txt", 'w')
f.write("写入一行新数据")
f.close()

如果打开文件模式中包含w(写入),那么向文件中写入内容时,会先清空原文件中的内容,然后再写入新的内容。因此运行上面程序,再次打开a.txt文件,只会看到新写入的内容:

1
写入一行新数据

而如果打开文件模式中包含a(追加),则不会清空原有内容,而是将新写入的内容会添加到原内容后边。

因此,采用不同的文件打开模式,会直接影响write()函数向文件中写入数据的效果。

另外,在写入文件完成后,一定要调用close()函数将打开的文件关闭,否则写入的内容不会保存到文件中。例如,将上面程序中最后一行f.close()删掉,再次运行此程序并打开a.txt,你会发现该文件是空的。这是因为,当我们在写入文件内容时,操作系统不会立刻把数据写入磁盘,而是先缓存起来,只有调用close()函数时,操作系统才会保证把没有写入的数据全部写入磁盘文件中。

除此之外,如果向文件写入数据后,不想马上关闭文件,也可以调用文件对象提供的flush()函数,它可以实现将缓冲区的数据写入文件中。

1
2
3
f = open("a.txt", 'w')
f.write("写入一行新数据")
f.flush()

打开a.txt文件,可以看到写入的新内容:

1
写入一行新数据

通过设置open()函数的buffering参数可以关闭缓冲区,这样数据不就可以直接写入文件中了?对于以二进制格式打开的文件,可以不使用缓冲区,写入的数据会直接进入磁盘文件;但对于以文本格式打开的文件,必须使用缓冲区,否则 Python 解释器会ValueError错误。

1
2
f = open("a.txt", 'w',buffering = 0)
f.write("写入一行新数据")

运行结果为:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 1, in <module>
f = open("a.txt", 'w',buffering = 0)
ValueError: can't have unbuffered text I/O

writelines()函数

writelines()函数可以实现将字符串列表写入文件中。

注意,写入函数只有write()writelines()函数,而没有名为writeline的函数。

1
2
3
4
5
f = open('a.txt', 'r')
n = open('b.txt','w+')
n.writelines(f.readlines())
n.close()
f.close()

执行此代码,在a.txt文件同级目录下会生成一个b.txt文件,且该文件中包含的数据和a.txt完全一样。

需要注意的是,使用writelines()函数向文件中写入多行数据时,不会自动给各行添加换行符。上面例子中,之所以b.txt文件中会逐行显示数据,是因为readlines()函数在读取各行数据时,读入了行尾的换行符。

close()函数:关闭文件

close()函数是专门用来关闭已打开文件的:

1
file.close()

其中,file表示已打开的文件对象。

即使用open()函数打开的文件,在操作完成之后,一定要调用close()函数将其关闭,否则程序的运行可能出现问题。

1
2
3
4
import os
f = open("my_file.txt",'w')
#...
os.remove("my_file.txt")

remove()函数的功能是删除指定的文件。但是,如果运行此程序,Python 解释器会报如下错误:

1
2
3
4
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 4, in <module>
os.remove("my_file.txt")
PermissionError: [WinError 32] 另一个程序正在使用此文件,进程无法访问。: 'my_file.txt'

显然,由于我们使用了open()函数打开了my_file.txt文件,但没有及时关闭,直接导致后续的remove()函数运行出现错误。因此,正确的程序应该是这样的:

1
2
3
4
5
import os
f = open("my_file.txt",'w')
f.close()
#...
os.remove("my_file.txt")

当确定my_file.txt文件可以被删除时,再次运行程序,可以发现该文件已经被成功删除了。

再举个例子,如果我们不调用close()函数关闭已打开的文件,确定不影响读取文件的操作,但会导致write()或者writeline()函数向文件中写数据时,写入失败。

1
2
f = open("my_file.txt", 'w')
f.write("hello world")

程序执行后,虽然 Python 解释器不报错,但打开my_file.txt文件会发现,根本没有写入成功。这是因为,在向以文本格式(而不是二进制格式)打开的文件中写入数据时,Python 出于效率的考虑,会先将数据临时存储到缓冲区中,只有使用close()函数关闭文件时,才会将缓冲区中的数据真正写入文件中。

因此,在上面程序的最后添加如下代码:

1
f.close()

再次运行程序,就会看到"hello world"成功写入到了a.txt文件。

当然在某些实际场景中,我们可能需要在将数据成功写入到文件中,但并不想关闭文件。这也是可以实现的,调用flush()函数即可:

1
2
3
f = open("my_file.txt", 'w')
f.write("hello world")
f.flush()

打开my_file.txt文件,会发现已经向文件中成功写入了字符串hello world

seek()和tell()函数

使用 open() 函数打开文件并读取文件中的内容时,总是会从文件的第一个字符(字节)开始读起。那么,有没有办法可以自定指定读取的起始位置呢?答案是肯定,这就需要移动文件指针的位置。

文件指针用于标明文件读写的起始位置。假如把文件看成一个水流,文件中每个数据(以 b 模式打开,每个数据就是一个字节;以普通模式打开,每个数据就是一个字符)就相当于一个水滴,而文件指针就标明了文件将要从文件的哪个位置开始读起。图 1 简单示意了文件指针的概念。

文件指针概念示意图
图 1 文件指针概念示意图

可以看到,通过移动文件指针的位置,再借助 read() 和 write() 函数,就可以轻松实现,读取文件中指定位置的数据(或者向文件中的指定位置写入数据)。

注意,当向文件中写入数据时,如果不是文件的尾部,写入位置的原有数据不会自行向后移动,新写入的数据会将文件中处于该位置的数据直接覆盖掉。

实现对文件指针的移动,文件对象提供了 tell() 函数和 seek() 函数。tell() 函数用于判断文件指针当前所处的位置,而 seek() 函数用于移动文件指针到文件的指定位置。

tell() 函数

1
file.tell()

其中,file 表示文件对象。

例如,在同一目录下,编写如下程序对 a.txt 文件做读取操作,a.txt 文件中内容为:
http://c.biancheng.net

读取 a.txt 的代码如下:

1
2
3
4
5
6
7
8
f = open("a.txt",'r')
print(f.tell())
print(f.read(3))
print(f.tell())
运行结果为:
0
htt
3

可以看到,当使用 open() 函数打开文件时,文件指针的起始位置为 0,表示位于文件的开头处,当使用 read() 函数从文件中读取 3 个字符之后,文件指针同时向后移动了 3 个字符的位置。这就表明,当程序使用文件对象读写数据时,文件指针会自动向后移动:读写了多少个数据,文件指针就自动向后移动多少个位置。

seek()函数

seek() 函数用于将文件指针移动至指定位置,该函数的语法格式如下:

1
file.seek(offset[, whence])

其中,各个参数的含义如下:
file:表示文件对象;
whence:作为可选参数,用于指定文件指针要放置的位置,该参数的参数值有 3 个选择:0 代表文件头(默认值)、1 代表当前位置、2 代表文件尾。
offset:表示相对于 whence 位置文件指针的偏移量,正数表示向后偏移,负数表示向前偏移。例如,当whence == 0 &&offset == 3(即 seek(3,0) ),表示文件指针移动至距离文件开头处 3 个字符的位置;当whence == 1 &&offset == 5(即 seek(5,1) ),表示文件指针向后移动,移动至距离当前位置 5 个字符处。
注意,当 offset 值非 0 时,Python 要求文件必须要以二进制格式打开,否则会抛出 io.UnsupportedOperation 错误。

下面程序示范了文件指针操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
f = open('a.txt', 'rb')
# 判断文件指针的位置
print(f.tell())
# 读取一个字节,文件指针自动后移1个数据
print(f.read(1))
print(f.tell())
# 将文件指针从文件开头,向后移动到 5 个字符的位置
f.seek(5)
print(f.tell())
print(f.read(1))
# 将文件指针从当前位置,向后移动到 5 个字符的位置
f.seek(5, 1)
print(f.tell())
print(f.read(1))
# 将文件指针从文件结尾,向前移动到距离 2 个字符的位置
f.seek(-1, 2)
print(f.tell())
print(f.read(1))
运行结果为:
0
b'h'
1
5
b'/'
11
b'a'
21
b't'

注意:由于程序中使用 seek() 时,使用了非 0 的偏移量,因此文件的打开方式中必须包含 b,否则就会报 io.UnsupportedOperation 错误,有兴趣的读者可自定尝试。

上面程序示范了使用 seek() 方法来移动文件指针,包括从文件开头、指针当前位置、文件结尾处开始计算。运行上面程序,结合程序输出结果可以体会文件指针移动的效果。
# with as
任何一门编程语言中,文件的输入输出、数据库的连接断开等,都是很常见的资源管理操作。但资源都是有限的,在写程序时,必须保证这些资源在使用过后得到释放,不然就容易造成资源泄露,轻者使得系统处理缓慢,严重时会使系统崩溃。

例如,前面在介绍文件操作时,一直强调打开的文件最后一定要关闭,否则会程序的运行造成意想不到的隐患。但是,即便使用 close() 做好了关闭文件的操作,如果在打开文件或文件操作过程中抛出了异常,还是无法及时关闭文件。

为了更好地避免此类问题,不同的编程语言都引入了不同的机制。在 Python 中,对应的解决方式是使用 with as 语句操作上下文管理器(context manager),它能够帮助我们自动分配并且释放资源。
简单的理解,同时包含 enter() 和 exit() 方法的对象就是上下文管理器。常见构建上下文管理器的方式有 2 种,分别是基于类实现和基于生成器实现。

例如,使用 with as 操作已经打开的文件对象(本身就是上下文管理器),无论期间是否抛出异常,都能保证 with as 语句执行完毕后自动关闭已经打开的文件。

with as 语句的基本语法格式为:

1
2
with 表达式 [as target]:
代码块

此格式中,用 [] 括起来的部分可以使用,也可以省略。其中,target 参数用于指定一个变量,该语句会将 expression 指定的结果保存到该变量中。with as 语句中的代码块如果不想执行任何语句,可以直接使用 pass 语句代替。

1
2
3
4
5
with open('a.txt', 'a') as f:
f.write("\nPython教程")

运行结果为:
Python教程

可以看到,通过使用with as语句,即便最终没有关闭文件,修改文件内容的操作也能成功。
# pickle模块:实现Python对象的持久化存储
Python 中有个序列化过程叫作 pickle,它能够实现任意对象与文本之间的相互转化,也可以实现任意对象与二进制之间的相互转化。也就是说,pickle 可以实现 Python 对象的存储及恢复。

值得一提的是,pickle 是 python 语言的一个标准模块,安装 python 的同时就已经安装了 pickle 库,因此它不需要再单独安装,使用 import 将其导入到程序中,就可以直接使用。

pickle 模块提供了以下 4 个函数供我们使用:

  • dumps():将 Python 中的对象序列化成二进制对象,并返回;
  • loads():读取给定的二进制对象数据,并将其转换为 Python 对象;
  • dump():将 Python 中的对象序列化成二进制对象,并写入文件;
  • load():读取指定的序列化数据文件,并返回对象。

以上这 4 个函数可以分成两类,其中 dumps 和 loads 实现基于内存的 Python 对象与二进制互转;dump 和 load 实现基于文件的 Python 对象与二进制互转。

pickle.dumps()函数

此函数用于将 Python 对象转为二进制对象,其语法格式如下:

1
dumps(obj, protocol=None, *, fix_imports=True)

此格式中各个参数的含义为:
obj:要转换的 Python 对象;
protocol:pickle 的转码协议,取值为 0、1、2、3、4,其中 0、1、2 对应 Python 早期的版本,3 和 4 则对应 Python 3.x 版本及之后的版本。未指定情况下,默认为 3。
其它参数:为了兼容 Python 2.x 版本而保留的参数,Python 3.x 中可以忽略。

1
2
3
4
5
6
7
import pickle
tup1 = ('I love Python', {1,2,3}, None)
#使用 dumps() 函数将 tup1 转成 p1
p1 = pickle.dumps(tup1)
print(p1)
输出结果为:
b'\x80\x03X\r\x00\x00\x00I love Pythonq\x00cbuiltins\nset\nq\x01]q\x02(K\x01K\x02K\x03e\x85q\x03Rq\x04N\x87q\x05.'

pickle.loads()函数

此函数用于将二进制对象转换成 Python 对象:

1
loads(data, *, fix_imports=True, encoding='ASCII', errors='strict')

其中,data 参数表示要转换的二进制对象,其它参数只是为了兼容 Python 2.x 版本而保留的,可以忽略。

【例 2】在例 1 的基础上,将 p1 对象反序列化为 Python 对象。

1
2
3
4
5
6
7
8
import pickle
tup1 = ('I love Python', {1,2,3}, None)
p1 = pickle.dumps(tup1)
#使用 loads() 函数将 p1 转成 Python 对象
t2 = pickle.loads(p1)
print(t2)
运行结果为:
('I love Python', {1, 2, 3}, None)

注意,在使用 loads() 函数将二进制对象反序列化成 Python 对象时,会自动识别转码协议,所以不需要将转码协议当作参数传入。并且,当待转换的二进制对象的字节数超过 pickle 的 Python 对象时,多余的字节将被忽略。

pickle.dump()函数

此函数用于将 Python 对象转换成二进制文件:

1
dump (obj, file,protocol=None, *, fix mports=True)

其中各个参数的具体含义如下:
obj:要转换的 Python 对象。
file:转换到指定的二进制文件中,要求该文件必须是以”wb”的打开方式进行操作。
protocol:和 dumps() 函数中 protocol 参数的含义完全相同,因此这里不再重复描述。
其他参数:为了兼容以前 Python 2.x版本而保留的参数,可以忽略。

【例 3】将 tup1 元组转换成二进制对象文件。

1
2
3
4
5
import pickle
tup1 = ('I love Python', {1,2,3}, None)
#使用 dumps() 函数将 tup1 转成 p1
with open ("a.txt", 'wb') as f: #打开文件
pickle.dump(tup1, f) #用 dump 函数将 Python 对象转成二进制对象文件

运行完此程序后,会在该程序文件同级目录中,生成 a.txt 文件,但由于其内容为二进制数据,因此直接打开会看到乱码。

pickle.load()函数

此函数和 dump() 函数相对应,用于将二进制对象文件转换成 Python 对象。

1
load(file, *, fix_imports=True, encoding='ASCII', errors='strict')

其中,file 参数表示要转换的二进制对象文件(必须以 “rb” 的打开方式操作文件),其它参数只是为了兼容 Python 2.x 版本而保留的参数,可以忽略。

【例 4】将例 3 转换的 a.txt 二进制文件对象转换为 Python 对象。

1
2
3
4
5
6
7
8
9
10
import pickle
tup1 = ('I love Python', {1,2,3}, None)
#使用 dumps() 函数将 tup1 转成 p1
with open ("a.txt", 'wb') as f: #打开文件
pickle.dump(tup1, f) #用 dump 函数将 Python 对象转成二进制对象文件
with open ("a.txt", 'rb') as f: #打开文件
t3 = pickle.load(f) #将二进制文件对象转换成 Python 对象
print(t3)
运行结果为:
('I love Python', {1, 2, 3}, None)

总结

看似强大的 pickle 模块,其实也有它的短板,即 pickle 不支持并发地访问持久性对象,在复杂的系统环境下,尤其是读取海量数据时,使用 pickle 会使整个系统的I/O读取性能成为瓶颈。这种情况下,可以使用 ZODB。

ZODB 是一个健壮的、多用户的和面向对象的数据库系统,专门用于存储 Python 语言中的对象数据,它能够存储和管理任意复杂的 Python 对象,并支持事务操作和并发控制。并且,ZODB 也是在 Python 的序列化操作基础之上实现的,因此要想有效地使用 ZODB,必须先学好 pickle。

fileinput模块:逐行读取多个文件

我们学会了使用open()read()(或者readline()、readlines())组合,来读取单个文件中的数据。但在某些场景中,可能需要读取多个文件的数据,这种情况下,再使用这个组合,显然就不合适了。

庆幸的是,Python 提供了fileinput模块,通过该模块中的input()函数,我们能同时打开指定的多个文件,还可以逐个读取这些文件中的内容。

1
fileinput.input(files="filename1, filename2, ...", inplace=False, backup='', bufsize=0, mode='r', openhook=None)

此函数会返回一个 FileInput 对象,它可以理解为是将多个指定文件合并之后的文件对象。其中,各个参数的含义如下:
files:多个文件的路径列表;
inplace:用于指定是否将标准输出的结果写回到文件,此参数默认值为 False;
backup:用于指定备份文件的扩展名;
bufsize:指定缓冲区的大小,默认为 0;
mode:打开文件的格式,默认为 r(只读格式);
openhook:控制文件的打开方式,例如编码格式等。
注意,和 open() 函数不同,input() 函数不能指定打开文件的编码格式,这意味着使用该函数读取的所有文件,除非以二进制方式进行读取,否则该文件编码格式都必须和当前操作系统默认的编码格式相同,不然 Python 解释器可能会提示 UnicodeDecodeError 错误。

和 open() 函数返回单个的文件对象不同,fileinput 对象无需调用类似 read()、readline()、readlines() 这样的函数,直接通过 for 循环即可按次序读取多个文件中的数据。

值得一提的是,fileinput 模块还提供了很多使用的函数(如表 1 所示),通过调用这些函数,可以帮我们更快地实现想要的功能。

表 1 fileinput 模块常用函数
函数名 功能描述
fileinput.filename() 返回当前正在读取的文件名称。
fileinput.fileno() 返回当前正在读取文件的文件描述符。
fileinput.lineno() 返回当前读取了多少行。
fileinput.filelineno() 返回当前正在读取的内容位于当前文件中的行号。
fileinput.isfirstline() 判断当前读取的内容在当前文件中是否位于第 1 行。
fileinput.nextfile() 关闭当前正在读取的文件,并开始读取下一个文件。
fileinput.close() 关闭 FileInput 对象。
文件描述符是一个文件的代号,其值为一个整数。后续章节将会介绍关于文件描述符的操作。

假设使用 input() 读取 2 个文件,分别为 my_file.txt 和 file.txt,它们位于同一目录,且各自包含的内容如下所示:

1
2
3
4
5
6
7
#file.txt
Python教程
http://c.biancheng.net/python/

#my_file.txt
Linux教程
http://c.biancheng.net/linux_tutorial/

下面程序演示了如何使用 input() 函数依次读取这 2 个文件:

1
2
3
4
5
6
7
import fileinput
#使用for循环遍历 fileinput 对象
for line in fileinput.input(files=('my_file.txt', 'file.txt')):
# 输出读取到的内容
print(line)
# 关闭文件流
fileinput.close()

在使用 fileinput 模块中的 input() 函数之前,一定要先引入 fileinput 模块。

程序执行结果为:

1
2
3
4
5
6
Linux教程

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

http://c.biancheng.net/python/

显然,读取文件内容的次序,取决于 input() 函数中文件名的先后次序。

linecache模块用法:随机读取文件指定行

除了可以借助fileinput模块实现读取文件外,Python 还提供了linecache模块。和前者不同,linecache模块擅长读取指定文件中的指定行。换句话说,如果我们想读取某个文件中指定行包含的数据,就可以使用linecache模块。

值得一提的是,linecache模块常用来读取 Python 源文件中的代码,它使用的是 UTF-8 编码格式来读取文件内容。这意味着,使用该模块读取的文件,其编码格式也必须为 UTF-8,否则要么读取出来的数据是乱码,要么直接读取失败(Python 解释器会报 SyntaxError 异常)。

要使用linecache模块,就必须知道其包含了哪些函数。linecache 模块中常用的函数及其功能如表 1 所示。

表 1 linecache模块常用函数及功能
函数基本格式 功能
linecache.getline(filename, lineno, module_globals=None) 读取指定模块中指定文件的指定行(仅读取指定文件时,无需指定模块)。其中,filename 参数用来指定文件名,lineno 用来指定行号,module_globals 参数用来指定要读取的具体模块名。注意,当指定文件以相对路径的方式传给 filename 参数时,该函数以按照 sys.path 规定的路径查找该文件。
linecache.clearcache() 如果程序某处,不再需要之前使用 getline() 函数读取的数据,则可以使用该函数清空缓存。
linecache.checkcache(filename=None) 检查缓存的有效性,即如果使用 getline() 函数读取的数据,其实在本地已经被修改,而我们需要的是新的数据,此时就可以使用该函数检查缓存的是否为新的数据。注意,如果省略文件名,该函数将检车所有缓存数据的有效性。

1
2
3
4
5
6
import linecache
import string
#读取string模块中第 3 行的数据
print(linecache.getline(string.__file__, 3))
# 读取普通文件的第2行
print(linecache.getline('my_file.txt', 2))

在执行该程序之前,需保证 my_file.txt 文件是以 UTF-8 编码格式保存的(Python 提供的模块,通常编码格式为 UTF-8)。在此基础上,执行该程序,其输出结果为:

1
2
3
Public module variables:

http://c.biancheng.net/linux_tutorial/

pathlib模块

pathlib模块中包含的是一些类,它们的继承关系如图 1 所示。

图 1 pathlib模块中类的组织结构
图 1 中,箭头连接的是有继承关系的两个类,以 PurePosixPath 和 PurePath 类为例,PurePosizPath 继承自 PurePath,即前者是后者的子类。

pathlib 模块的操作对象是各种操作系统中使用的路径(例如指定文件位置的路径,包括绝对路径和相对路径)。这里简单介绍一下图 1 中包含的几个类的具体功能:
PurePath 类会将路径看做是一个普通的字符串,它可以实现将多个指定的字符串拼接成适用于当前操作系统的路径格式,同时还可以判断任意两个路径是否相等。注意,使用 PurePath 操作的路径,它并不会关心该路径是否真实有效。
PurePosixPath 和 PureWindowsPath 是 PurePath 的子类,前者用于操作 UNIX(包括 Mac OS X)风格的路径,后者用于操作 Windows 风格的路径。
Path 类和以上 3 个类不同,它操作的路径一定是真实有效的。Path 类提供了判断路径是否真实存在的方法。
PosixPath 和 WindowPath 是 Path 的子类,分别用于操作 Unix(Mac OS X)风格的路径和 Windows 风格的路径。
注意,UNIX 操作系统和 Windows 操作系统上,路径的格式是完全不同的,主要区别在于根路径和路径分隔符,UNIX 系统的根路径是斜杠(/),而 Windows 系统的根路径是盘符(C:);UNIX 系统路径使用的分隔符是斜杠(/),而 Windows 使用的是反斜杠(\)。

PurePath 类的用法

PurePath类(以及PurePosixPath类和PureWindowsPath类)都提供了大量的构造方法、实例方法以及类实例属性,供我们使用。

PurePath类构造方法

需要注意的是,在使用PurePath类时,考虑到操作系统的不同,如果在 UNIX 或 Mac OS X 系统上使用 PurePath 创建对象,该类的构造方法实际返回的是PurePosixPath对象;反之,如果在 Windows 系统上使用 PurePath 创建对象,该类的构造方法返回的是 PureWindowsPath 对象。
当然,我们完全可以直接使用PurePosixPath类或者PureWindowsPath类创建指定操作系统使用的类对象。

例如,在 Windows 系统上执行如下语句:

1
2
3
4
5
6
from pathlib import *
# 创建PurePath,实际上使用PureWindowsPath
path = PurePath('my_file.txt')
print(type(path))
程序执行结果为:
<class 'pathlib.PureWindowsPath'>

显然,在 Windows 操作系统上,使用 PurePath 类构造函数创建的是 PureWindowsPath 类对象。

除此之外,PurePath 在创建对象时,也支持传入多个路径字符串,它们会被拼接成一个路径格式的字符串。例如:

1
2
3
4
5
6
from pathlib import *
# 创建PurePath,实际上使用PureWindowsPath
path = PurePath('http:','c.biancheng.net','python')
print(path)
程序执行结果为:
http:\c.biancheng.net\python

可以看到,由于本机为 Windows 系统,因此这里输出的是适用于 Windows 平台的路径。如果想在 Windows 系统上输出 UNIX 风格的路径字符串,就需要使用 PurePosixPath 类。例如:

1
2
3
4
5
from pathlib import *
path = PurePosixPath('http:','c.biancheng.net','python')
print(path)
程序执行结果为:
http:/c.biancheng.net/python

值的一提的是,如果在使用 PurePath 类构造方法时,不传入任何参数,则等同于传入点‘.’(表示当前路径)作为参数。例如:

1
2
3
4
5
from pathlib import *
path = PurePath()
print(path) # .
path = PurePath('.')
print(path) # .

另外,如果传入 PurePath 构造方法中的多个参数中,包含多个根路径,则只会有最后一个根路径及后面的子路径生效。例如:

1
2
3
from pathlib import *
path = PurePath('C://','D://','my_file.txt')
print(path) # D:\my_file.txt

注意,对于 Windows 风格的路径,只有盘符(如 C、D等)才能算根路径。

需要注意的是,如果传给PurePath构造方法的参数中包含有多余的斜杠或者点(.,表示当前路径),会直接被忽略(..不会被忽略)。

1
2
3
from pathlib import *
path = PurePath('C://./my_file.txt')
print(path) # C:\my_file.txt

PurePath类还重载各种比较运算符,多余同种风格的路径字符串来说,可以判断是否相等,也可以比较大小(实际上就是比较字符串的大小);对于不同种风格的路径字符串之间,只能判断是否相等(显然,不可能相等),但不能比较大小。

1
2
3
4
5
from pathlib import *
# Unix风格的路径区分大小写
print(PurePosixPath('C://my_file.txt') == PurePosixPath('c://my_file.txt'))
# Windows风格的路径不区分大小写
print(PureWindowsPath('C://my_file.txt') == PureWindowsPath('c://my_file.txt'))

程序执行结果为:

1
2
False
True

比较特殊的是,PurePath类对象支持直接使用斜杠(/)作为多个字符串之间的连接符:

1
2
3
from pathlib import *
path = PurePosixPath('C://')
print(path / 'my_file.txt') # C:/my_file.txt

通过以上方式构建的路径,其本质上就是字符串,因此我们完全可以使用 str() 将 PurePath 对象转换成字符串。例如:

1
2
3
4
from pathlib import *
# Unix风格的路径区分大小写
path = PurePosixPath('C://','my_file.txt')
print(str(path)) # C:/my_file.txt

PurePath类实例属性和实例方法

常用的以下PurePath类实例方法和属性。由于从本质上讲,PurePath的操作对象是字符串,因此这些实例属性和实例方法,实质也是对字符串进行操作。

类实例属性和实例方法名 功能描述
PurePath.parts 返回路径字符串中所包含的各部分。
PurePath.drive 返回路径字符串中的驱动器盘符。
PurePath.root 返回路径字符串中的根路径。
PurePath.anchor 返回路径字符串中的盘符和根路径。
PurePath.parents 返回当前路径的全部父路径。
PurPath.parent 返回当前路径的上一级路径,相当于 parents[0] 的返回值。
PurePath.name 返回当前路径中的文件名。
PurePath.suffixes 返回当前路径中的文件所有后缀名。
PurePath.suffix 返回当前路径中的文件后缀名。相当于 suffixes 属性返回的列表的最后一个元素。
PurePath.stem 返回当前路径中的主文件名。
PurePath.as_posix() 将当前路径转换成 UNIX 风格的路径。
PurePath.as_uri() 将当前路径转换成 URL。只有绝对路径才能转换,否则将会引发 ValueError。
PurePath.is_absolute() 判断当前路径是否为绝对路径。
PurePath.joinpath(*other) 将多个路径连接在一起,作用类似于前面介绍的斜杠(/)连接符。
PurePath.match(pattern) 判断当前路径是否匹配指定通配符。
PurePath.relative_to(*other) 获取当前路径中去除基准路径之后的结果。
PurePath.with_name(name) 将当前路径中的文件名替换成新文件名。如果当前路径中没有文件名,则会引发 ValueError。
PurePath.with_suffix(suffix) 将当前路径中的文件后缀名替换成新的后缀名。如果当前路径中没有后缀名,则会添加新的后缀名。

Path类的功能和用法

PurPath类相比,Path类的最大不同,就是支持对路径的真实性进行判断。

PathPurePath的子类,因此Path类除了支持PurePath提供的各种构造函数、实例属性以及实例方法之外,还提供甄别路径字符串有效性的方法,甚至还可以判断该路径对应的是文件还是文件夹,如果是文件,还支持对文件进行读写等操作。

PurePath一样,Path同样有 2 个子类,分别为PosixPath(表示 UNIX 风格的路径)和WindowsPath(表示 Windows 风格的路径)。

os.path模块

相比pathlib模块,os.path模块不仅提供了一些操作路径字符串的方法,还包含一些或者指定文件属性的一些方法。os.path模块常用的属性和方法:

方法 说明
os.path.abspath(path) 返回 path 的绝对路径。
os.path.basename(path) 获取 path 路径的基本名称,即 path 末尾到最后一个斜杠的位置之间的字符串。
os.path.commonprefix(list) 返回 list(多个路径)中,所有 path 共有的最长的路径。
os.path.dirname(path) 返回 path 路径中的目录部分。
os.path.exists(path) 判断 path 对应的文件是否存在,如果存在,返回 True;反之,返回 False。和 lexists() 的区别在于,exists()会自动判断失效的文件链接(类似 Windows 系统中文件的快捷方式),而 lexists() 却不会。
os.path.lexists(path) 判断路径是否存在,如果存在,则返回 True;反之,返回 False。
os.path.expanduser(path) 把 path 中包含的 ““ 和 “user” 转换成用户目录。
os.path.expandvars(path) 根据环境变量的值替换 path 中包含的 “$name” 和 “${name}”。
os.path.getatime(path) 返回 path 所指文件的最近访问时间(浮点型秒数)。
os.path.getmtime(path) 返回文件的最近修改时间(单位为秒)。
os.path.getctime(path) 返回文件的创建时间(单位为秒,自 1970 年 1 月 1 日起(又称 Unix 时间))。
os.path.getsize(path) 返回文件大小,如果文件不存在就返回错误。
os.path.isabs(path) 判断是否为绝对路径。
os.path.isfile(path) 判断路径是否为文件。
os.path.isdir(path) 判断路径是否为目录。
os.path.islink(path) 判断路径是否为链接文件(类似 Windows 系统中的快捷方式)。
os.path.ismount(path) 判断路径是否为挂载点。
os.path.join(path1[, path2[, …]]) 把目录和文件名合成一个路径。
os.path.normcase(path) 转换 path 的大小写和斜杠。
os.path.normpath(path) 规范 path 字符串形式。
os.path.realpath(path) 返回 path 的真实路径。
os.path.relpath(path[, start]) 从 start 开始计算相对路径。
os.path.samefile(path1, path2) 判断目录或文件是否相同。
os.path.sameopenfile(fp1, fp2) 判断 fp1 和 fp2 是否指向同一文件。
os.path.samestat(stat1, stat2) 判断 stat1 和 stat2 是否指向同一个文件。
os.path.split(path) 把路径分割成 dirname 和 basename,返回一个元组。
os.path.splitdrive(path) 一般用在 windows 下,返回驱动器名和路径组成的元组。
os.path.splitext(path) 分割路径,返回路径名和文件扩展名的元组。
os.path.splitunc(path) 把路径分割为加载点与文件。
os.path.walk(path, visit, arg) 遍历path,进入每个目录都调用 visit 函数,visit 函数必须有 3 个参数(arg, dirname, names),dirname 表示当前目录的目录名,names 代表当前目录下的所有文件名,args 则为 walk 的第三个参数。
os.path.supports_unicode_filenames 设置是否可以将任意 Unicode 字符串用作文件名。
1
2
3
4
5
6
7
8
9
10
11
from os import path
# 获取绝对路径
print(path.abspath("my_file.txt"))
# 获取共同前缀
print(path.commonprefix(['C://my_file.txt', 'C://a.txt']))
# 获取共同路径
print(path.commonpath(['http://c.biancheng.net/python/', 'http://c.biancheng.net/shell/']))
# 获取目录
print(path.dirname('C://my_file.txt'))
# 判断指定目录是否存在
print(path.exists('my_file.txt'))

程序执行结果为:

1
2
3
4
5
C:\Users\mengma\Desktop\my_file.txt
C://
http:\c.biancheng.net
C://
True

fnmatch模块:用于文件名的匹配

fnmatch模块主要用于文件名称的匹配,其能力比简单的字符串匹配更强大,但比使用正则表达式相比稍弱。如果在数据处理操作中,只需要使用简单的通配符就能完成文件名的匹配,则使用fnmatch模块是不错的选择。

fnmatch模块常用函数及功能:

函数名 功能
fnmatch.filter(names, pattern) 对 names 列表进行过滤,返回 names 列表中匹配 pattern 的文件名组成的子集合。
fnmatch.fnmatch(filename, pattern) 判断 filename 文件名,是否和指定 pattern 字符串匹配
fnmatch.fnmatchcase(filename, pattern) 和 fnmatch() 函数功能大致相同,只是该函数区分大小写。
fnmatch.translate(pattern) 将一个 UNIX shell 风格的 pattern 字符串,转换为正则表达式

fnmatch模块匹配文件名的模式使用的就是 UNIX shell 风格,其支持使用如下几个通配符:

  • *:可匹配任意个任意字符。
  • ?:可匹配一个任意字符。
  • [字符序列]:可匹配中括号里字符序列中的任意字符。该字符序列也支持中画线表示法。比如[a-c]可代表 a、b 和 c 字符中任意一个。
  • [!字符序列]:可匹配不在中括号里字符序列中的任意字符。
1
2
3
4
5
6
7
8
9
10
11
import fnmatch
#filter()
print(fnmatch.filter(['dlsf', 'ewro.txt', 'te.py', 'youe.py'], '*.txt'))
#fnmatch()
for file in ['word.doc','index.py','my_file.txt']:
if fnmatch.fnmatch(file,'*.txt'):
print(file)
#fnmatchcase()
print([addr for addr in ['word.doc','index.py','my_file.txt','a.TXT'] if fnmatch.fnmatchcase(addr, '*.txt')])
#translate()
print(fnmatch.translate('a*b.txt'))

程序执行结果为:

1
2
3
4
['ewro.txt']
my_file.txt
['my_file.txt']
(?s:a.*b\.txt)\Z

tempfile模块:生成临时文件和临时目录

tempfile模块专门用于创建临时文件和临时目录。tempfile模块中常用的函数:

tempfile 模块函数 功能描述
tempfile.TemporaryFile(mode=’w+b’, buffering=None, encoding=None, newline=None, suffix=None, prefix=None, dir=None) 创建临时文件。该函数返回一个类文件对象,也就是支持文件 I/O。
tempfile.NamedTemporaryFile(mode=’w+b’, buffering=None, encoding=None, newline=None, suffix=None, prefix=None, dir=None, delete=True) 创建临时文件。该函数的功能与上一个函数的功能大致相同,只是它生成的临时文件在文件系统中有文件名。
tempfile.SpooledTemporaryFile(max_size=0, mode=’w+b’, buffering=None, encoding=None, newline=None, suffix=None, prefix=None, dir=None) 创建临时文件。与 TemporaryFile 函数相比,当程序向该临时文件输出数据时,会先输出到内存中,直到超过 max_size 才会真正输出到物理磁盘中。
tempfile.TemporaryDirectory(suffix=None, prefix=None, dir=None) 生成临时目录。
tempfile.gettempdir() 获取系统的临时目录。
tempfile.gettempdirb() 与 gettempdir() 相同,只是该函数返回字节串。
tempfile.gettempprefix() 返回用于生成临时文件的前缀名。
tempfile.gettempprefixb() 与 gettempprefix() 相同,只是该函数返回字节串。

提示:表中有些函数包含很多参数,但这些参数都具有自己的默认值,因此如果没有特殊要求,可以不对其传参。

tempfile模块还提供了tempfile.mkstemp()tempfile.mkdtemp()两个低级别的函数。上面介绍的 4 个用于创建临时文件和临时目录的函数都是高级别的函数,高级别的函数支持自动清理,而且可以与with语句一起使用,而这两个低级别的函数则不支持,因此一般推荐使用高级别的函数来创建临时文件和临时目录。

此外,tempfile模块还提供了tempfile.tempdir属性,通过对该属性赋值可以改变系统的临时目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import tempfile
# 创建临时文件
fp = tempfile.TemporaryFile()
print(fp.name)
fp.write('两情若是久长时,'.encode('utf-8'))
fp.write('又岂在朝朝暮暮。'.encode('utf-8'))
# 将文件指针移到开始处,准备读取文件
fp.seek(0)
print(fp.read().decode('utf-8')) # 输出刚才写入的内容
# 关闭文件,该文件将会被自动删除
fp.close()
# 通过with语句创建临时文件,with会自动关闭临时文件
with tempfile.TemporaryFile() as fp:
# 写入内容
fp.write(b'I Love Python!')
# 将文件指针移到开始处,准备读取文件
fp.seek(0)
# 读取文件内容
print(fp.read()) # b'I Love Python!'
# 通过with语句创建临时目录
with tempfile.TemporaryDirectory() as tmpdirname:
print('创建临时目录', tmpdirname)

上面程序以两种方式来创建临时文件:

  • 第一种方式是手动创建临时文件,读写临时文件后需要主动关闭它,当程序关闭该临时文件时,该文件会被自动删除。
  • 第二种方式则是使用with语句创建临时文件,这样with语句会自动关闭临时文件。

上面程序最后还创建了临时目录。由于程序使用with语句来管理临时目录,因此程序也会自动删除该临时目录。

运行上面程序,可以看到如下输出结果:

1
2
3
4
C:\Users\admin\AppData\Local\Temp\tmphvehw9z1
两情若是久长时,又岂在朝朝暮暮。
b'I Love Python!'
创建临时目录C:\Users\admin\AppData\Local\Temp\tmp3sjbnwob

上面第一行输出结果就是程序生成的临时文件的文件名,最后一行输出结果就是程序生成的临时目录的目录名。需要注意的是,不要去找临时文件或临时文件夹,因为程序退出时该临时文件和临时文件夹都会被删除。

Python函数和lambda表达式

函数的本质就是一段有特定功能、可以重复使用的代码,这段代码已经被提前编写好了,并且为其起一个“好听”的名字。在后续编写程序过程中,如果需要同样的功能,直接通过起好的名字就可以调用这段代码。

函数的定义

定义函数需要用def关键字实现:

1
2
3
def 函数名(参数列表):
# 实现特定功能的多行代码
[return [返回值]]

其中,用[]括起来的为可选择部分,即可以使用,也可以省略。

各部分参数的含义:

  • 函数名:一个符合 Python 语法的标识符。
  • 形参列表:设置该函数可以接收多少个参数,多个参数之间用逗号(,)分隔。
  • [return [返回值] ]:整体作为函数的可选参参数,用于设置该函数的返回值。也就是说,一个函数,可以用返回值,也可以没有返回值,是否需要根据实际情况而定。

注意,在创建函数时,即使函数不需要参数,也必须保留一对空的(),否则 Python 解释器将提示invaild syntax错误。另外,如果想定义一个没有任何功能的空函数,可以使用pass语句作为占位符。

1
2
3
4
5
6
7
#定义个空函数,没有实际意义
def pass_dis():
pass
#定义一个比较字符串大小的函数
def str_max(str1,str2):
str = str1 if str1 > str2 else str2
return str

函数中的return语句可以直接返回一个表达式的值,例如修改上面的str_max()函数:

1
2
def str_max(str1,str2):
return str1 if str1 > str2 else str2

该函数的功能,和上面的str_max()函数是完全一样的,只是省略了创建str变量,因此函数代码更加简洁。

函数的调用

调用函数也就是执行函数。

1
[返回值] = 函数名([形参值])

其中,函数名即指的是要调用的函数的名称;形参值指的是当初创建函数时要求传入的各个形参的值。如果该函数有返回值,我们可以通过一个变量来接收该值,当然也可以不接受。

需要注意的是,创建函数有多少个形参,那么调用时就需要传入多少个值,且顺序必须和创建函数时一致。即便该函数没有参数,函数名后的小括号也不能省略。

1
2
3
pass_dis()
strmax = str_max("python","shell")
print(strmax) # shell

首先,对于调用空函数来说,由于函数本身并不包含任何有价值的执行代码,也没有返回值,所以调用空函数不会有任何效果。

其次,对于上面程序中调用str_max()函数,由于当初定义该函数为其设置了 2 个参数,因此这里在调用该参数,就必须传入 2 个参数。同时,由于该函数内部还使用了return语句,因此我们可以使用strmax变量来接收该函数的返回值。

为函数提供说明文档

通过调用 Python 的help()内置函数或者__doc__属性,我们可以查看某个函数的使用说明文档。事实上,无论是 Python 提供给我们的函数,还是自定义的函数,其说明文档都需要设计该函数的程序员自己编写。

其实,函数的说明文档,本质就是一段字符串,只不过作为说明文档,字符串的放置位置是有讲究的,函数的说明文档通常位于函数内部、所有代码的最前面。

1
2
3
4
5
6
7
8
9
#定义一个比较字符串大小的函数
def str_max(str1,str2):
'''
比较 2 个字符串的大小
'''
str = str1 if str1 > str2 else str2
return str
help(str_max)
#print(str_max.__doc__)

程序执行结果为:

1
2
3
4
Help on function str_max in module __main__:

str_max(str1, str2)
比较 2 个字符串的大小

上面程序中,还可以使用__doc__属性来获取str_max()函数的说明文档,即使用最后一行的输出语句,其输出结果为:

1
比较 2 个字符串的大小

函数值传递和引用传递

通常情况下,定义函数时都会选择有参数的函数形式,函数参数的作用是传递数据给函数,令其对接收的数据做具体的操作处理。

在使用函数时,经常会用到形式参数(简称“形参”)和实际参数(简称“实参”),二者都叫参数,之间的区别是:

  • 形式参数:在定义函数时,函数名后面括号中的参数就是形式参数:
    1
    2
    3
    #定义函数时,这里的函数参数 obj 就是形式参数
    def demo(obj):
    print(obj)
  • 实际参数:在调用函数时,函数名后面括号中的参数称为实际参数,也就是函数的调用者给函数的参数。
    1
    2
    3
    a = "测试"
    #调用已经定义好的 demo 函数,此时传入的函数参数 a 就是实际参数
    demo(a)
    根据实际参数的类型不同,函数参数的传递方式可分为 2 种,分别为值传递和引用(地址)传递:
  • 值传递:适用于实参类型为不可变类型(字符串、数字、元组);
  • 引用(地址)传递:适用于实参类型为可变类型(列表,字典);

值传递和引用传递的区别是,函数参数进行值传递后,若形参的值发生改变,不会影响实参的值;而函数参数继续引用传递后,改变形参的值,实参的值也会一同改变。

例如,定义一个名为demo的函数,分别为传入一个字符串类型的变量(代表值传递)和列表类型的变量(代表引用传递):

1
2
3
4
5
6
7
8
9
10
11
12
13
def demo(obj) :
obj += obj
print("形参值为:",obj)
print("-------值传递-----")
a = "测试"
print("a的值为:",a)
demo(a)
print("实参值为:",a)
print("-----引用传递-----")
a = [1,2,3]
print("a的值为:",a)
demo(a)
print("实参值为:",a)

运行结果为:

1
2
3
4
5
6
7
8
-------值传递-----
a的值为: 测试
形参值为: 测试测试
实参值为: 测试
-----引用传递-----
a的值为: [1, 2, 3]
形参值为: [1, 2, 3, 1, 2, 3]
实参值为: [1, 2, 3, 1, 2, 3]

分析运行结果不难看出,在执行值传递时,改变形式参数的值,实际参数并不会发生改变;而在进行引用传递时,改变形式参数的值,实际参数也会发生同样的改变。

位置参数

位置参数,有时也称必备参数,指的是必须按照正确的顺序将实际参数传到函数中,换句话说,调用函数时传入实际参数的数量和位置都必须和定义函数时保持一致。

实参和形参数量必须一致

在调用函数,指定的实际参数的数量,必须和形式参数的数量一致(传多传少都不行),否则 Python 解释器会抛出TypeError异常,并提示缺少必要的位置参数。

1
2
3
4
5
6
7
8
9
10
def girth(width , height):
return 2 * (width + height)
#调用函数时,必须传递 2 个参数,否则会引发错误
print(girth(3))
'''
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 4, in <module>
print(girth(3))
TypeError: girth() missing 1 required positional argument: 'height'
'''

同样,多传参数也会抛出异常:

1
2
3
4
5
6
7
8
9
10
def girth(width , height):
return 2 * (width + height)
#调用函数时,必须传递 2 个参数,否则会引发错误
print(girth(3,2,4))
'''
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 4, in <module>
print(girth(3,2,4))
TypeError: girth() takes 2 positional arguments but 3 were given
'''

实参和形参位置必须一致

在调用函数时,传入实际参数的位置必须和形式参数位置一一对应,否则会产生以下 2 种结果:

1. 抛出TypeError异常

当实际参数类型和形式参数类型不一致,并且在函数中,这两种类型之间不能正常转换,此时就会抛出TypeError异常。

1
2
3
4
5
6
7
8
9
10
11
def area(height,width):
return height * width / 2
print(area("测试", 3))
'''
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 3, in <module>
print(area("测试",3))
File "C:\Users\mengma\Desktop\1.py", line 2, in area
return height*width/2
TypeError: unsupported operand type(s) for /: 'str' and 'int'
'''

以上显示的异常信息,就是因为字符串类型和整形数值做除法运算。

2. 产生的结果和预期不符

调用函数时,如果指定的实际参数和形式参数的位置不一致,但它们的数据类型相同,那么程序将不会抛出异常,只不过导致运行结果和预期不符。

例如,设计一个求梯形面积的函数,并利用此函数求上底为 4cm,下底为 3cm,高为 5cm 的梯形的面积。但如果交互高和下低参数的传入位置,计算结果将导致错误:

1
2
3
4
def area(upper_base, lower_bottom, height):
return (upper_base + lower_bottom) * height / 2
print("正确结果为:",area(4, 3, 5)) # 正确结果为: 17.5
print("错误结果为:",area(4, 5, 3)) # 错误结果为: 13.5

关键字参数

关键字参数是指使用形式参数的名字来确定输入的参数值。通过此方式指定函数实参时,不再需要与形参的位置完全一致,只要将参数名写正确即可。

1
2
3
4
5
6
7
8
def dis_str(str1, str2):
print("str1:", str1)
print("str2:", str2)
#位置参数
dis_str("python", "shell")
#关键字参数
dis_str("python", str2="shell")
dis_str(str2="python", str1="shell")

程序执行结果为:

1
2
3
4
5
6
str1: python
str2: shell
str1: python
str2: shell
str1: shell
str2: python

可以看到,在调用有参函数时,既可以根据位置参数来调用,也可以使用关键字参数(程序中第 8 行)来调用。在使用关键字参数调用时,可以任意调换参数传参的位置。

当然,还可以像第 7 行代码这样,使用位置参数和关键字参数混合传参的方式。但需要注意,混合传参时关键字参数必须位于所有的位置参数之后。

1
2
# 位置参数必须放在关键字参数之前,下面代码错误
dis_str(str1="python", "shell")

Python 解释器会报如下错误:

1
SyntaxError: positional argument follows keyword argument

默认参数

在调用函数时如果不指定某个参数,Python 解释器会抛出异常。为了解决这个问题,Python 允许为参数设置默认值,即在定义函数时,直接给形式参数指定一个默认值。这样的话,即便调用函数时没有给拥有默认值的形参传递参数,该参数可以直接使用定义函数时设置的默认值。

1
2
def 函数名(..., 形参名,形参名=默认值):
代码块

注意,在使用此格式定义函数时,指定有默认值的形式参数必须在所有没默认值参数的最后,否则会产生语法错误。

1
2
3
4
5
6
#str1没有默认参数,str2有默认参数
def dis_str(str1, str2 = "python"):
print("str1:",str1)
print("str2:",str2)
dis_str("shell")
dis_str("java","golang")

运行结果为:

1
2
3
4
str1: shell
str2: python
str1: java
str2: golang

当然在调用dis_str()函数时,也可以给所有的参数传值,这时即便str2有默认值,它也会优先使用传递给它的新值。

同时,结合关键字参数,以下 3 种调用dis_str()函数的方式也是可以的:

1
2
3
dis_str(str1 = "shell")
dis_str("java", str2 = "golang")
dis_str(str1 = "java", str2 = "golang")

再次强调,当定义一个有默认值参数的函数时,有默认值的参数必须位于所有没默认值参数的后面。因此,下面例子中定义的函数是不正确的:

1
2
3
#语法错误
def dis_str(str1="python", str2, str3):
pass

显然,str1设有默认值,而str2str3没有默认值,因此str1必须位于str2str3之后。

对于自己自定义的函数,可以轻易知道哪个参数有默认值,但如果使用 Python 提供的内置函数,又或者其它第三方提供的函数,怎么知道哪些参数有默认值呢?

Pyhton 中,可以使用函数名.__defaults__查看函数的默认值参数的当前值,其返回值是一个元组。

1
print(dis_str.__defaults__)

程序执行结果为:

1
('python',)

None(空值)

在 Python 中,有一个特殊的常量None。和False不同,它不表示 0,也不表示空字符串,而表示没有值,也就是空值。

这里的空值并不代表空对象,即None[]、''不同:

1
2
3
4
>>> None is []
False
>>> None is ""
False

None有自己的数据类型,我们可以在 IDLE 中使用type()函数查看它的类型:

1
2
type(None)
<class 'NoneType'>

可以看到,它属于NoneType类型。

需要注意的是,NoneNoneType数据类型的唯一值,也就是说,我们不能再创建其它NoneType类型的变量,但是可以将None赋值给任何变量。如果希望变量中存储的东西不与任何其它值混淆,就可以使用None

除此之外,None常用于assert、判断以及函数无返回值的情况。print()函数返回值就是None。因为它的功能是在屏幕上显示文本,根本不需要返回任何值,所以print()就返回None

1
2
3
4
>>> spam = print('Hello!')
Hello!
>>> None == spam
True

另外,对于所有没有return语句的函数定义,Python 都会在末尾加上return None,使用不带值的return语句(也就是只有return关键字本身),那么就返回None

return函数返回值

def语句创建函数时,可以用return语句指定应该返回的值,该返回值可以是任意类型。需要注意的是,return语句在同一函数中可以出现多次,但只要有一个得到执行,就会直接结束函数的执行。

1
return [返回值]

其中,返回值参数可以指定,也可以省略不写(将返回空值None)。

1
2
3
4
5
6
7
8
def add(a, b):
c = a + b
return c
#函数赋值给变量
c = add(3, 4)
print(c) # 7
#函数返回值作为其他函数的实际参数
print(add(3, 4)) # 7

本例中,add()函数既可以用来计算两个数的和,也可以连接两个字符串,它会返回计算的结果。

通过return语句指定返回值后,我们在调用函数时,既可以将该函数赋值给一个变量,用变量保存函数的返回值,也可以将函数再作为某个函数的实际参数。

1
2
3
4
5
6
7
def isGreater0(x):
if x > 0:
return True
else:
return False
print(isGreater0(5)) # True
print(isGreater0(0)) # False

可以看到,函数中可以同时包含多个return语句,但需要注意的是,最终真正执行的做多只有 1 个,且一旦执行,函数运行会立即结束。

以上实例中,我们通过return语句,都仅返回了一个值,但其实通过return语句,可以返回多个值。

变量作用域

所谓作用域,就是变量的有效范围,就是变量可以在哪个范围以内使用。有些变量可以在整段代码的任意位置使用,有些变量只能在函数内部使用,有些变量只能在for循环内部使用。

变量的作用域由变量的定义位置决定,在不同位置定义的变量,它的作用域是不一样的。

局部变量

在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,这样的变量称为局部变量。

当函数被执行时,Python 会为其分配一块临时的存储空间,所有在函数内部定义的变量,都会存储在这块空间中。而在函数执行完毕后,这块临时存储空间随即会被释放并回收,该空间中存储的变量自然也就无法再被使用。

1
2
3
4
5
def demo():
add = "python"
print("函数内部 add =",add)
demo()
print("函数外部 add =",add)

程序执行结果为:

1
2
3
4
5
函数内部 add = python
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\file.py", line 6, in <module>
print("函数外部 add =",add)
NameError: name 'add' is not defined

可以看到,如果试图在函数外部访问其内部定义的变量,Python 解释器会报NameError错误,并提示我们没有定义要访问的变量,这也证实了当函数执行完毕后,其内部定义的变量会被销毁并回收。

值得一提的是,函数的参数也属于局部变量,只能在函数内部使用。

1
2
3
4
5
6
def demo(name,add):
print("函数内部 name =",name)
print("函数内部 add =",add)
demo("Python教程","python")
print("函数外部 name =",name)
print("函数外部 add =",add)

程序执行结果为:

1
2
3
4
5
6
函数内部 name = Python教程
函数内部 add = python
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\file.py", line 7, in <module>
print("函数外部 name =",name)
NameError: name 'name' is not defined

由于 Python 解释器是逐行运行程序代码,由此这里仅提示给我“name 没有定义”,实际上在函数外部访问add变量也会报同样的错误。

全局变量

除了在函数内部定义变量,Python 还允许在所有函数的外部定义变量,这样的变量称为全局变量。

和局部变量不同,全局变量的默认作用域是整个程序,即全局变量既可以在各个函数的外部使用,也可以在各函数内部使用。

定义全局变量的方式有以下 2 种:

  • 在函数体外定义的变量,一定是全局变量:
    1
    2
    3
    4
    5
    add = "shell"
    def text():
    print("函数体内访问:",add)
    text()
    print('函数体外访问:',add)
    运行结果为:
    1
    2
    函数体内访问: shell
    函数体外访问: shell
  • 在函数体内定义全局变量。即使用global关键字对变量进行修饰后,该变量就会变为全局变量。
    1
    2
    3
    4
    5
    6
    def text():
    global add
    add= "java"
    print("函数体内访问:", add)
    text()
    print('函数体外访问:', add)
    运行结果为:
    1
    2
    函数体内访问: java
    函数体外访问: java

注意,在使用global关键字修饰变量名时,不能直接给变量赋初值,否则会引发语法错误。

获取指定作用域范围中的变量

在一些特定场景中,我们可能需要获取某个作用域内(全局范围内或者局部范围内)所有的变量,Python 提供了以下 3 种方式:

1) globals()函数

globals()函数为 Python 的内置函数,它可以返回一个包含全局范围内所有变量的字典,该字典中的每个键值对,键为变量名,值为该变量的值。

举个例子:

1
2
3
4
5
6
7
8
#全局变量
Pyname = "Python教程"
Pyadd = "python"
def text():
#局部变量
Shename = "shell教程"
Sheadd= "shell"
print(globals())

程序执行结果为:

1
{ ...... , 'Pyname': 'Python教程', 'Pyadd': 'python', ......}

注意,globals()函数返回的字典中,会默认包含有很多变量,这些都是 Python 主程序内置的。

可以看到,通过调用globals()函数,我们可以得到一个包含所有全局变量的字典。并且,通过该字典,我们还可以访问指定变量,甚至如果需要,还可以修改它的值。例如,在上面程序的基础上,添加如下语句:

1
2
3
print(globals()['Pyname'])
globals()['Pyname'] = "Python入门教程"
print(Pyname)

程序执行结果为:

1
2
Python教程
Python入门教程

2) locals()函数

locals()函数也是 Python 内置函数之一,通过调用该函数,我们可以得到一个包含当前作用域内所有变量的字典。这里所谓的“当前作用域”指的是,在函数内部调用locals()函数,会获得包含所有局部变量的字典;而在全局范文内调用locals()函数,其功能和globals()函数相同。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#全局变量
Pyname = "Python教程"
Pyadd = "python"
def text():
#局部变量
Shename = "shell教程"
Sheadd= "shell"
print("函数内部的 locals:")
print(locals())
text()
print("函数外部的 locals:")
print(locals())
程序执行结果为:
函数内部的 locals:
{'Sheadd': 'shell', 'Shename': 'shell教程'}
函数外部的 locals:
{...... , 'Pyname': 'Python教程', 'Pyadd': 'python', ...... }

当使用locals()函数获取所有全局变量时,和globals()函数一样,其返回的字典中会默认包含有很多变量,这些都是 Python 主程序内置的,读者暂时不用理会它们。

注意,当使用locals()函数获得所有局部变量组成的字典时,可以向globals()函数那样,通过指定键访问对应的变量值,但无法对变量值做修改。例如:

1
2
3
4
5
6
7
8
9
10
11
#全局变量
Pyname = "Python教程"
Pyadd = "http://c.biancheng.net/python/"
def text():
#局部变量
Shename = "shell教程"
Sheadd= "http://c.biancheng.net/shell/"
print(locals()['Shename'])
locals()['Shename'] = "shell入门教程"
print(Shename)
text()

程序执行结果为:

1
2
shell教程
shell教程

显然,locals()返回的局部变量组成的字典,可以用来访问变量,但无法修改变量的值。

3) vars(object)

vars()函数也是 Python 内置函数,其功能是返回一个指定object对象范围内所有变量组成的字典。如果不传入object参数,vars()locals()的作用完全相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 #全局变量
Pyname = "Python教程"
Pyadd = "python"
class Demo:
name = "Python 教程"
add = "python"
print("有 object:")
print(vars(Demo))
print("无 object:")
print(vars())
程序执行结果为:
object
{...... , 'name': 'Python 教程', 'add': 'python', ......}
object
{...... , 'Pyname': 'Python教程', 'Pyadd': 'python', ...... }

局部函数

Python 支持在函数内部定义函数,此类函数又称为局部函数。

和局部变量一样,默认情况下局部函数只能在其所在函数的作用域内使用。

1
2
3
4
5
6
7
8
9
#全局函数
def outdef ():
#局部函数
def indef():
print("test")
#调用局部函数
indef()
#调用全局函数
outdef()

就如同全局函数返回其局部变量,就可以扩大该变量的作用域一样,通过将局部函数作为所在函数的返回值,也可以扩大局部函数的使用范围。例如,修改上面程序为:

1
2
3
4
5
6
7
8
9
10
11
#全局函数
def outdef ():
#局部函数
def indef():
print("调用局部函数")
#调用局部函数
return indef
#调用全局函数
new_indef = outdef()
# 调用全局函数中的局部函数
new_indef()

因此,对于局部函数的作用域,可以总结为:如果所在函数没有返回局部函数,则局部函数的可用范围仅限于所在函数内部;反之,如果所在函数将局部函数作为返回值,则局部函数的作用域就会扩大,既可以在所在函数内部使用,也可以在所在函数的作用域中使用。

以上面程序中的outdef()indef()为例,如果outdef()不将indef作为返回值,则indef()只能在outdef()函数内部使用;反之,则indef()函数既可以在outdef()函数内部使用,也可以在outdef()函数的作用域,也就是全局范围内使用。

另外值得一提的是,如果局部函数中定义有和所在函数中变量同名的变量,也会发生“遮蔽”的问题。

1
2
3
4
5
6
7
8
9
10
#全局函数
def outdef ():
name = "所在函数中定义的 name 变量"
#局部函数
def indef():
print(name)
name = "局部函数中定义的 name 变量"
indef()
#调用全局函数
outdef()

执行此程序,Python 解释器会报如下错误:

1
UnboundLocalError: local variable 'name' referenced before assignment

此错误直译过来的意思是“局部变量 name 还没定义就使用”。导致该错误的原因就在于,局部函数indef()中定义的name变量遮蔽了所在函数outdef()中定义的name变量。再加上,indef()函数中name变量的定义位于print()输出语句之后,导致print(name)语句在执行时找不到定义的name变量,因此程序报错。

由于这里的name变量也是局部变量,因此globals()函数或者globals关键字,并不适用于解决此问题。这里可以使用 Python 提供的nonlocal关键字。

1
2
3
4
5
6
7
8
9
10
11
12
#全局函数
def outdef ():
name = "所在函数中定义的 name 变量"
#局部函数
def indef():
nonlocal name
print(name)
#修改name变量的值
name = "局部函数中定义的 name 变量"
indef()
#调用全局函数
outdef()

程序执行结果为:

1
所在函数中定义的 name 变量

闭包

闭包,又称闭包函数或者闭合函数,其实和前面讲的嵌套函数类似,不同之处在于,闭包中外部函数返回的不是一个具体的值,而是一个函数。一般情况下,返回的函数会赋值给一个变量,这个变量可以在后面被继续执行调用。

例如,计算一个数的n次幂,用闭包可以写成下面的代码:

1
2
3
4
5
6
7
8
9
#闭包函数,其中 exponent 称为自由变量
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of # 返回值是 exponent_of 函数
square = nth_power(2) # 计算一个数的平方
cube = nth_power(3) # 计算一个数的立方
print(square(2)) # 计算 2 的平方
print(cube(2)) # 计算 2 的立方

运行结果为:

1
2
4
8

在上面程序中,外部函数nth_power()的返回值是函数exponent_of(),而不是一个具体的数值。

需要注意的是,在执行完square = nth_power(2)cube = nth_power(3)后,外部函数nth_power()的参数exponent会和内部函数exponent_of一起赋值给squrecube,这样在之后调用square(2)或者cube(2)时,程序就能顺利地输出结果,而不会报错说参数exponent没有定义。

为什么要闭包呢?上面的程序,完全可以写成下面的形式:

1
2
def nth_power_rewrite(base, exponent):
return base ** exponent

上面程序确实可以实现相同的功能,不过使用闭包,可以让程序变得更简洁易读。设想一下,比如需要计算很多个数的平方,那么写成下面哪一种形式更好呢?

1
2
3
4
5
6
7
8
9
# 不使用闭包
res1 = nth_power_rewrite(base1, 2)
res2 = nth_power_rewrite(base2, 2)
res3 = nth_power_rewrite(base3, 2)
# 使用闭包
square = nth_power(2)
res1 = square(base1)
res2 = square(base2)
res3 = square(base3)

显然第二种方式表达更为简洁,在每次调用函数时,都可以少输入一个参数。

其次,和缩减嵌套函数的优点类似,函数开头需要做一些额外工作,当需要多次调用该函数时,如果将那些额外工作的代码放在外部函数,就可以减少多次调用导致的不必要开销,提高程序的运行效率。

Python闭包的__closure__属性

闭包比普通的函数多了一个__closure__属性,该属性记录着自由变量的地址。当闭包被调用时,系统就会根据该地址找到对应的自由变量,完成整体的函数调用。

nth_power()为例,当其被调用时,可以通过__closure__属性获取自由变量(也就是程序中的exponent参数)存储的地址:

1
2
3
4
5
6
7
def nth_power(exponent):
def exponent_of(base):
return base ** exponent
return exponent_of
square = nth_power(2)
#查看 __closure__ 的值
print(square.__closure__)

输出结果为:

1
(<cell at 0x0000014454DFA948: int object at 0x00000000513CC6D0>,)

可以看到,显示的内容是一个int整数类型,这就是square中自由变量exponent的初始值。还可以看到,__closure__属性的类型是一个元组,这表明闭包可以支持多个自由变量的形式。

lambda表达式(匿名函数)

对于定义一个简单的函数,Python 还提供了另外一种方法,即 lambda 表达式。

lambda 表达式,又称匿名函数,常用来表示内部仅包含 1 行表达式的函数。如果一个函数的函数体仅有 1 行表达式,则该函数就可以用 lambda 表达式来代替。

1
name = lambda [list] : 表达式

其中,定义 lambda 表达式,必须使用 lambda 关键字;[list]作为可选参数,等同于定义函数是指定的参数列表;value为该表达式的名称。

该语法格式转换成普通函数的形式,如下所示:

1
2
3
def name(list):
return 表达式
name(list)

显然,使用普通方法定义此函数,需要 3 行代码,而使用 lambda 表达式仅需 1 行。

举个例子,如果设计一个求 2 个数之和的函数,使用普通函数的方式,定义如下:

1
2
3
def add(x, y):
return x + y
print(add(3,4)) # 7

由于上面程序中,add()函数内部仅有 1 行表达式,因此该函数可以直接用 lambda 表达式表示:

1
2
add = lambda x,y:x+y
print(add(3,4)) # 7

可以这样理解 lambda 表达式,其就是简单函数(函数体仅是单行的表达式)的简写版本。相比函数,lamba 表达式具有以下 2 个优势:

  • 对于单行函数,使用 lambda 表达式可以省去定义函数的过程,让代码更加简洁;
  • 对于不需要多次复用的函数,使用 lambda 表达式可以在用完之后立即释放,提高程序执行的性能。

eval()和exec()函数

eval()exec()函数都属于 Python 的内置函数。

eval()exec()函数的功能是相似的,都可以执行一个字符串形式的 Python 代码(代码以字符串的形式提供),相当于一个 Python 的解释器。二者不同之处在于,eval()执行完要返回结果,而exec()执行完不返回结果。

eval()和exec()的用法

1
2
eval(expression, globals=None, locals=None, /)
exec(expression, globals=None, locals=None, /)

可以看到,二者的语法格式除了函数名,其他都相同,其中各个参数的具体含义如下:

  • expression:这个参数是一个字符串,代表要执行的语句 。该语句受后面两个字典类型参数globalslocals的限制,只有在globals字典和locals字典作用域内的函数和变量才能被执行。
  • globals:这个参数管控的是一个全局的命名空间,即expression可以使用全局命名空间中的函数。如果只是提供了globals参数,而没有提供自定义的__builtins__,则系统会将当前环境中的__builtins__复制到自己提供的globals中,然后才会进行计算;如果连globals这个参数都没有被提供,则使用 Python 的全局命名空间。
  • locals:这个参数管控的是一个局部的命名空间,和globals类似,当它和globals中有重复或冲突时,以locals的为准。如果locals没有被提供,则默认为globals

注意,__builtins__是 Python 的内建模块,平时使用的int、str、abs都在这个模块中。通过print(dic["__builtins__"])语句可以查看__builtins__所对应的value

首先,通过如下的例子来演示参数globals作用域的作用,注意观察它是何时将__builtins__复制globals字典中去的:

1
2
3
4
5
dic={} #定义一个字典
dic['b'] = 3 #在 dic 中加一条元素,key 为 b
print (dic.keys()) #先将 dic 的 key 打印出来,有一个元素 b
exec("a = 4", dic) #在 exec 执行的语句后面跟一个作用域 dic
print(dic.keys()) #exec 后,dic 的 key 多了一个

运行结果为:

1
2
dict_keys(['b'])
dict_keys(['b', '__builtins__', 'a'])

上面的代码是在作用域dic下执行了一句a = 4的代码。可以看出,exec()之前dic中的key只有一个b。执行完exec()之后,系统在dic中生成了两个新的key,分别是a__builtins__。其中,a为执行语句生成的变量,系统将其放到指定的作用域字典里;__builtins__是系统加入的内置key

locals参数的用法就很简单了:

1
2
3
4
5
6
a=10
b=20
c=30
g={'a':6, 'b':8} #定义一个字典
t={'b':100, 'c':10} #定义一个字典
print(eval('a+b+c', g, t)) #定义一个字典 116

输出结果为:

1
116

exec()和eval()的区别

它们的区别在于,eval()执行完会返回结果,而exec()执行完不返回结果。

1
2
3
4
5
6
7
a = 1
exec("a = 2") #相当于直接执行 a=2
print(a)
a = exec("2+3") #相当于直接执行 2+3,但是并没有返回值,a 应为 None
print(a)
a = eval('2+3') #执行 2+3,并把结果返回给 a
print(a)

运行结果为:

1
2
3
2
None
5

可以看出,exec()中最适合放置运行后没有结果的语句,而eval()中适合放置有结果返回的语句。

如果eval()里放置一个没有结果返回的语句会怎样呢?例如下面代码:

1
a= eval("a = 2")

这时 Python 解释器会报SyntaxError错误,提示eval()中不识别等号语法。

eval() 和 exec() 函数的应用场景

在使用 Python 开发服务端程序时,这两个函数应用得非常广泛。例如,客户端向服务端发送一段字符串代码,服务端无需关心具体的内容,直接跳过eval()exec()来执行,这样的设计会使服务端与客户端的耦合度更低,系统更易扩展。

需要注意的是,在使用eval()或是exec()来处理请求代码时,函数eval()exec()常常会被黑客利用,成为可以执行系统级命令的入口点,进而来攻击网站。解决方法是:通过设置其命名空间里的可执行函数,来限制eval()exec()的执行范围。

Python流程控制

if else条件语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if 表达式:
代码块

if 表达式:
代码块 1
else:
代码块 2

if 表达式 1:
代码块 1
elif 表达式 2:
代码块 2
elif 表达式 3:
代码块 3
...//其它elif语句
else:
代码块 n

if else 如何判断表达式是否成立

ifelif后面的“表达式”的形式是很自由的,只要表达式有一个结果,不管这个结果是什么类型,Python 都能判断它是“真”还是“假”。

布尔类型(bool)只有两个值,分别是TrueFalse,Python 会把True当做“真”,把False当做“假”。

对于数字,Python 会把 0 和 0.0 当做“假”,把其它值当做“真”。

对于其它类型,当对象为空或者为None时,Python 会把它们当做“假”,其它情况当做真。比如,下面的表达式都是不成立的:

1
2
3
4
5
""  #空字符串
[] #空列表
() #空元组
{} #空字典
None #空值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
b = False
if b:
print('b是True')
else:
print('b是False')

n = 0
if n:
print('n不是零值')
else:
print('n是零值')

s = ""
if s:
print('s不是空字符串')
else:
print('s是空字符串')

l = []
if l:
print('l不是空列表')
else:
print('l是空列表')

d = {}
if d:
print('d不是空字典')
else:
print('d是空字典')

def func():
print("函数被调用")
if func():
print('func()返回值不是空')
else:
print('func()返回值为空')

运行结果:

1
2
3
4
5
6
7
b是False
n是零值
s是空字符串
l是空列表
d是空字典
函数被调用
func()返回值为空

说明:对于没有return语句的函数,返回值为空,也即None

pass语句

1
2
3
4
5
6
7
8
9
10
11
age = int( input("请输入你的年龄:") )
if age < 12 :
print("婴幼儿")
elif age >= 12 and age < 18:
print("青少年")
elif age >= 18 and age < 30:
print("成年人")
elif age >= 30 and age < 50:
#TODO: 成年人
else:
print("老年人")

当年龄大于等于 30 并且小于 50 时,我们没有使用print()语句,而是使用了一个注释,希望以后再处理成年人的情况。当 Python 执行到该elif分支时,会跳过注释,什么都不执行。

但是 Python 提供了一种更加专业的做法,就是空语句passpass是 Python 中的关键字,用来让解释器跳过此处,什么都不做。

就像上面的情况,有时候程序需要占一个位置,或者放一条语句,但又不希望这条语句做任何事情,此时就可以通过pass语句来实现。使用pass语句比使用注释更加优雅。

1
2
3
4
5
6
7
8
9
10
11
age = int( input("请输入你的年龄:") )
if age < 12 :
print("婴幼儿")
elif age >= 12 and age < 18:
print("青少年")
elif age >= 18 and age < 30:
print("成年人")
elif age >= 30 and age < 50:
pass
else:
print("老年人")

assert断言函数

assert语句,又称断言语句,可以看做是功能缩小版的if语句,它用于判断某个表达式的值,如果值为真,则程序可以继续往下执行;反之,Python 解释器会报AssertionError错误。

1
assert 表达式

assert语句的执行流程可以用if判断语句表示:

1
2
3
4
if 表达式==True:
程序继续执行
else:
程序报 AssertionError 错误

明明assert会令程序崩溃,为什么还要使用它呢?这是因为,与其让程序在晚些时候崩溃,不如在错误条件出现时,就直接让程序崩溃,这有利于我们对程序排错,提高程序的健壮性。

因此,assert语句通常用于检查用户的输入是否符合规定,还经常用作程序初期测试和调试过程中的辅助工具。

1
2
3
4
5
mathmark = int(input())
#断言数学考试分数是否位于正常范围内
assert 0 <= mathmark <= 100
#只有当 mathmark 位于 [0,100]范围内,程序才会继续执行
print("数学考试分数为:", mathmark)

运行该程序,测试数据如下:

1
2
90
数学考试分数为: 90

再次执行该程序,测试数据为:

1
2
3
4
5
159
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\file.py", line 3, in <module>
assert 0 <= mathmark <= 100
AssertionError

可以看到,当assert语句后的表达式值为真时,程序继续执行;反之,程序停止执行,并报AssertionError错误。

while循环语句

1
2
while 条件表达式:
代码块

for循环

1
2
for 迭代变量 in 字符串|列表|元组|字典|集合:
代码块
1
2
3
4
5
6
7
print("计算 1+2+...+100 的结果为:")
#保存累加结果的变量
result = 0
#逐个获取从 1 到 100 这些值,并做累加操作
for i in range(101):
result += i
print(result)
1
2
3
my_list = [1,2,3,4,5]
for ele in my_list:
print('ele =', ele)

程序执行结果为:

1
2
3
4
5
ele = 1
ele = 2
ele = 3
ele = 4
ele = 5

循环结构中else用法

Python 中,无论是while循环还是for循环,其后都可以紧跟着一个else代码块,它的作用是当循环条件为False跳出循环时,程序会最先执行else代码块中的代码。

1
2
3
4
5
6
7
add = "python"
i = 0
while i < len(add):
print(add[i],end="")
i = i + 1
else:
print("\n执行 else 代码块")

程序执行结果为:

1
2
python
执行 else 代码块

上面程序中,当i==len(add)结束循环时(确切的说,是在结束循环之前),Python 解释器会执行while循环后的else代码块。

修改上面程序,去掉else代码块:

1
2
3
4
5
6
7
add = "python"
i = 0
while i < len(add):
print(add[i],end="")
i = i + 1
#原本位于 else 代码块中的代码
print("\n执行 else 代码块")

程序执行结果为:

1
2
python
执行 else 代码块

那么,else代码块真的没有用吗?当然不是。

当然,我们也可以为for循环添加一个else代码块:

1
2
3
4
5
add = "python"
for i in add:
print(i,end="")
else:
print("\n执行 else 代码块")

程序执行结果为:

1
2
python
执行 else 代码块

break

在执行while循环或者for循环时,只要循环条件满足,程序将会一直执行循环体,不停地转圈。但在某些场景,我们可能希望在循环结束前就强制结束循环,Python 提供了 2 种强制离开当前循环体的办法:

  • 使用continue语句,可以跳过执行本次循环体中剩余的代码,转而执行下一次的循环。
  • 使用break语句,可以完全终止当前循环。

break语句可以立即终止当前循环的执行,跳出当前所在的循环结构。无论是while循环还是for循环,只要执行break语句,就会直接结束当前正在执行的循环体。

break语句的语法非常简单,只需要在相应whilefor语句中直接加入即可。

1
2
3
4
5
6
7
8
add = "python,shell"
# 一个简单的for循环
for i in add:
if i == ',' :
#终止循环
break
print(i,end="")
print("\n执行循环体外的代码")

运行结果为:

1
2
python
执行循环体外的代码

分析上面程序不难看出,当循环至add字符串中的逗号(,)时,程序执行break语句,其会直接终止当前的循环,跳出循环体。

break语句一般会结合if语句进行搭配使用,表示在某种条件下跳出循环体。

注意,for循环后也可以配备一个else语句。这种情况下,如果使用break语句跳出循环体,不会执行else中包含的代码。

1
2
3
4
5
6
7
8
9
add = "python,shell"
for i in add:
if i == ',' :
#终止循环
break
print(i,end="")
else:
print("执行 else 语句中的代码")
print("\n执行循环体外的代码")

程序执行结果为:

1
2
python
执行循环体外的代码

从输出结果可以看出,使用break跳出当前循环体之后,该循环后的else代码块也不会被执行。但是,如果将else代码块中的代码直接放在循环体的后面,则该部分代码将会被执行。

另外,对于嵌套的循环结构来说,break语句只会终止所在循环体的执行,而不会作用于所有的循环体。

1
2
3
4
5
6
7
add = "python,shell"
for i in range(3):
for j in add:
if j == ',':
break
print(j,end="")
print("\n跳出内循环")

程序执行结果为:

1
2
3
4
5
6
python
跳出内循环
python
跳出内循环
python
跳出内循环

分析上面程序,每当执行内层循环时,只要循环至add字符串中的逗号(,)就会执行break语句,它会立即停止执行当前所在的内存循环体,转而继续执行外层循环。

在嵌套循环结构中,如何同时跳出内层循环和外层循环呢?最简单的方法就是借用一个bool类型的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add = "python,shell"
#提前定义一个 bool 变量,并为其赋初值
flag = False
for i in range(3):
for j in add:
if j == ',':
#在 break 前,修改 flag 的值
flag = True
break
print(j,end="")
print("\n跳出内循环")
#在外层循环体中再次使用 break
if flag == True:
print("跳出外层循环")
break

可以看到,通过借助一个bool类型的变量flag,在跳出内循环时更改flag的值,同时在外层循环体中,判断flag的值是否发生改动,如有改动,则再次执行break跳出外层循环;反之,则继续执行外层循环。

因此,上面程序的执行结果为:

1
2
3
python
跳出内循环
跳出外层循环

当然,这里仅跳出了 2 层嵌套循环,此方法支持跳出多层嵌套循环。

continue

break语句相比,continue语句的作用则没有那么强大,它只会终止执行本次循环中剩下的代码,直接从下一次循环继续执行。

continue语句的用法和break语句一样,只要whilefor语句中的相应位置加入即可。

1
2
3
4
5
6
7
8
add = "python,shell"
# 一个简单的for循环
for i in add:
if i == ',' :
# 忽略本次循环的剩下语句
print('\n')
continue
print(i,end="")

运行结果:

1
2
python
shell

可以看到,当遍历add字符串至逗号(,)时,会进入if判断语句执行print()语句和continue语句。其中,print()语句起到换行的作用,而continue语句会使 Python 解释器忽略执行第 8 行代码,直接从下一次循环开始执行。

zip函数

zip()函数是 Python 内置函数之一,它可以将多个序列(列表、元组、字典、集合、字符串以及range()区间构成的列表)“压缩”成一个zip对象。所谓“压缩”,其实就是将这些序列中对应位置的元素重新组合,生成一个个新的元组。

1
zip(iterable, ...)

其中iterable,...表示多个列表、元组、字典、集合、字符串,甚至还可以为range()区间。

1
2
3
4
5
6
7
8
9
my_list = [11,12,13]
my_tuple = (21,22,23)
print([x for x in zip(my_list,my_tuple)])
my_dic = {31:2,32:4,33:5}
my_set = {41,42,43,44}
print([x for x in zip(my_dic)])
my_pychar = "python"
my_shechar = "shell"
print([x for x in zip(my_pychar,my_shechar)])

程序执行结果为:

1
2
3
[(11, 21), (12, 22), (13, 23)]
[(31,), (32,), (33,)]
[('p', 's'), ('y', 'h'), ('t', 'e'), ('h', 'l'), ('o', 'l')]

在使用zip()函数“压缩”多个序列时,它会分别取各序列中第 1 个元素、第 2 个元素、… 第n个元素,各自组成新的元组。需要注意的是,当多个序列中元素个数不一致时,会以最短的序列为准进行压缩。

另外,对于zip()函数返回的zip对象,既可以像上面程序那样,通过遍历提取其存储的元组,也可以向下面程序这样,通过调用list()函数将zip()对象强制转换成列表:

1
2
3
my_list = [11,12,13]
my_tuple = (21,22,23)
print(list(zip(my_list,my_tuple))) # [(11, 21), (12, 22), (13, 23)]

reversed函数

reserved()是 Pyton 内置函数之一,其功能是对于给定的序列(包括列表、元组、字符串以及range(n)区间),该函数可以返回一个逆序序列的迭代器(用于遍历该逆序序列)。

1
reversed(seq)

其中,seq可以是列表,元素,字符串以及range()生成的区间列表。

1
2
3
4
5
6
7
8
#将列表进行逆序
print([x for x in reversed([1,2,3,4,5])]) # [5, 4, 3, 2, 1]
#将元组进行逆序
print([x for x in reversed((1,2,3,4,5))]) # [5, 4, 3, 2, 1]
#将字符串进行逆序
print([x for x in reversed("abcdefg")]) # ['g', 'f', 'e', 'd', 'c', 'b', 'a']
#将 range() 生成的区间列表进行逆序
print([x for x in reversed(range(10))]) # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

除了使用列表推导式的方式,还可以使用list()函数,将reversed()函数逆序返回的迭代器,直接转换成列表。

1
2
#将列表进行逆序
print(list(reversed([1,2,3,4,5]))) # [5, 4, 3, 2, 1]

再次强调,使用reversed()函数进行逆序操作,并不会修改原来序列中元素的顺序:

1
2
3
4
a = [1,2,3,4,5]
#将列表进行逆序
print(list(reversed(a))) # [5, 4, 3, 2, 1]
print("a=",a) # a=[1, 2, 3, 4, 5]

sorted函数

sorted()作为 Python 内置函数之一,其功能是对序列(列表、元组、字典、集合、还包括字符串)进行排序。

1
list = sorted(iterable, key=None, reverse=False)  

其中,iterable表示指定的序列,key参数可以自定义排序规则;reverse参数指定以升序(False,默认)还是降序(True)进行排序。sorted()函数会返回一个排好序的列表。

注意,key参数和reverse参数是可选参数,即可以使用,也可以忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#对列表进行排序
a = [5,3,4,2,1]
print(sorted(a)) # [1, 2, 3, 4, 5]
#对元组进行排序
a = (5,4,3,1,2)
print(sorted(a)) # [1, 2, 3, 4, 5]
#字典默认按照key进行排序
a = {4:1, 5:2, 3:3, 2:6, 1:8}
print(sorted(a.items())) # [(1, 8), (2, 6), (3, 3), (4, 1), (5, 2)]
#对集合进行排序
a = {1,5,3,2,4}
print(sorted(a)) # [1, 2, 3, 4, 5]
#对字符串进行排序
a = "51423"
print(sorted(a)) # ['1', '2', '3', '4', '5']

再次强调,使用sorted()函数对序列进行排序,并不会在原序列的基础进行修改,而是会重新生成一个排好序的列表。

1
2
3
4
5
#对列表进行排序
a = [5,3,4,2,1]
print(sorted(a)) # [1, 2, 3, 4, 5]
#再次输出原来的列表 a
print(a) # [5, 3, 4, 2, 1]

显然,sorted()函数不会改变所传入的序列,而是返回一个新的、排序好的列表。

除此之外,sorted()函数默认对序列中元素进行升序排序,通过手动将其reverse参数值改为True,可实现降序排序。

1
2
3
#对列表进行排序
a = [5,3,4,2,1]
print(sorted(a, reverse=True)) # [5, 4, 3, 2, 1]

另外在调用sorted()函数时,还可传入一个key参数,它可以接受一个函数,该函数的功能是指定sorted()函数按照什么标准进行排序。

1
2
3
4
5
chars=['python', 'shell', 'java', 'golang']
#默认排序
print(sorted(chars)) # ['golang', 'java', 'python', 'shell']
#自定义按照字符串长度排序
print(sorted(chars,key=lambda x:len(x))) # ['java', 'shell', 'python', 'golang']

Python字符串常用方法

字符串拼接

拼接(连接)字符串可以直接将两个字符串紧挨着写在一起:

1
strname = "str1" "str2"

strname表示拼接以后的字符串变量名,str1str2是要拼接的字符串内容。使用这种写法,Python 会自动将两个字符串拼接在一起。

需要注意的是,这种写法只能拼接字符串常量。如果需要使用变量,就得借助+运算符来拼接:

1
strname = str1 + str2

当然,+运算符也能拼接字符串常量。

字符串和数字的拼接

Python 不允许直接拼接数字和字符串,所以必须先将数字转换成字符串。可以借助str()repr()函数将数字转换为字符串:

1
2
str(obj)
repr(obj)

obj表示要转换的对象,它可以是数字、列表、元组、字典等多种类型的数据。

1
2
3
4
5
name = "小明"
age = 8
course = 3
info = name + "已经" + str(age) + "岁了,已经上" + repr(course) + "年级了。"
print(info) # 小明已经8岁了,已经上3年级了。

str() 和 repr() 的区别

str()repr()函数虽然都可以将数字转换成字符串,但它们之间是有区别的:

  • str()用于将数据转换成适合人类阅读的字符串形式。
  • repr()用于将数据转换成适合解释器阅读的字符串形式(Python 表达式的形式),适合在开发和调试阶段使用;如果没有等价的语法,则会发生SyntaxError异常。
1
2
3
4
5
6
7
s = "http://www.baidu.com/test/"
s_str = str(s)
s_repr = repr(s)
print(type(s_str)) # <class 'str'>
print (s_str) # http://www.baidu.com/test/
print(type(s_repr)) # <class 'str'>
print (s_repr) # 'http://www.baidu.com/test/'

本例中,s本身就是一个字符串,但是我们依然使用str()repr()对它进行了转换。从运行结果可以看出,str()保留了字符串最原始的样子,而repr()使用引号将字符串包围起来,这就是 Python 字符串的表达式形式。

另外,在 Python 交互式编程环境中输入一个表达式(变量、加减乘除、逻辑运算等)时,Python 会自动使用repr()函数处理该表达式。

截取字符串(字符串切片)

从本质上讲,字符串是由多个字符构成的,字符之间是有顺序的,这个顺序号就称为索引(index)。Python 允许通过索引来操作字符串中的单个或者多个字符,比如获取指定索引处的字符,返回指定字符的索引值等。

获取单个字符

知道字符串名字以后,在方括号[ ]中使用索引即可访问对应的字符:

1
strname[index]

strname表示字符串名字,index表示索引值。

Python 允许从字符串的两端使用索引:

  • 当以字符串的左端(字符串的开头)为起点时,索引是从 0 开始计数的;字符串的第一个字符的索引为 0,第二个字符的索引为 1,第三个字符串的索引为 2 ……
  • 当以字符串的右端(字符串的末尾)为起点时,索引是从 -1 开始计数的;字符串的倒数第一个字符的索引为 -1,倒数第二个字符的索引为 -2,倒数第三个字符的索引为 -3 ……
1
2
3
4
5
url = 'http://www.test.com/'
#获取索引为10的字符
print(url[10]) # .
#获取索引为 6 的字符
print(url[-6]) # t

获取多个字符(字符串截去/字符串切片)

使用[ ]除了可以获取单个字符外,还可以指定一个范围来获取多个字符,也就是一个子串或者片段:

1
strname[start : end : step]

对各个部分的说明:

  • strname:要截取的字符串;
  • start:表示要截取的第一个字符所在的索引(截取时包含该字符)。如果不指定,默认为 0,也就是从字符串的开头截取;
  • end:表示要截取的最后一个字符所在的索引(截取时不包含该字符)。如果不指定,默认为字符串的长度;
  • step:指的是从start索引处的字符开始,每step个距离获取一个字符,直至end索引出的字符。step默认值为 1,当省略该值时,最后一个冒号也可以省略。
1
2
3
4
5
6
7
8
9
url = 'http://www.testdem.com/java/'
#获取索引从7处到22(不包含22)的子串
print(url[7: 22]) # www.testdem.com
#获取索引从7处到-6的子串
print(url[7: -6]) # www.testdem.com
#获取索引从-21到6的子串
print(url[-21: -6]) # www.testdem.com
#从索引3开始,每隔4个字符取出一个字符,直到索引22为止
print(url[3: 22: 4]) # pwtdo
1
2
3
4
5
6
7
8
9
url = 'http://www.testdem.com/java/'
#获取从索引5开始,直到末尾的子串
print(url[7: ]) # www.testdem.com
#获取从索引-21开始,直到末尾的子串
print(url[-21: ]) # www.testdem.com
#从开头截取字符串,直到索引22为止
print(url[: 22]) # http://www.testdem.com
#每隔3个字符取出一个字符
print(url[:: 3]) # hp/wed.ma/

len()函数

要想知道一个字符串有多少个字符(获得字符串长度),或者一个字符串占用多少个字节,可以使用len函数。

1
len(string)

其中string用于指定要进行长度统计的字符串。

1
2
a='http://www.test.com'
len(a) # 19

在实际开发中,除了常常要获取字符串的长度外,有时还要获取字符串的字节数。

在 Python 中,不同的字符所占的字节数不同,数字、英文字母、小数点、下划线以及空格,各占一个字节,而一个汉字可能占 2~4 个字节,具体占多少个,取决于采用的编码方式。例如,汉字在 GBK/GB2312 编码中占用 2 个字节,而在 UTF-8 编码中一般占用 3 个字节。

我们可以通过使用encode()方法,将字符串进行编码后再获取它的字节数。

1
2
str1 = "人生苦短,我用Python"
len(str1.encode()) # 27

因为汉字加中文标点符号共 7 个,占 21 个字节,而英文字母和英文的标点符号占 6 个字节,一共占用 27 个字节。

同理,如果要获取采用 GBK 编码的字符串的长度,可以执行如下代码:

1
2
str1 = "人生苦短,我用Python"
len(str1.encode('gbk')) # 20

split()方法

split()方法可以实现将一个字符串按照指定的分隔符切分成多个子串,这些子串会被保存到列表中(不包含分隔符),作为方法的返回值反馈回来。

1
str.split(sep,maxsplit)

此方法中各部分参数的含义分别是:

  • str:表示要进行分割的字符串;
  • sep:用于指定分隔符,可以包含多个字符。此参数默认为None,表示所有空字符,包括空格、换行符\n、制表符\t等。
  • maxsplit:可选参数,用于指定分割的次数,最后列表中子串的个数最多为maxsplit+1。如果不指定或者指定为 -1,则表示分割次数没有限制。

split方法中,如果不指定sep参数,需要以str.split(maxsplit=xxx)的格式指定maxsplit参数。

同内建函数(如len)的使用方式不同,字符串变量所拥有的方法,只能采用“字符串.方法名()”的方式调用。\

1
2
3
4
5
6
7
str = "小明 >>> xiaoming"
list1 = str.split() #采用默认分隔符进行分割
list1 # ['小明', '>>>', 'xiaoming']
list2 = str.split('>>>') #采用多个字符进行分割
list2 # ['小明 ', 'xiaoming']
list3 = str.split('>') #采用 > 字符进行分割
list3 # ['小明 ', '', '', 'xiaoming']

需要注意的是,在未指定sep参数时,split()方法默认采用空字符进行分割,但当字符串中有连续的空格或其他空字符时,都会被视为一个分隔符对字符串进行分割:

1
2
3
str = "小明   >>>   xiaoming"  #包含 3 个连续的空格
list6 = str.split()
list6 # ['小明', '>>>', 'xiaoming']

join()方法

join()方法是split()方法的逆方法,用来将列表(或元组)中包含的多个字符串连接成一个字符串。

使用join()方法合并字符串时,它会将列表(或元组)中多个字符串采用固定的分隔符连接在一起。例如,字符串www.baidu.com就可以看做是通过分隔符“.”将['www','baidu','com']列表合并为一个字符串的结果。

1
newstr = str.join(iterable)

各参数的含义如下:

  • newstr:表示合并后生成的新字符串;
  • str:用于指定合并时的分隔符;
  • iterable:做合并操作的源字符串数据,允许以列表、元组等形式提供。
1
2
list = ['www','baidu','com']
'.'.join(list) # 'www.baidu.com'
1
2
3
dir = '','usr','bin','env'
type(dir) # <class 'tuple'>
'/'.join(dir) # '/usr/bin/env'

count()方法

count方法用于检索指定字符串在另一字符串中出现的次数,如果检索的字符串不存在,则返回 0,否则返回出现的次数。

1
str.count(sub[,start[,end]])

各参数的具体含义如下:

  • str:表示原字符串;
  • sub:表示要检索的字符串;
  • start:指定检索的起始位置,也就是从什么位置开始检测。如果不指定,默认从头开始检索;
  • end:指定检索的终止位置,如果不指定,则表示一直检索到结尾。
1
2
3
4
str = "www.baidu.com"
str.count('.') # 2
str.count('.', 3) # 2
str.count('.', 4) # 1

字符串中各字符对应的检索值,从 0 开始,因此,本例中检索值 1 对应的是第 2 个字符‘.’,从输出结果可以分析出,从指定索引位置开始检索,其中也包含此索引位置。

1
2
3
str = "www.baidu.com"
str.count('.', 4, -3) # 1
str.count('.', 4, -4) # 0

find()方法

find()方法用于检索字符串中是否包含目标字符串,如果包含,则返回第一次出现该字符串的索引;反之,则返回 -1。

1
str.find(sub[,start[,end]])

各参数的含义:

  • str:表示原字符串;
  • sub:表示要检索的目标字符串;
  • start:表示开始检索的起始位置。如果不指定,则默认从头开始检索;
  • end:表示结束检索的结束位置。如果不指定,则默认一直检索到结尾。
1
2
3
4
str = "www.baidu.com"
str.find('.') # 3
str.find('.', 4) # 9
str.find('.', 4, -4) # -1

位于索引(4,-4)之间的字符串为baidu,由于其不包含“.”,因此find()方法的返回值为 -1。

Python 还提供了rfind()方法,rfind()是从字符串右边开始检索。

1
2
str = "www.baidu.com"
str.rfind('.') # 9

index()方法

find()方法类似,index()方法也可以用于检索是否包含指定的字符串,不同之处在于,当指定的字符串不存在时,index()方法会抛出异常。

1
str.index(sub[,start[,end]])

各参数的含义:

  • str:表示原字符串;
  • sub:表示要检索的子字符串;
  • start:表示检索开始的起始位置,如果不指定,默认从头开始检索;
  • end:表示检索的结束位置,如果不指定,默认一直检索到结尾。
1
2
3
4
5
6
7
str = "www.baidu.com"
str.index('.') # 3
str.index('z')
Traceback (most recent call last):
File "<pyshell#49>", line 1, in <module>
str.index('z')
ValueError: substring not found

字符串变量还具有rindex()方法,其作用和index()方法类似,不同之处在于它是从右边开始检索:

1
2
str = "www.baidu.com"
str.rindex('.') # 9

字符串对齐方法(ljust()、rjust()和center())

str提供了 3 种可用来进行文本对齐的方法,分别是ljust()、rjust()center()方法。

ljust()方法

ljust()方法的功能是向指定字符串的右侧填充指定字符,从而达到左对齐文本的目的。

1
S.ljust(width[, fillchar])

各个参数的含义:

  • S:表示要进行填充的字符串;
  • width:表示包括S本身长度在内,字符串要占的总长度;
  • fillchar:作为可选参数,用来指定填充字符串时所用的字符,默认情况使用空格。
1
2
3
4
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com'
print(S.ljust(35)) # http://www.baidu.com/python/
print(addr.ljust(35)) # http://www.baidu.com/

注意,该输出结果中除了明显可见的网址字符串外,其后还有空格字符存在,每行一共 35 个字符长度。

1
2
3
4
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com'
print(S.ljust(35,'-')) # http://www.baidu.com/python/------
print(addr.ljust(35,'-')) # http://www.baidu.com--------------

rjust()方法

rjust()ljust()方法类似,唯一的不同在于,rjust()方法是向字符串的左侧填充指定字符,从而达到右对齐文本的目的。

1
S.rjust(width[, fillchar])

其中各个参数的含义和ljust()完全相同。

1
2
3
4
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com'
print(S.rjust(35)) # http://www.baidu.com/python/
print(addr.rjust(35)) # http://www.baidu.com

可以看到,每行字符串都占用 35 个字节的位置,实现了整体的右对齐效果。

1
2
3
4
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com/'
print(S.rjust(35,'-')) # ------http://www.baidu.com/python/
print(addr.rjust(35,'-')) # ----------http://www.baidu.com/python/

center()方法

center()字符串方法与ljust()rjust()的用法类似,但它让文本居中,而不是左对齐或右对齐。

1
S.center(width[, fillchar])

其中各个参数的含义和ljust()、rjust()方法相同。

1
2
3
4
5
6
7
8
9
10
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com/'
print(S.center(35,)) # http://www.baidu.com/python/
print(addr.center(35,)) # http://www.baidu.com
```
```py
S = 'http://www.baidu.com/python/'
addr = 'http://www.baidu.com/'
print(S.center(35,'-')) # ---http://www.baidu.com/python/---
print(addr.center(35,'-')) # --------http://www.baidu.com--------

startswith()和endswith()方法

startswith()方法

startswith()方法用于检索字符串是否以指定字符串开头,如果是返回True;反之返回False

1
str.startswith(sub[,start[,end]])

各个参数的含义:

  • str:表示原字符串;
  • sub:要检索的子串;
  • start:指定检索开始的起始位置索引,如果不指定,则默认从头开始检索;
  • end:指定检索的结束位置索引,如果不指定,则默认一直检索在结束。
1
2
3
4
str = "www.baidu.com"
str.startswith("w") # True
str.startswith("http") # False
str.startswith("w", 2) # True

endswith()方法

endswith()方法用于检索字符串是否以指定字符串结尾,如果是则返回True;反之则返回False

1
str.endswith(sub[,start[,end]])

各参数的含义:

  • str:表示原字符串;
  • sub:表示要检索的字符串;
  • start:指定检索开始时的起始位置索引(字符串第一个字符对应的索引值为 0),如果不指定,默认从头开始检索。
  • end:指定检索的结束位置索引,如果不指定,默认一直检索到结束。
1
2
str = "www.baidu.com"
str.endswith("com") # True

字符串大小写转换

字符串变量提供了 3 种对字符串中的字母进行大小写转换的方法,分别是title()、lower()upper()

title()方法

title()方法用于将字符串中每个单词的首字母转为大写,其他字母全部转为小写,转换完成后,此方法会返回转换得到的字符串。如果字符串中没有需要被转换的字符,此方法会将字符串原封不动地返回。

1
str.title()

其中,str表示要进行转换的字符串。

1
2
3
4
str = "www.baidu.com"
str.title() # 'Www.Baidu.Co,'
str = "I LIKE C"
str.title() # 'I Like C'

lower()方法

lower()方法用于将字符串中的所有大写字母转换为小写字母,转换完成后,该方法会返回新得到的字符串。如果字符串中原本就都是小写字母,则该方法会返回原字符串。

1
str.lower()

其中,str表示要进行转换的字符串。

1
2
str = "I LIKE C"
str.lower() # 'i like c'

upper()方法

upper()的功能和lower()方法恰好相反,它用于将字符串中的所有小写字母转换为大写字母,和以上两种方法的返回方式相同,即如果转换成功,则返回新字符串;反之,则返回原字符串。

1
str.upper()

其中,str表示要进行转换的字符串。

1
2
str = "i like C"
str.upper() # 'I LIKE C'

需要注意的是,以上 3 个方法都仅限于将转换后的新字符串返回,而不会修改原字符串。

去除字符串中空格

在一些场景中,字符串前后不允许出现空格和特殊字符,此时就需要去除字符串中的空格和特殊字符。这里的特殊字符,指的是制表符(\t)、回车符(\r)、换行符(\n)等。

字符串变量提供了 3 种方法来删除字符串中多余的空格和特殊字符:

  • strip():删除字符串前后(左右两侧)的空格或特殊字符。
  • lstrip():删除字符串前面(左边)的空格或特殊字符。
  • rstrip():删除字符串后面(右边)的空格或特殊字符。

注意,Python 的str是不可变的(不可变的意思是指,字符串一旦形成,它所包含的字符序列就不能发生任何改变),因此这三个方法只是返回字符串前面或后面空白被删除之后的副本,并不会改变字符串本身。

strip()方法

strip()方法用于删除字符串左右两个的空格和特殊字符:

1
str.strip([chars])

其中,str表示原字符串,[chars]用来指定要删除的字符,可以同时指定多个,如果不手动指定,则默认会删除空格以及制表符、回车符、换行符等特殊字符。

1
2
3
4
str = "  www.baidu.com \t\n\r"
str.strip() # 'www.baidu.com'
str.strip(" ,\r") # 'www.baidu.com \t\n'
str # ' www.baidu.com \t\n\r'

通过strip()确实能够删除字符串左右两侧的空格和特殊字符,但并没有真正改变字符串本身。

lstrip()方法

lstrip()方法用于去掉字符串左侧的空格和特殊字符:

1
str.lstrip([chars])
1
2
str = "  www.baidu.com \t\n\r"
str.lstrip() # 'www.baidu.com \t\n\r'

rstrip()方法

rstrip()方法用于删除字符串右侧的空格和特殊字符:

1
str.rstrip([chars])
1
2
str = "  www.baidu.com \t\n\r"
str.rstrip() # ' www.baidu.com'

format()格式化输出方法

使用%操作符对各种类型的数据进行格式化输出,这是早期 Python 提供的方法。自 Python 2.6 版本开始,字符串类型提供了format()方法对字符串进行格式化。

1
str.format(args)

str用于指定字符串的显示样式;args用于指定要进行格式转换的项,如果有多项,之间有逗号进行分割。

在创建显示样式模板时,需要使用{}:来指定占位符,其完整的语法格式为:

1
{ [index][ : [ [fill] align] [sign] [#] [width] [.precision] [type] ] }

注意,格式中用 [] 括起来的参数都是可选参数,即可以使用,也可以不使用。各个参数的含义如下:

  • index:指定:后边设置的格式要作用到args中第几个数据,数据的索引值从 0 开始。如果省略此选项,则会根据args中数据的先后顺序自动分配。
  • fill:指定空白处填充的字符。注意,当填充字符为逗号(,)且作用于整数或浮点数时,该整数(或浮点数)会以逗号分隔的形式输出,例如(1000000会输出 1,000,000)。
  • align:指定数据的对齐方式。
  • sign:指定有无符号数。
  • width:指定输出数据时所占的宽度。
  • .precision:指定保留的小数位数。
  • type:指定输出数据的具体类型。
align 含义
< 数据左对齐。
> 数据右对齐。
= 数据右对齐,同时将符号放置在填充内容的最左侧,该选项只对数字类型有效。
^ 数据居中,此选项需和 width 参数一起使用。
sign参数 含义
+ 正数前加正号,负数前加负号。
- 正数前不加正号,负数前加负号。
空格 正数前加空格,负数前加负号。
# 对于二进制数、八进制数和十六进制数,使用此参数,各进制数前会分别显示 0b、0o、0x前缀;反之则不显示前缀。
type类型值 含义
s 对字符串类型格式化。
d 十进制整数。
c 将十进制整数自动转换成对应的 Unicode 字符。
e 或者 E 转换成科学计数法后,再格式化输出。
g 或 G 自动在 e 和 f(或 E 和 F)中切换。
b 将十进制数自动转换成二进制表示,再格式化输出。
o 将十进制数自动转换成八进制表示,再格式化输出。
x 或者 X 将十进制数自动转换成十六进制表示,再格式化输出。
f 或者 F 转换为浮点数(默认小数点后保留 6 位),再格式化输出。
% 显示百分比(默认显示小数点后 6 位)。
1
2
str="网站名称:{:>9s}\t网址:{:s}"
print(str.format("百度","www.baidu.com")) # 网站名称: 百度 网址:www.baidu.com

在实际开发中,数值类型有多种显示需求,比如货币形式、百分比形式等,使用format()方法可以将数值格式化为不同的形式。

1
2
3
4
5
6
7
8
#以货币形式显示
print("货币形式:{:,d}".format(1000000)) # 货币形式:1,000,000
#科学计数法表示
print("科学计数法:{:E}".format(1200.12)) # 科学计数法:1.200120E+03
#以十六进制表示
print("100的十六进制:{:#x}".format(100)) # 100的十六进制:0x64
#输出百分比形式
print("0.01的百分比表示:{:.0%}".format(0.01)) # 0.01的百分比表示:1%

encode()和decode()方法

最早的字符串编码是 ASCII 编码,它仅仅对 10 个数字、26 个大小写英文字母以及一些特殊字符进行了编码。ASCII 码做多只能表示 256 个符号,每个字符只需要占用 1 个字节。

随着信息技术的发展,各国的文字都需要进行编码,于是相继出现了 GBK、GB2312、UTF-8 编码等,其中 GBK 和 GB2312 是我国制定的中文编码标准,规定英文字符母占用 1 个字节,中文字符占用 2 个字节;而 UTF-8 是国际通过的编码格式,它包含了全世界所有国家需要用到的字符,其规定英文字符占用 1 个字节,中文字符占用 3 个字节。

Python 3.x 默认采用 UTF-8 编码格式,有效地解决了中文乱码的问题。

在 Python 中,有 2 种常用的字符串类型,分别为strbytes类型,其中str用来表示 Unicode 字符,bytes用来表示二进制数据。str类型和bytes类型之间就需要使用encode()decode()方法进行转换。

encode()方法

encode()方法为字符串类型(str)提供的方法,用于将str类型转换成bytes类型,这个过程也称为“编码”。

1
str.encode([encoding="utf-8"][,errors="strict"])

注意,格式中用[]括起来的参数为可选参数,也就是说,在使用此方法时,可以使用[]中的参数,也可以不使用。

encode()参数及含义:

  • str表示要进行转换的字符串。
  • encoding = "utf-8"指定进行编码时采用的字符编码,该选项默认采用 utf-8 编码。例如,如果想使用简体中文,可以设置 gb2312。
    当方法中只使用这一个参数时,可以省略前边的encoding=,直接写编码格式,例如str.encode("UTF-8")
  • errors = "strict"指定错误处理方式,其可选择值可以是:
  • strict:遇到非法字符就抛出异常。
  • ignore:忽略非法字符。
  • replace:用“?”替换非法字符。
  • xmlcharrefreplace:使用xml的字符引用。该参数的默认值为strict

注意,使用encode()方法对原字符串进行编码,不会直接修改原字符串,如果想修改原字符串,需要重新赋值。

1
2
3
str = "测试"
str.encode()
b'C\xe8\xaf\xad\xe8'

此方式默认采用 UTF-8 编码,也可以手动指定其它编码格式:

1
2
3
str = "测试"
str.encode('GBK')
b'C\xd3\xef\xd1\xd4'

decode()方法

encode()方法正好相反,decode()方法用于将bytes类型的二进制数据转换为str类型,这个过程也称为“解码”。

1
bytes.decode([encoding="utf-8"][,errors="strict"])

decode()参数及含义:

  • bytes表示要进行转换的二进制数据。
  • encoding="utf-8"指定解码时采用的字符编码,默认采用 utf-8 格式。当方法中只使用这一个参数时,可以省略encoding=,直接写编码方式即可。注意,对bytes类型数据解码,要选择和当初编码时一样的格式。
  • errors = "strict"指定错误处理方式,其可选择值可以是:
  • strict:遇到非法字符就抛出异常。
  • ignore:忽略非法字符。
  • replace:用?替换非法字符。
  • xmlcharrefreplace:使用xml的字符引用。该参数的默认值为strict
1
2
3
4
str = "小明"
bytes=str.encode()
bytes.decode()
'小明'

注意,如果编码时采用的不是默认的 UTF-8 编码,则解码时要选择和编码时一样的格式,否则会抛出异常:

1
2
3
4
5
6
7
8
9
str = "小明"
bytes = str.encode("GBK")
bytes.decode() #默认使用 UTF-8 编码,会抛出以下异常
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
bytes.decode()
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd3 in position 1: invalid continuation byte
bytes.decode("GBK")
'小明'

dir()和help()帮助函数

dir()函数用来列出某个类或者某个模块中的全部内容,包括变量、方法、函数和类等。

1
dir(obj)

obj表示要查看的对象。obj可以不写,此时dir()会列出当前范围内的变量、方法和定义的类型。

help()函数用来查看某个函数或者模块的帮助文档:

1
help(obj)

obj表示要查看的对象。obj可以不写,此时help()会进入帮助子程序。

掌握了以上两个函数,我们就可以自行查阅 Python 中所有方法、函数、变量、类的用法和功能了。

1
2
dir(str)
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

在 Python 标准库中,以__开头和结尾的方法都是私有的,不能在类的外部调用。

1
2
3
4
5
help(str.lower)
Help on method_descriptor:

lower(self, /)
Return a copy of the string converted to lowercase.

注意,使用help()查看某个函数的用法时,函数名后边不能带括号,例如将上面的命令写作help(str.lower())就是错误的。

Python集合

Python 中的集合,和数学中的集合概念一样,用来保存不重复的元素,即集合中的元素都是唯一的,互不相同。

从形式上看,和字典类似,Python 集合会将所有元素放在一对大括号{}中,相邻元素之间用,分隔:

1
{element1,element2,...,elementn}

其中,elementn表示集合中的元素,个数没有限制。

从内容上看,同一集合中,只能存储不可变的数据类型,包括整形、浮点型、字符串、元组,无法存储列表、字典、集合这些可变的数据类型,否则 Python 解释器会抛出TypeError错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> {{'a':1}}
Traceback (most recent call last):
File "<pyshell#8>", line 1, in <module>
{{'a':1}}
TypeError: unhashable type: 'dict'
>>> {[1,2,3]}
Traceback (most recent call last):
File "<pyshell#9>", line 1, in <module>
{[1,2,3]}
TypeError: unhashable type: 'list'
>>> {{1,2,3}}
Traceback (most recent call last):
File "<pyshell#10>", line 1, in <module>
{{1,2,3}}
TypeError: unhashable type: 'set'

并且需要注意的是,数据必须保证是唯一的,因为集合对于每种数据元素,只会保留一份。

由于 Python 中的集合是无序的,所以每次输出时元素的排序顺序可能都不相同。

Python 中有两种集合类型,一种是set类型的集合,另一种是frozenset类型的集合,它们唯一的区别是,set类型集合可以做添加、删除元素的操作,而forzenset类型集合不行。

创建集合

Python 提供了 2 种创建集合的方法,分别是使用{}创建和使用set()函数将列表、元组等类型数据转换为集合。

使用 {} 创建

创建set集合可以像列表、元素和字典一样,直接将集合赋值给变量:

1
setname = {element1,element2,...,elementn}

其中,setname表示集合的名称。

1
2
a = {1,'c',1,(1,2,3),'c'}
print(a) # {1, 'c', (1, 2, 3)}

set()函数创建集合

set()函数为内置函数,其功能是将字符串、列表、元组、range对象等可迭代对象转换成集合。

1
setname = set(iteration)

其中,iteration就表示字符串、列表、元组、range对象等数据。

1
2
3
4
5
6
set1 = set("hello")
set2 = set([1,2,3,4,5])
set3 = set((1,2,3,4,5))
print("set1:", set1) # set1: {'h', 'e', 'l', 'l', 'o'}
print("set2:", set2) # set2: {1, 2, 3, 4, 5}
print("set3:", set3) # set3: {1, 2, 3, 4, 5}

注意,如果要创建空集合,只能使用set()函数实现。因为直接使用一对{},Python 解释器会将其视为一个空字典。

访问集合元素

由于集合中的元素是无序的,因此无法向列表那样使用下标访问元素。Python 中,访问集合元素最常用的方法是使用循环结构,将集合中的数据逐一读取出来。

1
2
3
a = {1, 'c', 1, (1,2,3), 'c'}
for ele in a:
print(ele,end=' ')

运行结果为:

1
1 c (1, 2, 3)

删除集合

删除集合类型可以使用del()语句:

1
2
3
4
a = {1, 'c', 1, (1,2,3), 'c'}
print(a)
del(a)
print(a)

运行结果为:

1
2
3
4
5
{1, 'c', (1, 2, 3)}
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 4, in <module>
print(a)
NameError: name 'a' is not defined

向集合中添加元素

set集合中添加元素,可以使用set类型提供的add()方法实现:

1
setname.add(element)

其中,setname表示要添加元素的集合,element表示要添加的元素内容。

需要注意的是,使用add()方法添加的元素,只能是数字、字符串、元组或者布尔类型(TrueFalse)值,不能添加列表、字典、集合这类可变的数据,否则 Python 解释器会报TypeError错误。

1
2
3
4
5
a = {1, 2, 3}
a.add((1,2))
print(a)
a.add([1,2])
print(a)

运行结果为:

1
2
3
4
5
{(1, 2), 1, 2, 3}
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 4, in <module>
a.add([1,2])
TypeError: unhashable type: 'list'

从集合中删除元素

删除现有set集合中的指定元素,可以使用remove()方法:

1
setname.remove(element)

使用此方法删除集合中元素,需要注意的是,如果被删除元素本就不包含在集合中,则此方法会抛出KeyError错误:

1
2
3
4
5
a = {1, 2, 3}
a.remove(1)
print(a)
a.remove(1)
print(a)

运行结果为:

1
2
3
4
5
{2, 3}
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\1.py", line 4, in <module>
a.remove(1)
KeyError: 1

上面程序中,由于集合中的元素 1 已被删除,因此当再次尝试使用remove()方法删除时,会引发KeyError错误。

如果我们不想在删除失败时令解释器提示KeyError错误,还可以使用discard()方法,此方法和remove()方法的用法完全相同,唯一的区别就是,当删除集合中元素失败时,此方法不会抛出任何错误。

1
2
3
4
5
a = {1, 2, 3}
a.remove(1)
print(a)
a.discard(1)
print(a)

运行结果为:

1
2
{2, 3}
{2, 3}

集合做交集、并集、差集运算

有 2 个集合,分别为set1={1, 2, 3}set2={3, 4, 5}。以这两个集合为例,分别做不同运算的结果如表。

运算操作 Python运算符 含义 例子
交集 & 取两集合公共的元素 set1 & set2
{3}
并集 ` ` 取两集合全部的元素
差集 - 取一个集合中另一集合没有的元素 set1 - set2
{1,2}
set2 - set1
{4,5}
对称差集 ^ 取集合 A 和 B 中不属于 A&B 的元素 set1 ^ set2
{1,2,4,5}

集合方法

方法名 语法格式 功能 实例
add() set1.add() 向 set1 集合中添加数字、字符串、元组或者布尔类型 set1 = {1,2,3}
set1.add((1,2))
set1
{(1, 2), 1, 2, 3}
clear() set1.clear() 清空 set1 集合中所有元素 set1 = {1,2,3}
set1.clear()
set1
set()
set()才表示空集合,{}表示的是空字典
copy() set2 = set1.copy() 拷贝 set1 集合给 set2 set1 = {1,2,3}
set2 = set1.copy()
set1.add(4)
set1 {1, 2, 3, 4}
set1 {1, 2, 3}
difference() set3 = set1.difference(set2) 将 set1 中有而 set2 没有的元素给 set3 set1 = {1,2,3} set2 = {3,4} set3 = set1.difference(set2) set3 {1, 2}
difference_update() set1.difference_update(set2) 从 set1 中删除与 set2 相同的元素 set1 = {1,2,3}
set2 = {3,4}
set1.difference_update(set2)
set1 {1, 2}
discard() set1.discard(elem) 删除 set1 中的 elem 元素 set1 = {1,2,3}
set1.discard(2)
set1 {1, 3}
set1.discard(4) {1, 3}
intersection() set3 = set1.intersection(set2) 取 set1 和 set2 的交集给 set3 set1 = {1,2,3}
set2 = {3,4}
set3 = set1.intersection(set2)
set3 {3}
intersection_update() set1.intersection_update(set2) 取 set1和 set2 的交集,并更新给 set1 set1 = {1,2,3} set2 = {3,4}
set1.intersection_update(set2)
set1 {3}
isdisjoint() set1.isdisjoint(set2) 判断 set1 和 set2 是否没有交集,有交集返回 False;没有交集返回 True set1 = {1,2,3} set2 = {3,4}
set1.isdisjoint(set2)
False
issubset() set1.issubset(set2) 判断 set1 是否是 set2 的子集 set1 = {1,2,3} set2 = {1,2}
set1.issubset(set2)
False
issuperset() set1.issuperset(set2) 判断 set2 是否是 set1 的子集 set1 = {1,2,3} set2 = {1,2}
set1.issuperset(set2)
True
pop() a = set1.pop() 取 set1 中一个元素,并赋值给 a set1 = {1,2,3}
a = set1.pop()
set1 {2,3} a 1
remove() set1.remove(elem) 移除 set1 中的 elem 元素 set1 = {1,2,3} set1.remove(2)
set1 {1, 3}
set1.remove(4)
symmetric_difference() set3 = set1.symmetric_difference(set2) 取 set1 和 set2 中互不相同的元素,给 set3 set1 = {1,2,3} set2 = {3,4}
set3 = set1.symmetric_difference(set2)
set3 {1, 2, 4}
symmetric_difference_update() set1.symmetric_difference_update(set2) 取 set1 和 set2 中互不相同的元素,并更新给 set1 set1 = {1,2,3} set2 = {3,4}
set1.symmetric_difference_update(set2)
set1 {1, 2, 4}
union() set3 = set1.union(set2) 取 set1 和 set2 的并集,赋给 set3 set1 = {1,2,3} set2 = {3,4}
set3=set1.union(set2)
set3 {1, 2, 3, 4}
update() set1.update(elem) 添加列表或集合中的元素到 set1 set1 = {1,2,3}
set1.update([3,4])
set1 {1,2,3,4}

frozenset集合

set集合是可变序列,程序可以改变序列中的元素;frozenset集合是不可变序列,程序不能改变序列中的元素。set集合中所有能改变集合本身的方法,比如remove()、discard()、add()等,frozenset都不支持;set集合中不改变集合本身的方法,fronzenset都支持。

我们可以在交互式编程环境中输入dir(frozenset)来查看frozenset集合支持的方法:

1
2
>>> dir(frozenset)
['copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset', 'symmetric_difference', 'union']

frozenset集合的这些方法和set集合中同名方法的功能是一样的。

两种情况下可以使用fronzenset

  • 当集合的元素不需要改变时,我们可以使用fronzenset替代set,这样更加安全。
  • 有时候程序要求必须是不可变对象,这个时候也要使用fronzenset替代set。比如,字典(dict)的键(key)就要求是不可变对象。
1
2
3
4
5
6
7
8
9
s = {'Python', 'C', 'C++'}
fs = frozenset(['Java', 'Shell'])
s_sub = {'PHP', 'C#'}
#向set集合中添加frozenset
s.add(fs)
print('s =', s)
#向为set集合添加子set集合
s.add(s_sub)
print('s =', s)

运行结果:

1
2
3
4
5
s = {'Python', frozenset({'Java', 'Shell'}), 'C', 'C++'}
Traceback (most recent call last):
File "C:\Users\mozhiyan\Desktop\demo.py", line 11, in <module>
s.add(s_sub)
TypeError: unhashable type: 'set'

需要注意的是,set集合本身的元素必须是不可变的,所以set的元素不能是set,只能是frozenset。第 5 行代码向set中添加frozenset是没问题的,因为frozenset是不可变的;但是,第 8 行代码中尝试向set中添加子set,这是不允许的,因为set是可变的。

Python类特殊成员

new()

__new__() 是一种负责创建类实例的静态方法,它无需使用staticmethod装饰器修饰,且该方法会优先__init__()初始化方法被调用。

一般情况下,覆写__new__()的实现将会使用合适的参数调用其超类的super().__new__(),并在返回之前修改实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class demoClass:
instances_created = 0
def __new__(cls,*args,**kwargs):
print("__new__():",cls,args,kwargs)
instance = super().__new__(cls)
instance.number = cls.instances_created
cls.instances_created += 1
return instance
def __init__(self,attribute):
print("__init__():",self,attribute)
self.attribute = attribute
test1 = demoClass("abc")
test2 = demoClass("xyz")
print(test1.number,test1.instances_created)
print(test2.number,test2.instances_created)

输出结果为:

1
2
3
4
5
6
__new__(): <class '__main__.demoClass'> ('abc',) {}
__init__(): <__main__.demoClass object at 0x0000026FC0DF8080> abc
__new__(): <class '__main__.demoClass'> ('xyz',) {}
__init__(): <__main__.demoClass object at 0x0000026FC0DED358> xyz
0 2
1 2

__new__()通常会返回该类的一个实例,但有时也可能会返回其他类的实例,如果发生了这种情况,则会跳过对__init__()方法的调用。而在某些情况下(比如需要修改不可变类实例(Python 的某些内置类型)的创建行为),利用这一点会事半功倍。

1
2
3
4
5
6
7
8
9
class nonZero(int):
def __new__(cls,value):
return super().__new__(cls,value) if value != 0 else None
def __init__(self,skipped_value):
#此例中会跳过此方法
print("__init__()")
super().__init__()
print(type(nonZero(-12)))
print(type(nonZero(0)))

运行结果为:

1
2
3
__init__()
<class '__main__.nonZero'>
<class 'NoneType'>

那么,什么情况下使用__new__()呢?答案很简单,在__init__()不够用的时候。

例如,前面例子中对 Python 不可变的内置类型(如int、str、float等)进行了子类化,这是因为一旦创建了这样不可变的对象实例,就无法在__init__()方法中对其进行修改。

有些读者可能会认为,__new__()对执行重要的对象初始化很有用,如果用户忘记使用super(),可能会漏掉这一初始化。虽然这听上去很合理,但有一个主要的缺点,即如果使用这样的方法,那么即便初始化过程已经是预期的行为,程序员明确跳过初始化步骤也会变得更加困难。不仅如此,它还破坏了“__init__()中执行所有初始化工作”的潜规则。

注意,由于__new__()不限于返回同一个类的实例,所以很容易被滥用,不负责任地使用这种方法可能会对代码有害,所以要谨慎使用。一般来说,对于特定问题,最好搜索其他可用的解决方案,最好不要影响对象的创建过程,使其违背程序员的预期。比如说,前面提到的覆写不可变类型初始化的例子,完全可以用工厂方法(一种设计模式)来替代。

Python中大量使用__new__()方法且合理的,就是MetaClass元类。

repr()方法:显示属性

我们经常会直接输出类的实例化对象,例如:

1
2
3
4
class CLanguage:
pass
clangs = CLanguage()
print(clangs)

程序运行结果为:

1
<__main__.CLanguage object at 0x000001A7275221D0>

通常情况下,直接输出某个实例化对象,本意往往是想了解该对象的基本信息,例如该对象有哪些属性,它们的值各是多少等等。但默认情况下,我们得到的信息只会是“类名+object at+内存地址”,对我们了解该实例化对象帮助不大。

那么,有没有可能自定义输出实例化对象时的信息呢?答案是肯定,通过重写类的__repr__()方法即可。事实上,当我们输出某个实例化对象时,其调用的就是该对象的__repr__()方法,输出的是该方法的返回值。

以开头的程序为例,执行print(clangs)等同于执行print(clangs.__repr__()),程序的输出结果是一样的(输出的内存地址可能不同)。

__init__(self)的性质一样,Python 中的每个类都包含__repr__()方法,因为object类包含__reper__()方法,而 Python 中所有的类都直接或间接继承自object类。

默认情况下,__repr__()会返回和调用者有关的 “类名+object at+内存地址”信息。当然,我们还可以通过在类中重写这个方法,从而实现当输出实例化对象时,输出我们想要的信息。

1
2
3
4
5
6
7
8
class CLanguage:
def __init__(self):
self.name = "小明"
self.add = "xiaoming"
def __repr__(self):
return "CLanguage[name="+ self.name +",add=" + self.add +"]"
clangs = CLanguage()
print(clangs)

程序运行结果为:

1
CLanguage[name=小明,add=xiaoming]

由此可见,__repr__()方法是类的实例化对象用来做“自我介绍”的方法,默认情况下,它会返回当前对象的“类名+object at+内存地址”,而如果对该方法进行重写,可以为其制作自定义的自我描述信息。

del()方法:销毁对象

我们知道,Python 通过调用__init__()方法构造当前类的实例化对象,而__del__()方法,功能正好和__init__()相反,其用来销毁实例化对象。

事实上在编写程序时,如果之前创建的类实例化对象后续不再使用,最好在适当位置手动将其销毁,释放其占用的内存空间(整个过程称为垃圾回收(简称GC))。
大多数情况下,Python 开发者不需要手动进行垃圾回收,因为 Python 有自动的垃圾回收机制(下面会讲),能自动将不需要使用的实例对象进行销毁。

无论是手动销毁,还是 Python 自动帮我们销毁,都会调用__del__()方法。

1
2
3
4
5
6
7
class CLanguage:
def __init__(self):
print("调用 __init__() 方法构造对象")
def __del__(self):
print("调用__del__() 销毁对象,释放其空间")
clangs = CLanguage()
del clangs

程序运行结果为:

1
2
调用 __init__() 方法构造对象
调用__del__() 销毁对象,释放其空间

但是,千万不要误认为,只要为该实例对象调用 del() 方法,该对象所占用的内存空间就会被释放。举个例子:

1
2
3
4
5
6
7
8
9
10
class CLanguage:
def __init__(self):
print("调用 __init__() 方法构造对象")
def __del__(self):
print("调用__del__() 销毁对象,释放其空间")
clangs = CLanguage()
#添加一个引用clangs对象的实例对象
cl = clangs
del clangs
print("***********")

程序运行结果为:

1
2
3
调用 __init__() 方法构造对象
***********
调用__del__() 销毁对象,释放其空间

注意,最后一行输出信息,是程序执行即将结束时调用__del__()方法输出的。

可以看到,当程序中有其它变量(比如这里的 cl)引用该实例对象时,即便手动调用__del__()方法,该方法也不会立即执行。这和 Python 的垃圾回收机制的实现有关。

Python 采用自动引用计数(简称 ARC)的方式实现垃圾回收机制。该方法的核心思想是:每个 Python 对象都会配置一个计数器,初始 Python 实例对象的计数器值都为 0,如果有变量引用该实例对象,其计数器的值会加 1,依次类推;反之,每当一个变量取消对该实例对象的引用,计数器会减 1。如果一个 Python 对象的的计数器值为 0,则表明没有变量引用该 Python 对象,即证明程序不再需要它,此时 Python 就会自动调用__del__()方法将其回收。

以上面程序中的 clangs 为例,实际上构建 clangs 实例对象的过程分为 2 步,先使用 CLanguage() 调用该类中的__init__()方法构造出一个该类的对象(将其称为 C,计数器为 0),并立即用 clangs 这个变量作为所建实例对象的引用( C 的计数器值 + 1)。在此基础上,又有一个 clang 变量引用 clangs(其实相当于引用 CLanguage(),此时 C 的计数器再 +1 ),这时如果调用del clangs语句,只会导致 C 的计数器减 1(值变为 1),因为 C 的计数器值不为 0,因此 C 不会被销毁(不会执行__del__()方法)。

如果在上面程序结尾,添加如下语句:

1
2
3
4
5
6
7
del cl
print("-----------")
则程序的执行结果为:
调用 __init__() 方法构造对象
***********
调用__del__() 销毁对象,释放其空间
-----------

可以看到,当执行 del cl 语句时,其应用的对象实例对象 C 的计数器继续 -1(变为 0),对于计数器为 0 的实例对象,Python 会自动将其视为垃圾进行回收。

需要额外说明的是,如果我们重写子类的 del() 方法(父类为非object的类),则必须显式调用父类的 del() 方法,这样才能保证在回收子类对象时,其占用的资源(可能包含继承自父类的部分资源)能被彻底释放。为了说明这一点,这里举一个反例:

1
2
3
4
5
6
7
8
class CLanguage:
def __del__(self):
print("调用父类 __del__() 方法")
class cl(CLanguage):
def __del__(self):
print("调用子类 __del__() 方法")
c = cl()
del c

程序运行结果为:

1
调用子类 __del__() 方法

dir()用法:列出对象的所有属性(方法)名

dir()函数,通过此函数可以某个对象拥有的所有的属性名和方法名,该函数会返回一个包含有所有属性名和方法名的有序列表。

1
2
3
4
5
6
7
8
class CLanguage:
def __init__ (self,):
self.name = "小明"
self.add = "xiaoming"
def say():
pass
clangs = CLanguage()
print(dir(clangs))

程序运行结果为:

1
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'add', 'name', 'say']

注意,通过dir()函数,不仅仅输出本类中新添加的属性名和方法(最后 3 个),还会输出从父类(这里为object类)继承得到的属性名和方法名。

值得一提的是,dir()函数的内部实现,其实是在调用参数对象__dir__()方法的基础上,对该方法返回的属性名和方法名做了排序。

所以,除了使用dir()函数,我们完全可以自行调用该对象具有的__dir__()方法:

1
2
3
4
5
6
7
8
class CLanguage:
def __init__ (self,):
self.name = "小明"
self.add = "xiaoming"
def say():
pass
clangs = CLanguage()
print(clangs.__dir__())

程序运行结果为:

1
['name', 'add', '__module__', '__init__', 'say', '__dict__', '__weakref__', '__doc__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__new__', '__reduce_ex__', '__reduce__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

显然,使用__dir__()方法和dir()函数输出的数据是相同,仅仅顺序不同。

__dict__属性:查看对象内部所有属性名和属性值组成的字典

在 Python 类的内部,无论是类属性还是实例属性,都是以字典的形式进行存储的,其中属性名作为键,而值作为该键对应的值。

为了方便用户查看类中包含哪些属性,Python 类提供了__dict__属性。需要注意的一点是,该属性可以用类名或者类的实例对象来调用,用类名直接调用__dict__,会输出该由类中所有类属性组成的字典;而使用类的实例对象调用__dict__,会输出由类中所有实例属性组成的字典。

1
2
3
4
5
6
7
8
9
10
11
class CLanguage:
a = 1
b = 2
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
#通过类名调用__dict__
print(CLanguage.__dict__)
#通过类实例对象调用 __dict__
clangs = CLanguage()
print(clangs.__dict__)

程序输出结果为:

1
2
{'__module__': '__main__', 'a': 1, 'b': 2, '__init__': <function CLanguage.__init__ at 0x0000022C69833E18>, '__dict__': <attribute '__dict__' of 'CLanguage' objects>, '__weakref__': <attribute '__weakref__' of 'CLanguage' objects>, '__doc__': None}
{'name': '小明', 'add': 'xiaoming'}

不仅如此,对于具有继承关系的父类和子类来说,父类有自己的__dict__,同样子类也有自己的__dict__,它不会包含父类的__dict__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class CLanguage:
a = 1
b = 2
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"

class CL(CLanguage):
c = 1
d = 2
def __init__ (self):
self.na = "Python教程"
self.ad = "xiaohong"
#父类名调用__dict__
print(CLanguage.__dict__)
#子类名调用__dict__
print(CL.__dict__)
#父类实例对象调用 __dict__
clangs = CLanguage()
print(clangs.__dict__)
#子类实例对象调用 __dict__
cl = CL()
print(cl.__dict__)

运行结果为:

1
2
3
4
{'__module__': '__main__', 'a': 1, 'b': 2, '__init__': <function CLanguage.__init__ at 0x000001721A853E18>, '__dict__': <attribute '__dict__' of 'CLanguage' objects>, '__weakref__': <attribute '__weakref__' of 'CLanguage' objects>, '__doc__': None}
{'__module__': '__main__', 'c': 1, 'd': 2, '__init__': <function CL.__init__ at 0x000001721CD15510>, '__doc__': None}
{'name': '小明', 'add': 'xiaoming'}
{'na': 'Python教程', 'ad': 'xiaohong'}

显然,通过子类直接调用的__dict__中,并没有包含父类中的ab类属性;同样,通过子类对象调用的__dict__,也没有包含父类对象拥有的nameadd实例属性。

除此之外,借助由类实例对象调用__dict__属性获取的字典,可以使用字典的方式对其中实例属性的值进行修改,例如:

1
2
3
4
5
6
7
8
9
10
11
class CLanguage:
a = "aaa"
b = 2
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
#通过类实例对象调用 __dict__
clangs = CLanguage()
print(clangs.__dict__)
clangs.__dict__['name'] = "Python教程"
print(clangs.name)

程序运行结果为:

1
2
{'name': '小明', 'add': 'xiaoming'}
Python教程

注意,无法通过类似的方式修改类变量的值。

setattr()、getattr()、hasattr()

hasattr()函数

hasattr()函数用来判断某个类实例对象是否包含指定名称的属性或方法。

1
hasattr(obj, name)

其中obj指的是某个类的实例对象,name表示指定的属性名或方法名。同时,该函数会将判断的结果(True或者False)作为返回值反馈回来。

1
2
3
4
5
6
7
8
9
10
class CLanguage:
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
def say(self):
print("我正在学Python")
clangs = CLanguage()
print(hasattr(clangs,"name"))
print(hasattr(clangs,"add"))
print(hasattr(clangs,"say"))

程序输出结果为:

1
2
3
True
True
True

显然,无论是属性名还是方法名,都在hasattr()函数的匹配范围内。因此,我们只能通过该函数判断实例对象是否包含该名称的属性或方法,但不能精确判断,该名称代表的是属性还是方法。

getattr() 函数

getattr()函数获取某个类实例对象中指定属性的值。没错,和hasattr()函数不同,该函数只会从类对象包含的所有属性中进行查找。

1
getattr(obj, name[, default])

其中,obj表示指定的类实例对象,name表示指定的属性名,而default是可选参数,用于设定该函数的默认返回值,即当函数查找失败时,如果不指定default参数,则程序将直接报AttributeError错误,反之该函数将返回default指定的值。

1
2
3
4
5
6
7
8
9
10
11
class CLanguage:
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
def say(self):
print("我正在学Python")
clangs = CLanguage()
print(getattr(clangs,"name"))
print(getattr(clangs,"add"))
print(getattr(clangs,"say"))
print(getattr(clangs,"display",'nodisplay'))

程序执行结果为:

1
2
3
4
xiaoming
xiaoming
<bound method CLanguage.say of <__main__.CLanguage object at 0x000001FC2F2E3198>>
nodisplay

可以看到,对于类中已有的属性,getattr()会返回它们的值,而如果该名称为方法名,则返回该方法的状态信息;反之,如果该明白不为类对象所有,要么返回默认的参数,要么程序报AttributeError错误。

setattr()函数

setattr()函数的功能相对比较复杂,它最基础的功能是修改类实例对象中的属性值。其次,它还可以实现为实例对象动态添加属性或者方法。

1
setattr(obj, name, value)

首先,下面例子演示如何通过该函数修改某个类实例对象的属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
class CLanguage:
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
def say(self):
print("我正在学Python")
clangs = CLanguage()
print(clangs.name)
print(clangs.add)
setattr(clangs,"name","小红")
setattr(clangs,"add","xiaohong")
print(clangs.name)
print(clangs.add)

程序运行结果为:

1
2
3
4
小明
xiaoming
小红
xiaohong

甚至利用setattr()函数,还可以将类属性修改为一个类方法,同样也可以将类方法修改成一个类属性。

1
2
3
4
5
6
7
8
9
10
11
def say(self):
print("我正在学Python")
class CLanguage:
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
clangs = CLanguage()
print(clangs.name)
print(clangs.add)
setattr(clangs,"name",say)
clangs.name(clangs)

程序运行结果为:

1
2
3
小明
xiaoming
我正在学Python

显然,通过修改name属性的值为say(这是一个外部定义的函数),原来的name属性就变成了一个name()方法。

使用setattr()函数对实例对象中执行名称的属性或方法进行修改时,如果该名称查找失败,Python 解释器不会报错,而是会给该实例对象动态添加一个指定名称的属性或方法。例如:

1
2
3
4
5
6
7
8
9
def say(self):
print("我正在学Python")
class CLanguage:
pass
clangs = CLanguage()
setattr(clangs,"name","小明")
setattr(clangs,"say",say)
print(clangs.name)
clangs.say(clangs)

程序执行结果为:

1
2
小明
我正在学Python

可以看到,虽然CLanguage为空类,但通过setattr()函数,我们为clangs对象动态添加了一个name属性和一个say()方法。

issubclass和isinstance函数:检查类型

Python 提供了如下两个函数来检查类型:

  • issubclass(cls, class_or_tuple):检查cls是否为后一个类或元组包含的多个类中任意类的子类。
  • isinstance(obj, class_or_tuple):检查obj是否为后一个类或元组包含的多个类中任意类的对象。

通过使用上面两个函数,程序可以方便地先执行检查,然后才调用方法,这样可以保证程序不会出现意外情况。

如下程序示范了通过这两个函数来检查类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 定义一个字符串
hello = "Hello";
# "Hello"是str类的实例,输出True
print('"Hello"是否是str类的实例: ', isinstance(hello, str))
# "Hello"是object类的子类的实例,输出True
print('"Hello"是否是object类的实例: ', isinstance(hello, object))
# str是object类的子类,输出True
print('str是否是object类的子类: ', issubclass(str, object))
# "Hello"不是tuple类及其子类的实例,输出False
print('"Hello"是否是tuple类的实例: ', isinstance(hello, tuple))
# str不是tuple类的子类,输出False
print('str是否是tuple类的子类: ', issubclass(str, tuple))
# 定义一个列表
my_list = [2, 4]
# [2, 4]是list类的实例,输出True
print('[2, 4]是否是list类的实例: ', isinstance(my_list, list))
# [2, 4]是object类的子类的实例,输出True
print('[2, 4]是否是object类及其子类的实例: ', isinstance(my_list, object))
# list是object类的子类,输出True
print('list是否是object类的子类: ', issubclass(list, object))
# [2, 4]不是tuple类及其子类的实例,输出False
print('[2, 4]是否是tuple类及其子类的实例: ', isinstance([2, 4], tuple))
# list不是tuple类的子类,输出False
print('list是否是tuple类的子类: ', issubclass(list, tuple))

通过上面程序可以看出,issubclass()isinstance()两个函数的用法差不多,区别只是issubclass()的第一个参数是类名,而isinstance()的第一个参数是变量,这也与两个函数的意义对应:issubclass用于判断是否为子类,而isinstance()用于判断是否为该类或子类的实例。

issubclass()isinstance()两个函数的第二个参数都可使用元组。

1
2
3
4
5
6
data = (20, 'fkit')
print('data是否为列表或元组: ', isinstance(data, (list, tuple))) # True
# str不是list或者tuple的子类,输出False
print('str是否为list或tuple的子类: ', issubclass(str, (list, tuple)))
# str是list或tuple或object的子类,输出True
print('str是否为list或tuple或object的子类 ', issubclass(str, (list, tuple, object)))

此外,Python 为所有类都提供了一个__bases__属性,通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。例如如下代码:

1
2
3
4
5
6
7
8
9
class A:
pass
class B:
pass
class C(A, B):
pass
print('类A的所有父类:', A.__bases__)
print('类B的所有父类:', B.__bases__)
print('类C的所有父类:', C.__bases__)

运行上面程序,可以看到如下运行结果:

1
2
3
类A的所有父类: (<class 'object'>,)
类B的所有父类: (<class 'object'>,)
类C的所有父类: (<class '__main__.A'>, <class '__main__.B'>)

从上面的运行结果可以看出,如果在定义类时没有显式指定它的父类,则这些类默认的父类是object类。

Python 还为所有类都提供了一个__subclasses__()方法,通过该方法可以查看该类的所有直接子类,该方法返回该类的所有子类组成的列表。例如在上面程序中增加如下两行:

1
2
print('类A的所有子类:', A.__subclasses__())
print('类B的所有子类:', B.__subclasses__())

运行上面代码,可以看到如下输出结果:

1
2
类A的所有子类: [<class '__main__.C'>]
类B的所有子类: [<class '__main__.C'>]

call()

该方法的功能类似于在类中重载()运算符,使得类实例对象可以像调用普通函数那样,以“对象名()”的形式使用。

1
2
3
4
5
6
class CLanguage:
# 定义__call__方法
def __call__(self,name,add):
print("调用__call__()方法",name,add)
clangs = CLanguage()
clangs("小明", "xiaoming")

程序执行结果为:

1
调用__call__()方法 小明 xiaoming

可以看到,通过在CLanguage类中实现__call__()方法,使的clangs实例对象变为了可调用对象。

Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象。可调用对象包括自定义的函数、Python 内置函数以及本节所讲的类实例对象。

对于可调用对象,实际上“名称()”可以理解为是“名称.call()”的简写。仍以上面程序中定义的 clangs 实例对象为例,其最后一行代码还可以改写为如下形式:

1
clangs.__call__("C语言中文网","http://c.biancheng.net")

运行程序会发现,其运行结果和之前完全相同。

1
2
3
4
def say():
print("Python教程:http://c.biancheng.net/python")
say()
say.__call__()

程序执行结果为:

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

不仅如此,类中的实例方法也有以上 2 种调用方式。

call() 弥补 hasattr() 函数的短板

hasattr()函数的功能是查找类的实例对象中是否包含指定名称的属性或者方法,但该函数有一个缺陷,即它无法判断该指定的名称,到底是类属性还是类方法。

要解决这个问题,我们可以借助可调用对象的概念。要知道,类实例对象包含的方法,其实也属于可调用对象,但类属性却不是。

1
2
3
4
5
6
7
8
9
10
11
12
class CLanguage:
def __init__ (self):
self.name = "小明"
self.add = "xiaoming"
def say(self):
print("我正在学Python")
clangs = CLanguage()
if hasattr(clangs,"name"):
print(hasattr(clangs.name,"__call__"))
print("**********")
if hasattr(clangs,"say"):
print(hasattr(clangs.say,"__call__"))

程序执行结果为:

1
2
3
False
**********
True

可以看到,由于name是类属性,它没有以__call__为名的__call__()方法;而say是类方法,它是可调用对象,因此它有__call__()方法。

运算符重载

Python 中的各个序列类型,每个类型都有其独特的操作方法,例如列表类型支持直接做加法操作实现添加元素的功能,字符串类型支持直接做加法实现字符串的拼接功能,也就是说,同样的运算符对于不同序列类型的意义是不一样的,这是怎么做到的呢?

其实在 Python 内部,每种序列类型都是 Python 的一个类,例如列表是list类,字典是dict类等,这些序列类的内部使用了一个叫作“重载运算符”的技术来实现不同运算符所对应的操作。

所谓重载运算符,指的是在类中定义并实现一个与运算符对应的处理方法,这样当类对象在进行运算符操作时,系统就会调用类中相应的方法来处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyClass: #自定义一个类
def __init__(self, name , age): #定义该类的初始化函数
self.name = name #将传入的参数值赋值给成员交量
self.age = age
def __str__(self): #用于将值转化为字符串形式,等同于 str(obj)
return "name:"+self.name+";age:"+str(self.age)

__repr__ = __str__ #转化为供解释器读取的形式

def __lt__(self, record): #重载 self<record 运算符
if self.age < record.age:
return True
else:
return False

def __add__(self, record): #重载 + 号运算符
return MyClass(self.name, self.age+record.age)
myc = MyClass("Anna", 42) #实例化一个对象 Anna,并为其初始化
mycl = MyClass("Gary", 23) #实例化一个对象 Gary,并为其初始化
print(repr(myc)) #格式化对象 myc,
print(myc) #解释器读取对象 myc,调用 repr
print (str (myc)) #格式化对象 myc ,输出"name:Anna;age:42"
print(myc < mycl) #比较 myc<mycl 的结果,输出 False
print (myc+mycl) #进行两个 MyClass 对象的相加运算,输出 "name:Anna;age:65"

输出结果为:

1
2
3
4
5
name:Anna;age:42
name:Anna;age:42
name:Anna;age:42
False
name:Anna;age:65

这个例子中,MyClass 类中重载了repr、str、<、+运算符,并用MyClass实例化了两个对象mycmycl

通过将myc进行repr、str运算,从输出结果中可以看到,程序调用了重载的操作符方法__repr____str__。而令mycmycl进行 < 号的比较运算以及加法运算,从输出结果中可以看出,程序调用了重载 < 号的方法__lt____add__方法。

Python 常用重载运算符

重载运算符 含义
new 创建类,在 init 之前创建对象
init 类的构造函数,其功能是创建类对象时做初始化工作。
del 析构函数,其功能是销毁对象时进行回收资源的操作
add 加法运算符 +,当类对象 X 做例如 X+Y 或者 X+=Y 等操作,内部会调用此方法。但如果类中对 iadd 方法进行了重载,则类对象 X 在做 X+=Y 类似操作时,会优先选择调用 iadd 方法。
radd 当类对象 X 做类似 Y+X 的运算时,会调用此方法。
iadd 重载 += 运算符,也就是说,当类对象 X 做类似 X+=Y 的操作时,会调用此方法。
or “或”运算符
repr__,__str 格式转换方法,分别对应函数 repr(X)、str(X)
call 函数调用,类似于 X(*args, **kwargs) 语句
getattr 点号运算,用来获取类属性
setattr 属性赋值语句,类似于 X.any=value
delattr 删除属性,类似于 del X.any
getattribute 获取属性,类似于 X.any
getitem 索引运算,类似于 X[key],X[i:j]
setitem 索引赋值语句,类似于 X[key], X[i:j]=sequence
delitem 索引和分片删除
get, set, delete 描述符属性,类似于 X.attr,X.attr=value,del X.attr
len 计算长度,类似于 len(X)
lt__,__gt__,__le__,__ge__,__eq__,__ne 比较,分别对应于 <、>、<=、>=、=、!= 运算符。
iter__,__next 迭代环境下,生成迭代器与取下一条,类似于 I=iter(X) 和 next()
contains 成员关系测试,类似于 item in X
index 整数值,类似于 hex(X),bin(X),oct(X)
enter__,__exit 在对类对象执行类似 with obj as var 的操作之前,会先调用 enter 方法,其结果会传给 var;在最终结束该操作之前,会调用 exit 方法(常用于做一些清理、扫尾的工作)

迭代器

列表(list)、元组(tuple)、字典(dict)、集合(set)这些序列式容器,有一个共同的特性,它们都支持使用for循环遍历存储的元素,都是可迭代的,因此它们又有一个别称,即迭代器。

从字面来理解,迭代器指的就是支持迭代的容器,更确切的说,是支持迭代的容器类对象,这里的容器可以是列表、元组等这些 Python 提供的基础容器,也可以是自定义的容器类对象,只要该容器支持迭代即可。

如果要自定义实现一个迭代器,则类中必须实现如下 2 个方法:

  • __next__(self):返回容器的下一个元素。
  • __iter__(self):该方法返回一个迭代器(iterator)。

例如,下面程序自定义了一个简易的列表容器迭代器,支持迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class listDemo:
def __init__(self):
self.__date=[]
self.__step = 0
def __next__(self):
if self.__step <= 0:
raise StopIteration
self.__step -= 1
#返回下一个元素
return self.__date[self.__step]
def __iter__(self):
#实例对象本身就是迭代器对象,因此直接返回 self 即可
return self
#添加元素
def __setitem__(self,key,value):
self.__date.insert(key,value)
self.__step += 1
mylist = listDemo()
mylist[0]=1
mylist[1]=2
for i in mylist:
print (i)

程序执行结果为:

1
2
2
1

除此之外,Python 内置的iter()函数也会返回一个迭代器:

1
iter(obj[, sentinel])

其中,obj必须是一个可迭代的容器对象,而sentinel作为可选参数,如果使用此参数,要求 obj 必须是一个可调用对象。

可调用对象,指的是该类的实例对象可以像函数那样,直接以“对象名()”的形式被使用。通过在类中添加__call__()方法,就可以将该类的实例对象编程可调用对象。

我们常用的是仅有 1 个参数的iter()函数,通过传入一个可迭代的容器对象,我们可以获得一个迭代器,通过调用该迭代器中的__next__()方法即可实现迭代。例如;

1
2
3
4
5
6
7
# 将列表转换为迭代器
myIter = iter([1, 2, 3])
# 依次获取迭代器的下一个元素
print(myIter.__next__())
print(myIter.__next__())
print(myIter.__next__())
print(myIter.__next__())

运行结果为:

1
2
3
4
5
6
7
1
2
3
Traceback (most recent call last):
File "C:\Users\mengma\Desktop\demo.py", line 7, in <module>
print(myIter.__next__())
StopIteration

另外,也可以使用next()内置函数来迭代,即next(myIter),和__next__()方法是完全一样的。

从程序的执行结果可以看出,当迭代完存储的所有元素之后,如果继续迭代,则__next__()方法会抛出StopIteration异常。

iter()函数第 2 个参数的作用,如果使用该参数,则要求第一个obj参数必须传入可调用对象(可以不支持迭代),这样当使用返回的迭代器调用__next__()方法时,它会通过执行obj()调用__call__()方法,如果该方法的返回值和第 2 个参数值相同,则输出StopInteration异常;反之,则输出__call__()方法的返回值。

例如,修改 listDemo 类如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class listDemo:
def __init__(self):
self.__date=[]
self.__step = 0
def __setitem__(self,key,value):
self.__date.insert(key,value)
self.__step += 1
#是该类实例对象成为可调用对象
def __call__(self):
self.__step-=1
return self.__date[self.__step]
mylist = listDemo()
mylist[0]=1
mylist[1]=2
#将 mylist 变为迭代器
a = iter(mylist,1)
print(a.__next__())
print(a.__next__())

程序执行结果为:

1
2
3
4
5
2
Traceback (most recent call last):
File "D:\python3.6\1.py", line 20, in <module>
print(a.__next__())
StopIteration

输出结果中,之所以最终抛出StopIteration异常,是因为这里原本要输出的元素 1 和iter()函数的第 2 个参数相同。

迭代器本身是一个底层的特性和概念,在程序中并不常用,但它为生成器这一更有趣的特性提供了基础。

生成器

生成器本质上也是迭代器,不过它比较特殊。

list容器为例,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代;而生成器却不同,它可以实现在迭代的同时生成元素。
也就是说,对于可以用某种算法推算得到的多个数据,生成器并不会一次性生成它们,而是什么时候需要,才什么时候生成。

不仅如此,生成器的创建方式也比迭代器简单很多,大体分为以下 2 步:

  • 定义一个以yield关键字标识返回值的函数;
  • 调用刚刚创建的函数,即可创建一个生成器。
1
2
3
4
5
6
def intNum():
print("开始执行")
for i in range(5):
yield i
print("继续执行")
num = intNum()

由此,我们就成功创建了一个num生成器对象。显然,和普通函数不同,intNum()函数的返回值用的是 yield关键字,而不是return关键字,此类函数又成为生成器函数。

return相比,yield除了可以返回相应的值,还有一个更重要的功能,即每当程序执行完该语句时,程序就会暂停执行。不仅如此,即便调用生成器函数,Python 解释器也不会执行函数中的代码,它只会返回一个生成器(对象)。

要想使生成器函数得以执行,或者想使执行完yield语句立即暂停的程序得以继续执行,有以下 2 种方式:

  • 通过生成器(上面程序中的num)调用next()内置函数或者__next__()方法;
  • 通过for循环遍历生成器。

例如,在上面程序的基础上,添加如下语句:

1
2
3
4
5
6
7
#调用 next() 内置函数
print(next(num))
#调用 __next__() 方法
print(num.__next__())
#通过for循环遍历生成器
for i in num:
print(i)

程序执行结果为:

1
2
3
4
5
6
7
8
9
10
11
开始执行
0
继续执行
1
继续执行
2
继续执行
3
继续执行
4
继续执行

一个程序的执行流程:

  1. 首先,在创建有num生成器的前提下,通过其调用next()内置函数,会使 Python 解释器开始执行intNum()生成器函数中的代码,因此会输出“开始执行”,程序会一直执行到yield i,而此时的i==0,因此 Python 解释器输出“0”。由于受到yield的影响,程序会在此处暂停。
  2. 然后,我们使用 num 生成器调用__next__()方法,该方法的作用和next()函数完全相同(事实上,next()函数的底层执行的也是 __next__()方法),它会是程序继续执行,即输出“继续执行”,程序又会执行到yield i,此时i==1,因此输出“1”,然后程序暂停。
  3. 最后,我们使用for循环遍历num生成器,之所以能这么做,是因为for循环底层会不断地调用next()函数,使暂停的程序继续执行,因此会输出后续的结果。

注意,在 Python 2.x 版本中不能使用__next__()方法,可以使用next()内置函数,另外生成器还有next()方法(即以 num.next() 的方式调用)。

除此之外,还可以使用list()函数和tuple()函数,直接将生成器能生成的所有值存储成列表或者元组的形式。

1
2
3
4
num = intNum()
print(list(num))
num = intNum()
print(tuple(num))

程序执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
开始执行
继续执行
继续执行
继续执行
继续执行
继续执行
[0, 1, 2, 3, 4]
开始执行
继续执行
继续执行
继续执行
继续执行
继续执行
(0, 1, 2, 3, 4)

通过输出结果可以判断出,list()tuple()底层实现和for循环的遍历过程是类似的。

相比迭代器,生成器最明显的优势就是节省内存空间,即它不会一次性生成所有的数据,而是什么时候需要,什么时候生成。

@函数装饰器

Python 内置的 3 种函数装饰器,分别是@staticmethod、@classmethod@property,其中staticmethod()、classmethod()property()都是 Python 的内置函数。

那么,函数装饰器的工作原理是怎样的呢?假设用funA()函数装饰器去装饰funB()函数,如下所示:

1
2
3
4
5
6
7
8
9
#funA 作为装饰器函数
def funA(fn):
#...
fn() # 执行传入的fn参数
#...
return '...'
@funA
def funB():
#...

实际上,上面程序完全等价于下面的程序:

1
2
3
4
5
6
7
8
def funA(fn):
#...
fn() # 执行传入的fn参数
#...
return '...'
def funB():
#...
funB = funA(funB)

通过比对以上 2 段程序不难发现,使用函数装饰器A()去装饰另一个函数B(),其底层执行了如下 2 步操作:

  • B作为参数传给A()函数;
  • A()函数执行完成的返回值反馈回B
1
2
3
4
5
6
7
8
9
#funA 作为装饰器函数
def funA(fn):
print("小明")
fn() # 执行传入的fn参数
print("xiaoming")
return "装饰器函数的返回值"
@funA
def funB():
print("学习 Python")

程序执行流程为:

1
2
3
小明
学习 Python
xiaoming

在此基础上,如果在程序末尾添加如下语句:

1
print(funB)

其输出结果为:

1
装饰器函数的返回值

显然,被“@函数”修饰的函数不再是原来的函数,而是被替换成一个新的东西(取决于装饰器的返回值),即如果装饰器函数的返回值为普通变量,那么被修饰的函数名就变成了变量名;同样,如果装饰器返回的是一个函数的名称,那么被修饰的函数名依然表示一个函数。
实际上,所谓函数装饰器,就是通过装饰器函数,在不修改原函数的前提下,来对函数的功能进行合理的扩充。

带参数的函数装饰器

在分析funA()函数装饰器和funB()函数的关系时,当funB()函数无参数时,可以直接将funB作为funA()的参数传入。但是,如果被修饰的函数本身带有参数,那应该如何传值呢?

比较简单的解决方法就是在函数装饰器中嵌套一个函数,该函数带有的参数个数和被装饰器修饰的函数相同。

1
2
3
4
5
6
7
8
9
def funA(fn):
# 定义一个嵌套函数
def say(arc):
print("Python教程:",arc)
return say
@funA
def funB(arc):
print("funB():", a)
funB("http://c.biancheng.net/python")

程序执行结果为:

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

其实,它和如下程序是等价的:

1
2
3
4
5
6
7
8
9
10
def funA(fn):
# 定义一个嵌套函数
def say(arc):
print("Python教程:",arc)
return say
def funB(arc):
print("funB():", a)

funB = funA(funB)
funB("http://c.biancheng.net/python")

如果运行此程序会发现,它的输出结果和上面程序相同。

显然,通过funB()函数被装饰器funA()修饰,funB就被赋值为say。这意味着,虽然我们在程序显式调用的是funB()函数,但其实执行的是装饰器嵌套的say()函数。

但还有一个问题需要解决,即如果当前程序中,有多个(≥ 2)函数被同一个装饰器函数修饰,这些函数带有的参数个数并不相等,怎么办呢?

最简单的解决方式是用*args**kwargs作为装饰器内部嵌套函数的参数,*args**kwargs表示接受任意数量和类型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
def funA(fn):
# 定义一个嵌套函数
def say(*args,**kwargs):
fn(*args,**kwargs)
return say
@funA
def funB(arc):
print("百度:",arc)
@funA
def other_funB(name,arc):
print(name,arc)
funB("http://www.baidu.com")
other_funB("测试:","http://www.test.com")

运行结果为:

1
2
百度: http://www.baidu.com
测试: http://www.test.com

函数装饰器可以嵌套

上面示例中,都是使用一个装饰器的情况,但实际上,Python 也支持多个装饰器,比如:

1
2
3
4
5
@funA
@funB
@funC
def fun():
#...

上面程序的执行顺序是里到外,所以它等效于下面这行代码:

1
fun = funA(funB(funC(fun)))

Python字典

Python 字典(dict)是一种无序的、可变的序列,它的元素以“键值对(key-value)”的形式存储。相对地,列表(list)和元组(tuple)都是有序的序列,它们的元素在底层是挨着存放的。

字典类型是 Python 中唯一的映射类型。

字典中,习惯将各元素对应的索引称为键(key),各个键对应的元素称为值(value),键及其关联的值称为“键值对”。

总的来说,字典类型所具有的主要特征如表。

主要特征 解释
通过键而不是通过索引来读取元素 字典类型有时也称为关联数组或者散列表(hash)。它是通过键将一系列的值联系起来的,这样就可以通过键从字典中获取指定项,但不能通过索引来获取。
字典是任意数据类型的无序集合 和列表、元组不同,通常会将索引值 0 对应的元素称为第一个元素,而字典中的元素是无序的。
字典是可变的,并且可以任意嵌套 字典可以在原处增长或者缩短(无需生成一个副本),并且它支持任意深度的嵌套,即字典存储的值也可以是列表或其它的字典。
字典中的键必须唯一 字典中,不支持同一个键出现多次,否则只会保留最后一个键值对。
字典中的键必须不可变 字典中每个键值对的键是不可变的,只能使用数字、字符串或者元组,不能使用列表。

Python 中的字典类型相当于 Java 或者 C++ 中的 Map 对象。

和列表、元组一样,字典也有它自己的类型。Python 中,字典的数据类型为 dict,通过 type() 函数即可查看:

1
2
a = {'one': 1, 'two': 2, 'three': 3}  #a是一个字典类型
type(a) # <class 'dict'>

创建字典

1.使用 { } 创建字典

由于字典中每个元素都包含两部分,分别是键(key)和值(value),因此在创建字典时,键和值之间使用冒号:分隔,相邻元素之间使用逗号,分隔,所有元素放在大括号{ }中。

1
dictname = {'key':'value1', 'key2':'value2', ..., 'keyn':valuen}

其中dictname表示字典变量名,keyn: valuen表示各个元素的键值对。需要注意的是,同一字典中的各个键必须唯一,不能重复。

1
2
3
4
5
6
7
8
9
#使用字符串作为key
scores = {'数学': 95, '英语': 92, '语文': 84}
print(scores) # {'数学': 95, '英语': 92, '语文': 84}
#使用元组和数字作为key
dict1 = {(20, 30): 'great', 30: [1,2,3]}
print(dict1) # {(20, 30): 'great', 30: [1, 2, 3]}
#创建空元组
dict2 = {}
print(dict2) # {}

可以看到,字典的键可以是整数、字符串或者元组,只要符合唯一和不可变的特性就行;字典的值可以是 Python 支持的任意数据类型。

2.通过 fromkeys() 方法创建字典

还可以使用dict字典类型提供的fromkeys()方法创建带有默认值的字典:

1
dictname = dict.fromkeys(list,value=None)

其中,list参数表示字典中所有键的列表(list);value参数表示默认值,如果不写,则为空值None

1
2
3
knowledge = ['语文', '数学', '英语']
scores = dict.fromkeys(knowledge, 60)
print(scores) # {'语文': 60, '英语': 60, '数学': 60}

可以看到,knowledge列表中的元素全部作为了scores字典的键,而各个键对应的值都是 60。这种创建方式通常用于初始化字典,设置value的默认值。

3.通过 dict() 映射函数创建字典

通过dict()函数创建字典的写法有多种,下面列出了常用的几种方式,它们创建的都是同一个字典a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 写法1
# str 表示字符串类型的键,value 表示键对应的值。使用此方式创建字典时,字符串不能带引号。
a = dict(str1=value1, str2=value2, str3=value3)

# 写法2
#方式1
demo = [('two',2), ('one',1), ('three',3)]
#方式2
demo = [['two',2], ['one',1], ['three',3]]
#方式3
demo = (('two',2), ('one',1), ('three',3))
#方式4
demo = (['two',2], ['one',1], ['three',3])
a = dict(demo)
# 向 dict() 函数传入列表或元组,而它们中的元素又各自是包含 2 个元素的列表或元组,其中第一个元素作为键,第二个元素作为值。

# 方法3
keys = ['one', 'two', 'three'] #还可以是字符串或元组
values = [1, 2, 3] #还可以是字符串或元组
a = dict( zip(keys, values) )
# 通过应用 dict() 函数和 zip() 函数,可将前两个列表转换为对应的字典。

注意,无论采用以上哪种方式创建字典,字典中各元素的键都只能是字符串、元组或数字,不能是列表。列表是可变的,不能作为键。

如果不为dict()函数传入任何参数,则代表创建一个空的字典:

1
2
3
# 创建空的字典
d = dict()
print(d) # {}

访问字典

列表和元组是通过下标来访问元素的,而字典不同,它通过键来访问对应的值。因为字典中的元素是无序的,每个元素的位置都不固定,所以字典也不能像列表和元组那样,采用切片的方式一次性访问多个元素。

1
dictname[key]

其中,dictname表示字典变量的名字,key表示键名。注意,键必须是存在的,否则会抛出异常。

1
2
3
4
tup = (['two',26], ['one',88], ['three',100], ['four',-59])
dic = dict(tup)
print(dic['one']) #键存在
print(dic['five']) #键不存在

运行结果:

1
2
3
4
5
88
Traceback (most recent call last):
File "C:\Users\mozhiyan\Desktop\demo.py", line 4, in <module>
print(dic['five']) #键不存在
KeyError: 'five'

除了上面这种方式外,Python 更推荐使用dict类型提供的get()方法来获取指定键对应的值。当指定的键不存在时,get()方法不会抛出异常。

1
dictname.get(key[,default])

其中,dictname表示字典变量的名字;key表示指定的键;default用于指定要查询的键不存在时,此方法返回的默认值,如果不手动指定,会返回None

1
2
a = dict(two=0.65, one=88, three=100, four=-59)
print( a.get('one') ) # 88

注意,当键不存在时,get()返回空值None,如果想明确地提示用户该键不存在,那么可以手动设置get()的第二个参数:

1
2
a = dict(two=0.65, one=88, three=100, four=-59)
print( a.get('five', '该键不存在') ) # 该键不存在

删除字典

和删除列表、元组一样,手动删除字典也可以使用del关键字:

1
2
3
4
a = dict(two=0.65, one=88, three=100, four=-59)
print(a)
del a
print(a)

运行结果:

1
2
3
4
5
{'two': 0.65, 'one': 88, 'three': 100, 'four': -59}
Traceback (most recent call last):
File "C:\Users\mozhiyan\Desktop\demo.py", line 4, in <module>
print(a)
NameError: name 'a' is not defined

Python 自带垃圾回收功能,会自动销毁不用的字典,所以一般不需要通过del来手动删除。

字典基本操作

由于字典属于可变序列,所以我们可以任意操作字典中的键值对。Python 中,常见的字典操作有以下几种:

  • 向现有字典中添加新的键值对。
  • 修改现有字典中的键值对。
  • 从现有字典中删除指定的键值对。
  • 判断现有字典中是否存在指定的键值对。

字典是由一个一个的key-value构成的,key是找到数据的关键,Python 对字典的操作都是通过key来完成的。

字典添加键值对

为字典添加新的键值对很简单,直接给不存在的key赋值即可:

1
dictname[key] = value

对各个部分的说明:

  • dictname表示字典名称。
  • key表示新的键。
  • value表示新的值,只要是 Python 支持的数据类型都可以。
1
2
3
4
5
6
7
8
a = {'数学':95}
print(a) # {'数学': 95}
#添加新键值对
a['语文'] = 89
print(a) # {'数学': 95, '语文': 89}
#再次添加新键值对
a['英语'] = 90
print(a) # {'数学': 95, '语文': 89, '英语': 90}

字典修改键值对

Python 字典中键(key)的名字不能被修改,我们只能修改值(value)。

字典中各元素的键必须是唯一的,因此,如果新添加元素的键与已存在元素的键相同,那么键所对应的值就会被新的值替换掉,以此达到修改元素值的目的。

1
2
3
4
a = {'数学': 95, '语文': 89, '英语': 90}
print(a) # {'数学': 95, '语文': 89, '英语': 90}
a['语文'] = 100
print(a) # {'数学': 95, '语文': 100, '英语': 90}

可以看到,字典中没有再添加一个{'语文':100}键值对,而是对原有键值对{'语文': 89}中的value做了修改。

Python字典删除键值对

如果要删除字典中的键值对,还是可以使用del语句。

1
2
3
4
5
# 使用del语句删除键值对
a = {'数学': 95, '语文': 89, '英语': 90}
del a['语文']
del a['数学']
print(a) # {'英语': 90}

判断字典中是否存在指定键值对

如果要判断字典中是否存在指定键值对,首先应判断字典中是否有对应的键。判断字典是否包含指定键值对的键,可以使用innot in运算符。
需要指出的是,对于dict而言,innot in运算符都是基于key来判断的。

1
2
3
4
5
a = {'数学': 95, '语文': 89, '英语': 90}
# 判断 a 中是否包含名为'数学'的key
print('数学' in a) # True
# 判断 a 是否包含名为'物理'的key
print('物理' in a) # False

通过in(或not in)运算符,我们可以很轻易地判断出现有字典中是否包含某个键,如果存在,由于通过键可以很轻易的获取对应的值,因此很容易就能判断出字典中是否有指定的键值对。

字典方法

字典的数据类型为dict,我们可使用dir(dict)来查看该类型包含哪些方法:

1
2
dir(dict)
['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

keys()、values() 和 items() 方法

这三个方法都用来获取字典中的特定数据:

  • keys()方法用于返回字典中的所有键(key);
  • values()方法用于返回字典中所有键对应的值(value);
  • items()用于返回字典中所有的键值对(key-value)。
1
2
3
4
scores = {'数学': 95, '语文': 89, '英语': 90}
print(scores.keys()) # dict_keys(['数学', '语文', '英语'])
print(scores.values()) # dict_values([95, 89, 90])
print(scores.items()) # dict_items([('数学', 95), ('语文', 89), ('英语', 90)])

可以发现,keys()、values()items()返回值的类型分别为dict_keys、dict_valuesdict_items

需要注意的是,它们的返回值并不是我们常见的列表或者元组类型,因为 Python 3.x 不希望用户直接操作这几个方法的返回值。

如果想使用这三个方法返回的数据,一般有下面两种方案:

  1. 使用list()函数,将它们返回的数据转换成列表:
    1
    2
    3
    a = {'数学': 95, '语文': 89, '英语': 90}
    b = list(a.keys())
    print(b) # ['数学', '语文', '英语']
  2. 使用for in循环遍历它们的返回值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    a = {'数学': 95, '语文': 89, '英语': 90}
    for k in a.keys():
    print(k,end=' ')
    print("\n---------------")
    for v in a.values():
    print(v,end=' ')
    print("\n---------------")
    for k,v in a.items():
    print("key:",k," value:",v)
    运行结果为:
    1
    2
    3
    4
    5
    6
    7
    数学 语文 英语
    ---------------
    95 89 90
    ---------------
    key: 数学 value: 95
    key: 语文 value: 89
    key: 英语 value: 90

copy() 方法

copy()方法返回一个字典的拷贝,也即返回一个具有相同键值对的新字典:

1
2
3
a = {'one': 1, 'two': 2, 'three': [1,2,3]}
b = a.copy()
print(b) # {'one': 1, 'two': 2, 'three': [1, 2, 3]}

可以看到,copy()方法将字典a的数据全部拷贝给了字典b

注意,copy()方法所遵循的拷贝原理,既有深拷贝,也有浅拷贝。拿拷贝字典a为例,copy()方法只会对最表层的键值对进行深拷贝,也就是说,它会再申请一块内存用来存放{'one': 1, 'two': 2, 'three': []};而对于某些列表类型的值来说,此方法对其做的是浅拷贝,也就是说,b中的[1,2,3]的值不是自己独有,而是和a共有。

1
2
3
4
5
6
7
8
9
10
a = {'one': 1, 'two': 2, 'three': [1,2,3]}
b = a.copy()
#向 a 中添加新键值对,由于b已经提前将 a 所有键值对都深拷贝过来,因此 a 添加新键值对,不会影响 b。
a['four']=100
print(a) # {'one': 1, 'two': 2, 'three': [1, 2, 3], 'four': 100}
print(b) # {'one': 1, 'two': 2, 'three': [1, 2, 3]}
#由于 b 和 a 共享[1,2,3](浅拷贝),因此移除 a 中列表中的元素,也会影响 b。
a['three'].remove(1)
print(a) # {'one': 1, 'two': 2, 'three': [2, 3], 'four': 100}
print(b) # {'one': 1, 'two': 2, 'three': [2, 3]}

从运行结果不难看出,对a增加新键值对,b不变;而修改a某键值对中列表内的元素,b也会相应改变。

update() 方法

update()方法可以使用一个字典所包含的键值对来更新己有的字典。

在执行update()方法时,如果被更新的字典中己包含对应的键值对,那么原value会被覆盖;如果被更新的字典中不包含对应的键值对,则该键值对被添加进去。

1
2
3
a = {'one': 1, 'two': 2, 'three': 3}
a.update({'one':4.5, 'four': 9.3})
print(a) # {'one': 4.5, 'two': 2, 'three': 3, 'four': 9.3}

从运行结果可以看出,由于被更新的字典中已包含keyone的键值对,因此更新时该键值对的value将被改写;而被更新的字典中不包含keyfour的键值对,所以更新时会为原字典增加一个新的键值对。

pop() 和 popitem() 方法

pop()popitem()都用来删除字典中的键值对,不同的是,pop()用来删除指定的键值对,而popitem()用来随机删除一个键值对:

1
2
dictname.pop(key)
dictname.popitem()

其中,dictname表示字典名称,key表示键。

1
2
3
4
5
6
a = {'数学': 95, '语文': 89, '英语': 90, '化学': 83, '生物': 98, '物理': 89}
print(a) # {'数学': 95, '语文': 89, '英语': 90, '化学': 83, '生物': 98, '物理': 89}
a.pop('化学')
print(a) # {'数学': 95, '语文': 89, '英语': 90, '生物': 98, '物理': 89}
a.popitem()
print(a) # {'数学': 95, '语文': 89, '英语': 90, '生物': 98}

对 popitem() 的说明

其实,说popitem()随机删除字典中的一个键值对是不准确的,虽然字典是一种无须的列表,但键值对在底层也是有存储顺序的,popitem()总是弹出底层中的最后一个key-value,这和列表的pop()方法类似,都实现了数据结构中“出栈”的操作。

setdefault() 方法

setdefault()方法用来返回某个key对应的value

1
dictname.setdefault(key, defaultvalue)

说明,dictname表示字典名称,key表示键,defaultvalue表示默认值(可以不写,不写的话是None)。

当指定的key不存在时,setdefault()会先为这个不存在的key设置一个默认的defaultvalue,然后再返回defaultvalue

也就是说,setdefault()方法总能返回指定key对应的value

  • 如果该key存在,那么直接返回该key对应的value
  • 如果该key不存在,那么先为该key设置默认的defaultvalue,然后再返回该key对应的defaultvalue
1
2
3
4
5
6
7
8
9
10
11
a = {'数学': 95, '语文': 89, '英语': 90}
print(a) # {'数学': 95, '语文': 89, '英语': 90}
#key不存在,指定默认值
a.setdefault('物理', 94)
print(a) # {'数学': 95, '语文': 89, '英语': 90, '物理': 94}
#key不存在,不指定默认值
a.setdefault('化学')
print(a) # {'数学': 95, '语文': 89, '英语': 90, '物理': 94, '化学': None}
#key存在,指定默认值
a.setdefault('数学', 100)
print(a) # {'数学': 95, '语文': 89, '英语': 90, '物理': 94, '化学': None}

Python元组

元组(tuple)是 Python 中另一个重要的序列结构,和列表类似,元组也是由一系列按特定顺序排序的元素组成。

元组和列表的不同之处在于:

  • 列表的元素是可以更改的,包括修改元素值,删除和插入元素,所以列表是可变序列;
  • 而元组一旦被创建,它的元素就不可更改了,所以元组是不可变序列。

元组也可以看做是不可变的列表,通常情况下,元组用于保存无需修改的内容。

从形式上看,元组的所有元素都放在一对小括号( )中,相邻元素之间用逗号,分隔,如下所示:

1
(element1, element2, ... , elementn)

其中element1~elementn表示元组中的各个元素,个数没有限制,只要是 Python 支持的数据类型就可以。

从存储内容上看,元组可以存储整数、实数、字符串、列表、元组等任何类型的数据,并且在同一个元组中,元素的类型可以不同:

1
("www.baidu.com", 1, [2,'a'], ("abc",3.0))
1
type( ("www.baidu.com",1,[2,'a'],("abc",3.0)) ) # <class 'tuple'>

可以看到,元组是tuple类型。

Python创建元组

Python 提供了两种创建元组的方法。

使用 ( ) 直接创建

通过( )创建元组后,一般使用=将它赋值给某个变量:

1
tuplename = (element1, element2, ..., elementn)

其中,tuplename表示变量名,element1 ~ elementn表示元组的元素。

1
2
num = (7, 14, 21, 28, 35)
abc = ( "Python", 19, [1,2], ('c',2.0) )

在 Python 中,元组通常都是使用一对小括号将所有元素包围起来的,但小括号不是必须的,只要将各元素用逗号隔开,Python 就会将其视为元组。

1
2
course = "小明", "xiaoming"
print(course) # ('小明', 'xiaoming')

需要注意的一点是,当创建的元组中只有一个字符串类型的元素时,该元素后面必须要加一个逗号,,否则 Python 解释器会将它视为字符串。

1
2
3
4
5
6
7
8
#最后加上逗号
a =("cplus",)
print(type(a)) # <class 'tuple'>
print(a) # ('cplus',)
#最后不加逗号
b = ("socket")
print(type(b)) # <class 'str'>
print(b) # socket

使用tuple()函数创建元组

除了使用( )创建元组外,Python 还提供了一个内置的函数tuple(),用来将其它数据类型转换为元组类型。

1
tuple(data)

其中,data表示可以转化为元组的数据,包括字符串、元组、range对象等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#将字符串转换成元组
tup1 = tuple("hello")
print(tup1) # ('h', 'e', 'l', 'l', 'o')
#将列表转换成元组
list1 = ['Python', 'Java', 'C++', 'JavaScript']
tup2 = tuple(list1)
print(tup2) # ('Python', 'Java', 'C++', 'JavaScript')
#将字典转换成元组
dict1 = {'a':100, 'b':42, 'c':9}
tup3 = tuple(dict1)
print(tup3) # ('a', 'b', 'c')
#将区间转换成元组
range1 = range(1, 6)
tup4 = tuple(range1)
print(tup4) # (1, 2, 3, 4, 5)
#创建空元组
print(tuple()) # ()

访问元组元素

和列表一样,我们可以使用索引(Index)访问元组中的某个元素(得到的是一个元素的值),也可以使用切片访问元组中的一组元素(得到的是一个新的子元组)。

1
tuplename[i]

其中,tuplename表示元组名字,i表示索引值。元组的索引可以是正数,也可以是负数。

1
tuplename[start : end : step]

其中,start表示起始索引,end表示结束索引,step表示步长。

1
2
3
4
5
6
7
8
url = tuple("http://c.biancheng.net/shell/")
#使用索引访问元组中的某个元素
print(url[3]) #使用正数索引
print(url[-4]) #使用负数索引
#使用切片访问元组中的一组元素
print(url[9: 18]) #使用正数切片
print(url[9: 18: 3]) #指定步长
print(url[-6: -1]) #使用负数切片

运行结果:

1
2
3
4
5
p
e
('b', 'i', 'a', 'n', 'c', 'h', 'e', 'n', 'g')
('b', 'n', 'e')
('s', 'h', 'e', 'l', 'l')

修改元组

元组是不可变序列,元组中的元素不能被修改,所以我们只能创建一个新的元组去替代旧的元组。

1
2
3
4
5
tup = (100, 0.5, -36, 73)
print(tup) # (100, 0.5, -36, 73)
#对元组进行重新赋值
tup = ('小明',"xiaoming")
print(tup) # ('小明',"xiaoming")

另外,还可以通过连接多个元组(使用+可以拼接元组)的方式向元组中添加新元素:

1
2
3
4
5
tup1 = (100, 0.5, -36, 73)
tup2 = (3+12j, -54.6, 99)
print(tup1+tup2) # (100, 0.5, -36, 73, (3+12j), -54.6, 99)
print(tup1) # (100, 0.5, -36, 73)
print(tup2) # ((3+12j), -54.6, 99)

你看,使用+拼接元组以后,tup1tup2的内容没法发生改变,这说明生成的是一个新的元组。

删除元组

可以通过del关键字将其删除:

1
2
3
4
tup = ('百度', "http://www.baidu.com/")
print(tup)
del tup
print(tup)

运行结果为:

1
2
3
4
5
('百度', "http://www.baidu.com/")
Traceback (most recent call last):
File "C:\Users\mozhiyan\Desktop\demo.py", line 4, in <module>
print(tup)
NameError: name 'tup' is not defined

Python 自带垃圾回收功能,会自动销毁不用的元组,所以一般不需要通过del来手动删除。

  • Copyrights © 2017-2023 WSQ
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信