书籍目录
1- 初识Python
2- 环境搭建
3- 我的第一个Python程序
4- 注释
5- 变量
6- 基本数据类型
7- 字符串及其方法的使用
8- 运算符
9- 序列简介
10- 列表和元组的使用
11- 字典
12- 流程控制-if分支结构
13- 流程控制-循环结构
14- 函数入门
15- 函数的参数
16- 函数式编程及lambda表达式
17- 类和对象
18- 方法
19- 成员变量和封装
20- 继承和多态
21- 枚举类
22- 异常处理
23- 常见特殊方法
24- 模块和包
943
Python基础
免费
共24小节
Python编程基础 从入门到实践
离线

记忆

私信

1-初识Python

1.认识Python

Python是一种直译式(Interpreted )、面向对象(Object Oriented )、弱类型的脚本语言,它拥有完整的函数库,可以协助轻松地完成许多常见的工作。

所谓的直译式语言是指,直译器(Interpretor)会将程序代码一句一句直接执行,不需要经过编译(compile)动作,将语言先转换成机器码,再予以执行。目前它的直译器是CPython,这是由C语言编写的一个直译程序。

它也是一种功能强大而完善的通用型语言 。由 吉多·范罗苏姆(Guido van Rossum)于1991年开发。它以简洁、易读和可维护的代码而闻名,具有广泛的应用领域,包括Web开发、科学计算、人工智能、数据分析等。


2.优点

  • 简单明了:Python采用清晰简洁的语法,易于理解和学习,使得开发者能够以较少的代码实现复杂的功能。

  • 可读性强:Python的语法结构使得代码具有良好的可读性,易于团队合作和维护。

  • 跨平台性:Python可以在多个操作系统上运行,包括Windows、Linux、macOS等,具有很好的跨平台性。

  • 大量标准库和第三方库:Python拥有丰富的标准库,涵盖了各种常用功能,比如文件操作、网络通信等,同时还有庞大的第三方库生态系统,提供了众多功能强大的库供开发者使用。

  • 广泛应用领域:Python在Web开发、数据分析、机器学习等领域具有广泛的应用,可以满足不同领域的需求。


3.缺点

  • 运行速度相对较慢:与一些编译型语言相比,Python的执行速度较慢。但可以通过优化算法、使用C扩展等方式提高性能。

  • 全局解释器锁(GIL):Python的解释器中存在GIL,它会限制多线程的并行执行,导致在某些情况下无法充分利用多核处理器的性能。

  • 代码保护性较差:由于Python是一种动态类型语言,缺乏编译时的类型检查,因此在大型项目中对代码的保护性需要额外的注意


4.版本介绍

Python目前有两个主要的稳定版本:Python 2.x和Python 3.x。

Python 2.x系列:

  • Python 2.0于2000年发布,历经多个小版本的改进和更新,在2010年停止了更新。

  • Python 2.x系列是Python的早期版本,很多项目和库都是在此版本上开发的。

  • Python 2.x系列存在一些与Python 3.x不兼容的语法和特性,包括print语句、整数运算、Unicode字符串等方面。

  • 目前Python 2.x系列正在逐渐被弃用,官方已于2020年停止对Python 2.x版本的支持。

Python 3.x系列:

  • Python 3.0于2008年发布,引入了一些重大变化和改进,并且与Python 2.x不完全兼容。

  • Python 3.x系列修复了Python 2.x中的一些设计缺陷和不一致性,并提供了更好的性能和功能。

  • Python 3.x系列中的一些主要改变包括:print函数代替print语句、统一的整数类型、Unicode字符串、更严格的异常处理等。

  • Python 3.x系列在语言及标准库方面进行了较大的改进和优化,但也导致了与Python 2.x不兼容的问题。

  • 目前,Python 3.x系列已经成为官方推荐的版本,并且大多数新项目和库都使用Python 3.x进行开发。

需要注意的是,在迁移现有的Python 2.x代码到Python 3.x时,可能存在一些兼容性问题,特别是对于较大的项目或依赖于特定库的项目。因此,迁移过程需要仔细考虑和测试。

总体而言,如果您准备开始新的Python项目,建议使用Python 3.x版本,因为它具有更多的功能和改进。但如果您在维护旧项目或依赖于仅在Python 2.x上可用的库,则需要根据实际情况权衡选择。同时,为了保持最佳的兼容性和可移植性,确保使用兼容Python 2.x和Python 3.x的代码编写方式也是一个不错的选择。


5.应用范围

尽管Python是一个非常适合初学者学习的程序语言,在国外有许多儿童程序语言教学也是以Python为工具,然而它却是一个功能强大的程序语言,下列是它的部分应用。

  • 设计动画游戏。

  • 开发与管理网站。

  • 执行大数据分析。

  • 支持图形接口( Graphical User Interface,GUI)开发。

  • Google、Yahoo !、YouTube、NASA、Dropbox(文件分享服务)、Reddit(社交网站)在内部皆大量使用Python做开发工具。

  • 黑客攻防。

总体来说,Python语言以其简单易学、可读性强和丰富的库支持而深受开发者喜爱,在各个领域都有广泛的应用。同时,开发者也应该根据具体需求和性能要求,选择合适的编程语言。

2-环境搭建

在开发 Python 程序之前 , 必须先完成一些准备工作, 也就是在计算机上安装并配置 Python解释器。


1.Windows 上安装 Python

  1. 下载Python安装程序:

    • 访问Python官方下载网站(https://www.python.org/downloads )。

    • 因为2.X已停止维护,目前只提供3.X的下载

    • 找到适合自己的版本链接,下载Windows安装程序(.exe文件)。

  2. 运行安装程序:

    • 双击运行下载的Python安装程序(.exe文件)。

    • 在安装程序的欢迎界面中,确保勾选“Add Python to PATH”(将Python添加到环境变量中)选项,这样您就可以在命令行中直接使用Python。

  3. 完成安装:

    • 安装程序将复制Python解释器及其相关功能库到您选择的目录中。

    • 安装完成后,您可以选择启动IDLE(Python的官方IDE),也可以使用其他文本编辑器或IDE进行开发。

    • 验证安装是否成功:打开命令提示符,输入python --version,如果显示Python的版本号,则表示安装成功。

注意事项:

  • 如果您之前已经安装了Python,请确保卸载旧版本,并清理相关环境变量,以免出现冲突。

  • 安装过程中,务必选择将Python添加到环境变量中,这样可以方便地在任何位置使用Python。


2.Linux 上安装Python

  1. 检查系统是否已安装Python:

    打开终端(Terminal)。

    输入查看版本命令

python3 --version

查看系统中是否已经安装了Python。如果已经安装了Python 3.x版本,将显示其版本号。如果未找到Python或版本较旧,请继续进行下一步。

  1. 更新软件包列表:
sudo apt update
  1. 安装Python 3.x:
sudo apt install python3
  1. 验证安装:

    在终端中输入查看版本命令

    如果成功显示Python 3.x的版本号,则表示安装成功。

  2. 设置默认Python版本(可选):

    如果系统上同时安装了Python 2.x和Python 3.x,并且您希望将Python 3.x设置为默认版本,则可以使用以下命令:

sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1

输入以上命令后,再次输入以下命令,选择Python 3.x作为默认版本:

sudo update\-alternatives _\--config python_

根据提示,选择相应的数字,然后按回车确认选择。这样,当您在终端中输入 python 命令时,将启动Python 3.x。

注意事项:

  • 建议使用系统软件包管理工具(如APT、Yum等)进行Python的安装,以便轻松管理和更新。

  • 在不同的Linux发行版中,命令可能稍有不同


3.Mac OS 上安装 Python

  1. 下载Python安装程序:

    • 访问Python官方下载网站(https://www.python.org/downloads )。

    • 选择一个适合自己系统的版本。通常建议选择最新的稳定版本,比如Python 3.x系列。

    • 点击对应版本的下载链接,下载Mac OS的安装程序(.dmg文件)。

  2. 运行安装程序:

    • 双击下载的Python安装程序(.dmg文件)将其挂载为一个磁盘映像。

    • 在打开的磁盘映像窗口中,双击运行名为"Python.mpkg"或"Python.pkg"的安装包。

    • 根据向导进行安装,您可以使用默认设置或自定义一些选项,如安装目录等。

  3. 验证安装:

    • 打开终端(Terminal)。

    • 输入查看版本命令

    
    python3 --version
    
    

    如果成功显示Python 3.x的版本号,则表示安装成功。

    • 配置环境变量(可选):

    默认情况下,安装Python时会将其路径添加到环境变量中,因此您可以在终端中直接使用python3命令。

    如果您碰到无法直接使用python3命令的情况,可能需要手动配置环境变量。打开终端,执行以下命令来编辑用户根目录下的配置文件(如果该文件不存在则创建文件):

    nano ~/.bash\_profile
    
    • 在文件中添加以下内容:
    export PATH\="/usr/local/bin:$PATH"
    
    • 按 Ctrl + X 保存并退出编辑器。

    • 输入以下命令来使更改生效:

    source ~/.bash\_profile
    

注意事项:

  • 在macOS Big Sur及更高版本中,系统会默认安装Python 2.7.x,但建议使用Python 3.x版本进行开发。

  • 如果需要在开发中使用特定的Python库,建议使用pip(Python包管理器)来安装、管理和升级软件包。

  • 建议定期更新Python版本,以获得最新的功能和修复已知的问题。

3-我的第一个Python程序

按照惯例开始编写第一个程序 : Hello World 。

1.Python常用开发工具

在安装Python时己经提供了一个简单的编辑工具 : IDLE ,开发者使用 IDLE即可编写 Python程序,其在执行单行即时交互命令时比较方便,在开发项目时更推荐使用开发工具:

  1. PyCharm:JetBrains公司开发的专业Python集成开发环境(IDE),提供了强大的代码编辑、调试和项目管理功能。新手入门推荐使用

  2. Visual Studio Code:微软推出的轻量级跨平台代码编辑器,支持丰富的扩展插件,可以通过插件扩展为完整的Python开发环境。

  3. Anaconda:数据科学领域常用的Python发行版,内置了许多常用的数据分析和科学计算库,提供了Anaconda Navigator图形界面管理工具,以及Jupyter Notebook等工具。

  4. Spyder:专为科学计算和数据分析而设计的Python集成开发环境(IDE),内置了IPython控制台、变量查看器等工具。

  5. Sublime Text:一款轻量级的代码编辑器,提供了丰富的插件和主题,支持跨平台使用。

编写 Python 程序不要使用写字板,更不可使用 Word 等文档编辑器。因为写字板、Word 等工具是有格式的编辑器 ,当使用它们编辑一份文档时,这个文档中会包含一些隐藏的格式化字符,这些隐藏的字符会导致程序无法正常运行。


2.编辑源程序

  1. 在IDLE窗口可以执行File/New File  来创建一个空白Python文件

  2. 在窗口内输入打印语句:

print("Hello World")
  1. 将文件保存为hello_world.py,该文件就是 Python 程序的源程序。

3.使用IDLE运行Python程序

  1. 在IDLE窗口可以执行Run/Run Module 来执行 上述源程序

  2. 执行可得到结果:

Python 3.10.6 (tags/v3.10.6:9c7b4bd, Aug  1 2022, 21:53:49) [MSC v.1932 64 bit (AMD64)] 
on win32
Type "help", "copyright", "credits" or "license()" for more information.
    
======================= RESTART: D:/codes/hello_world.py =======================
Hello World

打印结果语句可以正常输出


4.使用命令行工具运行Python程序

运行 Python 程序实际上使用的是“ python ”命令,启动命令行窗口,进入hello_world.py所在的位置,在命令行窗口中直接输入如下命令:

python hello\_world.py

正常输出结果:

Hello World

可以看出,使用“ python ”命令的语法非常简单,该命令的语法格式如下 :

python3 <Python源程序文件路径>

在Windows系统上,路径不区分大小写,因此输入Python源程序路径时不需要考虑大小写。而在Mac OS 或Linux系统上,路径是区分大小写的,所以输入Python源程序路径时必须注意大小写问题。


5.交互式解释器

Python交互式解释器是一种以交互形式运行Python代码的工具。它提供了一个命令行界面,允许用户输入一行Python代码并立即执行,然后返回结果。使用交互式解释器可以方便地进行代码调试、快速尝试、学习和交互式的数据分析。

以下是一个使用Python交互式解释器的简单示例:

  1. 打开命令行终端或Python解释器(在电脑上安装了Python的情况下)。

  2. 键入python并按下回车键,启动Python交互式解释器。

  3. 在提示符 \>>> 后输入Python代码,并按下回车键执行。

例如,我们可以输入以下代码:

>>> x = 5
>>> y = 10
>>> z = x + y
>>> print(z)
15

输出是:15

在交互式解释器中,每输入一行代码并按下回车键执行后,结果会立即显示在下一行。

除了简单的表达式计算,交互式解释器还可以执行复杂的Python代码,甚至导入和使用各种功能强大的库。例如,我们可以使用math库来计算平方根:

>>> import math
>>> x = 9
>>> sqrt_x = math.sqrt(x)
>>> print(sqrt_x)
3.0

输出是:3.0

在交互式解释器中,我们可以方便地逐行执行和测试代码,以便检查代码的行为和结果。这对于快速尝试新的想法、调试代码或进行数据探索非常有用。

4-注释

在Python中,我们可以使用单行注释和多行注释来给代码添加注释说明。注释是一种用于解释代码意图、提供文档说明或暂时禁用代码的工具。

  1. 单行注释:在单行注释中,使用井号(#)开头,后面跟着注释内容。这样的注释只对当前行有效,不会影响其他代码。

下面是一个示例:

# 这是一个单行注释
x = 5  # 定义变量x并赋值为5

在上面的示例中,第一行是一个单行注释,用于解释注释所在行的目的。第二行的注释说明了代码的作用。


  1. 多行注释:多行注释是用于较长的注释块或注释多行的情况。在Python中,我们使用三个引号(‘’')或三个双引号(“”")来表示多行注释。多行注释可以跨越多行代码,并且可以包含多个段落。

以下是一个使用多行注释的示例:

'''
这是一个多行注释的示例。
这个注释可以包含多个段落。
'''

或者:

"""
这也是一个多行注释的示例。
这个注释也可以包含多个段落。
"""

多行注释通常用于文件或函数开头,用于提供整个文件或函数的说明文档。

请注意,多行注释通常被解释器忽略,不会影响代码的执行。而单行注释只在注释所在行之后起作用。

注释对于代码的可读性和维护非常重要,它可以帮助其他开发人员理解你的代码意图并提供文档。因此,在编写代码时,合理使用注释是一个良好的习惯。

5- 变量

1.认识变量

在Python中,变量用于存储数据,并通过一个标识符(变量名)来引用这些数据。变量可以是任何数据类型,如整数、浮点数、字符串、列表、字典等。

变量就像是一个容器,标识符(变量名)则为这个容器的名称。

1.1.声明变量

在Python中,我们不需要显式地声明变量的类型。变量的创建发生在第一次赋值时,通过使用等号(=)将值赋给变量。

x = 10  # 创建一个整数变量x,并赋值为10
y = "Hello"  # 创建一个字符串变量y,并赋值为"Hello"

1.2.动态类型

Python是一种动态类型语言,这意味着变量的类型可以根据赋给它们的值自动改变。同一个变量可以在不同的时间存储不同类型的数据。

x = 5  # x 是一个整数
x = "Hello"  # x 现在是一个字符串

1.3.弱类型

弱类型是指在Python中,变量的类型是灵活的,可以在运行时自动改变,而不需要显式声明。这种灵活性在某些情况下非常有用,但也需要小心处理,以避免潜在的错误。

例如,如果我们进行一些不同类型的操作,如将一个字符串和一个整数相加,Python会自动进行类型转换。在这个例子中,整数将被转换为字符串类型,并执行字符串拼接操作。然而,如果我们不小心处理类型转换,可能会导致意外的结果或错误。

以下是一个示例:

x = 10
y = "20"
z = x + y  # 这里会引发 TypeError: unsupported operand type(s) for +: 'int' and 'str' 错误

在上面的示例中,由于尝试将整数和字符串相加,Python不知道应该执行什么操作,因此会引发类型错误。为了避免这种情况,我们可以使用适当的类型转换,将数据转换为相同类型后再进行操作。

总结来说,Python的变量具有动态类型和弱类型的特征,这使得Python更加灵活和方便。但是,在编写代码时,我们需要注意处理类型转换以及确保变量的类型与预期一致,以避免潜在的错误和意外结果。


1.4.类型转换

我们可以使用类型转换来将一个数据类型转换为另一个数据类型。这在处理数据时非常有用,因为可能需要将一个类型的数据转换为另一个类型的数据,以便它们可以正确地进行计算或处理。

Python支持以下类型转换:

  1. int()函数:将其他数据类型转换为整数类型。
x = int(3.14)  # 将浮点数3.14转换为整数3
y = int("42")  # 将字符串"42"转换为整数42
z = int(True)  # 将布尔值True转换为整数1
  1. float()函数:将其他数据类型转换为浮点数类型。
x = float(3)  # 将整数3转换为浮点数3.0
y = float("3.14")  # 将字符串"3.14"转换为浮点数3.14
z = float(True)  # 将布尔值True转换为浮点数1.0
  1. str()函数:将其他数据类型转换为字符串类型。
x = str(3)  # 将整数3转换为字符串"3"
y = str(3.14)  # 将浮点数3.14转换为字符串"3.14"
z = str(True)  # 将布尔值True转换为字符串"True"
  1. bool()函数:将其他数据类型转换为布尔类型。
x = bool(0)  # 将整数0转换为布尔值False
y = bool("")  # 将空字符串转换为布尔值False
z = bool(3)  # 将非零整数转换为布尔值True

需要注意的是,如果进行不合法或无法完成的类型转换,Python将会抛出TypeError或ValueError异常。

以下是一个示例:

x = int("abc")  # 将字符串"abc"转换为整数,会发生 ValueError 错误

总结来说,Python提供了丰富的类型转换函数,可以方便地在不同数据类型之间进行转换。在使用类型转换时,我们需要注意数据类型的兼容性和正确性,以避免意外的结果或错误。


2.使用print函数输出变量

print()函数是Python中用于将信息输出到终端(命令行界面)的一个内置函数。它可以接受一个或多个参数,并将它们打印为文本。

语法:

print(value1, value2, ..., sep=' ', end='\n', file=sys.stdout, flush=False)

参数:

  • value1, value2, …:要打印的值,可以是一个或多个参数。

  • sep:可选参数,用于指定分隔符,默认为一个空格。将每个值打印为文本时,sep参数将被插入到相邻值之间。

  • end:可选参数,用于指定打印结束后的字符串,默认为换行符\n。它表示打印完所有值后,要附加在最后一个值之后的字符串。

  • file:可选参数,用于指定输出的文件对象。默认情况下,输出将打印到sys.stdout,即终端。

  • flush:可选参数,如果设置为True,则刷新输出缓冲。默认为False。

示例:

name = "Alice"
age = 25
height = 1.65
    
print("My name is", name, "and I am", age, "years old.")  # 打印多个值,使用默认的分隔符和换行符
# 输出:My name is Alice and I am 25 years old.
    
print("My name is " + name + " and I am " + str(age) + " years old.")  # 通过字符串拼接打印文本
# 输出:My name is Alice and I am 25 years old.
    
print("Height:", height, "m", sep="")  # 使用空字符串作为分隔符,不插入额外字符
# 输出:Height:1.65m
    
print("Hello", end=" ")  # 指定结束字符串为一个空格,而不是默认的换行符
print("World")

# 输出:
# Hello World

在上述示例中,我们使用print()函数打印了不同类型的值。

在第一个示例中,我们传递了多个参数,并使用默认的分隔符和换行符。

在第二个示例中,我们通过字符串拼接的方式构建了要打印的文本。

在第三个示例中,我们使用空字符串作为分隔符来控制打印结果。

最后一个示例演示了如何使用end参数来指定结束字符串。

总结来说,print()函数是Python中常用的输出函数,它能够将值转换为文本并打印到终端。我们可以使用sep参数来控制分隔符,使用end参数来指定结束字符串。通过掌握print()函数的使用,我们可以方便地输出信息和调试程序。


3.变量的命名规则

变量是程序中存储数据的最基本的容器。在定义变量时,需要遵守一些命名规则和约定。以下是Python变量的命名规则:

  1. 变量名只能包含字母、数字和下划线(_)这三种字符。

  2. 变量名第一个字符必须是字母或下划线,不能以数字开头。

  3. 变量名区分大小写,x和X是两个不同的变量名。

  4. 变量名应该具有描述性,使得变量的用途易于理解。

  5. 变量名不应该使用Python关键字或保留字,如if、else、while等,但可以包含关键字。

以下是一些符合命名规则的变量名的示例:

age = 25
name = "Alice"
average_score = 90.5
is_student = True

除了上述命名规则外,还有一些命名约定可以帮助编写更易读且易于维护的代码:

  1. 变量名应该使用小写字母,并使用下划线来分隔单词,以增加可读性,如student_name。

  2. 对于常量(不会改变的值),应该使用全大写字母来命名,并使用下划线来分隔单词,如MAX_NUMBER。

  3. 对于私有变量(只能在模块内部使用),可以在变量名前加上单个下划线,如_secret_key。

  4. 对于特殊变量(有特殊含义的变量),可以在变量名前后加上两个下划线,如__name__。

遵守这些命名规则和约定,可以帮助开发者编写出易读、易于维护的Python代码。


4.关键字和内置函数

4.1.关键字:

关键字 描述
False 布尔类型的假值
None 表示空值或缺失值
True 布尔类型的真值
and 逻辑与操作符
as 用于创建别名
assert 用于检查条件是否为真,如果为假则触发异常
break 跳出当前循环体
class 定义一个类
continue 跳过当前循环体的剩余部分
def 定义一个函数
del 删除对象的引用
elif if语句中的"else if"分支
else if语句中的默认分支
except 捕获异常
finally 无论是否发生异常都会执行的代码块
for 用于循环遍历序列、集合或迭代器
from 从模块中导入指定的部分或全部内容
global 声明全局变量
if 条件语句,如果条件为真,则执行相应的代码
import 导入一个模块
in 测试值是否存在于序列、集合或迭代器中
is 测试两个对象是否相等
lambda 创建一个匿名函数
nonlocal 声明非局部变量
not 逻辑非操作符
or 逻辑或操作符
pass 什么都不做,用作占位符
raise 抛出一个异常
return 从函数中返回一个值
try 定义一个代码块,并且在可能发生异常的地方进行处理
while 循环语句,只要条件为真,就会执行循环体
with 定义一个上下文管理器,用于简化资源的获取和释放操作
yield 在生成器函数中使用,返回一个值并暂停生成器的执行,保留当前状态

4.2.内置函数:

内置函数 描述
abs() 返回绝对值
all() 判断可迭代对象中的所有元素是否为True
any() 判断可迭代对象中的任一元素是否为True
bin() 将整数转换为二进制字符串
bool() 转换为布尔类型
chr() 将整数转换为Unicode字符
dict() 创建一个字典
enumerate() 返回一个可迭代对象的枚举,包含索引和对应的值
filter() 用于过滤序列,返回满足条件的元素组成的迭代器
float() 转换为浮点数
format() 格式化字符串
input() 从用户输入中读取一行数据
int() 转换为整数
len() 返回对象的长度
list() 创建一个列表
map() 对可迭代对象中的所有元素应用给定函数,并返回结果组成的迭代器
max() 返回可迭代对象中的最大值
min() 返回可迭代对象中的最小值
open() 打开文件并返回文件对象
print() 将信息输出到终端
range() 返回一个序列的数字范围
round() 四舍五入到指定的小数位数
set() 创建一个集合
sorted() 对可迭代对象进行排序,并返回一个新的排序后的列表
str() 转换为字符串类型
sum() 返回可迭代对象中所有元素之和
tuple() 创建一个元组
type() 返回对象的类型
zip() 将可迭代对象中对应的元素打包成元组后返回一个迭代器
isinstance() 检查对象是否是指定类型
range() 返回一个序列的数字范围
sorted() 对可迭代对象进行排序,并返回一个新的排序后的列表
help() 查看帮助信息
id() 返回对象的唯一标识符
isinstance() 检查对象是否是指定类型
max() 返回可迭代对象中的最大值
min() 返回可迭代对象中的最小值
sum() 返回可迭代对象中所有元素之和
pow() 返回指定数字的指数幂
input() 从用户输入中读取一行数据
round() 四舍五入到指定的小数位数
chr() 返回Unicode编码对应的字符
ord() 返回字符对应的Unicode编码
hex() 将整数转换为十六进制字符串
oct() 将整数转换为八进制字符串

注意:

上述列表只是列举了部分Python的关键字和内置函数,还有其他不在此处列出的关键字和内置函数。

上面这些内置函数的名字也不应该作为标识符,否则 Python 的内置函数会被覆盖 。


5.常量

在Python中,常量指的是在程序中不会被修改的值。

Python没有内置的常量类型,在实际开发中,开发者应该通过约定来遵守常量不被修改的原则,并在代码中遵循这种约定。

我们可以通过以下方式来使用常量:

  1. 使用全大写的命名规范:按照惯例,将常量的名称全部使用大写字母,并用下划线分隔单词

例如:PI = 3.14159

  1. 使用模块级别的变量:将常量定义为模块级别的变量,在整个模块中都可以访问。在Python中,每个.py文件都可以作为一个模块。

例如,在一个名为constants.py的文件中定义常量:

PI = 3.14159
GRAVITY = 9.8

然后在其他文件中,可以通过导入该模块来访问这些常量:

import constants
    
print(constants.PI)  # 输出:3.14159
print(constants.GRAVITY)  # 输出:9.8
  1. 使用枚举类:如果有一组相关的常量,可以使用枚举来定义。枚举类在Python中通过enum模块提供:
from enum import Enum
    
class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3
    
print(Color.RED)  # 输出:Color.RED
print(Color.RED.value)  # 输出:1

6-基本数据类型

1.type()函数

type()函数是一个内置函数,用于返回对象的类型。

语法:

type(object)

其中,object是要获取类型的对象。

示例:

x = 5
y = "Hello"
z = [1, 2, 3]
    
print(type(x))  # 输出:<class 'int'>
print(type(y))  # 输出:<class 'str'>
print(type(z))  # 输出:<class 'list'>

在上面的示例中,输出结果表示x是整数类型、y是字符串类型、z是列表类型。

type()函数在编写代码时可以用于判断对象的类型,从而根据不同的类型执行不同的操作。


2.数值数据类型

常见的数值数据类型有以下几种:

  1. 整数类型(int):表示整数值,例如:-5、0、10

  2. 浮点数类型(float):表示带有小数部分的数值,例如:3.14、-2.5

示例:

x = 10  # 整数类型
y = 3.14  # 浮点数类型
    
print(type(x))  # 输出:<class 'int'>
print(type(y))  # 输出:<class 'float'>

示例中,我定义了变量x为整数类型,y为浮点数类型,z为复数类型。通过type()函数可以查看这些变量的数据类型。

除了以上提到的基本数值类型,还可以使用各种库来处理更复杂的数值数据,如使用NumPy库处理数组、使用Pandas库处理数据框等。


3.布尔值数据类型

布尔值(bool)是一种逻辑数据类型,只有两个取值:True和False。

在Python中,布尔值用于表示真(True)和假(False)的状态。

示例:

x = True
y = False
    
print(type(x))  # 输出:<class 'bool'>
print(type(y))  # 输出:<class 'bool'>

布尔值主要用于条件判断和逻辑运算

在条件判断中,根据表达式的结果是否为True或者False来执行不同的操作,例如使用if语句进行条件判断。

在逻辑运算中,使用布尔值进行与、或、非等逻辑运算,例如使用andornot关键字进行逻辑运算。

示例:

x = True
y = False
    
if x:
    print("x 是真")
    
if not y:
    print("y 是假")
    
if x and y:
    print("x 和 y 都是真")
else:
    print("x 和 y 至少有一个是假")
    
if x or y:
    print("x 和 y 中至少有一个是真")
else:
    print("x 和 y 都是假")
    

在上面的示例中,根据变量xy的取值进行条件判断和逻辑运算,并根据结果执行不同的操作。输出结果为:

x 是真
y 是假
x 和 y 至少有一个是假
x 和 y 中至少有一个是真

总结来说,布尔值在Python中用于表示真和假的状态,常用于条件判断和逻辑运算。


4.字符串数据类型

  • 字符串是一种表示文本数据的数据类型

  • 字符串由字符序列组成,可以包含字母、数字、符号和空格等等。

  • 字符串是不可变(immutable)的,这意味着一旦创建了一个字符串对象,它的值就不能被修改。

  • 你可以使用单引号(')或双引号(")来定义一个字符串。

以下是一些例子:

my_string = 'Hello, World!'  # 使用单引号定义字符串
another_string = "I'm a Python programmer."  # 使用双引号定义字符串
    
multiline_string = '''This is a 
multiline string.'''             # 使用三引号定义多行字符串

字符串支持许多内置的操作和方法,例如连接字符串、索引、切片、替换、大小写转换等。

后文详解

7-字符串及其方法的使用

1.转义字符

转义字符是一种特殊的字符序列,用于表示无法直接输入或显示的字符。假如字符串既包含单引号,又包含双引号 ,此时必须使用转义字符 。

在 Python 中,转义字符以反斜杠(\)开头,后面跟着一个或多个字符。

常见的 Python 转义字符及其含义:

转义字符 含义
\\ 反斜杠
\' 单引号
\" 双引号
\n 换行符
\t 制表符
\b 退格(删除前一个字符)
\r 回车(将光标移到行首)
\f 换页(将光标移到下一行的开头)

以下是一些示例用法:

print("This is a backslash: \")
# 输出:This is a backslash: \
    
print('I\'m using single quotes.')
# 输出:I'm using single quotes.
    
print("She said, \"Hello!\"")
# 输出:She said, "Hello!"
    
print("Line 1\nLine 2")
# 输出:
# Line 1
# Line 2
    
print("Tab\tSeparated")
# 输出:Tab    Separated
    
print("Hello\bWorld")
# 输出:HellWorld
    
print("First line\rSecond line")
# 输出:Second line
    
print("Page 1\fPage 2")
# 输出:
# Page 1
#     Page 2

请注意,如果您想在字符串中使用反斜杠本身而不是转义字符,您可以使用两个连续的反斜杠(\\)。这是因为反斜杠在字符串中是一个特殊字符,需要使用转义字符来表示自己。


2.拼接字符串

在 Python 中,可以使用加号运算符 + 或字符串的 join() 方法来拼接字符串。

2.1.使用加号运算符 + 进行字符串拼接:

name = "Alice"
age = 25
message = "My name is " + name + " and I'm " + str(age) + " years old."
print(message)

# 输出:My name is Alice and I'm 25 years old.

在上述示例中,我们使用了加号运算符 + 将字符串拼接在一起。

注意,如果我们要将一个非字符串对象(如整数)拼接到字符串中,需要使用 str() 函数将其转换为字符串。


2.2.使用 join() 方法拼接字符串:

names = ["Alice", "Bob", "Charlie"]
message = ", ".join(names)
print(message)

# 输出:Alice, Bob, Charlie

在上述示例中,我们创建了一个名为 names 的字符串列表,并使用 join() 方法将列表中的字符串拼接起来。join() 方法以指定的连接符(在例子中为逗号和空格)将列表中的元素拼接在一起。


2.3. str()、repr() 函数

有时候,我们需要将字符串与数值进行拼接 ,而 Python 不允许直接拼接数值和字符串 ,程序必须先将数值转换成字符串 。

  1. str() 函数用于将对象转换为字符串表示形式。它返回一个可读性较好的字符串。对于大多数对象类型,  str() 函数会调用对象的 \_\_str\_\_() 方法来获取字符串表示形式。

  2. repr() 函数用于将对象转换为可供解释器读取的字符串表示形式。它返回一个准确且详细的字符串,通常用于调试和开发过程中。对于大多数对象类型,repr() 函数会调用对象的 \_\_repr\_\_() 方法来获取字符串表示形式。

number = 10
list_data = [1, 2, 3]
dictionary = {"name": "Alice", "age": 25}
    
print(str(number))
# 输出:10

print(str(list_data))
# 输出:[1, 2, 3]

print(str(dictionary))
# 输出:{'name': 'Alice', 'age': 25}
    
    
print(repr(number))
# 输出:10

print(repr(list_data))
# 输出:[1, 2, 3]

print(repr(dictionary))
# 输出:{'name': 'Alice', 'age': 25}

3.使用 input() 和 raw_input() 获取用户输入

input() 函数用于接收用户的输入,并将其作为字符串返回。它会等待用户在控制台输入内容,按下回车键后将输入内容作为字符串返回。

示例:

name = input("请输入您的名字: ")
print(f"您好,{name}!")

运行这段代码后,程序会在控制台输出提示信息 "请输入您的名字: ",然后等待用户输入。用户输入的内容将被赋值给变量 name,最后程序会根据用户输入的内容输出一条问候信息。

raw\_input() 是 Python 2.x 版本中的函数,而在 Python 3.x 中被移除。input() 函数在 Python 3.x 中取代了 raw\_input() 函数的功能。所以,如果你正在使用 Python 3.x 版本,应该使用 input() 函数。

请注意,在使用 input() 函数获取用户的输入时,要格外小心处理用户的输入内容,确保输入的内容符合预期,以避免意外错误或安全问题。


4.序列相关方法

4.1.字符串索引

  • Python字符串支持使用索引和切片来操作单个字符或子串。

  • 字符串中的第一个字符的索引为0,也可以从右边开始计算索引,最后一个字符的索引为-1。

  • 可以使用方括号[]加上索引或范围来获取对应的字符或子串。

例如:

  • 获取字符串s中索引2的字符可以使用s[2],

  • 获取从索引3到5(不包括5)的子串可以使用s[3:5],

  • 如果省略了起始索引或结束索引,则相当于从字符串的一端开始截取,

  • 获取从索引5到字符串结束处的子串可以使用s[5:],

  • 获取从字符串开始到索引5的子串可以使用s[:5]。

  • 还可以使用in运算符来判断是否包含某个子串,使用len函数来获取字符串长度,使用min和max函数来获取字符串中的最小字符和最大字符。

示例代码:

s = 'ydcode.cn is very good'

print(s[2])  # 输出:c
print(s[-4])  # 输出:g
print(s[3:5])  # 输出:od
print(s[3:-5])  # 输出:ode.cn is very
print(s[-6:-3])  # 输出:y g
print(s[5:])  # 输出:e.cn is very good
print(s[:-6])  # 输出:ydcode.cn is ver
print('very' in s)  # 输出:True
print('fkit' in s)  # 输出:False
print(len(s))  # 输出:22
print(max(s))  # 输出:y
print(min(s))  # 输出:空格

4.2.序列相关方法

  1. len():返回序列中元素的个数。
numbers = [1, 2, 3, 4, 5]
length = len(numbers)
print(length)  # 输出:5
  1. index():返回第一个匹配元素的索引值。
numbers = [1, 2, 3, 4, 5]
index = numbers.index(3)
print(index)  # 输出:2
  1. count():返回指定元素在序列中出现的次数。
numbers = [1, 2, 2, 3, 3, 3]
count = numbers.count(3)
print(count)  # 输出:3
  1. slice():获取序列的切片(子序列)。
numbers = [1, 2, 3, 4, 5]
subsequence = numbers[1:4]
print(subsequence)  # 输出:[2, 3, 4]
  1. append():向序列末尾添加一个元素。
numbers = [1, 2, 3]
numbers.append(4)
print(numbers)  # 输出:[1, 2, 3, 4]
  1. extend():将另一个序列的元素添加到当前序列末尾。
numbers = [1, 2, 3]
other_numbers = [4, 5, 6]
numbers.extend(other_numbers)
print(numbers)  # 输出:[1, 2, 3, 4, 5, 6]
  1. insert():在指定位置插入一个元素。
numbers = [1, 2, 3, 4]
numbers.insert(2, 5)
print(numbers)  # 输出:[1, 2, 5, 3, 4]
  1. remove():移除第一个匹配的元素。
numbers = [1, 2, 3, 2, 4]
numbers.remove(2)
print(numbers)  # 输出:[1, 3, 2, 4]
  1. pop():移除并返回指定位置的元素。
numbers = [1, 2, 3, 4]
removed_element = numbers.pop(2)
print(numbers)  # 输出:[1, 2, 4]
print(removed_element)  # 输出:3

这些是序列的一些常用方法,它们可以帮助我们对序列进行操作、查询和修改。


5.常用方法

  1. len():返回字符串的长度。
my_string = 'Hello, World!'
print(len(my_string))  # 输出:13
  1. count(substring):返回子字符串在字符串中出现的次数。
my_string = 'Hello, World!'
print(my_string.count('l'))  # 输出:3
  1. find(substring):返回子字符串在字符串中第一次出现的位置(索引),如果没有找到则返回-1。
my_string = 'Hello, World!'
print(my_string.find('World'))  # 输出:7
  1. replace(old\_str, new\_str):将字符串中的指定子字符串替换为新的字符串,返回替换后的新字符串。
my_string = 'Hello, World!'
new_string = my_string.replace('World', 'Python')
print(new_string)  # 输出:'Hello, Python!'
  1. split(separator):将字符串按照指定的分隔符(默认为空格)分割成一个列表。
my_string = 'apple, banana, orange'
my_list = my_string.split(', ')
print(my_list)  # 输出:['apple', 'banana', 'orange']
  1. join(iterable):将一个可迭代对象(如列表)中的元素合并成一个字符串,每个元素之间以指定的字符串(调用该方法的字符串)连接。
my_list = ['apple', 'banana', 'orange']
my_string = ', '.join(my_list)
print(my_string) # 输出:'apple, banana, orange'
  1. strip()lstrip() 和 rstrip() :

    • strip() 方法会去除开头和结尾的空格,返回新的字符串 'Hello, World!'

    • lstrip() 方法会去除开头的空格,返回新的字符串 'Hello, World!   '

    • rstrip() 方法会去除结尾的空格,返回新的字符串 '   Hello, World!'

my_string = '   Hello, World!   '
    
# 使用 strip() 方法去除开头和结尾的空格
new_string_1 = my_string.strip()
print(new_string_1)  # 输出:'Hello, World!'
    
# 使用 lstrip() 方法去除开头的空格
new_string_2 = my_string.lstrip()
print(new_string_2)  # 输出:'Hello, World!   '
    
# 使用 rstrip() 方法去除结尾的空格
new_string_3 = my_string.rstrip()
print(new_string_3)  # 输出:'   Hello, World!'
  1. isalpha()isdigit()isalnum():检查字符串是否只包含字母、数字或字母和数字的组合。
my_string_1 = 'HelloWorld'
my_string_2 = '2021'
my_string_3 = 'Hello2021'

print(my_string_1.isalpha())  # 输出:True
print(my_string_2.isdigit())  # 输出:True
print(my_string_3.isalnum())  # 输出:True
  1. upper():将字符串中所有字符转换为大写字母,并返回转换后的新字符串。
my_string = 'Hello, World!'
new_string = my_string.upper()
print(new_string)  # 输出:'HELLO, WORLD!'
  1. lower():将字符串中所有字符转换为小写字母,并返回转换后的新字符串。
my_string = 'Hello, World!'
new_string = my_string.lower()
print(new_string)  # 输出:'hello, world!'
  1. capitalize():将字符串中首字母变为大写字母,并返回转换后的新字符串。
my_string = 'hello, world!'
new_string = my_string.capitalize()
print(new_string)  # 输出:'Hello, world!'
  1. title():将字符串中每个单词的首字母变为大写字母,并返回转换后的新字符串。
my_string = 'hello, world!'
new_string = my_string.title()
print(new_string)  # 输出:'Hello, World!'
  1. swapcase():将字符串中大写字母变为小写字母,小写字母变为大写字母,并返回转换后的新字符串。
my_string = 'Hello, World!'
new_string = my_string.swapcase()
print(new_string)  # 输出:'hELLO, wORLD!'

8-运算符

1.赋值运算符

赋值运算符用于为变量或常量指定值, Python 使用“=”作为赋值运算符。

通常 ,使用赋值运算符将表达式的值赋给另一个变量

  • \=:将右边的值赋给左边的变量。
x = 10
  • +=:将右边的值加到左边的变量上并将结果赋给左边的变量。
x += 5  # 等价于 x = x + 5
  • \-=:将右边的值从左边的变量中减去并将结果赋给左边的变量。
x -= 3  # 等价于 x = x - 3
  • \*=:将右边的值乘以左边的变量并将结果赋给左边的变量。
x *= 2  # 等价于 x = x * 2
  • /=:将左边的变量除以右边的值并将结果赋给左边的变量。
    x /= 4  # 等价于 x = x / 4

2.算数运算符

算术运算符用于执行基本的数学运算。以下是常见的算术运算符及其示例:

  • +:相两个操作数。
x = 5 + 3  # 8
  • \-:从左操作数中去右操作数。
x = 10 - 4  # 6
  • \*:将两个操作数相
x = 2 * 3  # 6
  • /:将左操作数以右操作数(结果是浮点数)。
x = 10 / 3  # 3.3333333333333335
  • //:将左操作数除以右操作数并向下取整为最接近的整数。
x = 10 // 3  # 3
  • %:计算左操作数除以右操作数的余数
x = 10 % 3  # 1
  • \*\*:将左操作数的值提高到右操作数的
x = 2 ** 3  # 8

算术运算符可用于数字运算,可以进行加法、减法、乘法、除法、取模、幂等运算。

需要注意的是,除法运算符/得到的结果为浮点数,而取整除法运算符//得到的结果为整数。


3.比较运算符

比较运算符用于比较两个值,并返回一个布尔值(True或False)。

以下是常见的比较运算符及其示例:

  • \==:检查两个操作数是否相等。
x = 5 == 5  # True
  • !=:检查两个操作数是否不相等。
x = 5 != 3  # True
  • \>:检查左操作数是否大于右操作数。
x = 5 > 3  # True
  • <:检查左操作数是否小于右操作数。
x = 3 < 5  # True
  • \>=:检查左操作数是否大于或等于右操作数。
x = 5 >= 5  # True
  • <=:检查左操作数是否小于或等于右操作数。
x = 3 <= 5  # True

比较运算符可用于判断两个值之间的大小关系,并返回布尔值。如果判断条件成立,则返回True;否则返回False。注意,比较运算符可以用于数字、字符串、列表等多种数据类型的比较。


4.逻辑运算符

逻辑运算符用于组合和操作布尔值。

以下是常见的逻辑运算符及其示例:

  • and:如果两个操作数都为True,则结果为True;否则结果为False。
x = True and False  # False
  • or:如果两个操作数中至少一个为True,则结果为True;否则结果为False。
x = True or False  # True
  • not:对操作数进行取反操作,如果操作数为True,则结果为False;如果操作数为False,则结果为True。
x = not True  # False

逻辑运算符可用于组合多个布尔表达式,并根据条件的满足情况返回布尔值。通常用于条件语句和循环中判断条件的复杂性。使用逻辑运算符可以将多个条件组合起来,以便进行更复杂的逻辑判断。


5.三目运算符

三目运算符(也称为条件表达式)是一种简洁的表达方式,用于根据条件选择不同的值。它的语法形式如下:

<value1> if <condition> else <value2>

其中,<condition> 是一个布尔表达式,如果该表达式为 True,则返回 <value1>;否则返回 <value2>

以下是一个示例:

x = 5
result = "Odd" if x % 2 != 0 else "Even"
print(result)  # 输出 "Odd"

在上述代码中,如果变量 x 的值除以2的余数不等于0(即 x 是奇数),则将字符串 “Odd” 赋给变量 result,否则赋给它字符串 “Even”。最后,将 result 的值打印出来。

三目运算符可以简化条件判断和赋值的过程,使代码更加简洁和可读。但在使

用时要注意不要过度使用,以免影响代码的可读性。


6.in运算符

in 运算符用于检查一个值是否存在于一个序列(例如列表、元组、字符串等)中,如果存在则返回 True,否则返回 False

示例:

x = 3
numbers = [1, 2, 3, 4, 5]
    
print(x in numbers)  # 输出 True
    
name = "Alice"
print("l" in name)  # 输出 True
print("z" in name)  # 输出 False

在上述代码中,第一个例子中的 x in numbers 检查数字 3 是否存在于列表 numbers 中,因为存在,则返回 True。第二个例子中的 "l" in name 检查字符串 "l" 是否存在于字符串 name 中,因为存在,则返回 True;而 "z" in name 检查是否存在 "z",因为不存在,则返回 False

in 运算符可以用于各种序列类型,包括列表、元组、字符串等。它为我们提供了一种便捷的方式来判断一个值是否包含在一个序列中。


7.运算符的结合性和优先级

7.1.结合性

  • 所有的数学运算都是从左向右进行的

  • Python 语言中的大部分运算符也是从左向右结合的

  • 只有单目运算符、 赋值运算符和三目运算符例外,它们是从右向左结合的,也就是说,它们是从右向左运算的

  • 乘法和加法是两个可结合的运算符,也就是说,这两个运算符左右两边的操作数可以互换位置而不会影响结果

需要注意的是,在表达式中可以使用括号来明确运算符的结合性和优先级,提高代码的可读性和明确性。当存在多个运算符时,可以使用括号来明确指定运算的顺序。

例如,在以下表达式中,算术运算符具有从左到右的结合性:

result = 10 + 5 * 2  # 先计算5 * 2,然后再加上10

如果希望先计算10 + 5,可以使用括号来明确指定运算的顺序:

result = (10 + 5) * 2  # 先计算10 + 5,然后再乘以2

7.2.优先级

运算符有不同的优先级,所谓优先级就是在表达式运算中的运算顺序。

运算符说明 Python运算符 优先级
逻辑或运算符 or 2
逻辑与运算符 and 3
逻辑非运算符 not 4
成员运算符 in、not in 5
身份运算符 is、is not 6
比较运算符 ==、!=、>、<、>=、<= 7
按位或运算符 | 8
按位异或运算符 ^ 9
按位与运算符 & 10
加、减运算符 +、- 12
乘、除运算符 *、/、//、% 13
符号运算符 + 或 - 14
按位取反 ~ 15
乘方运算符 ** 16

请注意,表格中的优先级数字越小,优先级越高。

  • 注意:

    • 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则把它分成几步来完成。

    • 不要过多地依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,应尽量使用“()”来控制表达式的执行顺序。

9-序列简介

1.序列简介

在Python中,序列是一种数据类型,用于存储一系列有序的元素。常见的序列类型包括字符串(string)、列表(list)、元组(tuple)和范围(range)。它们都支持索引(indexing)、切片(slicing)和迭代(iteration)操作。

以下是对这些序列类型的简要介绍:

  1. 字符串(string):

    • 字符串是由字符组成的序列,用于表示文本信息

    • 字符串使用单引号或双引号括起来

    • 示例:name = "Alice"

  2. 列表(list):

    • 列表是由任意类型的元素组成,可以存储不同类型的元素

    • 列表是可变的,意味着可以修改、添加或删除列表中的元素

    • 列表使用方括号 \[ \] 表示,其中的元素用逗号 , 分隔

    • 示例:fruits = \['apple', 'banana', 'orange'\]

  3. 元组(tuple):

    • 元组是由任意类型的元素组成,可以存储不同类型的元素

    • 元组是不可变的,一旦创建就不能修改其内容。如果需要修改元组中的元素,需要重新创建一个新的元组

    • 元组使用圆括号 ( ) 表示,其中的元素用逗号 , 分隔

    • 示例:point = (3, 4)

  4. 范围(range):

    • 范围是由一系列连续的整数组成的序列,用于表示一个范围内的整数

    • 范围使用 range() 函数创建,可以指定起始值、结束值和步长

    • 示例:numbers = range(1, 6)

对于这些序列类型,您可以使用索引来访问单个元素,使用切片来访问指定范围内的元素,并使用循环来迭代访问所有元素。

示例:序列的基本操作

# 字符串操作
name = "Alice"
print(name[0])               # 输出:A
print(name[1:4])             # 输出:lic
for char in name:
    print(char)              # 逐个输出:A l i c e
    
# 列表操作
fruits = ['apple', 'banana', 'orange']
print(fruits[0])             # 输出:apple
print(fruits[1:])            # 输出:['banana', 'orange']
for fruit in fruits:
    print(fruit)             # 逐个输出:apple banana orange
    
# 元组操作
point = (3, 4)
print(point[1])              # 输出:4
for coordinate in point:
    print(coordinate)        # 逐个输出:3 4
    
# 范围操作
numbers = range(1, 6)
print(numbers[2])            # 输出:3
for num in numbers:
    print(num)               # 逐个输出:1 2 3 4 5

总结起来,列表适用于需要动态修改数据的场景,而元组适用于需要保持数据不变性、结构固定或者作为函数返回值等情况。根据具体需求选择合适的类型可以提高代码的可读性和性能。


2.序列封包和序列解包

Python还提供了序列封包(Sequence Packing)和序列解包(Sequence Unpacking)的功能

  • 程序把多个值赋给一个变量时,Python 会自动将多个值封装成元组。这种功能被称为序列封包

  • 序列解包是将序列(如元组、列表等)的各个元素直接赋值给多个变量。序列解包要求序列的元素个数和变量的个数相等。


序列封包示例:

# 序列封包:将10、20、30封装成元组后赋值给vals
vals = 10, 20, 30 
print(vals)  # (10, 20, 30) 
print(type(vals))  # <class 'tuple'> 
print(vals[1])  # 20 
    
a_tuple = tuple(range(1, 10, 2)) 
# 序列解包:将a_tuple元组的各元素依次赋值给a、b、c、d、e变量
a, b, c, d, e = a_tuple 
print(a, b, c, d, e)  # 1 3 5 7 9 
    
a_list = ['ydyx', 'ydyxit']
# 序列解包:将a_list序列的各元素依次赋值给a_str、b_str字符串
a_str, b_str = a_list 
print(a_str, b_str)  # ydyx ydyxit

如果同时运用序列封包和序列解包机制,赋值运算符就可以同时将多个值赋给多个变量。例如:

# 将10、20、30依次赋值给x、y、z
x, y, z = 10, 20, 30 
print(x, y, z)  # 10 20 30 

这种语法也可实现变量值的交换:

# 将x、y、z依次赋值给y、z、x,实现变量值的交换
x, y, z = y, z, x 
print(x, y, z)  # 20 30 10 

在序列解包时,还可以只解出部分变量,剩下的使用列表变量保存。为了实现这种解包方式,可以在左边被赋值的变量之前添加\*,表示该变量代表一个列表,可以保存多个集合元素。
序列解包示例如下:

# first、second保存前两个元素,rest列表保存剩下的元素
first, second, *rest = range(10) 
print(first)  # 0 
print(second)  # 1 
print(rest)  # [2, 3, 4, 5, 6, 7, 8, 9] 
    
# last保存最后一个元素,begin保存前面剩下的元素
*begin, last = range(10) 
print(begin)  # [0, 1, 2, 3, 4, 5, 6, 7, 8] 
print(last)  # 9 
    
# first保存第一个元素,last保存最后一个元素,middle保存中间剩下的元素
first, *middle, last = range(10) 
print(first)  # 0 
print(middle)  # [0, 1, 2, 3, 4, 5, 6, 7, 8] 
print(last)  # 9 

这些功能使得赋值运算符更灵活,可以更方便地处理序列元素的封装和解包操作。

10-列表和元组的使用

列表和元组的最大区别在于可变性,即列表是可变的,而元组是不可变的。

由于元组是不可变的,因此更加安全。如果程序只需要访问元素而不需要对它们进行修改,那么使用元组代替列表会更加安全。在使用元组时,可以避免意外修改元素而导致程序出错。

此外,元组的不可变性也使得它在某些特定场景下表现更好。例如,元组可以作为字典的键(因为键必须是不可变的类型),而列表则不能。在一些并发编程的环境下,元组也比列表更适合使用,因为元组是不可变的,所以多个线程可以安全地访问同一个元组。而对于列表,则需要进行额外的同步措施才能保证多线程安全访问。

总之,如果程序需要频繁地修改、添加和删除元素,那么应该使用列表;如果程序只需要访问元素而不需要修改,或者需要使用元组的不可变性特性,那么应该使用元组。

鉴于列表和元组只要不涉及改变元素的操作,其他用法是通用的,此处主讲列表的操作


1.创建列表

1.1.要创建一个列表,你可以使用方括号 [] 并在其中放置元素。

下面是一些创建列表的示例:

  1. 创建一个空列表:
my_list = []
  1. 创建一个包含多个元素的列表:
my_list = [1, 2, 3, 4, 5]
  1. 创建一个包含不同类型元素的列表:
my_list = [1, "hello", 3.14, True]
  1. 创建嵌套列表(列表中包含列表):
my_list = [[1, 2], [3, 4], [5, 6]]
  1. 使用列表生成式创建列表:
    my_list = [x for x in range(1, 6)]  # [1, 2, 3, 4, 5]
  1. 使用内置函数 list() 将其他可迭代对象(如元组、字符串等)转换为列表:
my_list = list((1, 2, 3, 4, 5))  #[1, 2, 3, 4, 5]

1.2.与list() 对应,Python提供了一个内置函数tuple()用于将其他可迭代对象(如列表、字符串、range等)转换为元组。

my_list = [1, 2, 3, 4, 5]
my_tuple_list = tuple(my_list)
print(my_tuple_list)                 #(1, 2, 3, 4, 5)
    
my_string = "hello"
my_tuple_string = tuple(my_string)
print(my_tuple_string)               #('h', 'e', 'l', 'l', 'o')
    
my_range = range(1, 6)
my_tuple_range = tuple(my_range)
print(my_tuple_range)                #(1, 2, 3, 4, 5)

2.增加列表元素

当你向列表中添加新元素时,可以使用以下三种方法:append()extend()insert()

  1. 使用 append() 方法添加一个元素到列表末尾:
my_list = [1, 2, 3]
my_list.append(4)

现在 my\_list 的内容为 \[1, 2, 3, 4\]

append() 方法将指定的元素添加到列表的末尾。


  1. 使用 extend() 方法添加多个元素到列表末尾(通过另一个可迭代对象):
my_list = [1, 2, 3]
new_elements = [4, 5, 6]
my_list.extend(new_elements)

现在 my\_list 的内容为 \[1, 2, 3, 4, 5, 6\]

extend() 方法接受一个可迭代对象作为参数,并将它的元素逐个添加到列表的末尾。


  1. 使用 insert() 方法在指定位置插入一个元素:
my_list = [1, 2, 3]
my_list.insert(1, 5)

现在 my\_list 的内容为 \[1, 5, 2, 3\]

insert() 方法接受两个参数:要插入的位置索引和要插入的元素。它将元素插入到指定索引位置,原来的元素依次后移。

需要注意的是,列表的索引从0开始。如果你使用 insert() 方法时指定的索引超出了列表长度,它将会自动添加到列表末尾。

这些方法在原地修改列表,而不会创建新的列表对象。这意味着你可以直接对原列表进行操作,而无需重新赋值。


3.删除列表元素

删除列表元素有多种方法,包括使用 del 语句、remove() 方法和 pop() 方法。下面是它们的详细说明:

  1. 使用 del 语句删除指定索引位置的元素:
my_list = [1, 2, 3, 4, 5]
del my_list[2]

现在 my\_list 的内容将是 \[1, 2, 4, 5\]。这将从列表中删除索引为2的元素。

  1. 使用 remove() 方法删除指定值的第一个匹配项:
my_list = [1, 2, 3, 4, 5]
my_list.remove(3)

现在 my\_list 的内容将是 \[1, 2, 4, 5\]。这将删除列表中第一个值为3的元素。

  1. 使用 pop() 方法删除指定索引位置的元素,并返回该元素的值:
my_list = [1, 2, 3, 4, 5]
removed_element = my_list.pop(2)
print(my_list)    # [1, 2, 4, 5]
print(removed_element)    # 3

现在 my\_list 的内容将是 \[1, 2, 4, 5\]removed\_element 的值将是 3。这将从列表中删除索引为2的元素,并返回其值。

需要注意的是,remove() 方法只会删除第一个匹配项,如果有多个相同的元素,要删除所有匹配项,你需要使用循环结构或其他方法。

总结起来,del 语句用于删除指定索引位置的元素,remove() 方法用于删除指定值的第一个匹配项,pop() 方法用于删除指定索引位置的元素,并返回其值。这些方法都会在原地修改列表。


4.修改列表元素

要修改列表中的元素,你可以直接通过索引对指定位置的元素赋新的值。可以使用正数索引,也可以使用负数索引。

示例:

  1. 修改列表中的单个元素:
my_list = [1, 2, 3, 4]
my_list[0] = 5

现在 my\_list 的内容将是 \[5, 2, 3, 4\]。这将把列表中索引为0的元素(原来的值为1)修改为5。

  1. 修改列表中的多个连续元素:
my_list = [1, 2, 3, 4, 5]
my_list[1:4] = [6, 7, 8]

现在 my\_list 的内容将是 \[1, 6, 7, 8, 5\]。这将把列表中索引从1到3的元素(即2、3、4)替换为 \[6, 7, 8\]

  1. 修改列表中的嵌套列表的元素:
my_list = [[1, 2], [3, 4], [5, 6]]
my_list[1][0] = 9

现在 my\_list 的内容将是 \[\[1, 2\], \[9, 4\], \[5, 6\]\]。这将把嵌套列表中索引为 \[1\]\[0\] 的元素(原来的值为3)修改为9。

需要注意的是,在修改嵌套列表元素时,可以通过多层索引来指定要修改的元素位置。

通过这些示例,你可以理解如何修改列表中的元素,无论是单个元素还是连续的多个元素。


5.列表常用方法

一些常见的列表方法及其示例:

  1. append() 方法:向列表末尾添加一个元素。
my_list = [1, 2, 3]
my_list.append(4)
print(my_list)  # 输出: [1, 2, 3, 4]
  1. extend() 方法:将一个列表的元素追加到另一个列表的末尾。
my_list1 = [1, 2, 3]
my_list2 = [4, 5, 6]
my_list1.extend(my_list2)
print(my_list1)  # 输出: [1, 2, 3, 4, 5, 6]
  1. insert() 方法:在指定位置插入一个元素。
my_list = [1, 2, 3]
my_list.insert(1, 4)
print(my_list)  # 输出: [1, 4, 2, 3]
  1. remove() 方法:删除列表中第一个匹配的元素。
my_list = [1, 2, 3, 2]
my_list.remove(2)
print(my_list)  # 输出: [1, 3, 2]
  1. pop() 方法:删除指定位置的元素,并返回该元素的值。
my_list = [1, 2, 3]
removed_element = my_list.pop(1)
print(my_list)         # 输出: [1, 3]
print(removed_element) # 输出: 2
  1. index() 方法:返回指定值第一次出现的索引位置。
my_list = [1, 2, 3, 2]
index = my_list.index(2)
print(index)  # 输出: 1
  1. count() 方法:返回指定元素在列表中出现的次数。
my_list = [1, 2, 3, 2]
count = my_list.count(2)
print(count)  # 输出: 2
  1. sort() 方法:对列表进行排序(原地排序)。
my_list = [1, 5, 3, 4, 7]
my_list.sort()
print(my_list)  # 输出: [1, 3, 4, 5, 7]
  1. reverse() 方法:反转列表中的元素(原地反转)。
my_list = [1, 5, 3, 4, 7]
my_list.reverse()
print(my_list)  # 输出: [7, 4, 3, 5, 1]

11-字典

1.认识字典

  • 字典是Python中一种基本的数据结构,也是一种非常有用的数据类型。

  • 字典是一个可变的、无序的、键值对的集合,可以通过键来访问值。

  • 可以理解为其存放了两组具有映射关系的数据,其中一组数据是关键数据,称为key,另一组数据可通过key来访问,称为value。如图

image

图11.1  字典key value 对应关系

  • 字典中的键必须是唯一的,而值可以不唯一。

  • 字典中的值可以是任何数据类型,包括整数、浮点数、字符串、列表、元组甚至其他字典。字

  • 典中的键必须是不可变的数据类型,例如整数、字符串和元组,但不能使用可变的数据类型,例如列表。这是因为字典中的键值对是通过哈希表实现的,哈希表只能处理不可变的数据类型。

  • 字典由花括号 {} 包围,并且每个键值对之间使用逗号 , 分隔,如下面的示例所示:

my_dict = {'name': 'Alice', 'age': 25}

这个字典有两个键值对:‘name’: ‘Alice’ 和 ‘age’: 25。

‘name’ 和 ‘age’ 称为字典的键,而 ‘Alice’ 和 25 称为字典的值。

可以使用键来访问值,例如:

print(my_dict['name'])  # 输出: 'Alice'

注意,如果键不存在于字典中,会抛出 KeyError 异常。


2.创建字典

可使用花括号 {} 来创建字典,也可以使用dict()或者字典推导式函数来创建

  1. 使用 dict() 构造函数: 可以使用 dict() 构造函数来创建字典。可以传递键值对作为参数,也可以传递包含键值对元组的列表作为参数。例如:
my_dict = dict(name='Bob', age=30, city='London')
print(my_dict)   
 
#输出结果:{'name': 'Bob', 'age': 30, 'city': 'London'}
    

# 或者
my_dict1 = dict([('name', 'Bob'), ('age', 30), ('city', 'London')])
print(my_dict1)    

#输出结果:{'name': 'Bob', 'age': 30, 'city': 'London'}

这两种方式都会创建一个具有相同键值对的字典。


  1. 使用键的列表和默认值: 可以使用 zip() 函数将键的列表和默认值列表合并,并使用 dict() 构造函数来创建字典。例如:
keys = ['name', 'age', 'city']
values = ['Carol', 35, 'Paris']
my_dict = dict(zip(keys, values))
print(my_dict)
    
#输出结果:{'name': 'Carol', 'age': 35, 'city': 'Paris'}

在这个示例中,我们将 keys 列表和 values 列表合并,然后使用 dict() 构造函数创建字典。结果是与前面示例相同的字典。

此例能更好的理解 key  value 为两个具有映射关系的列表


  1. 使用字典推导式: 可以使用字典推导式生成具有特定模式的字典。字典推导式由一个表达式和一个或多个迭代器组成。例如:
my_dict = {x: x**2 for x in range(1, 5)}
print(my_dict)
    
#输出结果:{1: 1, 2: 4, 3: 9, 4: 16}

在这个示例中,我们创建了一个字典,键是 1 到 4 的整数,值是每个键的平方。

无论选择哪种方式,都可以灵活创建字典并指定键和值。根据需求选择适合的方法,并根据需要设置键和对应的值。


3.使用字典

字典是可变的,而键是字典的关键数据,可以通过键来添加、修改和删除字典中的元素。

  1. 访问字典元素: 可以通过使用键来访问字典中的值。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
print(my_dict['name'])  # 输出:Alice

这个示例中,我们使用键 'name' 来访问字典 my\_dict 中的值。


  1. 更新字典元素: 可以通过指定键来更新字典中的值。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
my_dict['age'] = 26
print(my_dict)  # 输出:{'name': 'Alice', 'age': 26, 'city': 'New York'}

这个示例中,我们将键 'age' 对应的值更新为 26。


  1. 添加新元素: 可以通过指定新的键和值来向字典中添加新的元素。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
my_dict['country'] = 'USA'
print(my_dict)  # 输出:{'name': 'Alice', 'age': 25, 'city': 'New York', 'country': 'USA'}

这个示例中,我们添加了一个新的键值对 'country': 'USA' 到字典中。


  1. 删除元素: 可以使用 del 关键字来删除字典中的元素。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
del my_dict['age']
print(my_dict)  # 输出:{'name': 'Alice', 'city': 'New York'}

这个示例中,我们删除了键 'age' 对应的键值对。


  1. 检查键是否存在: 可以使用 in 关键字来检查字典中是否存在特定的键。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
if 'age' in my_dict:
    print('Age is present')
else:
    print('Age is not present')

这个示例中,我们检查键 'age' 是否存在于字典中,并相应地输出结果。


  1. 获取所有键或所有值: 可以使用 keys() 方法获取字典中所有的键,使用 values() 方法获取所有的值。例如:
my_dict = {'name': 'Alice', 'age': 25, 'city': 'New York'}
keys = my_dict.keys()
values = my_dict.values()
print(keys)  # 输出:dict_keys(['name', 'age', 'city'])
print(values)  # 输出:dict_values(['Alice', 25, 'New York'])

这个示例中,我们获取了字典 my\_dict 中所有的键和值。

字典是一种非常灵活和有用的数据结构,可以通过键来快速访问和更新值。

通过以上可以看出,字典的 key 是它的关键。换个角度来看,字典的 key 就相当于它的索引,只不过这些索引不一定是整数类型,字典的 key 可以是任意不可变类型。这使得字典和列表在某种程度上具有相似的功能,但也有一些重要的区别。

  1. 索引类型:字典的键可以是任意不可变类型,例如字符串、整数、元组等;而列表的索引必须是整数类型且从0开始递增。

  2. 排序:字典中的键值对没有固定的顺序,不能保证按照添加顺序或者其他方式进行排序。而列表的元素是有序的,可以根据索引来访问和操作。

  3. 可变性:字典是可变的,可以添加、删除和修改键值对;而列表也是可变的,可以通过索引进行修改,还可以添加、删除和改变元素。

  4. 存储方式:字典是基于散列表实现的,通过哈希函数将键映射到存储位置;列表则是基于数组实现的,使用连续的内存空间存储元素。

  5. 查询效率:由于字典使用哈希表实现,具有较快的查询速度,不受字典大小的影响;而列表的查询效率随着列表长度的增加而线性增加。

根据具体的需求,选择适当的数据结构可以提高程序的效率和可读性。如果需要按照特定顺序存储和访问元素,或者只需要整数类型的索引,那么列表可能更合适。如果需要根据任意不可变类型的键值对进行快速的查找和修改操作,那么字典是更好的选择。


4.字典常用方法

字典由 dict 类代表,可以使用dir(dict)来查看dict类中包含的方法。

>>> dir(dict)
[ 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']

以下是dict类的部分方法:


  1. clear()方法用于清空字典中的所有键值对
d = {'name': 'Alice', 'age': 25, 'city': 'Shanghai'}
d.clear()
print(d)  # 输出:{}

  1. copy()方法用于创建并返回一个字典的浅拷贝副本
d1 = {'name': 'Bob', 'age': 30}
d2 = d1.copy()
    
d2['name'] = 'Alice'
    
print(d1)  # 输出:{'name': 'Bob', 'age': 30}
print(d2)  # 输出:{'name': 'Alice', 'age': 30}

可以看到,修改d2的内容不会影响d1的内容。


  1. fromkeys(seq\[, value\])

fromkeys()方法用于创建一个新字典,以序列中的元素作为键,可选参数value作为所有键对应的默认值

keys = ['name', 'age', 'city']
default_value = 0
d = dict.fromkeys(keys, default_value)
    
print(d)  # 输出:{'name': 0, 'age': 0, 'city': 0}

  1. get(key\[, default\])

get()方法返回键key对应的值,如果键不存在,则返回默认值default(默认为None

d = {'name': 'Alice', 'age': 25}
name = d.get('name')
gender = d.get('gender', 'unknown')
    
print(name)    # 输出:'Alice'
print(gender)  # 输出:'unknown'

  1. items()方法返回一个包含字典所有键值对的视图,每个元素是一个由键和值组成的元组
d = {'name': 'Alice', 'age': 25}
items = d.items()
    
print(items)  # 输出:dict_items([('name', 'Alice'), ('age', 25)])

  1. keys()方法返回一个包含字典所有键的视图
d = {'name': 'Alice', 'age': 25}
keys = d.keys()
    
print(keys)  #输出:dict_keys(['name', 'age'])

  1. values()方法返回一个包含字典所有值的视图
d = {'name': 'Alice', 'age': 25}
values = d.values()
    
print(values)  # 输出:dict_values(['Alice', 25])

  1. pop(key\[, default\])

pop()方法用于移除并返回键key对应的值,如果键不存在,则返回默认值default(默认引发KeyError

d = {'name': 'Alice', 'age': 25}
age = d.pop('age')
gender = d.pop('gender', 'unknown')
    
print(d)    # 输出:{'name': 'Alice'}
print(age)    # 输出:25
print(gender) # 输出:'unknown'

  1. popitem()方法用于随机移除并返回一个键值对(字典为空时引发KeyError
d = {'name': 'Alice', 'age': 25}
item = d.popitem()
    
print(item)  # 输出:('age', 25)

  1. setdefault(key\[, default\])

setdefault()方法用于获取键key对应的值。如果键不存在,则将键值对插入字典并返回默认值default(默认为None

d = {'name': 'Alice', 'age': 25}
name = d.setdefault('name')
gender = d.setdefault('gender', 'unknown')
    
print(d)    # 输出:{'name': 'Alice', 'age': 25, 'gender': 'unknown'}
print(name)    # 输出:'Alice'
print(gender)  # 输出:'unknown'

  1. update(\[other\])

update()方法将其他字典或可迭代对象中的键值对更新到当前字典中

d1 = {'name': 'Alice', 'age': 25}
d2 = {'city': 'Shanghai', 'gender': 'female'}
d1.update(d2)
    
print(d1)  # 输出:{'name': 'Alice', 'age': 25, 'city': 'Shanghai', 'gender': 'female'}

这些方法可以帮助我们处理和操作字典,提高程序的效率和可读性。

12-流程控制-if分支结构

当我们使用 if 分支结构时,可以根据条件的结果执行不同的代码块。

下面详细介绍了 Python 中 if 分支结构的各种形式和用法。


1.基本形式

最基本的 if 分支结构由一个条件和一个代码块组成:

if condition:
    # 代码块

如果 condition 的结果为 True,则执行缩进的代码块;否则不执行。


2.if-else 结构

除了 if 之外,我们还可以使用 else 关键字来定义在条件为 False 时执行的代码块:

if condition:
    # 代码块1
else:
    # 代码块2

如果 condition 的结果为 True,则执行第一个代码块;否则执行第二个代码块。

示例 :判断一个数是否为正数

num = -5
    
if num > 0:
    print("该数是正数")
else:
    print("该数不是正数")
    
# 输出结果:该数不是整数

3.多个条件:if-elif-else 结构

当有多个条件需要判断时,我们可以使用 elif(即 “else if” 的缩写)关键字来添加额外的条件:

if condition1:
    # 代码块1
elif condition2:
    # 代码块2
else:
    # 代码块3

依次检查每个条件,如果第一个条件为 True,则执行代码块1;否则检查第二个条件,如果为 True,则执行代码块2;否则执行代码块3。

示例 :根据成绩等级打印不同的消息

score = 85
    
if score >= 90:
    print("优秀")
elif score >= 80:
    print("良好")
elif score >= 70:
    print("中等")
elif score >= 60:
    print("及格")
else:
    print("不及格")
    
# 输出结果:良好

4.嵌套的 if 结构

我们可以在一个条件块内部嵌套另一个条件块,从而实现更复杂的逻辑判断。以下是一个示例:

if condition1:
    # 代码块1
    if condition2:
        # 代码块2
    else:
        # 代码块3
else:
    # 代码块4

在上述代码中,内部的 if 结构位于外部 if 结构的代码块1中。根据条件1的结果,如果为 True,则继续判断条件2,并执行代码块2或代码块3;否则执行代码块4。

示例:

age = 20

if age >= 18:
    if age == 18:
        print("你刚好成年了!")
    else:
        print("你已经成年了!")
else:
    print("你还未成年!")
    
#输出结果: 你已经成年了!

5.if 表达式

除了常规的 if 分支结构,Python 还提供了一种特殊的 if 表达式形式:

value = <expression1> if condition else <expression2>

根据 condition 的结果,如果为 True,则返回 <expression1> 的值;否则返回 <expression2> 的值。

这种if表达式的形式可以在一行代码中完成条件判断和结果赋值,非常简洁高效。它可以用于根据条件快速选择不同的返回值或设置变量的值。

需要注意的是,if表达式不能处理多个条件和多个分支,只适用于简单的条件判断和结果选择。如果需要更复杂的条件分支,还是需要使用常规的if语句来实现。

示例:

age = 20

message = "成年人" if age >= 18 else "未成年人"
print(message)
    
#输出结果:成年人

6.pass 语句

在某些情况下,由于业务逻辑或其他原因,你可能会希望在条件为真时不执行任何操作。但是,Python不允许有空的条件语句块。这时,可以使用pass来填充条件为真时的空语句块。

if condition:
    pass  # 什么都不做
else:
    # 条件为假的代码块

总之,pass语句在Python中用作一个占位符,用于填充需要语法上有语句的位置,但实际上不需要执行任何操作的情况。它可以帮助你避免语法错误,并且提供代码结构上的完整性。


7.注意事项

在使用 if 分支结构时,请注意以下几点:

  • 分支结构的每个代码块要缩进相同数量的空格(通常是四个空格),这样 Python 才能识别代码块的范围。

  • 不同的条件之间是互斥的,只有第一个满足的条件的代码块会被执行。

  • if 分支结构可以嵌套使用,但过度嵌套会导致代码难以理解和维护,尽量避免过度复杂化。

以上是 Python 中 if 分支结构的详细解释和用法。根据条件的结果,我们可以选择执行特定的代码块,从而实现灵活的控制流程。

13-流程控制-循环结构

1.while循环

Python中的while循环用于重复执行一段代码,直到给定的条件不成立。其语法结构如下:

while condition:
    # 要重复执行的代码块

需要注意以下几点:

  1. condition条件:循环的执行取决于一个布尔表达式或值,称为循环条件。在每次循环开始之前,都会检查循环条件的值。如果条件为真,则执行循环中的代码块;如果条件为假,则跳过循环体直接执行循环后面的代码。

  2. 循环体:循环体是while循环中的代码块,它包含了要重复执行的语句。循环体中的语句将按顺序执行,直到循环条件为假才停止。

  3. 循环变量的更新:在循环体中通常需要对循环变量进行更新,以便改变循环条件的值。如果循环条件没有更新,循环可能会陷入无限循环,并导致程序无法继续执行。

示例:计算1到10的和

n = 1
sum = 0

while n <= 10:
    sum += n
    n += 1
print("1到10的和是:", sum)
    
#输出结果:1到10的和是: 55

在这个例子中,我们使用n来追踪要计算的数,sum存储数的总和。while循环条件n <= 10检查n是否小于等于10。只要条件为真,循环就会继续执行。在每次循环中,我们将n添加到sum中,然后将n增加1。当n的值为11时,循环条件为假,循环终止。最后,打印出1到10的和。

需要注意的是,如果循环条件一开始就为假,那么循环体的代码将不会执行,循环将被跳过。

在使用while循环时,要确保循环条件能在一定条件下变为假,否则会导致无限循环。为了避免无限循环,请确保在循环内部更新循环变量或使用适当的循环控制语句(如break语句)来终止循环。


2.for-in循环

for-in循环是一种用于遍历可迭代对象(如列表、元组、字符串等)中的元素的循环结构。它可以提供一种简洁的方式来重复执行代码,而不需要手动跟踪循环变量。

for-in循环的语法如下:

for item in iterable:
    # 要重复执行的代码块

需要注意以下几点:

  1. iterable可迭代对象:for-in循环用于遍历可迭代对象中的元素。可迭代对象是指具有迭代行为的对象,例如列表、元组、字符串等。在每次循环开始之前,for-in循环会自动从可迭代对象中获取下一个元素,并将其赋值给循环变量。

  2. 循环变量:循环变量(item)是for-in循环中的临时变量,用于依次存储可迭代对象中的每个元素。在循环体内部,可以使用循环变量来访问当前的元素,并执行相应的操作。

  3. 循环体:循环体是for-in循环中的代码块,它包含了要重复执行的语句。循环体中的语句将按顺序执行,并且在每次循环迭代时,将使用新的元素更新循环变量。

示例:计算列表中数字的总和

numbers = [1, 2, 3, 4, 5]
sum = 0
    
for num in numbers:
    sum += num
    
print("列表中数字的总和是:", sum)
    
#输出结果:列表中数字的总和是: 15

在这个例子中,我们有一个整数列表numbers和一个变量sum用于存储总和。for-in循环遍历列表中的每个元素,将当前元素赋值给变量num,然后将其累加到sum中。循环结束后,打印出列表中数字的总和。

使用for-in循环时,不需要显式地追踪循环变量,也不需要手动更新循环变量。循环会自动遍历可迭代对象中的每个元素,直到遍历完所有元素为止。

除了列表,for-in循环也可以用于其他可迭代对象,如元组、字符串等。它提供了一种简单而直观的方式来遍历集合中的元素,并对它们执行相应的操作。


3.循环的else

循环的else语句是在循环正常结束时执行的代码块。它与if语句中的else语句有所不同。

else语句和循环结构一起使用时,其语法如下:

for item in iterable:
    # 循环代码
else:
    # 循环结束后的代码

或者

while condition:
    # 循环代码
else:
    # 循环结束后的代码

当循环正常结束(没有被break语句中断)即循环条件为False时,程序会执行else块中的代码。如果循环被break语句中断,则else块中的代码不会执行。

示例:检查一个数字是否为素数

num = 17
    
for i in range(2, num):
    if num % i == 0:
        print(num, "不是素数")
        break
else:
    print(num, "是素数")
    
#输出结果:17是素数

在这个例子中,我们使用for循环遍历从2到num之间的所有数字。如果num能被某个数字整除,则说明它不是素数,我们使用break语句中断循环,并打印出相应的结果。如果循环正常结束,即没有找到能整除的数字,则else块中的代码会被执行,打印出num是素数的结果。

类似地,else语句也可以与while循环一起使用。在这种情况下,当循环条件变为假(False)时,即循环正常结束时,else块中的代码会被执行。

需要注意的是,循环的else语句并不是必需的,可以根据实际需求来决定是否使用。它主要用于在循环结束后执行一些特定的操作,例如处理未找到匹配项的情况或输出循环的统计结果等。


4.嵌套循环

嵌套循环是指在一个循环结构中嵌套另一个循环结构的情况。通过嵌套循环,可以在外部循环的每次迭代内执行内部循环,从而实现对多维数据结构的遍历。

嵌套循环的语法如下:

for outer_item in outer_iterable:
    # 外部循环代码
        
    for inner_item in inner_iterable:
        # 内部循环代码

其中,outer\_iterableinner\_iterable分别表示外部循环和内部循环的可迭代对象;outer\_iteminner\_item是外部循环和内部循环的循环变量,用于分别存储外部循环和内部循环中的元素。

示例:计算二维列表中所有数字的总和

matrix = [[1, 2, 3],
         [4, 5, 6],
         [7, 8, 9]]
    
total = 0
    
for row in matrix:
    for num in row:
        total += num
    
print("二维列表中所有数字的总和为:", total)
    
#输出结果:二维列表中所有数字的总和为: 45

在这个例子中,我们有一个二维列表matrix,其中包含了若干行和列的数字。外部循环遍历二维列表的每一行,将当前行赋值给变量row。内部循环遍历当前行的每个数字,将数字在累加到变量total中。最终,打印出二维列表中所有数字的总和。

使用嵌套循环时,内部循环会在外部循环的每次迭代内完整执行。也就是说,内部循环会先完整地遍历一遍,然后外部循环进入下一个迭代,内部循环再次执行。

嵌套循环可以有多层,可以根据需求在循环体内进行进一步的嵌套。但需要注意,嵌套循环的层数过多可能会导致代码可读性降低和性能下降。因此,在实际应用中,需要根据具体情况来决定是否使用嵌套循环以及嵌套的层数。


5.列表推导式

列表推导式(List Comprehension)是一种 Python 编程中常用的语法构造,用于快速生成新的列表。

它结合了循环和条件语句,允许在一行代码中描述如何从一个或多个可迭代对象中提取元素,并根据条件生成新的列表。

一般形式:

[expression for item in iterable if condition]

其中:

  • expression:表示要对每个元素执行的操作或表达式;

  • item:表示迭代过程中每个元素的变量名;

  • iterable:表示一个可迭代对象,如列表、元组或字符串等;

  • condition:表示一个可选的条件表达式,在满足条件时才会将元素包含在结果列表中。

列表推导式的执行过程是按照从左到右的顺序进行的,先进行迭代,然后对每个元素应用表达式,最后根据条件确定是否将元素添加到结果列表中。

示例:

  1. 生成平方数列表:
squares = [x ** 2 for x in range(1, 6)]
print(squares)
    
#输出结果:[1, 4, 9, 16, 25]
  1. 过滤偶数:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers = [x for x in numbers if x % 2 == 0]
print(even_numbers)
    
#输出结果:[2, 4, 6, 8, 10]
  1. 使用条件表达式生成新列表:
numbers = [1, 2, 3, 4, 5]
new_numbers = [x if x > 2 else 0 for x in numbers]
print(new_numbers)
    
#输出结果:[0, 0, 3, 4, 5]
  1. 处理嵌套结构:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened)
    
#输出结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
  1. 字符串处理:只取是字母的字符
message = 'Hello, World!'
letters = [char.lower() for char in message if char.isalpha()]
print(letters)
    
#输出结果:['h', 'e', 'l', 'l', 'o', 'w', 'o', 'r', 'l', 'd']

使用列表推导式可以让代码更简洁、易读,并且减少了使用循环和条件判断的代码量。

但需要注意,在处理大型数据集时,列表推导式可能会占用较多的内存,因为它会一次性生成整个列表。在这种情况下,可以考虑使用生成器表达式(Generator Expression)来代替列表推导式。


6.生成器表达式

生成器表达式(Generator Expression)是一种类似于列表推导式的语法构造,用于生成一个生成器(Generator)。

与列表推导式不同的是,生成器表达式不会一次性生成整个列表,而是按需生成值,从而节省内存和提高性能。

一般形式:

(expression for item in iterable if condition)

其中:

  • expression:表示要对每个元素执行的操作或表达式;

  • item:表示迭代过程中每个元素的变量名;

  • iterable:表示一个可迭代对象,如列表、元组或字符串等;

  • condition:表示一个可选的条件表达式,在满足条件时才会生成值。

与列表推导式的唯一区别在于使用圆括号 (...) 而不是方括号 \[...\]

生成器表达式的执行和使用方式与列表推导式类似,可以通过遍历生成器来逐个获取生成的值。下面是一个使用生成器表达式的示例:

numbers = [1, 2, 3, 4, 5]
squared_gen = (x ** 2 for x in numbers if x % 2 == 0)
    
print(squared_gen)       # <generator object <genexpr> at 0x00000123456789AB>
print(next(squared_gen))  # 4
print(next(squared_gen))  # 16
print(next(squared_gen))  # 引发 StopIteration 错误

在这个例子中,我们使用生成器表达式生成一个平方数的生成器。只有当数字为偶数时,才会计算其平方并生成值。通过调用 next() 函数来逐个获取生成器生成的值。

需要注意的是,当没有更多的值可供生成时,调用 next() 将引发 StopIteration 错误。

生成器表达式的优点是节省内存和提高性能,特别是对于大型数据集。因为它仅在需要时生成一个值,并且不需要创建整个列表。如果只需要遍历一次并且不需要随机访问列表中的元素,那么使用生成器表达式是一个很好的选择。


7.控制循环结构

在 Python 中,有多种方式可以控制循环结构的行为,比较常用的包括 breakcontinue 和 pass 语句。

  1. break 语句

break 语句用于跳出当前循环,不再执行循环中剩余的代码块,开始执行后续的语句。常用于满足某种条件时提前结束循环。

示例:

n = 0

while n < 5:
    if n == 3:
        break
    print(n)
    n += 1

输出:

0
1
2

在这个例子中,我们使用 while 循环打印数字,如果数字等于 3,则使用 break 语句提前结束循环,不再执行后续的代码块。

  1. continue 语句

continue 语句用于跳过本次循环中剩余的代码,开始下一轮循环。

常用于遇到某种情况需要跳过循环中的某些代码块。

示例:

n = 0

while n < 5:
    n += 1
    if n == 3:
        continue
    print(n)

输出:

1
2
4
5

在这个例子中,我们使用 while 循环打印数字,如果数字等于 3,则使用 continue 语句跳过本次循环中剩余的代码,开始下一轮循环。

  1. pass 语句

pass 语句用于占位符,通常在需要先定义一个函数或类的情况下临时使用。它不会执行任何操作,只是让 Python 解释器忽略这个代码块。下面是一个使用 pass 语句的示例:

for i in range(5):
    pass

在这个例子中,我们使用 pass 语句定义了一个空的 for 循环,因为我们暂时没有处理循环中的每个元素。

通过使用这些控制循环结构的语句,可以更加灵活地编写循环结构,并实现各种复杂的逻辑。

14-函数入门

1.了解函数

  • 简单来说,函数就是将实现特定功能的代码打包起来,并赋予一个名称,方便重复调用。使用函数可以减少代码重复,提高代码的可读性、可维护性和可扩展性。

  • 函数通常接收零个或多个参数,这些参数可以在函数内部被处理和计算。函数也可以返回零个或多个值,将计算结果返回给函数调用者。

  • 函数在程序设计中起到了重要作用,可以将复杂的逻辑进行分解和封装,从而降低代码的复杂度和难度。函数可以被模块化和重复利用,提高了程序的开发效率和代码的可重用性。


函数在形式上分为三种,一种是 Python 内置的函数, 一种是第三方库的函数,一种是我们自己定义的函数:

  1. Python内置函数:

    Python提供了许多内置函数,如print()、len()、type()等。这些函数是Python解释器默认提供的,无需导入任何模块就可以直接使用。

  2. 第三方库的函数:

    Python拥有丰富的第三方库,这些库通常提供了各种功能强大的函数。在使用这些函数之前,需要先安装相关的第三方库,并通过import语句导入所需的模块。例如,对于处理数学运算的函数,可以使用NumPy库中的函数;对于操作数据框的函数,可以使用Pandas库中的函数等。

  3. 自定义函数:

    我们可以根据自己的需求,在程序中定义自己的函数。通过使用def关键字来声明函数,并在函数体内编写实现特定功能的代码。自定义函数可以接收参数,并可选择性地返回值。通过自定义函数,可以将复杂的任务划分为更小的可重复使用的代码块,提高代码的可读性和可维护性。


2.定义及调用函数

无论哪种形式的函数, 函数定义方式都是一样的。函数定义后,我们就可以通过函数名和参数来调用函数


定义函数的规则如下:

  • 使用关键字def来定义函数,后跟函数名和括号(),然后以冒号:结尾。
  • 在括号内可以定义参数,多个参数之间使用逗号隔开。
  • 函数体必须缩进,一般使用四个空格或一个制表符的缩进。
  • 函数体由一条或多条语句组成,用于实现函数的功能。
  • 可以使用return语句返回函数的结果,也可以没有return语句。

语法:

def functionname( parameters ):
   "函数_文档字符串"
   function_suite
   return [expression]
  1. def:表示要定义一个函数
  2. functionname:函数名,用于调用该函数;从语法角度来看,函数名只要是一个合法的标识符即可;从程序的可读性角度来看,函数名应该由一个或多个有意义的单词连缀而成,每个单词的字母全部小写,单词与单词之间使用下画线分隔。
  3. parameters:参数列表,包括函数所需要的输入参数,多个参数之间用逗号隔开;函数需要几个关键的需要动态变化的数据,这些数据应该被定义成函数的参数
  4. 函数_文档字符串:用三个双引号括起来的字符串,用于描述函数的功能和作用
  5. function_suite:函数体,包含若干条语句,用于实现函数的功能
  6. return [expression]:函数返回值,可选项,用于返回函数执行结果,也可以没有返回值

函数示例,说明函数的语法和用法:

def greet(name):
    """这是一个打招呼的函数"""
    print("Hello, " + name + "!")

def add(a, b):
    """这是一个求和的函数"""
    return a + b

# 调用函数
greet("Alice")  # 输出:Hello, Alice!

result = add(3, 4)
print(result)  # 输出:7

在上面的示例中,我们定义了两个函数:greet和add。greet函数接受一个参数name,并打印出相应的问候语。add函数接受两个参数a和b,并返回它们的和。通过调用这些函数,我们可以实现相应的功能。

注意:

  • 函数定义应该放在调用之前,以确保函数在被调用时已经被定义。
  • 函数名应该具有描述性,以便于理解函数的功能。
  • 函数体内的注释可以使用三引号"""来添加,用于提供函数的说明文档。

3.为函数编写说明文档

编写文档是一个良好的编程习惯,可以提高代码的可读性和可维护性。

对于函数的文档,一般使用文档字符串(docstring)来进行描述。

文档字符串是包含在函数定义内部的字符串,用于描述函数的功能、输入参数、返回值以及其他相关信息。

它们位于函数定义的第一行或第二行,并用三个双引号括起来。

示例:

def add(a, b):
    """
    计算两个数的和

    参数:
    a (int): 第一个加数
    b (int): 第二个加数
    
    返回值:
    int: 两个数的和
    """
    result = a + b
    return result

在上面的示例中,“”"之间的部分就是文档字符串。

我们使用了一种常见的文档字符串格式:

  1. 首先是对函数功能的概括
  2. 然后是对输入参数的描述(包括参数名、类型和说明)
  3. 最后是对返回值的描述(包括类型和说明)

你可以根据实际情况编写更详细的文档字符串,包括示例用法、边界条件等。良好的文档字符串可以帮助其他人更好地理解和使用你的函数。

当你使用文档字符串编写函数文档后,可以通过help()函数或在交互式环境中使用函数名.__doc__来查看函数的文档:

help(add)
print(add.__doc__)

4.多个返回值函数

一个函数可以返回多个值,这些值将被封装为一个元组(tuple)对象。

示例:

def divide(num1, num2):
    quotient = num1 // num2
    remainder = num1 % num2
    return quotient, remainder

result = divide(15, 4)
print("商和余数分别为:", result)

#输出结果为:   商和余数分别为: (3, 3)

在上面的示例中,我们定义了一个名为divide的函数,它实现了两个整数的除法运算,并返回商和余数。

具体来说,函数的输入参数为num1和num2,然后计算出商和余数,并使用return语句返回一个包含商和余数的元组。

在调用该函数时,我们将15和4作为实参传入divide函数,得到的结果赋值给变量result,result是一个元组,包含商和余数两个值。最后输出结果。

如果需要获取函数返回值的不同部分,可以通过元组解包的方式进行操作:

quotient, remainder = divide(15, 4)
print("商为:", quotient)
print("余数为:", remainder)

#输出结果为:   商为: 3
#输出结果为:   余数为: 3

在上面的示例中,我们通过元组解包的方式将返回值拆开赋值给了两个变量quotient和remainder,然后分别输出了商和余数。


5.递归函数

递归函数就是在函数内部调用自己的一种特殊函数

递归函数通常会包含两个部分:基本情况和递归情况

  • 递归函数的基本情况(也称为递归出口)是一个终止条件,当满足这个条件时,函数不再进行递归操作,而是直接返回一个确定的值或执行一些特定的操作。

  • 递归函数的递归情况是指函数内部调用自己的情况,通常需要缩小问题的规模,使其能够达到基本情况。在处理递归情况时,函数的输入参数通常会发生变化,以反映当前问题的规模。

下面是一个简单的示例,它实现了阶乘的递归计算:

def factorial(n):
    # 基本情况
    if n == 1:
        return 1
    # 递归情况
    else:
        return n * factorial(n-1)

result = factorial(5)
print("5的阶乘为:", result)

#输出结果为:   5的阶乘为: 120

在上面的示例中,我们定义了一个名为factorial的函数,实现了计算给定整数的阶乘的功能。在函数内部,首先判断是否满足基本情况(即n=1),如果是,则直接返回1;否则,通过递归调用自己,将问题的规模缩小为n-1,并将计算结果乘以n。

在调用该函数时,我们传入了参数5,表示计算5的阶乘。函数执行过程中会多次调用自身,直到满足基本情况为止。最终将得到5! = 5 * 4 * 3 * 2 * 1 = 120的结果。

需要注意的是,在编写递归函数时,要确保递归求解的问题可以无限接近基本情况,并有明确的终止条件,否则可能会导致无限递归,使程序崩溃或出现其他错误。

15-函数的参数

在定义Python 函数时可定义形参(形式参数的意思),这些形参的值要等到调用时才能确定下来,由函数的调用者负责为形参传入实际参数值(即实参)

简单来说,就是谁调用函数,谁负责传入参数值。


1.关键字参数

关键字参数是一种使用关键字传递的函数参数。这种参数传递方式是指在函数调用时,通过指定参数名和对应的值来传递参数,而不是按照它们在函数定义中的位置来传递。

使用关键字参数有以下几个优点:

  • 具有可读性:通过使用参数名和值的形式,可以提高函数调用的可读性和可维护性。

  • 可以省略默认值:对于有默认值的参数,可以通过使用关键字参数来省略它们,而不必按照定义顺序传递所有参数。

  • 可以跳过部分参数:对于函数定义中有很多参数的情况,可以只传递所需的参数,而不必按照定义顺序传递所有参数。

下面是一个使用关键字参数的简单示例:

def print_person_info(name, age):
    print(f"{name} is {age} year(s) old.")


# 使用传统函数方式,根据位置传入参数值
print_person_info("Alice", 25)

# 使用关键字参数调用函数
print_person_info(name="Alice", age=25)

# 使用关键字参数调用函数时参数位置可变换
print_person_info(age=25, name="Alice")

# 部分使用位置参数,部分使用关键字参数
print_person_info("Alice", age=25)

#输出均为:    Alice is 25 year(s) old.

需要注意的是,在函数调用中,所有的位置参数必须放在关键字参数之前。否则会引发语法错误。


2.参数的默认值

函数的参数可以设置默认值,这样在函数调用时,如果没有为该参数提供值,就会使用默认值。参数的默认值在函数定义时通过赋值操作来指定。

以下是一个设置参数默认值的示例:

def print_person_info(name, age, city="Beijing"):
    print(f"{name} is {age} year(s) old and lives in {city}.")


# 不传递city参数,使用默认值
print_person_info("Alice", 25)

# 传递city参数,覆盖默认值
print_person_info("Bob", 30, city="Shanghai")

#输出结果为:    Alice is 25 year(s) old and lives in Beijing.
#输出结果为:    Bob is 30 year(s) old and lives in Shanghai.

在上面的示例中,函数print_person_info()定义了三个参数:name、age和city。其中,city参数通过赋值"Beijing"来设置了默认值。

当我们调用函数时,如果没有提供city参数,函数将使用默认值"Beijing"。在第一个函数调用中,我们只传递了name和age两个位置参数,而在第二个函数调用中,我们传递了name、age和city三个参数,并且指定了city参数的值为"Shanghai",这样就覆盖了默认值。

需要注意的是,默认值参数应该放在非默认值参数后面。也就是说,如果一个参数有默认值,它后面的所有参数都必须有默认值

另外,如果函数有多个参数,并且想要为其中某些参数提供默认值,而为后面的参数传递值时不指定参数名,可以使用关键字参数的方式来实现。


3.参数收集

参数收集是一种将可变数量的参数传递给函数的方法,这在不知道具体有多少个参数需要传递时非常有用。

在Python中,有两种类型的参数收集:位置参数收集和关键字参数收集。

  1. 位置参数收集:
    使用星号 * 可以在函数定义时收集任意数量的位置参数。这些参数被收集后会被转换为一个元组

    以下是一个使用位置参数收集的示例:

    def sum_numbers(*numbers):
        total = 0
        for num in numbers:
            total += num
        return total
    
    
    result = sum_numbers(1, 2, 3, 4, 5)
    print(result)  # 输出: 15
    

    在上面的示例中,sum_numbers() 函数使用 *numbers 收集任意数量的位置参数。当我们调用这个函数时,传递给函数的所有参数都被收集到 numbers 元组中,在函数内部可以对其进行遍历和操作。


  1. 关键字参数收集:
    使用双星号 ** 可以在函数定义时收集任意数量的关键字参数。这些参数被收集后会被转换为一个字典

    其中关键字是参数名,对应的值是参数值

    以下是一个使用关键字参数收集的示例:

    def print_info(**info):
        for key, value in info.items():
            print(f"{key}: {value}")
    
    
    print_info(name="Alice", age=25, city="Beijing")
    # 输出:
    # name: Alice
    # age: 25
    # city: Beijing
    

    在上面的示例中,print_info() 函数使用 **info 收集任意数量的关键字参数。当我们调用这个函数时,传递给函数的所有关键字参数都被收集到 info 字典中,在函数内部可以对其进行遍历和操作。

通过位置参数收集和关键字参数收集,我们可以在函数定义中处理不定数量的参数,使函数更加灵活和通用。

更多示例:

  1. 使用可变长度的位置参数收集和关键字参数收集,实现一个计算多个数字之和的函数。

    def sum_numbers(*numbers, **options):
        total = 0
        for num in numbers:
            total += num
        if 'negatives' in options:
            # 如果传入了 negatives 参数,则输出所有负数
            negatives = [num for num in numbers if num < 0]
            print("Negative numbers:", negatives)
        return total
    
    
    result = sum_numbers(1, 2, 3, -4, -5, negatives=True)
    print(result)  
    
    # 输出: Negative numbers: [-4, -5] 7
    # 输出: -3
    

    在这个示例中,我们定义了一个计算多个数字之和的函数 sum_numbers()。我们使用可变长度的位置参数收集 *numbers 来收集任意数量的数字,使用可变长度的关键字参数收集 **options 来接收一些配置选项。在函数体内,我们首先对所有位置参数进行求和,然后根据传入的 negatives 参数决定是否输出所有负数。


  1. 使用可变长度的位置参数收集,实现一个将多个字符串拼接起来的函数。

    def concat_strings(*strings, sep=' '):
        return sep.join(strings)
    
    
    result = concat_strings('Hello', 'world', '!')
    print(result)  # 输出: Hello world !
    
    result = concat_strings('Hello', 'world', '!', sep=', ')
    print(result)  # 输出: Hello, world, !
    

    在这个示例中,我们定义了一个将多个字符串拼接起来的函数 concat_strings()。我们使用可变长度的位置参数收集 *strings 来收集任意数量的字符串,并使用 join() 方法将它们拼接起来。函数还提供了一个可选参数 sep,用于指定拼接时要使用的分隔符,如果不指定,默认为一个空格。


4.逆向参数收集

逆向参数收集,也称为参数分拆或解包。

它的作用是将已有的位置参数列表或关键字参数字典打散成多个单独的值,以便将它们作为参数传递给另一个函数或方法。

逆向参数收集使用的是 * 和 ** 运算符。当 * 作为前缀应用于一个序列(数组或元组)时,它将打散这个序列,使得其中的每个元素都成为单独的位置参数。当 ** 作为前缀应用于一个字典时,它将打散这个字典,使得其中的每个键值对都成为单独的关键字参数。

以下是一些示例:

  1. 将列表中的元素作为位置参数传递给函数:

    def foo(a, b, c):
        print(a, b, c)
    
    
    args = [1, 2, 3]
    foo(*args)  
    
    # 输出: 1 2 3
    

    在这个示例中,我们定义了一个函数 foo(),它接受三个位置参数。然后我们创建了一个序列 args,其中包含三个元素。最后,我们使用 * 运算符将 args 序列打散,将其中的元素作为位置参数传递给函数 foo()。

  2. 将字典中的键值对作为关键字参数传递给函数:

    def bar(x, y, z):
        print(x, y, z)
    
    
    kwargs = {'y': 2, 'z': 3}
    bar(1, **kwargs)  
    
    # 输出: 1 2 3
    

    在这个示例中,我们定义了一个函数 bar(),它接受三个关键字参数。然后我们创建了一个字典 kwargs,其中包含两个键值对。最后,我们使用 ** 运算符将 kwargs 字典打散,将其中的键值对作为关键字参数传递给函数 bar()。其中键名对应函数参数名,值对应参数值。

  3. 在函数中使用多个默认参数,但只给其中的一部分赋值。这种情况下可以将所有的参数定义为位置参数,并使用逆向参数收集接收剩余的未赋值的参数。

    def func(a, b, c=1, d=2, e=3):
        print(a, b, c, d, e)
     
    
    args = [1, 2]
    func(*args)  
    
    # 输出: 1 2 1 2 3
    

    在这个示例中,我们定义了一个函数 func(),它包含五个参数,其中 c、d 和 e 都有默认值。然后我们创建了一个包含两个元素的列表 args,其中只包含了两个必需的位置参数。最后,我们使用 * 运算符将 args 序列打散,将其中的元素作为位置参数传递给函数 func()。

  4. 在函数调用时,需要将一个序列中的元素作为关键字参数传递,而不是位置参数。

    def func(a, b, c):
        print(a, b, c)
     
    
    kwargs = {'c': 'c'}
    args = [1, 2]
    func(*args, **kwargs)  
    
    # 输出: 1 2 c
    

    在这个示例中,我们定义了一个函数 func(),它包含三个参数。然后我们创建了一个字典 kwargs,其中包含一个键值对,对应函数的关键字参数。同时,我们也创建了一个列表 args,其中包含两个必需的位置参数。最后,我们使用 * 和 ** 运算符将 args 和 kwargs 打散,将其中的元素和键值对分别作为位置参数和关键字参数传递给函数 func()。

  5. 调用函数时需要传递一个动态参数列表。

    def func(a, *args):
        print(a, args)
     
    
    func(1, 2, 3, 4)  
    
    # 输出: 1 (2, 3, 4)
    

    在这个示例中,我们定义了一个函数 func(),它包含一个必需的位置参数和一个可变长度的位置参数收集 *args。当我们在调用函数时传入了多个参数时,可以使用逆向参数收集将其打散并传递给可变长度的 *args 参数。

逆向参数收集是 Python 中非常实用且常见的一种技巧,可以避免手动地挨个传递参数,简化代码并提高可读性。同时也要注意收集的对象必须是可迭代的序列或字典,否则会抛出错误。


5.局部函数

局部函数是定义在其他函数内部的函数。与全局函数不同,局部函数只能在包含它们的函数内部使用。

局部函数通常用来实现一些辅助性的功能,或者将复杂的逻辑拆分为更小的、单独可测试的部分。由于局部函数仅限于外层包含函数的作用域,因此可以防止其他代码无意中访问或修改这些函数。

示例:

# 定义外部函数 get_math_func,该函数包含局部函数 def get_math_func(type, m)
def get_math_func(type, m):
    # 定义一个计算平方的局部函数 def square(n)
    def square(n):
        return n * n

    # 定义一个计算立方的局部函数 def cube(n)
    def cube(n):
        return n * n * n

    # 定义一个计算阶乘的局部函数 def factorial(n)
    def factorial(n):
        # 初始化 result 为 1
        result = 1
        for index in range(2, n + 1):
            # 计算阶乘
            result *= index
        return result

    # 调用局部函数
    if type == "square":
        return square(m)
    elif type == "cube":
        return cube(m)
    else:
        return factorial(m)


print(get_math_func("square", 3))  # 输出 9
print(get_math_func("cube", 3))  # 输出 27
print(get_math_func("", 3))  # 输出 6

这个代码定义了一个外部函数 get_math_func,该函数包含了三个局部函数:square、cube 和 factorial。这些局部函数可以计算数字的平方、立方和阶乘。

当调用 get_math_func 函数时,它会根据传入的 type 参数选择相应的局部函数,并将 m 参数传递给该函数进行计算。如果 type 参数既不是 “square” 也不是 “cube”,则调用 factorial 函数计算阶乘。

在代码末尾,我们分别调用了 get_math_func 函数三次,并传入了不同的参数。第一次调用返回结果为 9(3 的平方),第二次调用返回结果为 27(3 的立方),第三次调用返回结果为 6(3 的阶乘)。

这个示例展示了如何在函数内部定义多个局部函数,并根据需要进行调用。通过使用局部函数,我们可以将复杂的计算任务拆分为更小、更可管理的部分,提高代码的可读性和可维护性。


6.变量作用域

变量作用域是指变量在程序中可以被访问的范围。

在不同的作用域中,变量可能具有不同的可见性和生存周期。

Python 中有四种变量作用域:局部作用域(Local)、嵌套作用域(Enclosing)、全局作用域(Global)和内建作用域(Built-in)。局部作用域和全局作用域 比较常用。

  1. 局部作用域(Local Scope):

    • 局部作用域是在函数或方法内部定义的变量所在的作用域。

    • 只能在定义它们的函数内部访问。

    • 当函数执行完毕或遇到 return 语句时,局部作用域中的变量会被销毁。

    def func():
        x = 10
        print(x)  # 在局部作用域中访问变量
    
    
    func()  # 输出:10
    print(x)  # 错误,x 在全局作用域中无法访问
    
  2. 嵌套作用域(Enclosing Scope):

    • 嵌套作用域是指在一个函数内部定义了另一个函数,内部函数可以访问外部函数中的变量。

    • 内部函数可以访问自己的局部变量、外部函数的变量和全局变量。

    • 外部函数无法访问内部函数的变量。

    def outer_func():
        x = 10
     
        def inner_func():
            y = 20
            print(x, y)  # 在嵌套作用域中访问变量
     
        inner_func()
    
    
    outer_func()  # 输出:10 20
    print(x)  # 错误,x 在全局作用域中无法访问
    print(y)  # 错误,y 在外部函数作用域中无法访问
    
  3. 全局作用域(Global Scope):

    • 全局作用域是在模块层级定义的变量所在的作用域。

    • 可以在整个模块中访问这些变量。

    • 在函数内部可以使用 global 关键字声明一个变量为全局变量,从而在函数内部修改全局变量的值。

    x = 10  # 全局变量
    
    def func():
        global x  # 声明 x 为全局变量
        x += 1
        print(x)  # 在全局作用域中访问变量
    
    func()  # 输出:11
    print(x)  # 输出:11
    
  4. 内建作用域(Built-in Scope):

    • 内建作用域是指 Python 解释器默认提供的一些内置函数和异常名称所在的作用域。

    • 可以在任何地方直接访问这些内置函数和异常名称。

    print(max([1, 2, 3]))  # 使用内置函数 max()
    print(ValueError)  # 访问内置异常 ValueError
    

    输出结果:

    3
    <class 'ValueError'>
    

了解变量作用域可以帮助我们合理地定义和使用变量,避免命名冲突和不必要的错误。在编写程序时,需要根据实际需求选择适当的作用域来存储和访问变量,以确保代码的正确性和可维护性。

16-函数式编程及lambda表达式

函数本身也是一个对象,函数既可用于赋值,也可用作其他函数的参数,还可作为其他函数的返回值。

1.函数式编程

函数可以像其他类型的变量一样被使用,也可以被赋值给其他变量,作为参数传递给其他函数,或者作为函数的返回值。这种将函数作为变量使用的特性称为函数式编程。在函数式编程中,函数被当作一等公民(first-class citizen),可以与其他类型的变量一样进行操作,并且具有灵活的使用方式。

当我们谈到函数作为变量时,通常有三类主要的用法,包括函数赋值给变量、函数作为参数传递给其他函数以及函数作为另一个函数的返回值。

下面我将更详细地介绍这三种用法:

  1. 将函数赋值给变量:
  • 函数可以被赋值给变量,从而使函数变量成为对函数的引用。
  • 这样做的好处之一是可以使用不同的名称引用同一个函数,或者在不同的上下文中使用函数。
  • 对函数赋值给变量后,可以通过该变量调用函数。
def greet(name):
    print("Hello, " + name)


say_hello = greet  # 将 greet 函数赋值给 say_hello 变量
say_hello("Alice")  # 调用 say_hello 变量,输出:Hello, Alice

  1. 将函数作为参数传递给其他函数:
  • 函数可以作为数据类型一样被传递给其他函数作为参数。
  • 这种方式允许我们将一个函数作为另一个函数的逻辑组成部分,实现更加灵活和可扩展的代码。
  • 这种用法经常在回调函数、装饰器、高阶函数等编程范式中使用。
def add(a, b):
    return a + b


def calculate(func, x, y):
    result = func(x, y)  # 调用传入的函数参数
    print("Result:", result)


calculate(add, 5, 3)  # 将 add 函数作为参数传递给 calculate 函数,输出:Result: 8

  1. 将函数作为另一个函数的返回值:
  • 函数可以作为另一个函数的返回值,允许在函数内部定义其他函数。
  • 这种方式通常用于创建闭包(Closure),即内嵌函数可以访问其外部函数的局部变量。
  • 返回的函数具有对其外部函数中变量的引用,即使外部函数已经执行完毕,这些引用仍然存在。
def multiply_by(n):
    def multiply(x):
        return x * n
    return multiply


multiply_by_2 = multiply_by(2)  # 返回一个函数 multiply,该函数将参数乘以 2
print(multiply_by_2(5))  # 输出:10,调用返回的函数 multiply_by_2

image

若此处看不明白,可以如上图,在编辑器内第8行使用右键打上断点,使用DEBUG模式逐步解析传值过程;此处只需知道DEBUG模式,后续会详细介绍。

通过上述方式使用函数,我们可以实现更加模块化、可复用和灵活的代码。这些方式支持高阶函数编程,可以提高代码的可读性和可维护性,同时也为实现一些复杂的编程模式提供了更多的灵活性。


2.lambda表达式

  • 如果说函数是命名的、方便复用的代码块,那么lambda表达式则是功能更灵活的代码块,它可以在程序中被传递和调用

  • lambda表达式是一种匿名函数,也称为“lambda函数”。

  • 它是一种简洁的方式来定义单行的小型函数,通常用于需要一个函数对象作为参数的场景或者在代码中需要一个临时的函数。

语法如下:

lambda arguments: expression

其中,arguments 是该函数的参数列表,可以包含零个或多个参数,多个参数使用逗号分隔。expression 是该函数的返回值表达式,也就是函数的执行逻辑。

下面是一个使用lambda表达式求平方的示例:

square = lambda x: x**2

print(square(5))  # 输出 25

在上面的示例中,我们定义了一个lambda表达式 lambda x: x**2,它接收一个参数 x,并返回 x 的平方。

我们将lambda表达式赋值给变量 square,然后可以像调用普通函数一样使用这个变量来调用lambda函数,并传入相应的参数。

输出结果为25,表示对5进行平方运算得到了25。

lambda表达式通常用于一次性的、简单的函数需求,避免了使用def关键字去定义一个命名函数,从而减少了代码量,使代码更加简洁和易读。

17-类和对象

Python 是一种面向对象的编程语言,它支持面向对象的编程方式,并提供了简单、够用的语法功能。Python 的面向对象比较简单,不像其他面向对象语言提供了大量繁杂的面向对象特征,但足以满足大多数程序员的需求。

在 Python 中,创建类和对象都非常容易。通过定义一个类,我们可以实现封装、继承和多态等面向对象的特性。封装可以隐藏类的实现细节,使得代码更加模块化、更加可维护;继承可以让代码重复使用,减少代码的冗余;多态可以让不同的对象以不同的方式响应相同的方法调用。

Python 支持单继承与多重继承。子类继承父类后,可以调用父类的变量和方法,也可以重写父类的方法,从而实现对父类的定制化。

在 Python 中,类和对象的关系类似于魔术,每个对象都拥有自己的属性和方法,同时也可以访问类的属性和方法。通过创建类和对象,我们可以利用面向对象的思想来构建复杂的软件系统,提高代码的可维护性和可扩展性。

总之,Python 具备完备的面向对象编程特性,其简单易用的语法方式也让 Python 成为了广受欢迎的编程语言之一


1.类和对象

类和对象是面向对象编程的核心概念,在Python中也得到了广泛应用

1.1.类(Class)

类是一种抽象数据类型,它将数据属性和方法封装在一个单独的实体中。类定义了一组属性和方法,用于描述具有相同特征和行为的对象集合。

类由以下几个要素组成:

  • 类名:类的名称,用于标识类的唯一性。通常使用大驼峰命名法(首字母大写)。
  • 属性(属性/实例变量):类中的数据,用于描述对象的特征。每个对象都有自己的属性值。
  • 方法:类中定义的函数,用于定义对象的行为。方法可以访问和修改对象的属性。

以下是一个示例类的详细解释:

class Person:
    # 属性的定义
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 方法的定义
    def say_hello(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")

在上面的示例中,我们定义了一个名为 Person 的类。该类有两个属性 name 和 age,用于描述人的姓名和年龄。这些属性在类的所有实例中都是独立的。

类还定义了一个方法 say_hello,用于打印出一个人的问候语。方法可以访问类的属性,例如在 say_hello 方法中使用了 self.name 和 self.age。

1.2.对象(Object)

对象是类的实例化(即根据类创建的具体实例)。当我们实例化一个类时,系统会为该类分配内存空间,并创建一个独立的对象。

以下是基于上述示例创建对象的代码:

person1 = Person("Alice", 25)
person2 = Person("Bob", 30)

在上述代码中,我们创建了两个 Person 类的对象 person1 和 person2。这些对象都代表了一个具体的人,拥有自己的属性值。

1.3.类与对象的关系

类是对象的模板,对象是类的实例。一个类可以创建多个对象,每个对象都有自己独特的属性值,但共享类定义的方法。

类可以看作是对象的蓝图或模具。当我们创建一个对象时,系统会根据类的定义在内存中分配空间,并将属性和方法复制到对象中。因此,通过实例化类,我们可以得到一个具体的对象,其属性和方法都来自于类。

通过类,我们可以轻松地创建多个具有相同行为和特征的对象,同时可以根据需要访问对象的属性和调用对象的方法。

例如,在上面的示例中,我们创建了两个 Person 类的对象 person1 和 person2。这些对象都具有相同的方法 say_hello,但是它们的属性值是不同的。

当我们对类进行修改时,所有基于该类创建的对象都会受到影响,因为它们共享类定义的方法和属性。但一个对象的状态的改变(例如修改对象的属性值)并不会影响其他对象,因为每个对象都有自己的独立状态。

综上所述,类和对象是面向对象编程的关键概念,类提供了对一组具有相同特征和行为的对象的定义,而对象是这些类的具体实例。通过使用类和对象,我们可以更好地组织和管理代码,并实现代码的复用和扩展性。


2.定义类

在面向对象编程中,定义类是指创建一个新的类并规定该类包括的属性和方法。以下是定义类的基本步骤:

  • 使用关键字 class 后跟类名来声明一个类。类名通常使用大驼峰命名法(首字母大写)。
class MyClass:
    # 类定义
    pass
  • 在类的定义块内,定义类的属性和方法。属性用于描述类的特征,方法用于定义类的行为。属性和方法的定义使用函数的形式。
class MyClass:
    # 属性的定义
    attribute = "value"

    # 方法的定义
    def method(self):
        # 方法体
        pass
  • 类中的方法可以访问类的属性和其他方法。在方法定义中,第一个参数必须是 self,它表示方法所属的对象实例。

  • 类定义结束后,我们可以通过实例化类来创建具体的对象。

my_object = MyClass()

现在,让我们来详细解释一下如何定义类的属性和方法:

  1. 定义属性
    属性是类的特征信息,用于描述类的状态。在类定义中,我们可以为类添加各种属性,并对其进行赋值。属性可以是任意数据类型,如整数、浮点数、字符串、列表等。

    例如,我们可以定义一个 Person 类,该类有两个属性 name 和 age 来描述一个人的姓名和年龄:

    class Person:
        name = "Alice"
        age = 25
    
  2. 定义方法
    方法是类的行为,用于定义类的操作。在类定义中,我们使用函数的形式来定义方法。方法可以对类的属性进行操作、输出结果等。

    在方法定义中,第一个参数必须是 self,它表示方法所属的对象实例。通过 self 参数,方法可以访问该对象的属性和其他方法。

    例如,我们可以定义一个 Person 类的方法 say_hello,用于打印出一个人的问候语:

    class Person:
        name = "Alice"
        age = 25
     
        def say_hello(self):
            print(f"Hello, my name is {self.name} and I am {self.age} years old.")
    

    在上述例子中,say_hello 方法可以通过 self.name 和 self.age 访问类的属性,并打印出一个问候语。


3.实例化和调用

  1. 实例化对象:
  • 在面向对象编程中,对象是类的一个具体实例。要创建一个对象,我们需要实例化相应的类。
  • 实例化对象的过程是通过调用类的构造函数 init 来完成的。构造函数是一个特殊的方法,它在创建对象时被自动调用。
  • 构造函数可以接受参数,用于设置对象的初始属性值。参数可以根据情况自定义,也可以省略不传入。在构造函数中,我们可以使用参数来初始化对象的属性。
  • 当实例化一个对象时,系统会为对象分配内存空间,并将类的属性和方法复制到对象中。每个对象都是独立的,它们可以拥有相同的属性,但属性的值可以不同。

示例:

# 定义一个名为Person的类
class Person:
    def __init__(self, name, age):
        self.name = name  # 设置name属性
        self.age = age    # 设置age属性
        
# 实例化一个Person对象
person = Person("Alice", 25)
  1. 对象的属性访问:
  • 一旦对象被实例化,我们就可以使用对象来访问类的属性。
  • 使用点号 . 运算符,我们可以获取对象的属性值或对其进行赋值。例如:object.attribute。
  • 对象的属性可以是任意数据类型,如整数、浮点数、字符串、列表等。我们可以根据需要定义和修改对象的属性值。
  • 通过访问对象的属性,我们可以获取对象的状态信息,并进行相关操作。

示例:

# 访问对象的属性
print(person.name)  # 输出:"Alice"
print(person.age)   # 输出:25

# 修改对象的属性值
person.age = 30
print(person.age)   # 输出:30
  1. 对象的方法调用:
  • 通过对象的方法,我们可以执行与对象相关的操作。类的方法是定义在类中的函数。
  • 使用点号 . 运算符,我们可以调用对象的方法。例如:object.method()。
  • 在类的方法定义中,第一个参数必须是 self,它表示方法所属的对象实例。通过 self 参数,方法可以访问该对象的属性和其他方法。
  • 通过调用对象的方法,我们可以执行特定的行为和操作,以修改对象的状态或返回特定的结果。

示例:

# 在Person类中定义一个say_hello方法
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    # 定义一个say_hello方法
    def say_hello(self):
        print("Hello, my name is", self.name, "and I am", self.age, "years old.")
    
# 实例化一个Person对象
person = Person("Alice", 25)

# 调用对象的方法
person.say_hello()  # 输出:"Hello, my name is Alice and I am 25 years old."

总结起来,对象的产生和使用包括实例化对象和对对象进行属性访问和方法调用。实例化对象需要调用类的构造函数,并可以传入参数来初始化对象的属性值。通过访问对象的属性,我们可以获取和修改对象的状态。通过调用对象的方法,我们可以执行与对象相关的操作。对象的产生和使用过程使得代码更加模块化、易读、易于维护和复用,提高了程序的可扩展性和灵活性。


4.实例方法和自动绑定self

当我们在类中定义方法时,通常会将第一个参数命名为 self,这个参数表示实例对象自身。通过在方法定义中包含 self,我们可以使实例方法与实例对象进行自动绑定。

4.1.实例方法

  • 实例方法是定义在类中的方法,它绑定到类的实例对象上,并且可以访问和操作该实例对象的属性。
  • 实例方法定义时需要包含 self 参数,用于表示实例对象自身。通过 self 参数,我们可以访问实例对象的属性和调用其他实例方法。
  • 当我们调用实例方法时,无需显式传递 self 参数,Python 会自动将实例对象作为第一个参数传递给方法。

示例代码:

# 定义一个名为Person的类
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 定义一个实例方法
    def say_hello(self):
        print("Hello, my name is", self.name, "and I am", self.age, "years old.")

# 实例化一个Person对象
person = Person("Alice", 25)

# 调用对象的实例方法
person.say_hello()  # 输出:"Hello, my name is Alice and I am 25 years old."

4.2.自动绑定 self

  • 当我们通过实例对象调用实例方法时,Python 会自动将实例对象作为第一个参数传递给方法(即绑定 self 参数)。这个过程称为自动绑定 self。
  • 自动绑定 self 的作用是让实例方法能够访问和操作实例对象的属性。通过 self,方法可以引用实例对象的属性,并对其进行读取或修改。

示例代码:

# 定义一个名为Person的类
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    # 定义一个实例方法
    def say_hello(self):
        print("Hello, my name is", self.name, "and I am", self.age, "years old.")
        
    # 定义另一个实例方法
    def change_name(self, new_name):
        self.name = new_name
        print("Changed name to", self.name)

# 实例化一个Person对象
person = Person("Alice", 25)

# 调用对象的实例方法
person.say_hello()       # 输出:"Hello, my name is Alice and I am 25 years old."
person.change_name("Bob")  # 输出:"Changed name to Bob"
person.say_hello()       # 输出:"Hello, my name is Bob and I am 25 years old."

通过自动绑定 self,我们可以在实例方法中访问和操作实例对象的属性,从而实现对实例对象的状态和行为的控制。自动绑定 self 使得代码更加简洁和易于理解,同时也符合面向对象编程的规范。

18-方法

方法是类或对象的行为特征的抽象,但 Python 的方法其实也是函数,其定义方式、调用方式和函数都非常相似,因此 Python 的方法并不仅仅是单纯的方法,它与函数也有莫大的关系。

1.类方法和静态方法

除了实例方法之外,Python 还提供了类方法和静态方法两种类型的方法,它们在类中定义并与类本身相关联。

1.1.类方法

  • 类方法是定义在类中的方法,使用 @classmethod 装饰器进行修饰。
  • 类方法绑定到类而不是实例对象上,因此可以通过类名直接调用,无需实例化对象。
  • 类方法的第一个参数通常被命名为 cls,表示类本身。通过 cls 参数,我们可以访问和操作类的属性和调用其他类方法。
  • 类方法常用于创建备用构造函数、访问类属性或在类级别上执行操作。

示例代码:

# 定义一个名为Person的类
class Person:
    count = 0  # 类属性

    def __init__(self, name):
        self.name = name
        Person.count += 1  # 类属性的访问

    # 类方法
    @classmethod
    def get_count(cls):
        return cls.count

# 实例化两个Person对象
person1 = Person("Alice")
person2 = Person("Bob")

# 调用类方法
print(Person.get_count())  # 输出:2

1.2.静态方法

  • 静态方法是定义在类中的方法,使用 @staticmethod 装饰器进行修饰。
  • 静态方法既不绑定到类,也不绑定到实例对象。它们与类和实例对象无关,因此无法访问类或实例的属性。
  • 静态方法通常用于执行与类相关的功能,但不依赖于类或实例的状态。

示例代码:

# 定义一个名为Math的类
class Math:
    @staticmethod
    def add(a, b):
        return a + b

# 调用静态方法
result = Math.add(3, 4)
print(result)  # 输出:7

类方法和静态方法提供了额外的功能和灵活性,使得我们可以在类级别上执行操作而不依赖于实例对象。类方法对于与类紧密相关的任务非常有用,而静态方法则适用于那些不依赖于类和实例对象而只需执行特定功能的情况。


2.@函数装饰器

Python 中的装饰器是一种特殊的函数,可以用于修改或增强其他函数的功能。在 Python 中,我们可以使用 @ 符号来应用装饰器。

函数装饰器的语法如下:

@decorator_function
def some_function():
    # 函数体

其中,decorator_function 是装饰器函数的名称,some_function 是需要被装饰的函数名称。

当 Python 解释器解释到被装饰的函数时,会自动将其作为参数传递给装饰器函数,并将返回值替换原来的函数。这样就实现了对原函数的增强或修改功能。

下面是一些常用的函数装饰器及其用法:

  1. @staticmethod 装饰器
  • 用于将函数转换为静态方法。
  • 静态方法与类和实例无关,无法访问类或实例属性,只能访问自己的参数和全局变量。
  • 通常用于执行与类相关的功能,但不依赖于类或实例的状态。

示例代码:

class MyClass:
    @staticmethod
    def my_static_method(a, b):
        return a + b

# 调用静态方法
result = MyClass.my_static_method(3, 4)
print(result)  # 输出:7
  1. @classmethod 装饰器
  • 用于将函数转换为类方法。
  • 类方法绑定到类而不是实例对象上,因此可以通过类名直接调用,无需实例化对象。类方法的第一个参数通常被命名为 cls,表示类本身。
  • 通常用于创建备用构造函数、访问类属性或在类级别上执行操作。

示例代码:

class MyClass:
    count = 0

    def __init__(self):
        MyClass.count += 1

    @classmethod
    def get_count(cls):
        return cls.count

# 实例化两个对象
obj1 = MyClass()
obj2 = MyClass()

# 调用类方法
print(MyClass.get_count())  # 输出:2
  1. @property 装饰器
  • 用于将方法转换为只读属性。
  • 通常用于将计算属性转换为实例属性,从而简化属性的访问。

示例代码:

class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def diameter(self):
        return self.radius * 2

# 创建一个Circle对象
c = Circle(5)

# 使用属性访问器获取diameter属性的值
print(c.diameter)  # 输出:10
  1. @abstractmethod 装饰器
  • 用于定义抽象方法。
  • 抽象方法是一种特殊的方法,只有方法签名而没有具体实现内容。
  • 通常用于定义接口和抽象基类。

示例代码:

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

# 实例化一个Rectangle对象
r = Rectangle(5, 10)

# 调用实现的area方法
print(r.area())  # 输出:50

函数装饰器是 Python 中一种强大的编程技巧,在很多情况下可以简化代码并提高代码的可读性和可维护性。为了使用它们,我们需要熟悉其语法和实现方式,并清楚地理解其应用场景和作用。

19-成员变量和封装

在类体内定义的变量,默认属于类本身。如果把类当成类命名空间,那么该类变量其实就是定义在类命名空间内的变量,

1.类变量和实例变量

在类命名空间内定义的变量就属于类变量,Python 可以使用类来读取、修改类变量。

1.1.类变量(Class Variables)

  • 类变量是在类定义中声明的变量,位于方法体之外。
  • 类变量可以由类的所有实例共享,并且可以通过类名或实例访问。
  • 类变量通常用于存储与整个类相关的数据,例如类的特征、默认值等。
  • 类变量在类的所有实例中具有相同的值。

示例代码:

class MyClass:
    class_variable = 123

# 访问类变量
print(MyClass.class_variable)  # 输出:123

# 修改类变量
MyClass.class_variable = 456
print(MyClass.class_variable)  # 输出:456

1.2.实例变量(Instance Variables)

  • 实例变量是类中声明的变量,并在方法中使用 self 关键字引用。
  • 每个类的实例都有自己的实例变量,它们不会被其他实例所共享。
  • 实例变量的值可以在每个实例对象创建时进行初始化,并且可以在实例的方法中进行访问和修改。
  • 实例变量在不同的实例之间具有不同的值。

示例代码:

class MyClass:
    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

# 创建两个实例
obj1 = MyClass(1)
obj2 = MyClass(2)

# 访问实例变量
print(obj1.instance_variable)  # 输出:1
print(obj2.instance_variable)  # 输出:2

# 修改实例变量
obj1.instance_variable = 3
print(obj1.instance_variable)  # 输出:3

1.3.类变量与实例变量的区别

  1. 作用域:类变量在整个类范围内可见,而实例变量只在实例对象内部可见。
  2. 共享性:类变量由类的所有实例共享,而实例变量是每个实例对象独有的。
  3. 修改方式:类变量可以通过类名直接修改,也可以通过实例对象进行修改;实例变量只能通过实例对象进行修改。
  4. 默认值:类变量可以为所有实例设置默认值,而实例变量需要在每个实例对象创建时手动初始化。

示例代码:

class MyClass:
    class_variable = "Class Variable"

    def __init__(self, instance_variable):
        self.instance_variable = instance_variable

# 类变量与实例变量默认值
print(MyClass.class_variable)  # 输出:Class Variable
obj = MyClass("Instance Variable")
print(obj.instance_variable)  # 输出:Instance Variable

# 修改类变量
MyClass.class_variable = "New Class Variable"
print(MyClass.class_variable)  # 输出:New Class Variable

# 修改实例变量
obj.instance_variable = "New Instance Variable"
print(obj.instance_variable)  # 输出:New Instance Variable

了解类变量和实例变量的区别对于正确使用它们非常重要。类变量用于存储与整个类相关的共享数据,而实例变量用于存储每个实例对象的独特状态和属性。通过合理使用类变量和实例变量,可以更好地组织和管理类及其实例的数据。


2.使用property()函数定义属性

2.1.property()函数

使用property()函数定义一个属性。通过使用property()函数,可以将一个方法(getter或setter)绑定到一个属性上,从而实现对这个属性的访问和修改。

property()函数的语法如下:

property(fget=None, fset=None, fdel=None, doc=None)
  • fget: 用于获取属性值的方法(getter)
  • fset: 用于设置属性值的方法(setter)
  • fdel: 用于删除属性值的方法(deleter)
  • doc: 属性的文档字符串

其中,fget参数是必需的。如果只需要读取属性值,则只需要提供fget方法;如果还需要修改属性值,则需要同时提供fget和fset方法;如果还需要删除属性,则需要同时提供fget、fset和fdel方法。

示例:定义了一个名为 Rectangle 的类,实现了一个描述矩形的属性 size

class Rectangle:
    # 定义构造方法
    def __init__(self, width, height):
        self.width = width
        self.height = height

    # 定义 setsize() 函数
    def setsize(self, size):
        self.width, self.height = size

    # 定义 getsize() 函数
    def getsize(self):
        return self.width, self.height

    # 定义 delsize() 函数
    def delsize(self):
        self.width, self.height = 0, 0

    # 使用 property 定义属性
    size = property(getsize, setsize, delsize, "用于描述矩形大小的属性")


# 访问size属性的说明文档
print(Rectangle.size.__doc__)  # 输出: 用于描述矩形大小的属性

# 通过内置的help()函数查看 Rectangle.size的说明文档
help(Rectangle.size)

# 实例化 Rectangle 类
rect = Rectangle(4, 3)

# 访问rect的size属性
print(rect.size)  # 输出: (4,3)

# 对rect的size属性赋值
rect.size = 9, 7

# 访问rect的width、height 实例变量
print(rect.width)  # 输出: 9
print(rect.height)  # 输出: 7

# 删除rect的size属性
del rect.size

# 访问rect的width、height实例变量
print(rect.width)  # 输出: 0
print(rect.height)  # 输出: 0

以上示例代码定义了一个 Rectangle 类,使用 property() 函数定义了一个成员属性 size,用于描述矩形的大小。

具体来说,Rectangle 类的构造方法接收两个参数 width 和 height,分别表示矩形的宽度和高度。使用 self.width 和 self.height 实例变量来存储矩形的大小信息。同时,定义了 getsize()、setsize() 和 delsize() 函数,分别用于获取、设置和删除矩形的大小属性。

在使用 property() 函数时,需要传入 getsize、setsize 和 delsize 函数作为参数,这样就可以使用 size 成员属性来访问矩形的大小信息。在定义该属性时一共传入了4个参数,这意味着该属性可读、可写、可删除,也有说明文档。所以,该程序尝试对 Rectangle 对象的 size属性进行读、写、删除操作,其实这种读、写、删除操作分别被委托给 gctsize()、setsize()和delsize()方法来实现。

在创建 Rectangle 对象后,可以通过访问 size 属性来获取矩形的大小信息。同时,也可以将新的大小信息赋值给 size 属性,这样就会自动调用 setsize() 函数更新矩形大小。

最后,还演示了如何删除 size 属性,并通过访问 width 和 height 实例变量来验证 size 是否删除成功。

综上所述,property() 函数提供了一种方便的方式来定义成员属性,可以实现对实例变量的封装,使得程序更加安全可靠。同时,还能够为属性添加 get、set 和 delete 方法,方便地控制属性的读写操作。


2.2.@property 装饰器

还可使用 @property 装饰器来修饰方法,使之成为属性。

使用 @property 装饰器定义属性的语法如下:

class ClassName:
    def __init__(self):
        self._attribute = None
    
    @property
    def attribute(self):
        return self._attribute
    
    @attribute.setter
    def attribute(self, value):
        self._attribute = value

其中:

  • ClassName 是类名;
  • attribute 是属性名,可以根据实际需求进行命名;
  • _attribute 是具体存储属性值的变量名,通常约定为私有变量。

下面是一个使用 @property 装饰器定义属性的示例:

class Circle:
    def __init__(self, radius):
        self._radius = radius
    
    @property
    def radius(self):
        return self._radius
    
    @property
    def area(self):
        return 3.14 * self._radius ** 2
    
    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("半径必须大于0!")
        self._radius = value

# 创建 Circle 对象
c = Circle(5)

# 访问属性
print(c.radius)  # 输出:5
print(c.area)  # 输出:78.5

# 修改属性
c.radius = 7
print(c.radius)  # 输出:7
print(c.area)  # 输出:153.86

# 尝试修改半径为负数,将会抛出异常
c.radius = -1  # 抛出 ValueError 异常   raise ValueError("半径必须大于0!")

在上述示例中,Circle 类定义了一个半径属性 radius,并使用 @property 装饰器将其封装为只读属性。使用装饰器 @property 装饰的方法即为获取属性值的方法(getter),可以通过实例对象直接访问该属性。同时,Circle 类还定义了一个只读属性 area,用于计算圆的面积。

另外,Circle 类还使用 @radius.setter 装饰器定义了一个设置属性值的方法(setter),这样半径属性 radius 就变为读写属性了。方法确保半径值大于 0,若尝试设置为负数时,将会引发 ValueError 异常。

这样,在创建 Circle 对象后,就可以直接通过访问属性的方式获取半径和面积了,同时也可以通过属性的 setter 方法修改半径值。


3.封装

封装是面向对象编程中的一个重要概念,它指的是将数据和操作数据的方法封装在一个类中,对外部隐藏内部实现细节,只暴露必要的接口供其他对象使用。

封装有以下几个主要目的和好处:

  • 数据隐藏:封装可以隐藏类的内部数据和实现细节,只暴露必要的接口。这样可以避免外部对象直接修改类的内部数据,提高了代码的安全性和可靠性。

  • 信息隐藏:通过封装,可以将类的属性和方法进行适当的分类和组织,使得类的接口更加清晰和易于理解。外部对象只需要关注类的公共接口,而不需要了解类的具体实现细节,减少了对类的依赖性,提高了代码的可维护性和可扩展性。

  • 代码复用:封装可以将特定功能的实现封装为一个独立的类或对象,供其他对象重复使用。这样可以提高代码的复用性和可维护性,减少了重复编写相似功能的代码。

  • 过程隐藏:封装可以将一系列操作过程封装为一个简单的接口,降低了外部对象使用的复杂度。外部对象只需要调用封装好的接口,而不需要关心内部的具体操作过程,提高代码的简洁性和易用性。

在实现封装时,常用的技术包括:

  • 访问修饰符:通过使用访问修饰符(如 public、private、protected)来控制属性和方法的可访问性。private修饰的属性和方法只能在类内部访问,public修饰的属性和方法可以在类的外部访问。

  • 属性和方法的定义:将类的属性定义为私有的(private),通过公共的方法来访问和修改属性。这样可以控制对属性的访问和修改,确保数据的安全性。

  • 信息隐藏和接口设计:合理组织类的属性和方法,将相关的操作放在一起,提供清晰简洁的接口,避免暴露不必要的细节。

封装是面向对象编程的重要原则之一,它通过将数据和操作进行封装,提高了代码的安全性、可维护性和灵活性。合理的封装设计能够使代码更加清晰、易读、易用,提高开发效率和代码质量。

让我们以一个汽车类为例来说明封装的概念。

class Car:
    def __init__(self, make, model, year):
        self.__make = make  # 私有属性,只能在类内部访问
        self.__model = model
        self.__year = year
        self.__mileage = 0

    def get_make(self):
        return self.__make

    def get_model(self):
        return self.__model

    def get_year(self):
        return self.__year

    def get_mileage(self):
        return self.__mileage

    def drive(self, miles):
        self.__mileage += miles

    def set_year(self, year):
        self.__year = year

在这个例子中,我们定义了一个 Car 类,用于表示汽车对象。类的属性包括制造商(make)、型号(model)、年份(year)和行驶里程(mileage)。这些属性都是私有属性,使用双下划线(__)作为前缀,意味着它们只能在类的内部访问。

为了访问这些私有属性,我们提供了一系列的公共方法(也被称为访问器或getter方法),比如 get_make()、get_model()、get_year() 和 get_mileage()。这些方法允许外部对象获取私有属性的值,但不能直接修改它们。

除了访问器方法,我们还提供了一个 drive() 方法,用于模拟汽车的行驶操作。这个方法接收一个里程数作为参数,并将它加到 mileage 属性上。

此外,我们还提供了一个 set_year() 方法,用于修改汽车的年份。

通过封装,我们隐藏了类的内部实现细节,外部对象无法直接修改或访问私有属性,而是必须通过公共方法来进行操作。这样不仅保护了数据的安全性,还提供了清晰的接口和简化的操作方式。

下面是使用这个 Car 类的示例代码:

car = Car("Toyota", "Camry", 2020)
print(car.get_make())  # 输出:Toyota
print(car.get_model())  # 输出:Camry
print(car.get_year())  # 输出:2020
print(car.get_mileage())  # 输出:0

car.drive(100)
print(car.get_mileage())  # 输出:100

car.set_year(2022)
print(car.get_year())  # 输出:2022

在这个示例中,我们创建了一个名为 car 的 Car 对象,并使用公共方法获取和修改其属性值。通过封装,我们可以在外部使用简单明确的方法来操作对象,而无需关心类的内部实现细节。


4.额外拓展:下划线和双下划线

在变量命名中,下划线和双下划线具有不同的含义和作用。下面是它们的介绍:

  1. 下划线(_):
  • 在变量命名中,下划线通常用作单词之间的分隔符,以提高变量的可读性。例如:first_name、total_amount。
  • 在Python中,约定俗成地将以下划线开头的名称视为私有属性或方法,表示它们应该被视为私有的,只能在类的内部访问。例如:_private_var、_private_method()。
  • 在导入模块时,可以使用下划线 _ 来代替模块名中的长命名或不必要的前缀部分。例如:import module_name as mn。

  1. 双下划线(__):
  • 双下划线用于名称修饰符,提供了一种方式来避免命名冲突。
  • 当在类定义中使用双下划线作为前缀时,它会进行名称修饰,称为名称修饰符(name mangling)。这意味着在类内部,变量名会被修改,以避免与子类中的相同名称发生冲突。这是Python中的一种封装机制。
  • 例如,在一个类中定义了一个私有属性 __private_var,它会被重命名为 _ClassName__private_var。这样,即使子类中有一个同名的私有属性,也不会发生冲突。
  • 双下划线名称修饰符的主要目的是防止意外访问或修改类的内部属性。但请注意,它并不提供真正的强制执行封装,因为仍然可以通过特定方式访问和修改这些属性。

需要注意的是,在变量命名中使用下划线或双下划线只是一种约定,没有硬性规定。对于普通变量,我们可以根据个人或团队的偏好选择使用或不使用下划线,以及单下划线还是双下划线。对于类的私有属性,使用双下划线作为前缀是一种常见的命名约定。

20-继承和多态

面向对象的三大特征(封装,继承和多态),我们前面学习了封装的各种知识,本节我们学习继承和多态。

1.继承

继承是面向对象的三大特征之一,也是实现软件复用的重要手段。Python 的继承是多继承机制,即一个子类可以同时有多个直接父类。

1.1.继承语法

它允许一个类(称为子类)继承另一个类(称为父类)的属性和方法。子类可以通过继承获得父类的特性,并且可以在此基础上新增或修改自己的属性和方法。

继承的语法:

class 父类名:
    # 父类的属性和方法

class 子类名(父类名):
    # 子类的属性和方法

子类名即为子类的名称,父类名则是子类继承的父类的名称。子类通过圆括号中的父类名来指定它要继承的类。

当我们使用继承时,子类会自动拥有父类的属性和方法。下面我们详细说明继承的一些关键点:

  1. 子类继承父类的属性和方法:

    • 子类可以直接访问并使用父类的非私有属性和方法。
    • 子类也可以新增自己的属性和方法。
  2. 方法重写(Override):

    • 如果子类中定义了与父类相同名称的方法,则子类将覆盖(重写)父类的方法。
    • 子类可以在重写的方法中使用super()关键字调用父类的方法。
  3. 访问控制:

    • 子类可以访问并使用父类中的公有属性和方法。
    • 子类无法直接访问父类的私有属性和方法。
  4. 多层继承:

    • 一个类可以同时作为多个子类的父类。
    • 子类可以继承自己的父类以及父类的父类,形成多层继承结构。
  5. 继承链:

    • 继承链是通过继承建立的类之间的关系。
    • 子类可以继承所有祖先类的属性和方法。
    • 如果多个类之间存在继承关系,则可以通过继承链追溯到最顶层的父类。

这些是继承的基本概念和语法,帮助我们实现代码的重用和组织。通过合理地设计继承关系,我们可以提高代码的可维护性和扩展性。

需要注意的是,过度使用继承可能会导致代码的复杂性增加,因为子类会继承父类的所有属性和方法,包括可能并不需要的部分。在设计中,应当遵循单一职责原则,尽量将类的功能划分清晰,以便更好地利用继承来组织代码。

示例:

class Animal:
    def __init__(self, name):
        self.name = name

    def eat(self):
        print(f"{self.name} is eating.")

    def sleep(self):
        print(f"{self.name} is sleeping.")


class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def bark(self):
        print(f"{self.name} is barking.")

    def fetch(self):
        print(f"{self.name} is fetching.")

在这个例子中,我们定义了一个Animal父类和一个Dog子类。Animal类具有name属性和 eat()sleep() 方法,而Dog类通过继承Animal类,在其基础上新增了 breed 属性和 bark()fetch() 方法。

注意到在Dog类的定义中,我们使用了class 子类名(父类名)的语法来指定它继承自Animal父类。在Dog类的构造函数中,我们通过 super().__init__(name) 调用了父类Animal的构造函数,并传递了name参数。

接下来,我们可以创建对象并调用相应的方法:

animal = Animal("Animal")
animal.eat()  # 输出:"Animal is eating."
animal.sleep()  # 输出:"Animal is sleeping."

dog = Dog("Buddy", "Golden Retriever")
dog.eat()  # 输出:"Buddy is eating."
dog.sleep()  # 输出:"Buddy is sleeping."
dog.bark()  # 输出:"Buddy is barking."
dog.fetch()  # 输出:"Buddy is fetching."

在这个例子中,我们创建了一个Animal对象和一个Dog对象,并分别调用了它们的相应方法。注意到Dog对象不仅继承了Animal类的属性和方法,还具有自己新增的属性和方法。

在实际编程中,继承可以帮助我们避免重复编写相似的代码,提高代码的可重用性和可维护性。它也是实现多态性的基础,多态性允许我们使用一个父类类型的变量来引用子类对象,从而实现对不同子类对象的统一操作。

需要注意的是,继承应该满足"是什么"的关系,即子类应该是父类的一种特殊类型。如果两个类之间没有明显的"is-a"关系,那么可能不适合使用继承。


1.2.多继承

多继承是指一个子类可以同时继承自多个父类。在Python中,实现多继承非常简单,只需要在定义子类时,将多个父类以逗号分隔放在括号内即可。

下面我们来详细介绍多继承的一些关键点:

  1. 定义多继承类:

    class 子类名(父类1, 父类2, ...):
        # 子类的属性和方法
    
  2. 方法解析顺序(MRO):

    • MRO 是指确定多继承类中方法调用的顺序。
    • Python 使用 C3 线性化算法来计算 MRO。
    • 可以通过 类名.mro 查看类的方法解析顺序。
  3. 多继承的冲突解决:

    • 当多个父类中出现同名方法时,子类会继承第一个被找到的同名方法。
    • 如果需要调用其他父类中的同名方法,可以使用 super() 关键字。
  4. 钻石继承问题:

    • 钻石继承是指多继承中存在一个公共父类,而其他父类又继承自这个公共父类。
    • 为了避免钻石继承带来的问题,Python 使用广度优先搜索(DFS)的方式来计算方法解析顺序。
    • 如果有多个父类出现在同一层级,且继承自同一个公共父类,则按照父类在子类定义中的顺序进行方法解析。
  5. 使用多继承的注意事项:

    • 多继承可以使代码更灵活,但也增加了复杂性。在使用多继承时,应当谨慎设计类的关系,避免出现过于复杂的继承链。
    • 理解和规范地使用 super() 关键字可以帮助解决多继承中的方法调用问题。
    • 在设计类时,尽量遵循单一职责原则和组合优于继承原则,以减少多继承带来的潜在问题。

示例1:

class A:
    def method(self):
        print("Method from A")

class B:
    def method(self):
        print("Method from B")

class C(A, B):
    pass

c = C()
c.method()  # 输出:"Method from A"

在这个例子中,我们定义了三个类:A、B 和 C。A 和 B 类都有一个同名的 method() 方法。C 类继承自 A 和 B 类,但并未定义自己的 method() 方法。因此,在创建 C 类的实例并调用 method() 方法时,由于 A 在 B 之前继承,所以输出为 “Method from A”。

示例2:

假设我们有两个父类,一个是Animal,另一个是Flying,然后我们定义一个子类Bird来继承这两个父类。那么可以这样写:

class Animal:
    def eat(self):
        print("Animal is eating.")

class Flying:
    def fly(self):
        print("Flying in the sky.")

class Bird(Animal, Flying):
    def sing(self):
        print("Bird is singing.")

bird = Bird()
bird.eat()  # 输出:"Animal is eating."
bird.fly()  # 输出:"Flying in the sky."
bird.sing()  # 输出:"Bird is singing."

在上面的例子中,Bird 类同时继承了 Animal 和 Flying 两个父类,并且定义了自己的sing()方法。

通过创建 Bird 类的实例 bird ,我们可以调用该实例继承的父类方法和子类自己的方法。


1.3.重写父类方法

通过重写父类方法可以实现子类特有的行为。

重写父类方法可以使用与父类同名的方法,在子类中重新定义该方法,并覆盖或修改父类中原有的实现逻辑。在Python中,使用super()函数调用父类方法。

下面是一个简单的例子,演示重写父类方法的实现过程:

# 父类
class Animal:
    def say(self):
        print("I'm an animal.")

# 子类
class Cat(Animal):
    def say(self):
        print("I'm a cat.")
        super().say() # 调用父类方法

# 实例化对象
animal = Animal()
cat = Cat()

# 调用方法
animal.say() # 输出 "I'm an animal."
cat.say() # 输出 "I'm a cat." 和 "I'm an animal."

在上面的例子中,Animal是一个父类,Cat是一个子类,它重写了父类中的say()方法。当我们创建一个Animal对象并调用它的say()方法时,输出的结果为"I’m an animal.",这是因为该方法是父类中定义的。当我们创建一个Cat对象并调用它的say()方法时,输出的结果为"I’m a cat.“和"I’m an animal.”,这是因为在子类中重写的方法中使用了super()函数调用了父类的say()方法。

需要注意的是,Python中没有访问修饰符的概念,因此无需考虑访问修饰符的问题。此外,Python中也没有方法签名的概念,因此可以在子类中对参数列表进行修改,但要保证参数个数和类型与父类中的方法相同。


1.4.使用super函数调用父类的构造方法

使用super()函数可以调用父类的构造方法。调用父类的构造方法可以让子类继承父类的属性,并进行一些额外的初始化操作。

使用super()函数调用父类的构造方法的语法如下:

super().__init__(args)

其中,init()方法是Python中的构造方法。

下面是一个简单的例子,演示使用super()函数调用父类的构造方法的实现过程:

# 父类
class Animal:
    def __init__(self, name):
        self.name = name

# 子类
class Cat(Animal):
    def __init__(self, name, color):
        super().__init__(name) # 调用父类构造方法
        self.color = color

# 实例化对象
cat = Cat("Tom", "black")

# 输出结果
print(cat.name) # 输出 "Tom"
print(cat.color) # 输出 "black"

在上面的例子中,Animal是一个父类,Cat是一个子类,它继承了父类的__init__()方法,并在其基础上进行了扩展。当实例化一个Cat对象时,我们需要同时传入name和color两个参数。在子类中的__init__()方法中,我们通过调用super()函数来调用父类的构造方法,并将name作为参数传入。这样,子类就能继承父类的name属性,并添加自己的color属性。

需要注意的是,如果子类没有定义自己的__init__()方法,则会默认继承父类的__init__()方法。如果子类定义了自己的__init__()方法,则需要在其中使用super()函数调用父类的构造方法,以便子类能够继承父类的属性和方法。


1.5.多层继承

多层继承是指在类的继承关系中,某个类不仅继承了一个父类,还同时继承了该父类的某个子类。在多层继承中,子类可以访问其所继承的所有祖先类中的属性和方法,包括祖先类的祖先类,以此类推。

以下是一个简单的多层继承的例子:

# 祖先类A
class A:
    def method_A(self):
        print("This is method A")

# 父类B,继承自A
class B(A):
    def method_B(self):
        print("This is method B")

# 子类C,继承自B
class C(B):
    def method_C(self):
        print("This is method C")

# 实例化对象
c = C()
c.method_A() # 输出 "This is method A"
c.method_B() # 输出 "This is method B"
c.method_C() # 输出 "This is method C"

在上述例子中,类A是祖先类,类B继承自A,类C继承自B。这样,类C就可以访问类A、B中的所有属性和方法。通过实例化对象c并分别调用method_A、method_B、method_C方法,可以看到输出结果分别是"This is method A"、“This is method B”、“This is method C”,说明多层继承关系被正确建立。

需要注意的是,在实际编程中,过多的继承可能会使代码变得复杂难懂,因为类之间的关系变得更加复杂。因此,应该尽量避免多层继承,而是使用组合等其他方式来实现类之间的关联。


1.6.子类构造函数

子类构造函数是指子类中定义的用于初始化子类对象的构造函数。子类构造函数可以通过调用父类构造函数来初始化继承自父类的属性,也可以定义独有的属性和方法。

子类构造函数的定义方式为在子类中定义一个名为__init__的方法,并在该方法中使用super()调用父类的构造方法,然后再定义子类独有的属性和方法。在子类构造函数中,可以通过self访问子类的属性和方法,也可以通过super()访问父类的属性和方法。

以下是一个简单的子类构造函数的例子:

# 父类
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 子类
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)
        self.breed = breed

    def bark(self):
        print("Woof!")

# 实例化对象
dog = Dog("Buddy", 2, "Golden Retriever")
print(dog.name)    # 输出 "Buddy"
print(dog.age)     # 输出 2
print(dog.breed)   # 输出 "Golden Retriever"
dog.bark()         # 输出 "Woof!"

在上述例子中,类Animal是父类,其构造函数__init__用于初始化动物的名称和年龄属性;类Dog是子类,它继承自类Animal,并在其构造函数中调用了父类的构造函数,用于初始化继承自父类的名称和年龄属性,同时定义了自己独有的品种属性。然后,定义了一个bark方法用于输出汪汪叫声。

实例化对象dog时,我们传入了三个参数,分别是名字、年龄和品种。通过访问对象的属性和方法,可以看到输出结果分别是"Buddy"、2、“Golden Retriever"和"Woof!”,说明子类构造函数被正确地定义和初始化。

需要注意的是,在子类构造函数中使用super()调用父类构造函数时,应该确保调用的是正确的父类构造函数。如果子类继承了多个父类,需要明确指定调用哪个父类的构造函数。


2.Python动态性

我们知道 Python 是动态语言,动态语言和静态语言最大的不同就是函数和类的定义, 在动态语言中函数和类的定义不是编译时产生的,而是在运行时动态创建的, 所以我们就没必要先定义好一个类,我们可以在代码的任何地方根据需要来随时的定义类,Python 的 type()metaclass 可以帮我们动态创建一个类。


2.1.使用 type 创建类

type 是 Python 内置的函数对象类,这个类很特殊,我们可以把他当做函数来看待, 我们查看 type 类的源码,发现他的构造函数的声明为: type(object) -> the object's type, type(name, bases, dict) -> a new type 。也就是说当给它传入一个参数时,它返回这个参数所属的类型; 当传入三个参数时,它返回一个新的类型(大家可以认为是一个新的类),这样我们就可以用这个类来创建对象了。

要使用 type 创建一个类,我们需要对 type 类构建的对象依次传入3个参数: class的名称,继承的父类集合(可以继承多个父类,是个 tuple类型),class 的方法名称与函数绑定。比如下面的例子我们把函数 func 绑定到方法名 Human 上。

def func(self, name):  # 在类之前定义要绑定的函数
    print("I'm " + name)

Human = type('Human', (object,), {"talk": func})  # 创建 Human 类,该类继承 object,有一个talk函数
ruhua = Human()
ruhua.talk("ruhua")

通过 type 函数创建的类和直接写 class 定义的类是完全一样的,实际上 Python 解释器遇到 class 定义时,仅仅是扫描一下class 定义的语法,然后调用 type 函数创建出 class。 我们在使用 type 创建类时,完全可以后期动态绑定函数和属性,要注意给类动态绑定和给类的对象动态绑定的区别。

def func(self):  # 在类之前定义要绑定的函数
    print(u"我是类绑定的")

def func2():
    print(u"我是某个对象绑定的")

Human = type('Human', (object,), {})
Human.func = func
Human.data = "我是类的属性"

ruhua = Human()
ruhua.func2 = func2
ruhua.data2 = "我是某个对象的属性"

ruhua.func()        # 正确
print(ruhua.data)   # 正确
ruhua.func2()       # 正确
print(ruhua.data2)  # 正确

zhaoritian = Human()
zhaoritian.func()        # 正确
print(zhaoritian.data)   # 正确
zhaoritian.func2()       # 错误
print(zhaoritian.data2)  # 错误

2.2.使用 metaclass 创建类

metaclass 是元类,我们可以使用 metaclass 创建出类,然后根据这个类创建对象,这就和用 type 实现一样的效果。

除了使用 type 动态创建类以外,还可以使用 自己定义的元类(metaclass)来创建类, 自己定义一个元类,首先该类要继承 type 类,我们还需要定义一个 __new__ 函数, __new__ 函数接收到的参数依次是:当前准备创建的类的对象,类的名字,类继承的父类集合,类的函数集合。 我们在使用 metaclass 创建类,按照默认习惯,metaclass 的类名总是以 Metaclass 结尾,以便清楚地表示这是一个 metaclass。

class MyObjectMetaclass(type):
    def __new__(cls, name, bases, funcs):
        def setdata(self, value):
            self.data = value
        funcs['setdata'] = setdata
        return type.__new__(cls, name, bases, funcs)

class MyObject(object, metaclass=MyObjectMetaclass):
    pass

myobject = MyObject()
myobject.setdata("hello")
print(myobject.data)

在常规代码编写中,我们很少会用到 type 和 metaclass 去创建类,但是我们还是需要了解知道这些知识。


3.多态

当子类和父类存在同名的成员函数时,通过某个子类的对象调用该函数,总是会调用相应子类的函数,该行为称为多态。

class Animal(object):
    def talk(self):
        print("Animal talking")

class Cat(Animal):
    def talk(self):
        print("Cat talking")

class Bird(Animal):
    def talk(self):
        print("Bird talking")

def func(animal):
    animal.talk()

onecat = Cat()
onebird = Bird()

func(onecat)     # "Cat talking"
func(onebird)    # "Bird talking"

Python中的多态和其它静态语言(比如 c++,java等)中多态的实现方法是不一样的, c++ 中是通过基类的指针或引用来实现运行时多态。由于 Python 是动态语言,并不需要提前给一个变量定义好类型,在 Python 中其实我们就是直接通过子类的对象调用子类中重新定义的该函数而已(这是理所当然的正常语法)。

21-枚举类

1.介绍枚举

当使用枚举类型时,可以从 enum 模块导入 Enum 类。然后,定义一个继承自 Enum 类的新类,该类将成为我们的枚举类型。在这个类中,我们可以定义枚举类型的成员。

以下是一个简单的定义枚举类型并引用其成员的例子:

from enum import Enum

# 定义枚举类型
class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
    THURSDAY = 4
    FRIDAY = 5
    SATURDAY = 6
    SUNDAY = 7

# 引用枚举类型的成员
print(Weekday.MONDAY)    # 输出 "Weekday.MONDAY"
print(Weekday.MONDAY.value)    # 输出 1

在上述例子中,我们定义了一个Weekday枚举类型,并依次定义了七个枚举类型的成员。每个成员都是一个唯一的对象,它们的名称和值分别为"MONDAY"、1;“TUESDAY”、2……“SUNDAY”、7。在引用枚举类型的成员时,可以通过名称或值来引用。例如,Weekday.MONDAY表示"MONDAY"这个成员对象,而Weekday.MONDAY.value则表示该成员的值1。

需要注意的是,枚举类型的成员通常用全大写字母命名,并且不能被修改。如果尝试修改枚举类型的成员,会抛出AttributeError异常。

枚举类型在实际的编程中有很多用处,例如表示星期几、颜色、方向等固定的数目和范围的值。通过使用枚举类型,可以让代码更加清晰和易于维护。


*以下是一些关于枚举的详细信息:

  1. 枚举成员的值:枚举成员可以具有不同的值,可以是整数、浮点数、字符串等。如果没有给定值,则默认从 1 开始递增。
class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3
  1. 访问枚举成员:可以通过成员名或值来引用枚举成员。
print(Weekday.MONDAY)    # 输出 "Weekday.MONDAY"
print(Weekday.MONDAY.value)    # 输出 1
  1. 迭代枚举成员:可以使用循环遍历枚举类型的所有成员。
for day in Weekday:
    print(day)
# 输出:
# Weekday.MONDAY
# Weekday.TUESDAY
# Weekday.WEDNESDAY
  1. 比较枚举成员:可以使用相等运算符(==)或身份运算符(is)进行比较。
print(Weekday.MONDAY == Weekday.TUESDAY)    # 输出 False
print(Weekday.MONDAY is Weekday.MONDAY)     # 输出 True
  1. 枚举成员的属性和方法:可以为枚举成员定义属性和方法。
class Weekday(Enum):
    MONDAY = 1
    TUESDAY = 2
    WEDNESDAY = 3

    def is_weekend(self):
        return self in (Weekday.SATURDAY, Weekday.SUNDAY)

print(Weekday.SATURDAY.is_weekend())    # 输出 True

需要注意的是,枚举类型的成员是唯一的,即使它们具有相同的值。枚举类型还提供了一些实用的方法,例如 Enum.name 可以返回成员的名称, Enum.members 可以获取所有成员的字典。

使用枚举类型可以帮助我们在代码中更好地表示固定的、有限的值集合,提高代码可读性和可维护性。它还提供了一些便捷的功能来处理枚举成员。

2.枚举的构造器

枚举类型的构造器是在枚举成员之间定义的,它可以接受参数并将其传递给每个枚举成员的构造函数。通过这种方式,我们可以为枚举成员提供不同的构造参数。

以下是关于枚举构造器的详细介绍和具体示例:

  1. 枚举构造器的定义:枚举构造器是在枚举成员定义之前用 init 方法定义的。

    from enum import Enum
    
    class Color(Enum):
        def __init__(self, rgb_value, name):
            self.rgb_value = rgb_value
            self.name = name
    
        RED = (255, 0, 0, 'Red')
        GREEN = (0, 255, 0, 'Green')
        BLUE = (0, 0, 255, 'Blue')
    

    在上述例子中,我们定义了一个 Color 枚举类型,并为每个枚举成员定义了构造器。构造器接受两个参数 rgb_value 和 name,并将其分别赋值给每个枚举成员的实例变量。


  1. 枚举构造器的使用:在使用枚举构造器时,每个枚举成员都会调用构造器,并将构造器的参数传递给它们的构造函数。

    print(Color.RED.rgb_value)    # 输出 (255, 0, 0)
    print(Color.GREEN.name)       # 输出 "Green"
    

    在上述例子中,我们可以通过访问枚举成员的实例变量来获取它们的构造器参数。

需要注意的是,枚举构造器不会创建新的枚举成员实例。它只是在定义枚举成员时提供了一种机制来传递参数。

使用枚举构造器可以为每个枚举成员提供不同的构造参数,使得枚举类型更灵活和可定制。这在某些场景下非常有用,例如,当我们希望为每个枚举成员指定不同的属性或行为时。

22-异常处理

异常机制是现代编程语言中的一个重要特性,它可以帮助程序更好地处理错误和异常情况,使得代码更加健壮、可靠。Python 的异常机制主要通过 try、except、else、finally 和 raise 这五个关键字来实现,下面我将对这些关键字逐一进行详细描述:

  1. try 块:

    • 在 try 块中包含了可能会引发异常的代码。当程序执行到 try 块时,Python 会监视其中的代码,一旦发生异常,就会跳出当前的 try 块并进入相应的 except 块进行异常处理。
  2. except 块:

    • 当在 try 块中发生异常时,Python 会寻找与异常类型匹配的 except 块,并执行其中的代码。一个 except 块通常包含对应的异常类型和异常处理的代码逻辑。
  3. else 块:

    • 在多个 except 块之后可以添加一个 else 块,它用于处理在 try 块中没有发生异常时需要执行的代码。如果在 try 块中没有触发任何异常,那么 else 块中的代码将被执行。
  4. finally 块:

    • 不管是否发生异常,finally 块中的代码都会被执行。通常用于回收在 try 块中打开的物理资源,如文件或网络连接,以确保资源的正确释放。
  5. raise 语句:

    • raise 用于手动引发一个异常,可以单独作为语句使用,也可以与 except 块配合使用。它可以引发预定义的内建异常,也可以引发用户自定义的异常对象。

综合来看,通过这些关键字的组合使用,Python 的异常机制能够让程序员更加灵活地处理各种异常情况,有效地分离正常业务代码和异常处理代码,提高代码的可读性和健壮性。同时,异常机制还能够保证资源的及时释放,避免出现资源泄露等问题。因此,合理利用异常机制可以使程序更加稳定、可靠。


1.异常处理机制

Python 的异常处理机制可以让程序具有极好的容错性,让程序更加健壮。当程序运行出现意外情况时,系统会自动生成一个 Error 对象来通知程序,从而实现将“业务实现代码”和“错误处理代码”分离,提供更好的可读性。

1.1.使用 try…except 捕获异常

在Python中,异常处理是一种重要的错误处理机制。当我们的代码可能会遇到一些无法预料的错误时,我们可以使用try…except语句来捕获这些异常并进行处理。

try语句块包含可能引发异常的代码。如果在try语句块中的代码执行过程中发生了异常,那么程序将停止执行try语句块中的剩余代码,并跳转到except语句块中执行。

except语句块用于捕获和处理异常。你可以在except语句块中指定要捕获的异常类型,也可以不指定任何异常类型,这样它将捕获所有的异常。

try:
    num = int(input("请输入一个整数:"))
    result = 10 / num
    print("结果是:", result)
except ZeroDivisionError:
    print("除数不能为0!")
except ValueError:
    print("输入的不是一个整数!")

测试输出:

请输入一个整数:0
除数不能为0!


请输入一个整数:2.5
输入的不是一个整数!


请输入一个整数:5
结果是: 2.0

在这个例子中,我们首先尝试将用户输入的字符串转换为整数,并计算10除以这个整数的结果。如果用户输入的是0,那么在计算过程中会引发ZeroDivisionError异常;如果用户输入的不是一个整数,那么在转换过程中会引发ValueError异常。我们使用try…except语句捕获了这两种异常,并分别输出了相应的错误提示信息。

注意事项:

  • 在使用try…except语句时,需要确保try语句块中的代码是可能出现异常的代码。

  • except语句块可以有多个,每个except语句块用于捕获一种特定类型的异常。如果没有指定异常类型,那么它将捕获所有的异常。

  • 可以使用finally语句块来执行无论是否发生异常都需要执行的代码。


1.2.异常类的继承体系

当Python解释器接收到异常对象时,它会按照以下顺序寻找对应的except块:

  • 首先,检查异常对象是否是当前except块后的异常类或其子类的实例。如果是,则调用该except块来处理该异常。

  • 如果异常对象不是当前except块后的异常类或其子类的实例,那么继续检查下一个except块。

  • 如果没有找到匹配的except块,那么程序将终止并显示一个错误消息。

异常类的继承体系是指在面向对象编程语言中,异常类之间的继承关系。在大多数编程语言中,异常类都是通过继承来建立层次化的结构,使得不同类型的异常可以被捕获和处理。以Python为例,Python中的异常类也是通过继承来组织的。

在Python中,所有的内置异常类都是从内置异常类 BaseException 继承而来的。BaseException 是所有内建异常的基类,在它下面有一系列不同类型的异常类,如 Exception、ArithmeticError、LookupError 等等。这些异常类又分别派生出更具体的异常类,用于表示特定的错误或异常情况。

以下是Python中一些常见异常类的继承关系:

  • BaseException
    • Exception
      • StopIteration
      • ZeroDivisionError
    • ArithmeticError
      • OverflowError
      • FloatingPointError
    • LookupError
      • IndexError
      • KeyError

上述异常类的继承关系中,BaseException 是所有异常类的根类,而 Exception 是大部分常规异常类的基类。其他的异常类则依次继承自 Exception 或其子类,构成了一个继承体系。

通过异常类的继承体系,我们可以很方便地进行异常的捕获和处理。例如,我们可以使用 except 块来捕获特定类型的异常,也可以通过捕获基类异常来捕获其所有子类异常。这种层级化的结构使得异常的处理更加灵活和精细化。

举个简单的例子,在Python中可以这样处理异常:

try:
    a = 10 / 0  # 会引发 ZeroDivisionError
except ZeroDivisionError as e:
    print("除零错误:", e)
except ArithmeticError as e:
    print("算术错误:", e)
except Exception as e:
    print("其他异常:", e)

在这个例子中,ZeroDivisionError 是 ArithmeticError 的子类,而 ArithmeticError 又是 Exception 的子类,因此当发生除零错误时,会按照异常类的继承体系逐级匹配,直到找到合适的异常处理块。

通过继承体系,我们可以更好地组织和分类异常,提高程序的可读性和可维护性。


1.3.多异常捕获

多异常捕获指的是在一个 try 块中捕获多种不同类型的异常,从而能够根据不同的异常类型做出相应的处理。

在Python中,可以使用多个 except 块来实现多异常捕获。每个 except 块用于捕获特定类型的异常,这样就能够针对不同类型的异常做出不同的处理。

下面是一个简单的多异常捕获的示例:

try:
    x = int(input("请输入一个整数: "))
    result = 10 / x
    print("计算结果:", result)
except ValueError:
    print("数值输入错误,请输入一个整数")
except ZeroDivisionError:
    print("除零错误,请输入非零的数")
except Exception as e:
    print("发生未知错误:", e)

在这个示例中,我们使用了三个 except 块来捕获可能发生的异常:

  • 第一个 except 块捕获 ValueError,用于处理用户输入不是整数的情况。

  • 第二个 except 块捕获 ZeroDivisionError,用于处理除零的情况。

  • 第三个 except 块捕获所有其他类型的异常,它使用了 Exception 类作为通配符,用于处理除前两种异常外的所有其他异常。

通过这种方式,我们可以根据不同的异常类型给出对应的提示或处理,使程序更加健壮和友好。

另外,还可以使用元组来将多个异常类型放在一起,进行统一处理,例如:

try:
    # 可能引发多种异常的代码
except (ValueError, TypeError):
    # 对于 ValueError 或 TypeError,进行统一处理
except (ZeroDivisionError, ArithmeticError):
    # 对于 ZeroDivisionError 或 ArithmeticError,进行统一处理

总之,多异常捕获使得我们能够更加灵活地处理程序中可能出现的不同类型的异常,提高了程序的健壮性和可靠性。


1.4.访问异常信息

异常对象通常具有一些标准的属性和方法,可以用来获取关于异常的信息。下面是对这些属性和方法的整理,并举一个简单的例子:

  1. args 属性:返回异常的错误编号和描述字符串

    • args 属性返回一个包含错误信息的元组,其中可能包括错误编号和描述字符串等信息。
  2. errno 属性:返回异常的错误编号

    • 一些特定的异常对象可能会包含错误编号,通过该属性可以获取错误的编号信息。这个属性并非所有异常都会有。
  3. strerror 属性:返回异常的描述字符串

    • strerror 属性返回异常的描述字符串,即对异常的简要描述信息。
  4. with_traceback() 方法:处理异常的传播轨迹信息

    • with_traceback() 方法用于处理异常的传播轨迹信息,可以将异常的堆栈信息输出或者进行其他自定义的处理。
      下面是一个简单的例子,演示了如何使用这些属性和方法:
import sys

try:
    x = 1 / 0
except ZeroDivisionError as e:
    print("捕获到除零异常:", e)
    print("异常信息:", e.args)  # 获取异常的错误信息

    e_with_traceback = sys.exc_info()[2]  # 获取异常的传播轨迹信息
    e.with_traceback(e_with_traceback)  # 处理异常的传播轨迹信息
捕获到除零异常: division by zero
异常信息: ('division by zero',)

在这个例子中,我们捕获了一个除零异常(ZeroDivisionError),并使用了该异常对象的属性和方法来获取和处理异常的相关信息。


def foo():
    try:
        fis = open("a.txt")
    except Exception as e:
        # 访问异常的错误编号和详细信息
        print(e.args)
        # 访问异常的错误编号
        if hasattr(e, 'errno'):
            print(e.errno)
        # 访问异常的详细信息
        print(e.strerror)

根据上述两个例子我们可以发现,不同类型的异常可能会提供不同的属性和方法,这取决于异常类的具体实现。

通过使用这些属性和方法,我们可以更加灵活和细致地处理异常,从而实现更加健壮的程序设计。


1.5.else块

try 语句可以包含一个可选的 else 块。else 块会在 try 块中没有发生任何异常时执行,它通常用于包含在没有发生异常时需要执行的代码。当然,如果异常发生了,else 块内的代码就不会被执行。

下面是一个简单的例子来演示 try...except...else 的用法:

def divide(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("除数不能为0!")
    else:
        print("结果是:", result)


# 测试
divide(6, 2)  # 输出:结果是: 3.0
divide(4, 0)  # 输出:除数不能为0!

在这个例子中,divide 函数尝试将两个参数相除。如果没有发生异常,else 块会打印出计算的结果;如果发生了除零异常,那么异常会被捕获,但 else 块内的代码不会执行。

需要注意的是,else 块是可选的,而且只有在没有异常发生时才会执行。如果异常被捕获,else 块内的代码将被跳过。

综上所述,else 块用于指定在没有发生异常时要执行的代码,它使得我们能够更加清晰地区分出异常处理代码和正常逻辑代码。


1.6.使用 finally 回收资源

有些时候,程序在 try 块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等)这些物理资源都必须被显式回收。

try 语句还可以包含一个可选的 finally 块,finally 块内的代码会在无论是否发生异常都会被执行,通常用于进行一些清理操作,比如释放资源。

下面是一个例子来演示 try...except...finally 的用法:

def read_file(file_name):
    try:
        f = open(file_name, 'r')
        content = f.read()
    except FileNotFoundError:
        print(f"文件 '{file_name}' 不存在")
    else:
        print("文件内容:", content)
    finally:
        if 'f' in locals():
            f.close()
            print(f"已关闭文件 '{file_name}'")


# 测试
read_file("example.txt")

在这个例子中,read_file 函数尝试打开并读取指定的文件。如果文件不存在,则会捕获 FileNotFoundError 异常并打印相关信息;如果成功打开文件并读取内容,会输出文件的内容;最后无论是否发生异常,finally 块会确保打开的文件被关闭。

需要注意的是,finally 块内的代码在 try 块结束后(包括正常结束、发生异常和异常被捕获等情况下)都会被执行。所以无论是否发生异常,我们都可以在 finally 块中执行一些必要的清理操作,比如关闭文件或者释放其他资源。

需要注意的是,在 finally 块中我们加入了一个条件 if 'f' in locals(): 会在当前局部作用域中所有变量的字典内查找是否有我们打的文件变量名,如果有,则关闭文件并且打印相关信息;如果没有加这个条件判断,当文件没有正常打开时尝试关闭,会触发报错。

综上所述,finally 块用于指定在 try 块结束后必须执行的清理代码,确保程序能够正确地释放资源。


1.7.异常处理嵌套

异常处理嵌套是指在一个 except 块内部再次使用 try...except 结构来捕获另一个可能发生的异常。这种嵌套结构允许我们更细致地处理不同类型的异常,并且可以在内部的 except 块中采取适当的措施来应对特定的异常情况。

以下是一个示例,演示了异常处理嵌套的用法:

def divide_numbers(x, y):
    try:
        result = x / y
    except ZeroDivisionError:
        print("除数不能为零!")
    except Exception as e:
        try:
            # 在这个嵌套的 except 块中再次尝试除法运算
            result = x / int(y)  # 尝试将 y 转换为整数
            print(f"嵌套-结果为: {result}")
        except ValueError:
            print("除数必须为数字")
        except ZeroDivisionError:
            print("内部除数不能为零!")
        except Exception as inner_e:
            print(f"发生了其他异常:{inner_e}")
    else:
        print(f"结果为: {result}")


# 测试
divide_numbers(10, '2')

上述例子调用函数 divide_numbers(10, '2') 的参数 y 值为 ‘2’ ,没办法参与运算,会抛出类型不正确的异常,但是我们在第一层的 except 捕获时没有捕获类型异常,其会进入 except Exception 异常内再次参与处理,经过运行后输出结果为:嵌套-结果为: 5.0

外部的 except Exception as e 块内再次使用了 try...except 结构来处理可能发生的异常。这种嵌套结构可以帮助我们更准确地识别和处理各种异常情况,从而编写更健壮的代码。

需要注意的是,异常处理嵌套应该谨慎使用,避免过于复杂的嵌套结构,以免降低代码的可读性和可维护性。在实际应用中,可以根据具体情况选择是否需要使用异常处理嵌套结构,一般建议非必要情况下不超过两层嵌套。


2.使用raise引发异常

当程序出现错误时,系统会自动引发异常。除此之外,Python 也允许程序自行引发异常,自行引发异常使用 raise 语句来完成。

2.1.引发异常

raise 是在 Python 中用来手动引发异常的关键字。通过 raise 关键字,我们可以在代码中显式地引发特定类型的异常,从而控制程序的异常流程。

下面是一个简单的例子,演示了如何使用 raise 来引发一个自定义的异常:

def divide_numbers(x, y):
    if y == 0:
        raise ZeroDivisionError("除数不能为零!")
    result = x / y
    return result


# 测试
try:
    print(divide_numbers(10, 0))
except ZeroDivisionError as e:
    print("捕获到异常:", e)

在这个例子中,我们定义了一个 divide_numbers 函数,如果除数 y 为零,就会使用 raise 引发一个 ZeroDivisionError 异常,并传递异常消息 “除数不能为零!”。然后在调用函数时,在 try...except 结构中捕获并处理这个异常。


2.2.自定义异常类

除了引发标准库中定义的异常,我们也可以自定义异常类,并通过 raise 关键字来引发自定义的异常。例如:

class MyCustomError(Exception):
    pass

def some_function():
    # ...
    if some_condition:
        raise MyCustomError("发生了自定义异常!")
    # ...


# 测试
try:
    some_function()
except MyCustomError as e:
    print("捕获到自定义异常:", e)

通过这种方式,我们可以根据实际需求自定义各种类型的异常,并在程序中使用 raise 关键字来引发这些异常。这样可以更好地组织和管理程序中的异常情况,使代码更具表达力和可读性。


2.3.except 和 raise 同时使用

在实际应用中对异常可能需要更复杂的处理方式:当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次引发异常,让该方法的调用者也能捕获到异常。

为了实现这种通过多个方法协作处理同一个异常的情形,可以在except块中结合raise 语句来完成。

def divide_numbers(x, y):
    try:
        result = x / y
    except ZeroDivisionError as e:
        print("捕获到除零异常:", e)
        # 重新引发另一个异常
        raise ValueError("除数不能为零!") from e
    except Exception as e:
        print("捕获到其他异常:", e)
        # 引发自定义异常
        raise RuntimeError("发生了运行时错误!") from None
    else:
        print("结果为:", result)


# 测试
try:
    divide_numbers(10, 0)
except ValueError as e:
    print("捕获到值错误异常:", e)
except RuntimeError as e:
    print("捕获到运行时错误异常:", e)

输出:

捕获到除零异常: division by zero
捕获到值错误异常: 除数不能为零!

在这个例子中,divide_numbers 函数尝试进行除法运算,如果捕获到 ZeroDivisionError 则会打印异常信息并重新引发一个 ValueError,传递原始异常对象作为新异常的原因。另外,在捕获到其他类型的异常时,会打印异常信息并引发一个新的 RuntimeError

在测试代码中,我们使用 try...except 结构分别捕获了 ValueErrorRuntimeError 异常,并打印了相应的异常信息。

通过这种方式,我们可以在捕获到异常后通过 raise 语句重新引发新的异常,或者在不同的异常情况下引发不同类型的异常,从而更精细地控制异常的处理流程。


2.4.raise 不需要参数

raise 关键字可以不带参数使用,用于重新引发当前上下文中捕获的异常。这种用法常常在异常处理过程中,帮助我们在某个异常被捕获后,将其重新引发到更高层的异常处理环境中,从而实现异常的传递和集中处理。

以下是一个简单的例子,演示了 raise 不带参数时的用法:

def divide_numbers(x, y):
    try:
        result = x / y
    except ZeroDivisionError as e:
        print("捕获到除零异常:", e)
        # 重新引发当前异常
        raise
    else:
        print("结果为:", result)


# 测试
try:
    divide_numbers(10, 0)
except ZeroDivisionError as e:
    print("捕获到除零异常,并重新引发:", e)

输出:

捕获到除零异常: division by zero
捕获到除零异常,并重新引发: division by zero

在这个例子中,divide_numbers 函数尝试进行除法运算,如果捕获到 ZeroDivisionError 则会打印异常信息并重新引发当前上下文中捕获的异常。在这里的 raise 没有提供任何参数,表示重新引发当前上下文中捕获的异常。

在测试代码中,我们使用 try...except 结构捕获了重新引发的 ZeroDivisionError 异常,并打印了相应的异常信息。

通过这种方式,我们可以在捕获到异常后通过 raise 关键字不带参数来重新引发当前上下文中捕获的异常,从而将异常传递给更高层的异常处理环境,实现异常的集中处理和统一管理。


3.异常处理规则

当涉及异常处理时,有几个重要的规则需要特别关注:

  1. 不要过度使用异常:

    异常处理应该用于处理真正意外的情况,而不应该被用作控制流程的手段。过度使用异常会使代码难以理解和维护,并且会降低程序的性能。因此,应该尽量避免在日常的控制流程中使用异常。

  2. 不要使用过于庞大的 try 块:

    过大的 try 块可能会导致隐藏在其中的错误难以察觉,也会使代码的可读性和可维护性变差。应该尽量将 try 块限制在尽可能小的范围内,只捕获那些预料之中的具体异常。

  3. 不要忽略捕获到的异常:

    捕获到异常后,应该进行适当的处理,而不是简单地忽略它们。忽略异常可能会导致程序继续执行处于不确定状态,甚至隐藏潜在的问题。因此,需要对捕获到的异常进行适当的处理,比如记录日志、给出提示或者采取其他合适的补救措施。

遵循以上规则可以帮助保持代码的清晰性、可读性和稳定性,提高程序的质量和可靠性。异常处理应该被看作是一种用来处理真正意外情况的机制,而不是用来替代正常的控制流程或者忽略问题的手段。

23-常见特殊方法

在 Python 类中有些方法名、属性名的前后都添加了双下画线,这种方法、属性通常都属于Python 的特殊方法和特殊属性,开发者可以通过重写这些方法或直接调用这些方法来实现特殊的功能。最常见的特殊方法就是前面介绍的构造方法: __init__ ,开发者可以通过重写类中的 __init__ 方法来实现自己的初始化逻辑。

Python 是一门尽量简单的语言,它不像某些语言(如 Java)需要让类实现接口,并实现接口中的方法。Python 采用的是一种“约定”的机制,Python 按照约定,以特殊名字的方法、属性来提供特殊的功能。

Python 类中的特殊方法、特殊属性有些需要开发者重写,有些则可以直接调用,掌握这些常见的特殊方法、特殊属性也是非常重要的。

1.常见特殊方法

下面是一些常见的特殊方法

1.1.重写 __repr__ 方法

__repr__() 方法是一个特殊的方法,用于返回对象的字符串表示形式。当我们打印一个对象时,Python 将调用该对象的 __repr__() 方法,以便显示该对象的信息。

__repr__() 是Python类中的一个特殊方法,由于object类已提供了该方法,而所有的Python类都是object类的子类,因此所有的Python对象都具有 __repr__() 方法。

  • 如果没有定义 __repr__() 方法,则默认情况下会使用对象的内存地址表示该对象。

假设我们有一个类 Person 表示一个人,但是它没有定义 __repr__() 方法。

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

p = Person('Tom', 30)
print(p)

输出结果如下:

<__main__.Person object at 0x0000013A669E64D0>

可以看到,输出结果中包含了对象的类型和内存地址

object类提供的 __repr__() 方法总是返回该对象实现类的 “类名+object at+内存地址” 值,这个返回值并不能真正实现 “显示对象的信息” 的功能,因此,如果用户需要自定义类能实现 “显示该对象的信息” 的功能,就必须重写 __repr__() 方法。

  • 重写 __repr__() 方法有以下几个用处:

    1. 能够提供给程序员更好的可读性和理解性,使得对象可以更方便地被调试和测试。

    2. 可以向其他开发人员展示在对象中包含了哪些数据,并减少在查看源代码时需要深入细节的次数。

    3. 在交互式环境中,能够更好地显示对象。

举个例子,假设我们有一个类 Person 表示一个人,它有一个名字和一个年龄属性。如果我们打印一个 Person 对象,我们希望看到类似于 “Person(name=‘Tom’, age=30)” 这样的输出。

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

    def __repr__(self):
        return f"Person(name='{self.name}', age={self.age})"

p = Person('Tom', 30)
print(p)

输出如下:

Person(name='Tom', age=30)

可以看到,通过重写 __repr__() 方法,我们得到了一个更具可读性的字符串表示形式。这将使得调试和测试更容易进行,同时也有助于其他开发人员理解代码中的对象结构。


1.2.析构方法: __del__

__init__() 方法对应的是 __del__() 方法; __init__() 方法用于初始化 Python 对象,而 __del__() 则用于销毁 Python 对象:当一个对象没有被任何引用指向时,系统会自动将其回收,并在回收之前调用该对象的 __del__() 方法。

需要注意的是,Python 的垃圾回收机制是不确定的,因此不能保证 __del__() 方法会在对象被回收的时候立即执行。如果一个对象被另一个对象所引用,那么就算这个对象已经没有其他引用指向,也不会立即被回收。此时,__del__() 方法也不会立即执行,直到所有引用该对象的其他对象都被回收后,才会执行。

由于 __del__() 方法是在对象被回收前执行的,因此可以在其中执行一些清理工作,例如释放对象所占用的资源等。同时,也应该注意避免在 __del__() 方法中引用其他对象,因为这可能会导致对象循环引用,从而导致内存泄漏。

下面是一个例子,演示了如何在 __del__() 方法中释放一个对象所占用的资源:

class Car:
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        self.engine = Engine()

    def __repr__(self):
        return f"{self.brand} {self.model}"

    def __del__(self):
        print(f"Deleting {self}")
        self.engine.stop()

class Engine:
    def __init__(self):
        self.running = False

    def start(self):
        self.running = True
        print("Engine started")

    def stop(self):
        if self.running:
            print("Engine stopped")
            self.running = False

engine = Engine()

car1 = Car("Tesla", "Model S")
car2 = Car("BMW", "i8")

engine.start()

del car1
del car2

engine.stop()

结果:

Engine started
Deleting Tesla Model S
Deleting BMW i8
Engine stopped

在上述例子中,Car 类包含一个 Engine 对象,表示车辆的发动机。在 __del__() 方法中,我们调用了 engine.stop() 方法,以释放发动机所占用的资源。在运行程序的过程中,我们创建了两个 Car 对象,并将它们赋值给 car1 和 car2 变量。然后,我们使用 del 关键字删除这两个变量,以便让它们被回收。最后,我们调用了 engine.stop() 方法,以停止发动机。可以看到,在程序运行结束之前,系统先回收了 car1 和 car2 对象,并分别调用了它们的 __del__() 方法,然后才执行了 engine.stop() 方法。


1.3.__dir__ 方法

__dir__ 方法是Python中用于自定义对象的特殊方法之一,它返回对象的属性名称列表。当你在一个对象上调用 dir(obj) 时,实际上是在调用 obj.__dir__() 方法。这个方法通常被用来定制对象在交互式解释器中的显示行为,或者在需要列出对象属性时提供更灵活的控制。

下面是一个简单的例子,演示了如何自定义一个类以覆盖 __dir__ 方法:

class CustomDirExample:
    def __init__(self):
        self.name = "Alice"
        self.age = 30
        self.gender = "female"

    def __dir__(self):
        return ['name', 'age']  # 只返回指定的属性


obj = CustomDirExample()
print(dir(obj))   #输出: ['age', 'name']

在这个例子中,CustomDirExample 类中定义了 __dir__ 方法,该方法返回一个包含 ‘name’ 和 ‘age’ 的列表。因此,当我们调用 dir(obj) 时,只会显示这两个属性,而不会包含 gender 属性。

需要注意的是,正常情况下,不需要手动调用 __dir__ 方法,因为 dir(obj) 会自动调用它。只有在需要特定定制对象属性列表的情况下才需要使用它。

总之,__dir__ 方法允许我们在需要时对对象的属性列表进行定制,从而提供更加灵活的对象属性控制。


1.4.__dict__ 属性

__dict__ 是 Python 中每个对象都有的一个属性,它是一个字典,用于存储对象的所有属性和对应的取值。这意味着当你定义一个类,并创建类的实例后,实例的所有属性都会存储在 __dict__ 这个字典中。

让我用一个简单的例子来说明:

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


person1 = Person("Alice", 30)
print(person1.__dict__)   #输出: {'name': 'Alice', 'age': 30}

#通过__dict__访问name属性
print(person1.__dict__["name"])    #输出: Alice  

在这个例子中,当我们打印 person1.__dict__ 时,会输出 {'name': 'Alice', 'age': 30} ,这就是 __dict__ 的作用。它提供了一个方便的方式来查看对象的所有属性及其对应的值。

除了查看属性外,__dict__ 还可以用来动态地添加、修改和删除对象的属性。例如:

person1.__dict__["gender"] = "female"  # 动态添加属性
print(person1.__dict__)   #输出: {'name': 'Alice', 'age': 30, 'gender': 'female'}

person1.age = 31  # 修改属性值
print(person1.__dict__)   #输出: {'name': 'Alice', 'age': 31, 'gender': 'female'}

del person1.__dict__["age"]  # 删除属性
print(person1.__dict__)   #输出: {'name': 'Alice', 'gender': 'female'}

通过直接操作 __dict__,我们可以绕过属性访问控制,因此在实际开发中,应谨慎使用这种方式。

总之,__dict__ 属性为我们提供了一种直接观察、修改对象属性的方式,同时也可以用于动态地管理对象的属性。


1.5.操作对象属性时的一些特殊方法

当需要自定义对象属性的访问、设置和删除行为时,可以使用一些特殊方法来实现。以下是几个常用的特殊方法及其作用:

  1. __getattribute__(self, name)

作用:在访问属性时被调用,无论属性是否存在都会触发。

使用场景:用于拦截所有属性访问,包括存在和不存在的属性。

示例:

class CustomGetAttributeExample:
    def __init__(self):
        self.age = 30

    def __getattribute__(self, name):
        print(f"Accessing attribute: {name}")
        return object.__getattribute__(self, name)


obj = CustomGetAttributeExample()
print(obj.age)  # 触发 __getattribute__ 方法

输出

Accessing attribute: age
30

在这个例子中,我们定义了一个名为 CustomGetAttributeExample 的类,其中包含了一个 __init__ 方法用于初始化对象的属性,并且定义了一个 __getattribute__ 方法来定制属性的访问行为。

当我们创建 CustomGetAttributeExample 的实例对象 obj 并尝试访问其属性 age 时,即执行 print(obj.age) 时,由于我们在类中定义了 __getattribute__ 方法,因此该方法会被触发。

__getattribute__ 方法中,我们首先打印出正在访问的属性名称 name ,然后利用 object.__getattribute__(self, name) 来获取实际的属性值。这样,我们就实现了在访问属性时插入额外的逻辑,即打印出正在访问的属性名称。

因此,当我们执行 print(obj.age) 时,会先打印出 "Accessing attribute: age",然后返回 self.age 的值,也就是30。最终输出结果为30,同时触发了 __getattribute__ 方法中的额外逻辑。

  1. __getattr__(self, name)

作用:在访问不存在的属性时被调用。

使用场景:用于动态地提供属性值,或者在属性不存在时执行特定操作。

示例:

class CustomGetAttrExample:
    def __getattr__(self, name):
        if name == 'age':
            return 30
        else:
            raise AttributeError(f"'CustomGetAttrExample' object has no attribute '{name}'")


obj = CustomGetAttrExample()
print(obj.age)  # 输出 30
print(obj.name)  # 触发 __getattr__ 方法

image

在这个例子中,当我们尝试访问 age 属性时,__getattr__ 方法被调用并返回了预定义的值。而当我们尝试访问其他不存在的属性时,__getattr__ 方法会抛出一个 AttributeError 异常。

  1. __setattr__(self, name, value)

作用:在设置属性时被调用。

使用场景:用于控制属性的赋值行为,执行额外的逻辑或者验证。

示例:

class CustomSetAttrExample:
    def __setattr__(self, name, value):
        if name == 'age':
            self.__dict__[name] = value
        else:
            raise AttributeError("Can't set attribute")


obj = CustomSetAttrExample()
obj.age = 30  # 设置属性值
print(obj.age)  # 输出 30

obj.name = "Alice"  # 触发 AttributeError

image

在这个例子中,我们在 __setattr__ 方法中进行了特殊处理,只有当属性名为 age 时才允许设置属性值,否则会触发 AttributeError

  1. __delattr__(self, name)

作用:在删除属性时被调用。

使用场景:用于执行额外的清理操作或者控制属性删除行为。

示例:

class CustomDelAttrExample:
    def __init__(self):
        self.age = 30

    def __delattr__(self, name):
        if name == 'age':
            raise AttributeError("Can't delete attribute")
        else:
            del self.__dict__[name]


obj = CustomDelAttrExample()
del obj.age  # 触发 AttributeError

image

在这个例子中,我们定义了一个名为 CustomDelAttrExample 的类,其中包含了一个 __init__ 方法用于初始化对象的属性,并且定义了一个 __delattr__ 方法来定制属性的删除行为。

当我们创建 CustomDelAttrExample 的实例对象obj并尝试删除其属性age时,即执行 del obj.age 时,由于我们在类中定义了 __delattr__ 方法,因此该方法会被触发。

__delattr__ 方法中,我们首先检查要删除的属性名称name,如果是age,则抛出 AttributeError 异常,表示不能删除该属性。否则,使用 del self.__dict__[name] 来从实例的 __dict__ 属性中删除指定的属性。

因此,当我们执行 del obj.age 时,由于age是一个禁止删除的属性,所以会触发 AttributeError 异常,而不会删除age属性。

*通过这些特殊方法,可以对属性的访问、设置和删除行为进行灵活的定制,满足特定的需求,同时也需要注意避免破坏对象的正常行为逻辑。


2.与反射相关的属性和方法

如果程序在运行过程中要动态判断是否包含某个属性(包括方法),甚至要动态设置某个属性值,则可通过Python的反射支持来实现。


2.1.动态操作属性

当需要动态检查对象是否包含某些属性(包括方法)时,可以使用以下几个函数:

  1. hasattr(obj, name):检查obj对象是否包含名为name的属性或方法。

  2. getattr(object, name[, default]):获取object对象中名为name的属性的属性值,若属性不存在则返回默认值。

  3. setattr(obj, name, value):将obj对象的name属性设为value。

下面是一个示例程序,演示了如何通过以上函数来动态操作Python对象的属性:

class MyClass:
    def __init__(self):
        self.attr1 = 10


obj = MyClass()

# 检查是否包含属性 'attr1'
if hasattr(obj, 'attr1'):
    print(f"obj has attribute 'attr1'")
else:
    print(f"obj does not have attribute 'attr1'")

#输出: obj has attribute 'attr1'

在这个示例中,我们首先定义了一个名为MyClass的类,该类具有一个初始化方法 __init__ ,在其中设置了一个属性attr1。然后我们创建了MyClass类的实例对象obj。接着,我们使用hasattr函数来检查对象obj是否包含属性attr1,并根据结果输出相应的消息。

接下来,我们可以使用getattr函数动态获取对象的属性。getattr函数接受三个参数:对象、属性名和可选的默认值。如果对象包含指定的属性,则返回其值;否则返回设定的默认值。

# 获取属性 'attr1' 的值
value = getattr(obj, 'attr1', 'default')
print(f"The value of attr1 is: {value}")   #输出: The value of attr1 is: 10

在这里,我们使用getattr函数获取了对象obj的属性attr1的值,并打印出来。由于attr1存在,因此将返回其实际值10。

最后,我们可以使用setattr函数动态设置对象的属性。setattr函数接受三个参数:对象、属性名和新的属性值,用来设置对象的指定属性。

# 设置属性 'attr2' 的值
setattr(obj, 'attr2', 20)
print(f"The value of attr2 is: {obj.attr2}")   #输出: The value of attr2 is: 20

在这个例子中,我们使用setattr函数给对象obj动态添加了一个新属性attr2,并设置其值为20。随后打印出了attr2的值,证实了属性的成功添加和赋值操作。


2.2.__call__ 属性

在Python中,可以使用hasattr函数来判断对象是否包含某个属性或方法,但要确定这个属性到底是一个数据属性还是一个可调用的方法,我们可以进一步检查它是否包含 __call__ 属性。如果一个属性包含 __call__ 属性,那么它就是可调用的方法,否则它是一个数据属性。

下面是一个示例程序,演示了如何判断一个属性或方法是否可调用:

class MyClass:
    def __init__(self):
        self.data_attr = 10

    def method(self):
        pass


obj = MyClass()

# 判断属性是否存在
if hasattr(obj, 'data_attr'):
    print(f"obj has data attribute 'data_attr'")

# 判断方法是否存在并可调用
if hasattr(obj, 'method'):
    print(f"obj has method 'method'")
    if hasattr(obj.method, '__call__'):
        print(f"method 'method' is callable")
    else:
        print(f"method 'method' is not callable")

在这个示例中,我们首先创建了一个MyClass类的实例对象obj,并定义了一个数据属性 data_attr和一个方法 method。然后我们使用 hasattr 函数来检查对象obj是否包含 data_attr 属性和 method 方法。接着,我们进一步使用 hasattr 来检查 method 方法是否包含 __call__ 属性,以确定它是否可调用。

通过这样的方式,我们可以对对象的属性和方法进行更加细致的判断,以满足程序需要。


3.与序列相关的特殊方法

3.1.序列相关方法

对于Python中的序列(如列表、元组等),可以通过实现特殊方法来实现不可变序列或可变序列。下面我将详细介绍每个特殊方法,并举例说明如何实现它们。

  1. __len__(self) 方法:

该方法用于返回序列中的元素个数,决定序列的长度。

class MySequence:
    def __init__(self, elements):
        self.elements = elements

    def __len__(self):
        return len(self.elements)


seq = MySequence([1, 2, 3, 4, 5])
print(len(seq))  # 输出:5
  1. __getitem__(self, key) 方法:

该方法用于获取指定索引对应的元素,key应为整数值或slice对象。

class MySequence:
    def __init__(self, elements):
        self.elements = elements

    def __getitem__(self, key):
        return self.elements[key]


seq = MySequence([1, 2, 3, 4, 5])
print(seq[2])  # 输出:3
  1. __contains__(self, item) 方法:

该方法用于判断序列是否包含指定元素。

class MySequence:
    def __init__(self, elements):
        self.elements = elements

    def __contains__(self, item):
        return item in self.elements


seq = MySequence([1, 2, 3, 4, 5])
print(3 in seq)  # 输出:True
  1. __setitem__(self, key, value) 方法:

该方法用于设置指定索引对应的元素,key应为整数值或slice对象。

class MyMutableSequence:
    def __init__(self, elements):
        self.elements = elements

    def __setitem__(self, key, value):
        self.elements[key] = value


seq = MyMutableSequence([1, 2, 3, 4, 5])
seq[2] = 10
print(seq.elements)  # 输出:[1, 2, 10, 4, 5]
  1. __delitem__(self, key) 方法:

该方法用于删除指定索引对应的元素。

class MyMutableSequence:
    def __init__(self, elements):
        self.elements = elements

    def __delitem__(self, key):
        del self.elements[key]


seq = MyMutableSequence([1, 2, 3, 4, 5])
del seq[2]
print(seq.elements)  # 输出:[1, 2, 4, 5]

通过实现上述特殊方法,可以定义自己的不可变序列或可变序列,并根据需要进行元素的访问、修改和删除操作。


3.2.实现迭代器

前面介绍了使用 for 循环遍历列表、元组和字典等,这些对象都是可迭代的,因此它们都属于迭代器。

当需要实现迭代器时,可以通过实现 __iter__()__next__() 两个方法来实现。这两个方法定义了迭代器的行为,分别用于返回迭代器对象自身和获取下一个元素。

  1. __iter__(self) 方法:

该方法返回迭代器对象自身,通常在该方法中直接返回self。

class MyIterator:
    def __init__(self, data):
        self.data = data
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index < len(self.data):
            item = self.data[self.index]
            self.index += 1
            return item
        else:
            raise StopIteration


my_iter = MyIterator([1, 2, 3, 4, 5])
for item in my_iter:
    print(item)

示例定义了一个名为 MyIterator 的迭代器类,该类实现了迭代器协议,因此可以使用 for...in 语句对其进行遍历。

在这个类中,__init__ 方法用于初始化数据和索引,其中 data 参数是要进行迭代的数据,index 参数表示当前迭代的位置。

__iter__ 方法返回迭代器自身,因为迭代器应该同时是可迭代的,所以需要实现 __iter__ 方法并返回 self

__next__ 方法定义了迭代的行为。在每次调用时,它会检查索引是否小于数据长度,如果是,则返回当前索引位置的元素,并将索引向前移动一位;如果索引超出了数据范围,则抛出 StopIteration 异常,以此告知调用者迭代已经结束。

最后,通过创建 MyIterator 类的实例 my_iter,并使用 for...in 循环对其进行遍历,打印出每个元素的值。

在这个例子中,输出结果将会是:

1
2
3
4
5

因为 for...in 循环会不断调用 my_iter__next__ 方法,直到 StopIteration 异常被触发,表示迭代结束。

  1. __reversed__(self) 方法:

该方法主要为内建的 reversed() 函数提供支持,当程序调用 reversed() 函数对指定迭代器执行反转时,实际上是由该方法实现的。

Python中内建函数 reversed() 是用于对序列进行反转的,但它要求被反转的对象必须是一个序列类型,而不是自定义的迭代器对象。因此,我们无法直接使用 reversed() 来反转自定义的迭代器对象。

如果需要自定义迭代器并支持反转操作,可以考虑重写 __reversed__() 方法,以支持内建函数 reversed() 对自定义迭代器的操作。

class MyReversibleIterator:
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index > 0:
            self.index -= 1
            return self.data[self.index]
        else:
            raise StopIteration

    def __reversed__(self):
        return MyReversibleIterator.reversed_iterator(self.data)

    @staticmethod
    def reversed_iterator(data):
        index = len(data) - 1
        while index >= 0:
            yield data[index]
            index -= 1


my_reversible_iter = MyReversibleIterator([1, 2, 3, 4, 5])
for item in my_reversible_iter:
    print(item)

for item in reversed(my_reversible_iter):
    print(item)

输出

5
4
3
2
1
5
4
3
2
1

我们通过定义 __reversed__() 方法和静态方法 reversed_iterator() 来支持自定义迭代器的反转操作。这样就可以正常使用内建函数 reversed() 对自定义的迭代器对象执行反转操作了。

这两个示例展示了如何通过实现 __iter__()__next__() 方法来创建自定义的迭代器,并且支持内建的 reversed() 函数。


3.生成器

生成器和迭代器的功能非常相似,它也会提供 __next__() 方法,这意味着程序同样可调用内置的 next() 函数来获取生成器的下一个值,也可使用 for 循环来遍历生成器。

生成器与迭代器的区别在于:

迭代器通常是先定义一个迭代器类,然后通过创建实例来创建迭代器

生成器则是先定义一个包含 yield 语句的函数,然后通过调用该函数来创建生成器。

生成器是一种非常优秀的语法,Pvthon 使用生成器可以让程序变得很优雅。


3.1.创建生成器

当我们定义一个生成器函数时,实际上我们定义了一种特殊的函数,它不是像普通函数一样一次性执行完毕,而是可以产生多个值,并且在产生每个值后暂停执行,等待下一次调用。这种特性使得生成器在处理大量数据或者需要逐步产生结果的情况下非常高效。

生成器函数使用 yield 语句来产生值,yield 的作用类似于 return,但不同的是生成器函数每次执行到 yield 时会暂停并保存当前状态,使得下一次调用时可以从暂停的地方继续执行。

下面是创建生成器的一般步骤:

  1. 定义一个函数,其中包含生成器逻辑。
  2. 在函数内部使用 yield 语句产生值。
  3. 每次调用生成器时,使用 next() 函数获取下一个值。

让我们看一个例子,在这个例子中,我们定义一个生成器函数来生成指定范围内的偶数:

def even_numbers_generator(n):
    for i in range(2, n+1, 2):
        yield i


# 使用生成器来打印范围内的偶数
even_gen = even_numbers_generator(10)
for num in even_gen:
    print(num)

输出

2
4
6
8
10

在这个例子中,even_numbers_generator 函数是一个生成器函数,它使用 yield 语句来产生指定范围内的偶数。然后我们创建了一个名为 even_gen 的生成器对象,并使用 for 循环来获取生成器产生的偶数并打印出来。

总之,生成器提供了一种高效的方式来逐步产生数据,并且可以大大节省内存空间。生成器的使用可以使得处理大规模或者需要逐步处理的数据变得更加简单和高效。


3.2.生成器的方法

生成器是 Python 中非常强大且灵活的工具,除了基本的生成器函数之外,生成器还提供了一些方法来操作、控制和组合生成器,下面我将详细介绍一些常用的生成器方法,并给出相应的示例。

  1. send(value) 方法

send() 方法用于向生成器发送数据,这个数据会成为生成器内部 yield 表达式的值。它使得我们可以在生成器中动态地向 yield 语句提供值。

def echo_generator():
    while True:
        received = yield
        print('You said:', received)

gen = echo_generator()
next(gen)  # 启动生成器
gen.send('Hello')  # 输出: You said: Hello
  1. close() 方法

close() 方法用于关闭生成器,当调用这个方法时,生成器会抛出一个 GeneratorExit 异常,可以在生成器内部捕获这个异常并进行清理工作。

def countdown(num):
    while num > 0:
        yield num
        num -= 1

counter = countdown(5)
print(next(counter))  # 输出: 5
counter.close()
  1. throw() 方法

throw() 方法用于在生成器中抛出指定的异常。这可以让我们在生成器内部处理异常情况。

def exception_generator():
    try:
        yield 1
    except ValueError:
        yield 999

gen = exception_generator()
next(gen)  # 启动生成器
gen.throw(ValueError)  # 输出: 999
  1. yield from 语句

yield from 语句可以将另一个可迭代对象(比如另一个生成器)的值委托给当前生成器,使得生成器之间可以相互协作。

def subgenerator():
    yield 42

def maingenerator():
    result = yield from subgenerator()
    print(result)

gen = maingenerator()
next(gen)  # 输出: 42

这些方法使得生成器在实际应用中更加灵活和强大,可以通过生成器方法来实现状态管理、异常处理以及多个生成器之间的协作,从而使得生成器在处理复杂逻辑时更加方便和高效。

24-模块和包

1.模块化编程

对于一个真实的 Python程序,我们不可能自己完成所有的工作,通常都需要借助于第三方类库。此外,也不可能在一个源文件中编写整个程序的源代码,这些都需要以模块化的方式来组织项目的源代码。

1.1.导入模块

当你在 Python 中使用模块时,有多种方法可以导入模块。让我们更详细地认识每种方法。

  1. 直接导入整个模块:

    通过 import 语句,你可以直接导入整个模块。这使得模块中的所有内容都可用,并通过 . 操作符来访问。

    import math
    print(math.sqrt(16))  # 输出 4.0
    
  2. 给模块指定别名:

    当模块名很长或与现有变量名冲突时,你可以为模块指定一个别名,以便更轻松地使用。

    import numpy as np
    a = np.array([1, 2, 3])
    
  3. 导入模块中的部分内容:

    使用 from ... import ... 语句,你可以只导入模块中的某几个函数或类。

    from random import randint
    num = randint(1, 10)
    
  4. 导入模块中的所有内容:

    尽管不建议频繁使用,但有时你可能需要导入模块中的所有内容。这可以通过使用 from ... import * 语句来实现。

    from os import *
    print(getcwd())  # 输出当前工作目录
    
  5. 条件导入模块:

    有时你可能需要根据条件来选择性地导入模块。这可以通过条件语句结合普通的 import 语句来实现。

    if some_condition:
        import module1
    else:
        import module2
    
  6. 动态导入模块:

    有时你可能需要根据变量的值来动态地导入模块。Python 提供了 __import__ 函数来实现这一点。

    module_name = "math"
    some_module = __import__(module_name)
    print(some_module.sqrt(25))  # 输出 5.0
    

以上是Python中常见的导入模块的方式,选择合适的方式取决于你的需求和偏好。


1.2.定义模块

掌握了导入模块的语法之后,下一个问题来了:模块到底是什么? 怎样才能定义自己的模块呢?

模块就是 Python 程序!

任何 Python 程序都可作为模块导入。前面我们写的所有 Python 程序都可作为模块导入。

换而言之,随便写的一个 Python 程序,其实都可作为模块导入。对于任何程序,只要导入了模块,即可使用该模块内的所有成员。

一个模块就是一个包含了Python代码的文件。这些文件可以包含函数、类和变量的定义,以及可以执行的Python语句。通过使用import语句,你可以将这些模块导入到其他Python程序中,以便重复使用其中的代码。

下面是一个简单的示例来定义一个模块:

假设你有一个名为 my_module.py 的文件,其中包含以下内容:

# my_module.py

def greet(name):
    print("Hello, " + name)

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

    def get_name(self):
        return self.name

在上面的示例中,my_module.py 定义了一个 greet 函数和一个 Person 类。现在,如果你想在另一个 Python 文件中使用这个模块,你可以这样做:

import my_module

my_module.greet("Alice")

alice = my_module.Person("Alice")
print(alice.get_name())

当你运行第二个Python文件时,它会导入 my_module 模块,并使用其中定义的函数和类。

这就是一个简单的例子来定义一个模块。可以看出,模块提供了一种有效的方式来组织和重用 Python 代码。


1.3.为模块编写说明文档

与前面介绍的函数、类相同的是,在实际开发中往往也应该为模块编写说明文档;否则,其他开发者将不知道该模块有什么作用,以及包含哪些功能。

示例:

def greet(name):
    """
    这个函数接受一个字符串参数`name`,并打印出一个问候语。

    参数:
    name (str): 要问候的对象的名字。

    返回值:
    无
    """
    print("Hello, " + name)

def calculate_sum(a, b):
    """
    这个函数接受两个数字参数 `a` 和 `b`,并返回它们的和。

    参数:
    a (int/float): 第一个数字
    b (int/float): 第二个数字

    返回值:
    int/float: a 和 b 的和
    """
    return a + b

2.加载模块

在编写一个 Python 模块之后,如果直接用 importfrom...import 来导入该模块,Python 通常并不能加载该模块。道理很简单:Python 怎么知道到哪里去找这个模块呢?

为了让 Python 能找到我们编写(或第三方提供)的模块,可以用以下两种方式来告诉它:

  • 使用环境变量。
  • 将模块放在默认的模块加载路径下。

下面详细介绍这两种方式。

2.1.使用环境变量

Python 将会根据 PYTHONPATH 环境变量的值来确定到哪里去加载模块。PYTHONPATH 环境变量的值是多个路径的集合,这样Python 就会依次搜索 PYTHONPATH 环境变量所指定的多个路径,试图从中找到程序想要加载的模块。

2.1.1 Windows 平台:

  1. 打开系统属性:右键点击“此电脑”,选择“属性”。

  2. 进入高级系统设置:在左侧面板中选择“高级系统设置”,然后点击“环境变量”按钮。

  3. 配置用户环境变量:在“用户变量”部分,点击“新建”按钮。

    • 变量名:输入你的变量名称,比如 MY_VARIABLE
    • 变量值:输入变量的值,比如 C:\myfolder(注意:Windows使用分号 ; 来分隔不同的路径)
  4. 配置系统环境变量:在“系统变量”部分,点击“新建”按钮,然后输入变量名和变量值,点击“确定”。

2.1.2. Linux 平台:

  1. 编辑文件 ~/.bashrc 或 ~/.bash_profile:在终端中使用文本编辑器打开 ~/.bashrc 或者 ~/.bash_profile 文件。

    • 例如:vi ~/.bashrc 或者 nano ~/.bashrc
  2. 添加环境变量:在文件末尾添加类似如下的行:

    export MY_VARIABLE="variable_value"
    

    然后保存并关闭文件。

  3. 使环境变量生效:在终端中执行以下命令,以使新配置的环境变量生效:

    source ~/.bashrc
    

    或者

    source ~/.bash_profile
    

2.1.3. macOS 平台:

  1. 编辑文件 ~/.bash_profile:在终端中使用文本编辑器打开 ~/.bash_profile 文件。

    • 例如:vi ~/.bash_profile 或者 nano ~/.bash_profile
  2. 添加环境变量:在文件末尾添加类似如下的行:

    export MY_VARIABLE="variable_value"
    

    然后保存并关闭文件。

  3. 使环境变量生效:在终端中执行以下命令,以使新配置的环境变量生效:

    source ~/.bash_profile
    

以上步骤中的 MY_VARIABLE 应替换为你要设置的环境变量的名称,而 variable_value 则是你要设置的变量值。这些步骤应该可以帮助你在不同的操作系统上成功配置环境变量。

具体安装及环境配置可再次参考第二节 2-环境搭建 内容


2.2.默认的模块加载路径

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

Python默认的模块加载路径由sys.path变量代表,因此可通过在交互式解释器中执行如下命令来查看Pvthon默认的模块加载路径。

image

上面代码使用 pprint 模块下的 pprint() 函数代替普通的 print() 函数,这是因为如果要打印的内容
很多,使用 pprint 可以显示更友好的打印结果。

上面的运行结果就是 Python 3.x 默认的模块加载路径,这是因为作者将 Python 安装在了 E:\py 路径下。如果将 Python 安装在其他路径下,上面的运行结果应该略有差异。

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


2.3.模块的 __all__ 变量

在默认情况下,如果使用 from 模块名 import * 这样的语句来导入模块,程序会导入该模块中所有不以下画线开头的程序单元,这是很容易想到的结果。

有时候模块中虽然包含很多成员,但并不希望每个成员都被暴露出来供外界使用,此时可借助于模块的 __all__ 变量,将变量的值设置成一个列表,只有该列表中的程序单元才会被暴露出来。

举例来说,假设有一个名为 my_module 的模块,其中包含了以下成员:spam , eggs , ham , bacon 和 _internal_func。如果我们只希望暴露 spam 和 eggs ,可以按照如下方式操作:

# my_module.py
# 测试 __all__ 变量的模块

def spam():
    print("This is spam")

def eggs():
    print("This is eggs")

def ham():
    print("This is ham")

def bacon():
    print("This is bacon")

def _internal_func():
    print("This is an internal function")


# 定义 __all__ 变量,默认只导入 spam 和 eggs 两个程序单元
__all__ = ['spam', 'eggs']

在这个示例中,__all__ 变量被设置为 [‘spam’, ‘eggs’] ,这意味着只有 spam 和 eggs 会被暴露出来供外部使用。其他函数和变量将不会被默认导入。

# test_module.py
# 测试调用 my_module 模块的程序单元

from my_module import *

spam()
eggs()

ham()   # 会提示找不到 ham() 函数
bacon()   # 会提示找不到 bacon() 函数

当使用 from my_module import * 语句时,只有在 __all__ 列表中的成员才会被导入,而 ham , bacon 和 _internal_func 将不会被导入。这样可以有效地控制模块中哪些成员对外可见,从而提高代码的可维护性和安全性。


3.包

对于一个需要实际应用的模块而言,往往会具有很多程序单元,包括变量、函数和类等,如果将整个模块的所有内容都定义在同一个 Python 源文件中,这个文件将会变得非常庞大,显然并不利于模块化开发。


3.1.什么是包

包(Package)是用来组织模块的一种方式。一个包其实就是一个包含 __init__.py 文件的目录,该目录下可以包含多个模块或子包。

举个例子,假设你有一个名为my_package的包,它的目录结构如下:

my_package/
    __init__.py
    module1.py
    module2.py
    subpackage1/
        __init__.py
        submodule1.py
        submodule2.py
    subpackage2/
        __init__.py
        submodule3.py

在这个例子中,my_package 就是一个包,module1.py 和module2.py 是 my_package 包的两个模块。另外,my_package 包下还包含了两个子包 subpackage1 和 subpackage2,每个子包也都有自己的 __init__.py 文件以及一些模块。

使用包的好处在于它可以帮助组织大型程序的代码结构,避免模块名冲突,并且让代码更加可维护和易于理解。当你在一个项目中工作时,将相关的模块放在同一个包中能够使代码更具有结构性和组织性。

要在Python中使用包,你可以通过import语句来导入包、模块或者子包。例如,要导入 my_package 包中的 module1.py ,可以使用以下语句:

from my_package import module1

3.2.定义包

掌握了包是什么之后,接下来学习如何定义包。定义包更简单,主要有两步:

1.创建一个文件夹,该文件夹的名字就是该包的包名。

2.在该文件夹内添加一个 __init__.py 文件即可。

假设我们要创建一个名为 my_package 的包,那么我们可以按照如下方式组织文件结构:

my_package/
    __init__.py

这样就定义了一个名为 my_package 的包。在这个包中,__init__.py 文件可以是空文件,也可以包含一些初始化代码,例如导入其他模块、设定一些初始变量等。

通过这样的方式,Python解释器就能识别出这个文件夹是一个包,从而可以使用import语句来导入这个包中的模块或子包了。