# mysite **Repository Path**: tangyifei/mysite ## Basic Information - **Project Name**: mysite - **Description**: mysite主要使用python3语法进行编码,使用的web框架是Django。 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-05-29 - **Last Updated**: 2021-10-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 1、Django项目搭建全过程、Web框架与Django [使用PyCharm搭建Django项目](https://blog.csdn.net/weixin_29932613/article/details/113507414?utm_term=python3%E5%88%9B%E5%BB%BAweb%E9%A1%B9%E7%9B%AE&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduweb~default-8-113507414&spm=3001.4430) [使用命令行搭建Django项目](https://blog.csdn.net/Viewinfinitely/article/details/106413188?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-0&spm=1001.2101.3001.4242) [Django连接mysql数据库并进行数据的相关操作](https://zhuanlan.zhihu.com/p/74423815?utm_source=wechat_session) [Django解决跨域](https://www.cnblogs.com/wujiest/p/13657699.html) 注意要安装django-cors-headers [Django使用JSonResponse返回Json数据乱码问题](https://blog.csdn.net/qq_45339157/article/details/115699833) 注意要安装rest_framework包 Django使用manage.py runserver 启动报如下的错误处理: ``` You have 19 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, blog, contenttypes, sessions. Run 'python manage.py migrate' to apply them. ``` 解决方法: Ctrl+C 结束掉服务输入下面的命令进行数据重构 manage.py migrate 此时再次运行服务就正常了。 blog项目主要使用Django之Model操作数据库,实现最基本的增删改查功能。 order项目主要使用pymysql连接数据库使用原生的sql进行数据的增删改查操作。 ### 1.1、Django修改时区 如果插入的数据库时间不正确,请在settings.py中修改时区,如下: ``` LANGUAGE_CODE = 'zh-Hans' TIME_ZONE = 'Asia/Shanghai' USE_I18N = True USE_L10N = True USE_TZ = False ``` ### 1.2、文件上传 可以参考[Django实现任意文件上传(最简单的方法)](https://cloud.tencent.com/developer/article/1028175) ### 1.3、用户注册与登录 用户注册与登录参考login模块,本文参考[Django用户登录与注册系统](https://blog.csdn.net/laikaikai/article/details/80563387) ## 2、LINUX中部署mysite 第一步、Linux下升级安装python3.8.6并配置pip 使用 python -V 查看版本,发现是2.7.5的版本,系统自带安装了python2.7.5版本,所以需要在不删除的情况下再次安装一个python3.8.6的版本。 第二步、安装python3.8.6,[官方下载地址](https://www.python.org/downloads/source/) ,找到对应版本点击Download Gzipped source tarball进行下载, 下载完成后上传到测试机中的/data/python3目录,然后使用 unzip Python-3.8.6.tgz 进行解压,安装相关的依赖: ``` yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc libffi-devel ``` 然后 cd Python-3.8.6 进入目录,执行 ./configure --prefix=/usr/local/python3 安装,然后使用 make && make install 进行编译, 将系统默认的python备份,使用 mv /usr/bin/python /usr/bin/python2.7.5 进行备份,创建软连接: ``` ln -s /usr/local/python3/bin/python3.8 /usr/bin/python ln -s /usr/local/python3/bin/python3.8 /usr/bin/python3 ``` 使用 python -V 查看版本,安装成功 第三步、修改yum配置,升级python3.8以后,yum命令会不能运行了,所以需要修改下yum的对应的头, 将yum、urlgrabber-ext-down两个文件内的的 #!/usr/bin/python 改为 #! /usr/bin/python2.7 : ``` vi /usr/bin/yum vi /usr/libexec/urlgrabber-ext-down ``` 第四步、备份2.7版本的软连接 mv /usr/bin/pip /usr/bin/pip2.7.5,配置pip3的软连接 ,pip3在python安装路径的 bin 目录下: ``` ln -s /usr/local/python3/bin/pip3 /usr/bin/pip ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3 ``` 查看版本 pip -V pip3 -V 然后在测试环境中创建一个/data/service/mysite目录,把该项目上传到mysite目录中,进入mysite目录,结构如下: ``` [root@app mysite]# ll 总用量 1276 drwxr-xr-x 4 root root 4096 6月 1 14:08 blog -rw-r--r-- 1 root root 11752 6月 1 15:10 blog.log drwxr-xr-x 3 root root 4096 6月 1 14:08 common -rw-r--r-- 1 root root 0 5月 26 15:03 db.sqlite3 drwxr-xr-x 4 root root 4096 6月 1 14:08 login -rw-r--r-- 1 root root 684 5月 26 14:52 manage.py drwxr-xr-x 3 root root 4096 6月 1 14:08 mysite -rw-r--r-- 1 root root 1237428 6月 1 14:52 mysite.zip drwxr-xr-x 4 root root 4096 6月 1 14:08 order -rw-r--r-- 1 root root 2103 5月 31 09:41 README.md drwxr-xr-x 2 root root 4096 6月 1 14:08 sql -rwxr-xr-x 1 root root 63 6月 1 15:08 start.sh drwxr-xr-x 5 root root 4096 6月 1 14:08 static -rwxr-xr-x 1 root root 37 6月 1 15:01 stop.sh drwxr-xr-x 3 root root 4096 6月 1 14:08 templates drwxr-xr-x 4 root root 4096 6月 1 14:08 wms ``` 其中 start.sh启动脚本的内容如下: ``` nohup python3 manage.py runserver 0.0.0.0:8001 >>mysite.log>&1 & ``` stop.sh的内容如下: ``` ps -aux | grep python3|xargs kill -9 ``` 或者直接找到监听8001的进程直接杀掉即可。 在启动start.sh脚本之前,需要使用pip安装相关的依赖模块,如下: - pip install django==3.2.3 - pip install captcha - pip install django-cors-headers - pip install djangorestframework - pip install django-simple-captcha - pip install django_apscheduler 然后启动start.sh脚本即可启动。 ## 3、数据库连接池配置 如果使用pymysql连接数据库参考mysite目录中的mysql_db.py中的数据库连接池的配置,参考 [Python实现Mysql数据库连接池](https://www.jianshu.com/p/53262bb292e5) 如果使用Django,修改settings.py中的DATABASES的配置,如下: ``` DATABASES = { 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # Django默认使用的是sqlite3数据库 # 'NAME': BASE_DIR / 'db.sqlite3', # 指定数据库所在的路径 # 'ENGINE': 'django.db.backends.mysql', # 表示使用的是mysql数据库的引擎 'ENGINE': 'mysql', # 表示使用的是mysql文件夹中数据库的引擎,就是本项目目录mysql下的数据库连接,使用连接池的技术避免耗尽数据库的连接 'POOL_SIZE': 20, # 每个进程的连接池的大小,总连接数=20*总进程数 'NAME': 'python_project', # 数据库的名字,可以在mysql的提示符下先创建好 'USER': 'root', # 数据库用户名 'PASSWORD': 'bmkjxt', # 数据库密码 'HOST': '154.222.22.76', # 数据库主机,留空默认为"localhost" 'PORT': '3306', # 数据库使用的端口 # 配置settings的CONN_MAX_AGE参数,使其小于数据库的wait_timeout配置,使用 SHOW GLOBAL VARIABLES LIKE "%wait%" 查看wait_timeout的sql, # 比mysql默认的wait_timeout小10秒,mysql数据库默认的是28800,即8小时,超过8小时,MySQL就会放弃连接 'CONN_MAX_AGE': 28790, # 连接的存活时间,在存活时间内,连接是可以重用的 'CHARSET': 'utf-8', 'OPTIONS': { 'init_command': 'SET default_storage_engine=INNODB', } } } ``` 然后进入mysql目录下面增加__init__.py空文件和base.py文件。 ## 4、django-apscheduler的实现定时调度 django-apscheduler 可以动态的添加任务,删除任务等操作。 ### 4.1、安装模块 pip install django-apscheduler ### 4.2、使用 1. 在django项目修改settings.py文件,在INSTALLED_APPS中加入django-apscheduler应用: ``` INSTALLED_APPS = [ ...... 'django_apscheduler',#定时执行任务 ] ``` 2. 执行迁移命令(创建两张表:django_apscheduler_djangojob和django_apscheduler_djangojobexecution): python manage.py migrate 3. 新建一个task包并在下面建一个task.py文件,内容如下: ``` from datetime import datetime from apscheduler.schedulers.background import BackgroundScheduler from django_apscheduler.jobstores import DjangoJobStore, register_job, register_events # 实例化调度器 try: scheduler = BackgroundScheduler() # 调度器使用DjangoJobStore() scheduler.add_jobstore(DjangoJobStore(), "default") """ 另一种方式为每天固定时间执行任务,对应代码为: @register_job(scheduler, 'cron', day_of_week='mon-fri', hour='9', minute='30', second='10',id='task_time') 设置定时任务,选择方式为interval,时间间隔为10s id: 任务的名字,不传的话会自动生成。不过为了之后对任务进行暂停、开启、删除等操作,建议给一个名字。并且是唯一的,如果多个任务取一个名字,之前的任务就会被覆盖。 """ @register_job(scheduler, "interval", seconds=1, id="test_func") def my_job(): print("Schedule start!") # 这里写你要执行的任务 func() register_events(scheduler) scheduler.start() except Exception as e: print(e) # 有错误就停止定时器 scheduler.shutdown() def func(): now = datetime.now() ts = now.strftime('%Y-%m-%d %H:%M:%S') print('do func time :', ts) ``` 4. 然后在任意一个views.py中引入scheduler,我这里在blog的views.py中引入了该定时任务,使得每次启动Django框架时,定时任务都会同时启动。 ``` # 引入定时任务的执行 from task.task import scheduler ``` 5. 使用manage.py runserver 启动,定时任务会自动启动,数据库的django_apscheduler_djangojob和django_apscheduler_djangojobexecution这两个表会增加相应的定时任务内容, 当程序关闭后,这两张表中的数据清空,然后重新执行定时任务,不然会报apscheduler.schedulers.SchedulerNotRunningError: Scheduler is not running的错误。 参考[Django------详解django-apscheduler的使用方法](https://www.cnblogs.com/huangjiyong/p/12492281.html) ## 5、爬虫与反爬虫处理网页的爬取 参考[Python3网络爬虫快速入门实战解析](https://blog.csdn.net/c406495762/article/details/78123502) ## 6、发送请求 发送请求请参考order项目的order_views.py的 get_transaction_detail_by_tx_id 方法。 ## 7、日志配置 日志配置参考order项目的order_views.py的 get_transaction_detail_by_tx_id 方法,print相关的打印、启动日志会在控制台或者在linux中(如server.sh中的run.log)输出,配置的日志框架打印的日志(比如get_transaction_detail_by_tx_id日志打印方法)会输出到mysite.log日志文件中。 ## 8、说明 Python3的相关高级语法请参考blog项目中的views模块最上边相关实例的说明,图片相关的处理参考image下面的相关实例(包括灰度化处理、图像二值化处理等)。 ## 9、Python干货 [干货!学习 Python 看这篇管够!!!](https://mp.weixin.qq.com/s/OpkcjU6uCkCdDLx3Y9aGWg) 下面的小节也是根据这个干货地址进行学习记录的。 ## 10、python标识符 合法的 Python 标识符,需要遵守如下规定: 第一个字符必须是字母或下划线(_) 剩下的字符可以是字母和数字或下划线 大小写敏感 不能是 Python 的关键字,例如 def、class 就不能作为标识符 以下划线开头的标识符是有特殊意义的。以单下划线开头 _foo 的代表不能直接访问的类属性,需通过类提供的接口进行访问,不能用 from xxx import * 而导入。 以双下划线开头的 __foo 代表类的私有成员,以双下划线开头和结尾的 __foo__ 代表 Python 里特殊方法专用的标识,如 __init__() 代表类的构造函数。 Python 可以同一行显示多条语句,方法是用分号 ; 分开,如: > > > print("hello");print("world"); 结果如下: hello world ## 11、多行语句 Python 语句中一般以新行作为为语句的结束符,但是我们可以使用斜杠( \)将一行的语句分为多行显示,如下所示: total = item_one + \ item_two + \ item_three 语句中包含[], {} 或 () 括号就不需要使用多行连接符。如下实例: ``` days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'] ``` ## 12、print输出 print() 默认输出是换行的,如果要实现不换行需要加上end参数。 ``` x="a" y="b" print(x, end=' ') print(y, end=' ') ``` ## 13、Python 变量与数据类型 Python 中的变量不需要声明,每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 在 Python 中,变量就是变量,它没有类型,我们所说的"类型"是变量所指的内存中对象的类型。 name = "neo" 上述代码声明了一个变量,变量名为:name, 变量 name 的值为"neo"。 ### 13.1、变量赋值 在 Python 中,等号 = 是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量。 a = 123 # a 是整数 a = 'abc' # a 是字符串 这种变量本身类型不固定的语言称之为动态语言,与之对应的就是静态语言。静态语言在定义变量时必须指定变量类型, 如果赋值的时候类型不匹配,就会报错。例如 Java 是静态语言,这样赋值就会报错: #### 13.1.1、多个变量赋值 Python 允许你同时为多个变量赋值。 例如:a = b = c = 1 以上实例,创建一个整型对象,值为 1,从后向前赋值,三个变量被赋予相同的数值。 您也可以为多个对象指定多个变量。例如: a, b, c = 1, 2, "neo" 以上实例,两个整型对象 1 和 2 的分配给变量 a 和 b,字符串对象 "neo" 分配给变量 c。 ### 13.2、常量 所谓常量就是不能变的变量,比如常用的数学常数 π 就是一个常量。在 Python 中,通常用全部大写的变量名表示常量: BI = 3.14 但事实上 BI 仍然是个变量,Python 根本无法保证 BI 不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果你一定要改,语法也不会报错。 ### 13.3、数据类型 Python3 中有六个标准的数据类型:Number(数字)、String(字符串)、List(列表)、Tuple(元组)、Sets(集合)、Dictionary(字典)。 Python3 的六个标准数据类型中: 不可变数据(3 个):Number(数字)、String(字符串)、Tuple(元组); 可变数据(3 个):List(列表)、Dictionary(字典)、Set(集合)。 #### 13.3.1、数字 Python3 支持 int、float、bool、complex(复数)。 数字类型是顾名思义是用来存储数值的,需要记住的是,有点和 Java 的字符串味道差不多,如果改变了数字数据类型的值,将重新分配内存空间。 Python 支持三种不同的数值类型: 整型(Int) - 通常被称为是整型或整数,是正或负整数,不带小数点。Python3 整型是没有限制大小的,可以当作 Long 类型使用,所以 Python3 没有 Python2 的 Long 类型。 浮点型(float) - 浮点型由整数部分与小数部分组成,浮点型也可以使用科学计数法表示(2.5e2 = 2.5 x 102 = 250) 复数( (complex)) - 复数由实数部分和虚数部分构成,可以用a + bj,或者complex(a,b)表示, 复数的实部a和虚部b都是浮点型。 ##### 13.3.1.1、数字类型转换 int(x) 将x转换为一个整数。 float(x) 将x转换到一个浮点数。 complex(x) 将x转换到一个复数,实数部分为 x,虚数部分为 0。 complex(x, y) 将 x 和 y 转换到一个复数,实数部分为 x,虚数部分为 y。x 和 y 是数字表达式。额外说明 ##### 13.3.1.2、数值运算 和别的语言一样,数字类型支持各种常见的运算,不过 Python 的运算比别的大多数常见语言都更加丰富,此外,还有大量丰富的方法,提供更高效的开发。 数值运算示例: ``` print (5 + 4) # 加法 输出 9 print (4.3 - 2) # 减法 输出 2.3 print (3 * 7) # 乘法 输出 21 print (2 / 4) # 除法,得到一个浮点数 输出 0.5 print (2 // 4) # 除法,得到一个整数 输出 0 print (17 % 3) # 取余 输出 2 print (2 ** 5) # 乘方 输出 32 ``` #### 13.3.2、字符串 创建字符串可以使用单引号、双引号、三单引号和三双引号,其中三引号可以多行定义字符串,Python 不支持单字符类型,单字符在Python也是作为一个字符串使用。 我们定义一个 s='python'语句,它在计算机中的执行顺序是先在内存中创建一个字符串 Python ,在程序栈寄存器中创建一个变量 s,最后把 Python 的地址赋给s 。 再来看看字符串的一些常见操作: ``` s = '学习Python' # 切片 s[0], s[-1], s[3:], s[::-1] # '学', 'n', 'ython', 'nohtyP的习学' # 替换,还可以使用正则表达式替换 s.replace('Python', 'Java') # '学习Java' # 查找,find()、index()、rfind()、rindex() s.find('P') # 2, 返回第一次出现的子串的下标 s.find('h', 2) # 5, 设定下标2开始查找 s.find('23333') # -1, 查找不到返回-1 s.index('y') # 3, 返回第一次出现的子串的下标 s.index('P') # 不同与find(), 查找不到会抛出异常 # 转大小写, upper()、lower()、swapcase()、capitalize()、istitle()、isupper()、islower() s.upper() # '学习PYTHON' s.swapcase() # '学习pYTHON', 大小写互换 s.istitle() # True s.islower() # False # 去空格,strip()、lstrip()、rstrip() # 格式化 s1 = '%s %s' % ('Windrivder', 21) # 'Windrivder 21' s2 = '{}, {}'.format(21, 'Windridver') # 推荐使用format格式化字符串 s3 = '{0}, {1}, {0}'.format('Windrivder', 21) s4 = '{name}: {age}'.format(age=21, name='Windrivder') # 连接与分割,使用 + 连接字符串,每次操作会重新计算、开辟、释放内存,效率很低,所以推荐使用join l = ['2017', '03', '29', '22:00'] s5 = '-'.join(l) # '2017-03-29-22:00' s6 = s5.split('-') # ['2017', '03', '29', '22:00'] ``` 另外还有一点需要注意的是字符串编码,所有的 Python 字符串都是 Unicode 字符串,当需要将文件保存到外设或进行网络传输时,就要进行编码转换,将字符转换为字节,以提高效率。 ``` # encode 将字符转换为字节 str = '学习Python' print (str.encode()) # 默认编码是 UTF-8 输出:b'\xe5\xad\xa6\xe4\xb9\xa0Python' print (str.encode('gbk')) # 输出 b'\xd1\xa7\xcf\xb0Python' # decode 将字节转换为字符 print (str.encode().decode('utf8')) # 输出 '学习Python' print (str.encode('gbk').decode('gbk')) # 输出 '学习Python' ``` #### 13.3.3、List(列表) 类似 Java List 集合接口 列表是写在方括号 [] 之间、用逗号分隔开的元素列表,列表可以完成大多数集合类的数据结构实现。列表中元素的类型可以不相同,它支持数字,字符串甚至可以包含列表(所谓嵌套),列表中的元素是可以改变。 ``` Weekday = ['Monday','Tuesday','Wednesday','Thursday','Friday'] print(Weekday[0]) # 输出 Monday #list 搜索 print(Weekday.index("Wednesday")) #list 增加元素 Weekday.append("new") print(Weekday) # list 删除 Weekday.remove("Thursday") print(Weekday) ``` #### 13.3.4、Tuple(元组) 元组(tuple)与列表类似,不同之处在于元组的元素不能修改。元组写在小括号 () 里,元素之间用逗号隔开,组中的元素类型也可以不相同。 元组是一个不可变的list,里面的元素不可以修改,诸如列表的插入元素、删除元素、添加元素、清空元素、修改元素等功能,如果元组里面有列表,修改列表里面的数据则是支持的, 在元组中通通没有,你是无法对其进行修改的, python支持多个返回值,使用元组来实现多个返回值。 元组创建很简单,只需要在括号中添加元素(不需要括号也可以),并使用逗号隔开即可。 ==注意:== 元组中只包含一个元素时,需要在元素后面添加逗号,否则括号会被当作运算符使用。 ``` >>> TupNum = (34) >>> type(TupNum) # 不加逗号是整型 >>> TupNum = (34,) >>> type(TupNum) # 加上逗号变元组 ``` 元组中的值一旦定义就不能修改,但是我们可以通过元组与元组之间的连接关系来对元组进行修改。 复制元组: ``` tup1 = ('abc') # 元组复制需要加上分隔符后面复制的内容就按照分隔符分隔开 (tup1,) * 3 ('abc', 'abc', 'abc') ``` 将列表转换为元组调用tuple(seq)方法 ``` letters = ('a', 'b', 'c', 'd', 'e', 'f', 'g') print(letters[0]) # 输出 'a' print(letters[0:3]) # 输出一组 ('a', 'b', 'c') ``` ##### 13.3.4.1、具名元组 具名元组又可以称之为带字段名的记录,实现带字段名,需要一个库(collections)的支持,你需要导入它,如下: ``` from collections import namedtuple # 生成一个City类 City = namedtuple("City", "name country polulation coordinates") # 实例化 tokyo = City("Tokyo", 'JP', '36.93', ('35.68','139,69')) print(tokyo) # City(name='Tokyo', country='JP', polulation='36.93', coordinates=('35.68', '139,69')) print(tokyo.name) # Tokyo ``` #### 13.3.5、Sets(集合) 类似 Java Set 集合接口。 集合(set)是一个无序不重复元素的序列,使用大括号 {} 或者 set() 函数创建集合,注意:创建一个空集合必须用 set() 而不是 {} ,因为 {} 是用来创建一个空字典。 集合不能被切片也不能被索引,除了做集合运算之外,集合元素可以被添加还有删除: ``` a_set = {1, 2, 3, 4} # 添加 a_set.add(5) print(a_set) # 输出{1, 2, 3, 4, 5} # 删除 a_set.discard(5) print(a_set) # 输出{1, 2, 3, 4} ``` #### 13.3.6、Dictionary(字典) 类似 Java Map 集合接口 字典是一种映射类型,它的元素是键值对,字典的关键字必须为不可变类型,且不能重复。创建空字典使用 {} 。 ``` Logo_code = { 'BIDU': 'Baidu', 'SINA': 'Sina', 'YOKU': 'Youku' } print(Logo_code) # 输出{'BIDU': 'Baidu', 'YOKU': 'Youku', 'SINA': 'Sina'} print(Logo_code['SINA']) # 输出键为 'SINA' 的值 结果为Sina print(Logo_code.keys()) # 输出所有键 结果为 dict_keys(['BIDU', 'SINA', 'YOKU']) print(Logo_code.values()) # 输出所有值 结果为dict_values(['Baidu', 'Sina', 'Youku']) print(len(Logo_code)) # 输出字段长度 结果为3 ``` ## 14、Python 流程控制 ### 14.1、for 循环 ``` for 后跟变量名,in 后跟序列,注意加冒号 for 循环每次从序列中取一个值放到变量中 此处的序列主要指 列表 元组 字符串 文件 ``` 示例: ``` for letter in 'Python': # 第一个实例 print('当前字母 :', letter) fruits = ['banana', 'apple', 'mango'] for fruit in fruits: # 第二个实例 print('当前字母 :', fruit) print("Good bye!") ``` ### 14.2、range() 函数 如果你需要一个数值序列,内置函数 range() 会很方便,它生成一个等差级数链表: 语法: range (start, end, scan): 参数含义: start:计数从 start 开始。默认是从 0 开始。例如 range(5) 等价于 range(0, 5); end:计数到 end 结束,但不包括 end.例如:range(0, 5) 是[0, 1, 2, 3, 4]没有 5 scan:每次跳跃的间距,默认为1。例如:range(0, 5) 等价于 range(0, 5, 1) ``` for i in range(6): print(i) print(range(6), 'finish') # 打印结果:0 1 2 3 4 5 for i in range(6, 10): print(i) print(range(6, 10), 'finish') # 打印结果:6 7 8 9 for i in range(6, 12, 2): print(i) print(range(6, 12, 2), 'finish') # 打印结果:6 8 10 ``` 需要迭代链表索引的话,如下所示结合使 用 range() 和 len(): ``` a = ['i', 'love', 'coding', 'and', 'free'] for i in range(len(a)): print(i, a[i]) ``` ### 14.3、break 用法 break 语句可以跳出 for 和 while 的循环体。如果你从 for 或 while 循环中终止,任何对应的循环 else 块将不执行。 示例: ``` for letter in 'ityouknow': # 第一个实例 if letter == 'n': # 字母为 n 时中断 break print('当前字母 :', letter) ``` 打印结果: ``` 当前字母 : i 当前字母 : t 当前字母 : y 当前字母 : o 当前字母 : u 当前字母 : k ``` ### 14.4、pass 语句 Python pass 是空语句,是为了保持程序结构的完整性。它用于那些语法上必须要有什么语句,但程序什么也不做的场合. ``` while True: pass # Busy-wait for keyboard interrupt (Ctrl+C) # 这通常用于创建最小结构的类: class MyEmptyClass: pass ``` ## 15、函数 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段,所以我经常说函数是程序员规模化使用的基础。 函数能提高应用的模块性,和代码的重复利用率。在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。善于利用函数,可以减少重复编写程序段的工作量。 定义一个函数有如下几个步骤: 函数代码块以 def 关键词开头,后接函数标识符名称和圆括号()。 任何传入参数和自变量必须放在圆括号中间。圆括号之间可以用于定义参数。 函数的第一行语句可以选择性地使用文档字符串—用于存放函数说明。 函数内容以冒号起始,并且缩进。 return 表达式 结束函数,选择性地返回一个值给调用方。不带表达式的return相当于返回 None。 ``` def 函数名(参数列表): 函数体 ``` 默认情况下,参数值和参数名称是按函数声明中定义的顺序匹配起来的。 ### 15.1、多个返回值 某些情况下,我们需要一个函数返回多个值,Python 也是支持的。 ``` #定义多个返回值函数 def more(x, y): nx = x + 2 ny = y - 2 return nx, ny #调用函数 x, y = more(10, 10) print(x, y) ``` ### 15.2、函数的参数 函数在初始化时,默认值只会执行一次,所以在默认值为可变对象(列表、字典以及大多数类实例),我们可以如下操作: ``` def f(a, l=None): if l is None: l = [] l.append(a) return l # 再次调用函数 print(f(1)) print(f(2)) print(f(3)) # 返回值 # [1] # [2] # [3] ``` #### 15.2.1、可变参数 可变参数也就是我们对于函数中定义的参数是可以一个或多个可以变化的,其中 *args代表着可以传入一个list或者tuple, **args代表着可以传入一个dict。举个例子: ``` def variable_fun(kind, *arguments, **keywords): print("friend : ", kind, ";") print("-" * 40) for arg in arguments: print(arg) print("-" * 40) for kw in keywords: print(kw, ":", keywords[kw]) # 函数调用 variable_fun("xiaoming", "hello xiaoming", "nice to meet you!", mother="xiaoma", father="xiaoba", son="see you") ``` 结果如下: ``` friend : xiaoming ; ---------------------------------------- hello xiaoming nice to meet you! ---------------------------------------- mother : xiaoma father : xiaoba son : see you ``` #### 15.2.2、关键字参数 关键字参数允许你调用函数时传入0个或任意个含参数名的参数,这样可以让我们灵活的去进行参数的调用。举个例子: ``` def key_fun(voltage, state='a stiff', action='voom', type='Norwegian Blue'): print("-- This key_fun wouldn't", action, end=' ') print("if you put", voltage, "volts through it.") print("-- Lovely plumage, the", type) print("-- It's", state, "!") # 函数调用 key_fun(1000) # 1 positional argument print("-" * 40) key_fun(voltage=1000) # 1 keyword argument print("-" * 40) key_fun(voltage=1000000, action='VOOOOOM') # 2 keyword arguments print("-" * 40) key_fun(action='VOOOOOM', voltage=1000000) # 2 keyword arguments print("-" * 40) key_fun('a million', 'bereft of life', 'jump') # 3 positional arguments print("-" * 40) key_fun('a thousand', state='pushing up the daisies') # 1 positional, 1 keyword ``` 打印结果如下: ``` -- This key_fun wouldn't voom if you put 1000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! ---------------------------------------- -- This key_fun wouldn't voom if you put 1000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! ---------------------------------------- -- This key_fun wouldn't VOOOOOM if you put 1000000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! ---------------------------------------- -- This key_fun wouldn't VOOOOOM if you put 1000000 volts through it. -- Lovely plumage, the Norwegian Blue -- It's a stiff ! ---------------------------------------- -- This key_fun wouldn't jump if you put a million volts through it. -- Lovely plumage, the Norwegian Blue -- It's bereft of life ! ---------------------------------------- -- This key_fun wouldn't voom if you put a thousand volts through it. -- Lovely plumage, the Norwegian Blue -- It's pushing up the daisies ! ``` 注意不可以重复传值,否则会报如下错误: ``` # TypeError: key_fun() got multiple values for argument 'voltage' key_fun(100, voltage=1000) # error ``` ### 15.3、高阶函数 在函数式编程中,我们可以将函数当作变量一样自由使用。一个函数接收另一个函数作为参数,这种函数称之为高阶函数。 ``` def high_func(f, arr): return [f(x) for x in arr] def square(n): return n ** 2 # 使用自定义函数 print(high_func(square, list(range(10)))) 打印结果: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] ``` #### 15.3.1、高阶函数map 根据提供的函数对指定序列做映射, 并返回映射后的序列,定义: map(func, *iterables) --> map object function # 序列中的每个元素需要执行的操作, 可以是匿名函数 *iterables # 一个或多个序列 正如前面所举的例子 high_func 函数, map 函数是 high_func 函数高阶版,可以传入一个函数和多个序列。 ``` from math import factorial def square(n): return n ** 2 # 使用python自带数学函数 facMap = map(factorial, list(range(10))) print(list(facMap)) # print out: [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880] # 使用自定义函数 squareMap = map(square, list(range(10))) print(list(squareMap)) # print out: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] ``` 可以看到输出了同样的结果,只是与 python2.X 不用的是, python3.X 中返回 map类 ,而前者直接返回一个列表。 我们使用匿名函数,也可以传入多个序列,如下: ``` # 使用匿名函数 lamMap = map(lambda x: x * 2, list(range(10))) print(list(lamMap)) # print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # 传入多个序列 mutiMap = map(lambda x, y: x+y, list(range(10)), list(range(11, 15))) print(list(mutiMap)) # print out: [11, 13, 15, 17] ``` #### 15.3.2、高阶函数reduce 大致上来讲, reduce 函数需要传入一个有两个参数的函数,然后用这个函数从左至右顺序遍历序列并生成结果,定义如下: reduce(function, sequence[, initial]) -> value function # 函数, 序列中的每个元素需要执行的操作, 可以是匿名函数 sequence # 需要执行操作的序列 initial # 可选,初始参数 最后返回函数的计算结果, 和初始参数类型相同 简单举个例子: ``` # 注意,现在 reduce() 函数已经放入到functools包中。 from functools import reduce result = reduce(lambda x, y: x + y, [1, 2, 3, 4, 5]) print(result) # print out 15 ``` 我们可以看到,序列 [1, 2, 3, 4, 5] 通过匿名函数进行了累加。 设定初始值: ``` # 设定初始参数: s = reduce(lambda x, y: x + y, ['1', '2', '3', '4', '5'], "数字 = ") print(s) # print out:数字 = 12345 ``` 需要注意的是:序列数据类型需要和初始参数一致。 #### 15.3.3、高阶函数filter filter() 函数用来过滤序列中不符合条件的值,返回一个迭代器,该迭代器生成那些函数(项)为true的iterable项。如果函数为None,则返回为true的项。定义如下: filter(function or None, iterable) --> filter object function or None # 过滤操作执行的函数 iterable # 需要过滤的序列 举个例子: ``` def boy(n): if n % 2 == 0: return True return False # 自定义函数 filterList = filter(boy, list(range(20))) print(list(filterList)) # print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] # 自定义函数 filterList2 = filter(lambda n: n % 2 == 0, list(range(20))) print(list(filterList2)) # print out: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] ``` 上面我们可以看到,列表中不能被 2 整除的数据都被排除了。 #### 15.3.4、高阶函数sorted sorted 函数默认将序列升序排列后返回一个新的 list,还可以自定义键函数来进行排序,也可以设置 reverse 参数确定是升序还是降序,如果 reverse = True 则为降序。函数定义如下: def sorted(iterable: Iterable[_T], *, key: Optional[Callable[[_T], Any]] = ..., reverse: bool = ...) -> List[_T]: ... iterable # 序列 key # 可以用来计算的排序函数。 reverse # 排序规则,reverse = True降序,reverse = False 升序(默认)。 举个简单例子: ``` list01 = [5, -1, 3, 6, -7, 8, -11, 2] list02 = ['apple', 'pig', 'monkey', 'money'] print(sorted(list01)) # print out: [-11, -7, -1, 2, 3, 5, 6, 8] print(sorted(list01, key=abs)) # print out: [-1, 2, 3, 5, 6, -7, 8, -11] # 默认升序 print(sorted(list02)) # print out: ['apple', 'money', 'monkey', 'pig'] # 降序 print(sorted(list02, reverse=True)) # print out: ['pig', 'monkey', 'money', 'apple'] # 匿名函数排序 print(sorted(list02, key=lambda x: len(x), reverse=True)) # print out: ['monkey', 'apple', 'money', 'pig'] ``` ## 16、Python 模块和包 ### 16.1、模块、包、库之间的概念 模块(module)其实就是 py 文件,里面定义了一些函数、类、变量等 包(package)是多个模块的聚合体形成的文件夹,里面可以是多个 py 文件,也可以嵌套文件夹 库是参考其他编程语言的说法,是指完成一定功能的代码集合,在 Python 中的形式就是模块和包 模块其实一个 py 文件,用来封装一组功能;包是将一类模块归集到一起,比模块的概念更大一些;库就是由其它程序员封装好的功能组,一般比包的概念更大一些。 ``` # 导入模块 import hello # 现在可以调用模块里包含的函数了 hello.sayhello() ``` 一个模块只会被导入一次,不管你执行了多少次import。这样可以防止导入模块被一遍又一遍地执行。 ### 16.2、from ... import ... 模块提供了类似名字空间的限制,允许 Python 从模块中导入指定的符号(变量、函数、类等)到当前模块。导入后,这些符号就可以直接使用,而不需要前缀模块名。 语法如下: ``` from modname import name1[, name2[, ... nameN]] ``` 例如,要导入模块 hello 的 sayhello 函数,使用如下语句: ``` ## 直接导入方法 from hello import sayhello sayhello() ``` ### 16.3、from … import * 语句 把一个模块的所有内容全都导入到当前的命名空间也是可行的,只需使用如下声明: from modname import * 这提供了一个简单的方法来导入一个模块中的所有项目。 我们在 hello.py 中再添加一个 world 方法。 ``` def world(): print("Python World!") ``` 在 do.py 文件中引入全部方法进行调用。 ``` ## 导入所有方法 from hello import * sayhello() world() ``` 执行后输出: Hello World! Python World! 证明 hello 模块中的两个方法都可以直接调用,实际项目中不推荐被过多地使用。 ### 16.4、包 包(package)是 Python 中对模块的更高一级的抽象。简单来说,Python 允许用户把目录当成模块看待。这样一来,目录中的不同模块文件,就变成了「包」里面的子模块。此外,包目录下还可以有子目录,这些子目录也可以是 Python 包。这种分层,对模块识别、管理,都是非常有好处的。 特别地,对于一些大型 Python 工具包,内里可能有成百上千个不同功能的模块。科学计算领域,SciPy, NumPy, Matplotlib 等第三方工具,都是用包的形式发布的。 #### 16.4.1、包定义 常见的包结构如下: ``` pakageName -------__init__.py -------moduleName1.py -------moduleName2.py ------- ... ``` 包路径下必须存在 __init__.py 文件。 示例: ``` 我们创建一个 cal 的包,包中有一个计算器的 model ,结构如下: cal -------__init__.py -------calculator.py calculator.py 模块的代码如下: def add(a,b) : return a+b def reduce(a,b) : return a-b def multiply(a,b) : return a*b def divide(a,b) : return a/b ``` #### 16.4.2、使用Python包 Python 包的使用和模块的使用类似,下面是导入的语法: import 包名.包名.模块名 比如我们在 do.py 中导入 calculator.py ``` # 导入包 import cal.calculator # 使用包的模块的方法 print(cal.calculator.add(1,2)) ``` 但是导入调用的时候包名比较长,这样就可以使用from ... import ...语句来简化一下。 ``` # 导入包 from cal import calculator # 使用包的模块的方法 print(calculator.multiply(3,6)) ``` 当包名越来越长的时候效果也会越好。 ## 17、Python 数据结构--序列 ### 17.1、序列概览 Python中有6种内建的序列。其中列表和元组是最常见的类型。其他包括字符串、Unicode字符串、buffer对象和xrange对象。 列表和元组的主要区别是列表可以修改,而元组不能,如果要根据要求来添加元素,这时候列表适用性会更好,但是当序列不能修改的时候,使用元组则更合适,使用元组与Python的运作方式相关。 在python中几乎所有的情况下列表都可以替代元组,但是特殊情况不能(当使用元组作为字典的不可修改的键时,此时键不能修改,所以不能使用列表) 例如: ``` #定义一个学生序列 >>> stuinfo=['zhangsan','lisi','wangwu',18,20] ``` 同时序列中还可以包含序列,例如数据库:database ``` # 定义学生姓名和学生年龄,然后再定义一个属于自己的数据库将两个列表加入 > > > stuname=['zhangsan','lisi','wangwu'] > > > stuage=[18,20,16] > > > database=[stuname,stuage] > > > database [['zhangsan', 'lisi', 'wangwu'], [18, 20, 16]] ``` 注意: python还有一种名为容器(container)的数据结构,容器可以包含其他任意对象,容器主要包括序列和映射(例如:字典)两类。序列的每个元素都有自己的编号,而映射每个元素则有一个叫做“键”的名字。集合是另一种容器(在后面章节会作详细讲解)。 ### 17.2、通用序列操作 python中所有序列类型都可以进行一些特定的操作,这些操作包括:索引、分片、相加、相乘以及检查某个元素是否属于序列的成员-->成员资格检测 除此之外,Python还有计算序列长度,找出最大和最小元素的内建函数。 #### 17.2.1、索引 序列中所有元素都有编号,这些编号是从0开始,依次递增,访问这些元素通过下标即可访问,而这个编号就是索引,例如: ``` >>> database[0] ['zhangsan', 'lisi', 'wangwu'] >>> database[1] [18, 20, 16] ``` ``` # 字符串序列的索引 > > > str='hello' > > > str[0] 'h' > > > str[1] 'e' ``` 注意: 字符串就是一个由字符组成的序列,索引0指向第一个元素 以上这就是索引,所有序列中都可以通过元素编号的方式进行索引,当使用负数索引时,Python会从右边到左进行索引,-1是从序列的最后一个元素开始,如下: ``` # 从最后一个元素开始 > > > str[-1] 'o' # 从倒数第二个元素开始 > > > str[-2] 'l' ``` #### 17.2.2、分片 同样的和索引类似,分片是通过冒号操作来访问一定范围内的元素,例如: ``` # 构建一个序列tag,里面包含一个元素 >>> tag=['https://www.cnblogs.com/yangyuqig/p/10101663.html'] # 拿到这个元素后通过分片取出一个范围的值 >>> tag[0][0:24] 'https://www.cnblogs.com/' ``` 由上所知,分片操作的的实现需要提供两个索引作为边界,是一个左闭右开的区间,也就是第1个索引包含在分片内,而第2个索引不包含在这个分片内,例如: ``` >>> num=[1,2,3,4,5,6,7,8,9,10] # 表示从第四个到最后一个元素 >>> num[3:10] [4, 5, 6, 7, 8, 9, 10] ``` 分片步长操作: 分片操作可以给元素设置步长,在开始和结束的时候指定相应步长获取元素,例如: ``` # 按照步长为2返回第1个和第6个之间的元素 num=[1,2,3,4,5,6,7,8,9,10] >>> num[0:6:2] [1, 3, 5] ``` 另外需要注意的是负数步长是从元素尾部到前遍历整个序列,所以负数的分片开始索引一定要大于结束索引 ``` >>> num[7:-1] [8, 9] ``` 当开始索引和结束索引是负数时开始索引必须小于结束索引: ``` >>> num[-9:-1] [2, 3, 4, 5, 6, 7, 8, 9] ``` 对于一个正数步长,Python会从序列的头部开始向右提取元素,直达最后一个元素,而对于负数步长,则是从序列的尾部开始向左提取元素,直达提取到第一个,例如: ``` # 提取前6个元素,步长为2 >>> num[:6:2] [1, 3, 5] # 提取从后往前的8个元素,步长为2 >>> num[:2:-2] [10, 8, 6, 4] ``` #### 17.2.3、序列相加 注意:只有相同类型的序列才能进行连接操作,序列相加通过加号“+”进行序列和序列之间的连接操作: ``` >>> 'hello'+' world !' 'hello world !' >>> [1,2,3]+['zhangsan','lisi','wangwu'] [1, 2, 3, 'zhangsan', 'lisi', 'wangwu'] ``` #### 17.2.4、序列相乘 一个数字x乘以一个序列会产生一个新的序列,原来的序列会被重置成x次 ``` >>> ['hello'+' world !']*3 ['hello world !', 'hello world !', 'hello world !'] ``` #### 17.2.5、检查一个元素是否在一个序列 检查一个元素是否在一个序列中使用运算符in进行检查, in运算符返回检查某个条件的布尔值,若为真返回true,否则返回false,例如: ``` >>> str='hello' >>> 'h' in str True >>> 'x' in str False ``` #### 17.2.6、序列长度、最大值和最小值 序列长度、最大值和最小值使用内建函数len、max、min进行检测,len返回序列中所包含的元素数量,max和min分别返回序列中最大值和最小值的元素 ``` >>> len([11,34,23]) 3 >>> max(11,34,23) 34 >>> min(11,34,23) 11 ``` ## 18、Python 列表 ### 18.1、列表函数 #### 18.1.1、list函数 如果对字符串赋值后想要改变字符串中的某个值,因为字符串不能像列表一样可更改,如果想改变这时候可以利用 list 函数,如下: ``` > > > ll=list('hello') > > > ll ['h', 'e', 'l', 'l', 'o'] > > > ll[2] 'l' > > > ll[2]='5' > > > ll ['h', 'e', '5', 'l', 'o'] ``` 注意:list 函数适用于所有类型的序列,而不只是字符串 #### 18.1.2、len函数 len 函数返回列表中的元素个数 ``` >>> list1 = ['baidu', 'google', 12, 23]; >>> len(list1) 4 ``` #### 18.1.3、max 函数 ``` max 函数返回列表元素最大值 >>> list_num=[2,3,5,6,8,12] >>> max(list_num) 12 ``` #### 18.1.4、min 函数 返回列表元素最小值 ``` >>> list_num=[2,3,5,6,8,12] >>> min(list_num) 2 ``` ### 18.2、列表方法 列表提供了几个详细的方法,这些方法用于检查或者修改列表中的内容 #### 18.2.1、append方法 append 方法用于在列表的末尾追加新的内容 ``` list_append = [1,2,3,4] list_append.append(5) list_append [1, 2, 3, 4, 5] ``` #### 18.2.2、count方法 count 方法用于统计某个元素在列表中出现的次数 ``` num [1, 2, 3, 4, 5, 5, 5, 5, 6] # 统计num列表中5出现的次数 num.count(5) 4 # 统计字母a出现的次数 name=['a','a','abf','ark','nhk'] name.count('a') 2 ``` #### 18.2.3、extend方法 extend 方法表示追加内容,它可以在列表的末尾一次性追加另一个序列中的多个值,也就是用新列表扩展原有列表 ``` a =[1,2,3] b = [4,5,6] # 将列表b追加在列表a后面 a.extend(b) a [1, 2, 3, 4, 5, 6] ``` 此操作和列表的相加操作,但是追加操作改变原有列表,而相加不改变原有列表,例如: ``` a = [1,2,3] b = [4,5,6] a + b [1, 2, 3, 4, 5, 6] # 连接操作不改变原有列表,下面的a仍然是[1, 2, 3] a [1, 2, 3] ``` #### 18.2.4、index方法 index 方法用于从列表中找出某个元素第一次匹配的位置的索引位置 ``` content = ['where','who','lisi','cntent','who'] content.index('who') 1 ==注意:== 以上方法中有两个位置有‘who’,但是只找出了第一次匹配的索引位置元素 ``` #### 18.2.5、insert方法 insert 方法用于向列表中插入对象 ``` num = [1,2,5,6,7] num.insert(2,3) num [1, 2, 3, 5, 6, 7] num.insert(3,4) num [1, 2, 3, 4, 5, 6, 7] ``` #### 18.2.6、pop方法 pop 方法会移除列表中的一个元素(默认是最后一个),并且返回该元素的值 ``` x = [1,2,3] x.pop() 3 x [1, 2] x.pop() 2 x [1] ``` 注意:pop 方法是唯一一个既能修改列表又能返回元素值的方法(除了None),pop 和 append 方法是 Python 中数据结构的出栈和入栈,如果追加(append)刚刚出栈(pop)的值,得到的还是原来的列表 ``` x = [1,2,3] x.append(x.pop()) x [1, 2, 3] ``` #### 18.2.7、remove方法 remove 方法用于移除列表中第一个匹配的元素 ``` content=['where', 'who', 'lisi', 'cntent', 'who', 'who'] # 移除了第一个匹配的元素 content.remove('who') content ['where', 'lisi', 'cntent', 'who', 'who'] ``` #### 18.2.8、reverse方法 reverse 方法是将列表中的元素进行反转操作 ``` x [1, 2, 3] # 元素反向存储 x.reverse() x [3, 2, 1] ``` #### 18.2.9、sort方法 sort 方法用于在原位置排序,‘原位置排序’意味着改变原来的列表而让列表中的元素有顺序排列 ``` x = [2,3,5,6,1,4,7] x.sort() x [1, 2, 3, 4, 5, 6, 7] ``` #### 18.2.10、clear方法 clear 方法用于清空列表 ``` >>> list1=['baidu', 'google', 12, 23] >>> list1 ['baidu', 'google', 12, 23] # 清空列表内容 >>> list1.clear() >>> list1 [] ``` #### 18.2.11、copy方法 copy 方法是复制列表 ``` >>> list1 = ['baidu', 'google', 12, 23]; >>> list1.copy() ['baidu', 'google', 12, 23] >>> list2 = list1.copy() >>> list2 ['baidu', 'google', 12, 23] ``` ### 18.3、列表基本操作 列表可以使用所有适用于序列的标准操作,比如之前所学的索引、分片、连接和相乘,更有趣的是,列表是可以修改的,也就是定义的列表内容可以根据需求更改,本节介绍一些改变列表的方法:如元素赋值、元素删除、分片赋值以及列表方法(但是请注意,并不是所有的列表方法都能真正改变列表) #### 18.3.1、改变列表:元素赋值 在列表中要给指定的元素赋值时,我们需要指定特定的索引标记来为列表中某个特定的,位置明确的元素赋值,比如 x[3]=5 ``` # x=[1,2,3,4,5] x [1, 2, 3, 4, 5] # 改变列表第四个元素的内容 x[3]=5 x [1, 2, 3, 5, 5] ``` ==注意:== 不能为一个位置不存在的元素赋值,如果列表长度为2,则不能为索引为10 的元素进行赋值,如果需要赋值则需要创建一个长度为11的列表。 #### 18.3.2、删除列表元素 若要删除列表中的元素,直接利用del删除即可 ``` # 定义长度为4的姓名列表 names=['zhangsan','lisi','wangwu','zhaoliu'] names ['zhangsan', 'lisi', 'wangwu', 'zhaoliu'] # 删除第三个元素 del names[2] # 最后列表长度由4变为3 names ['zhangsan', 'lisi', 'zhaoliu'] ``` del 语句还能用于删除其他元素,也可以用于变量的删除操作。 #### 18.3.3、分片赋值 在 Python 中对序列或者列表的分片操作是一个很强大的特性,分片赋值会显得更加强大,例如: ``` # 定义一个list name = list('Pyther') # 改变 list 中的最后两个值 name[4:]='on' name ['P', 'y', 't', 'h', 'o', 'n'] 从上可知,程序可以一次为多个元素赋值,在分片赋值时,可以使用与原序列不等长的序列将分片替换,例如: name_re = list('perl') name_re ['p', 'e', 'r', 'l'] # 分片替换 name_re[1:] = list('ython') name_re ['p', 'y', 't', 'h', 'o', 'n'] 分片赋值还可以在不需要更改原有列表任何内容的情况下进行新元素插入 num = [1,4,5] # 在第一个元素后插入新的元素 num[1:1]=[2,3] num [1, 2, 3, 4, 5] 同理也可以通过分片操作来删除列表中的元素,同样也支持负数分片操作 num [1, 2, 3, 4, 5] # 给第一个和第三个元素之间分片赋值一个空序列,即删除元素 num[1:3] = [] num [1, 4, 5] # 负数分片操作 num[-1:-1] = [5,5,5] num [1, 2, 3, 4, 5, 5, 5, 5] ``` ## 19、类和对象 ### 19.1、内置方法 Python 创建任何一个类的时候,都会包含一些内置的方法,主要包括如下: ``` __init__ 构造函数,在生成对象时调用 __del__ 析构函数,释放对象时使用 __repr__ 打印,转换 __setitem__ 按照索引赋值 __getitem__ 按照索引获取值 __len__ 获得长度 __cmp__ 比较运算 __call__ 函数调用 __add__ 加运算 __sub__ 减运算 __mul__ 乘运算 __div__ 除运算 __mod__ 求余运算 __pow__ 乘方 ``` ### 19.2、类方法 类方法是将类本身作为对象进行操作的方法。 定义与使用 ``` 类方法(可调类变量、可被实例调用、可被类调用) 1、类方法通过@classmethod装饰器实现,只能访问类变量,不能访问实例变量; 2、通过cls参数传递当前类对象,不需要实例化。 ''' class Car(object): name = 'BMW' def __init__(self, name): self.name = name @classmethod def run(cls,speed): print(cls.name,speed,'行驶') # 访问方式1 c = Car("宝马") c.run("100迈") # 访问方式2 Car.run("100迈") ``` ### 19.3、静态方法 静态方法是类中的函数,不需要实例。 定义与使用 ``` 静态方法(可调类变量、可被实例调用、可被类调用) 1、用 @staticmethod 装饰的不带 self 参数的方法; 2、静态方法名义上归类管理,实际中在静态方法中无法访问类和实例中的任何属性; 3、调用时并不需要传递类或实例。 ''' class Car(object): name = 'BMW' def __init__(self, name): self.name = name @staticmethod def run(speed): print(Car.name,speed,'行驶') # 访问方式1 c = Car("宝马") c.run("100迈") # 访问方式2 Car.run("100迈") ``` ### 19.4、实例方法 实例方法就是类的实例能够使用的方法。 定义与使用 ``` # 实例方法(可调类变量、可调实例变量、可被实例调用) # 第一个参数强制为实例对象 self。 class Car(object): name = 'BMW' def __init__(self, name): self.name = name def run(self,speed): print(self.name,speed,'行驶') # 访问 c = Car("宝马") c.run("100迈") ``` ### 19.5、类的继承 定义与使用 ``` # 基本语法:class ClassName(BaseClassName) # 父类 class Car(object): name = 'BMW' def __init__(self, name): self.name = name def run(self,speed): print(self.name,speed,'行驶') # 子类 class BMWCar(Car): conf = "经济适用型" pass # 调用父类 Car 中 run 方法 bc = BMWCar("BMW经济适用型轿车") bc.run("100迈") ``` ### 19.6、类的多态 定义与使用 ``` # 父类 class Car(object): name = 'BMW' def __init__(self, name): self.name = name def run(self,speed): print('Car-->',self.name,speed,'行驶') # 子类1 class BMWCar(Car): def run(self,speed): print('BMWCar-->',self.name,speed,'行驶') # 子类2 class SVWCar(Car): def run(self,speed): print('SVWCar-->',self.name,speed,'行驶') # 调用 run 方法 c = Car("Car") c.run("120迈") bc = BMWCar("宝马") bc.run("100迈") sc = SVWCar("大众") sc.run("80迈") # 输出结果 Car--> Car 120迈 行驶 BMWCar--> 宝马 100迈 行驶 SVWCar--> 大众 80迈 行驶 ``` ## 20、字典 Python 中的字典提供了一种灵活的访问和组织数据的方式 键必须不可变,所以可以用数字,字符串或元组充当,所以用列表就不行。 字典是由很多值组成的集合 字典的索引可以是不同的数据类型,同样也不止是整数,也有字符串 字典的索引被称为“键”,键及键所关联的值叫键值对(类似于Java中的Map集合) 字典是另一种可变容器模型,且可存储任意类型对象。 字典的每个键值 key=>value 对用冒号 : 分割,每个键值对之间用逗号 , 分割,整个字典包括在花括号 {} 中 ,格式如下所示: dictionary = {'url1':'baidu', 'url':'google', 'num1':12, 'num2':34}; 键一般是唯一的,如果键重复,最后的一个键值对会替换前面的键值对,值没有唯一性要求,如下: ``` dic1 = {'name':'zhangsan','age':23,'address':'BeiJing','name':'lisi'} # 查看字典值发现重复的键值后面的替换前面的 dic1 {'name': 'lisi', 'age': 23, 'address': 'BeiJing'} dic1['name'] 'lisi' ``` 值可以取任何数据类型,但键必须是不可变的,如字符串,数字或元组,如下: dict = {'Alice': '2341', 'Beth': '9102', 'Cecil': '3258',('a','b'):(12,43)} ### 20.1、添加和更新字典数据 向字典添加新内容的方法是增加新的键/值对,修改或删除已有键/值对如下实例: ``` dict = {'Name': 'Fiona', 'Age': 10, 'Class': 'Three'} # 更新 dict['Age'] = 8 # 添加 dict['School'] = "Middle School" # 查看字典数据 dict {'Name': 'Fiona', 'Age': 8, 'Class': 'Three', 'School': 'Middle School'} ``` ### 20.2、删除字典数据 对字典元素的删除操作能单一删除也能将整个字典清空,显示的删除一个字典使用 del 命令“ ``` dict = {'Name': 'Fiona', 'Age': 10, 'Class': 'Three'} # 删除键是'Name'的条目 del dict['Name'] # 清空字典所有条目,删除字典内所有元素,clear() 方法没有任何返回值 dict.clear() # 删除整个字典元素 del dict print ("dict['Age']: ", dict['Age']) print ("dict['School']: ", dict['School']) 以上打印语句这会引发一个异常,因为用 del 后的字典不再存在: Traceback (most recent call last): File "test.py", line 12, in print("dict['Age']: ", dict['Age']) TypeError: 'type' object is not subscriptable ``` ### 20.3、深拷贝与浅拷贝 ``` dict = {'Name': 'Runoob', 'Age': 7, 'Class': 'First'} dict11 = dict.copy() print(dict11) print("新复制的字典为 : ", dict11) dict1 = {'user': 'runoob', 'num': [1, 2, 3]} # 浅拷贝: 引用对象 赋值,实例中 dict2 其实是 dict1 的引用,即别名,所以输出结果都是一致的, 浅拷贝随 dict1 的修改而修改 dict2 = dict1 # 拷贝,dict3 对父对象进行了深拷贝,深拷贝不会随dict1 修改而修改 dict3 = dict1.copy() # 修改 data 数据 dict1['user'] = 'root' dict1['num'].remove(1) # 输出结果 print(dict1) print(dict2) print(dict3) ``` 结果如下: ``` {'Name': 'Runoob', 'Age': 7, 'Class': 'First'} 新复制的字典为 : {'Name': 'Runoob', 'Age': 7, 'Class': 'First'} {'user': 'root', 'num': [2, 3]} {'user': 'root', 'num': [2, 3]} {'user': 'runoob', 'num': [2, 3]} ``` 赋值(浅拷贝)会随父对象的修改而修改,深拷贝不会随父对象的修改而修改。 ### 20.4、dict.fromkeys() 创建一个新字典,以序列seq中元素做字典的键,val为字典所有键对应的初始值,该方法返回一个新的字典 fromkeys() 方法语法 dict.fromkeys(seq[, value]) ``` # 参数 seq -- 字典键值列表。 value -- 可选参数, 设置键序列(seq)对应的值,默认为 None。 实例: # dict.fromkeys(seq[, value]) seq = ('name', 'age', 'class') # 不指定值 dict = dict.fromkeys(seq) print("新的字典为 : %s" % str(dict)) # 赋值 10 dict = dict.fromkeys(seq, 10) print("新的字典为 : %s" % str(dict)) # 赋值一个元组 dict = dict.fromkeys(seq,('zs',8,'Two')) print("新的字典为 : %s" % str(dict)) 执行结果返回一个新的字典,如果不指定值默认为None,以上结果输出结果为: 新的字典为 : {'name': None, 'age': None, 'class': None} 新的字典为 : {'name': 10, 'age': 10, 'class': 10} 新的字典为 : {'name': ('zs', 8, 'Two'), 'age': ('zs', 8, 'Two'), 'class': ('zs', 8, 'Two')} ``` ### 20.5、dict.get() 返回指定键的值,如果值不在字典中返回default值。 get() 方法语法 dict.get(key, default=None) ``` # 参数 key -- 字典中要查找的键。 default -- 如果指定键的值不存在时,返回该默认值值。 实例: # get ()方法的应用举例 dict = {'Name': 'Mary', 'Age': 20} print ("Age 值为 : %s" % dict.get('Age')) print ("Name 值为 : %s" % dict.get('Name')) print ("Sex 值为 : %s" % dict.get('Sex', "NA")) 以上结果输出为: Age 值为 : 20 Name 值为 : Mary Sex 值为 : NA ``` ### 20.6、dict.items() item() 方法以列表返回可遍历的(键, 值) 元组数组 ``` dict = {'Name': 'Mary', 'Age': 17} print ("Value : %s" % dict.items()) # 输出结果为: Value : dict_items([('Age', 17), ('Name', 'Mary')]) 可遍历的元组数组举例: dict1 = {'老大':'25岁', '老二':'20岁', '老三':'12岁', } print(dict1.items()) for key,values in dict1.items(): print(key + '已经' + values + '了') 以上结果输出为: 老大已经25岁了 老二已经20岁了 老三已经12岁了 ``` ### 20.7、dict.keys() 返回一个迭代器,可以使用 list() 来转换为列表 ``` keys()方法语法: dict.keys() 实例: dict = {'Name': 'Mary', 'Age': 17} print(dict.keys()) 以上结果输出为: dict_keys(['Name', 'Age']) 由结果看出结果返回一个迭代对象,这时候我们可以使用 list 转换为列表: list1 = list(dict.keys()) print ("转换后的结果为 : %s" % list1) # 输出结果为一个列表,后续可以对其进行相应操作: 转换后的结果为 : ['Name', 'Age'] ``` ### 20.8、dict.setdefault() Python 字典 setdefault() 方法和 get() 方法类似, 如果 key 在 字典中,返回对应的值。如果不在字典中,则插入 key 及设置的默认值 default,并返回 default ,default 默认值为 None。 setdefault()方法语法: ``` dict.setdefault(key, default=None) # 参数 key -- 查找的键值。 default -- 键不存在时,设置的默认键值。 实例: dict = {'Name': 'Mary', 'Age': 17} print ("Age 键的值为 : %s" % dict.setdefault('Age', None)) print ("Sex 键的值为 : %s" % dict.setdefault('Sex', None)) print ("新字典为:", dict) 以上结果输出为: Age 键的值为 : 17 Sex 键的值为 : None 新字典为:{'Age': 17, 'Name': 'Mary', 'Sex': None} ``` ### 20.9、dict..update(dict2) Python 字典 update() 函数把字典参数 dict2 的 key/value(键/值) 对更新到字典 dict 里。 语法: dict.update(dict2) ``` # 参数 dict2 -- 添加到指定字典dict里的字典。 实例: dict = {'Name': 'Mary', 'Age': 17} dict2 = {'Sex': 'female' } # 将 dict2 中的结果添加到字典 dict 中  dict.update(dict2) print ("更新字典 dict : ", dict) 以上结果输出为: 更新字典 dict : {'Name': 'Mary', 'Age': 17, 'Sex': 'female'} ``` ### 20.10、dict.values() Python 字典 values() 方法返回一个迭代器,可以使用 list() 来转换为列表,列表为字典中的所有值。 ``` dict = { 'Name': 'Mary','Sex': 'male', 'Age': 7} print("字典所有值为 : ", list(dict.values())) 以上结果输出为: 字典所有值为 : ['Mary', 'male', 7] ``` ### 20.11、dict.pop(key[,default]) Python 字典 pop() 方法删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。否则,返回default值。 pop()方法语法: ``` pop(key[,default]) #参数 key: 要删除的键值 default: 如果没有 key,返回 default 值 # 返回值 返回被删除的值。 实例: dict = {'Name': 'Mary', 'Age': 17} result = dict.pop('Age') # 删除 print(dict) print(result) 以上结果输出为: {'Name': 'Mary'} 17 ``` ### 20.12、dict.popitem() Python 字典 popitem() 方法随机返回一个键值对(key,value)形式,按照 LIFO(Last In First Out 后进先出法) 顺序规则,即最末尾的键值对。如果字典已经为空,却调用了此方法,就报出KeyError异常。 实例: ``` dict = {'Name': 'Mary', 'Age': 17} pop_obj=dict.popitem() print(pop_obj) print(dict) 以上结果输出为: ('Age', 17) {'Name': 'Mary'} 将字典清空: dict = {'Name': 'Mary', 'Age': 17} del dict print(dict.popitem()) 结果输出为: Traceback (most recent call last): File "test.py", line 4, in print(dict.popitem()) TypeError: descriptor 'popitem' of 'dict' object needs an argument ``` ### 20.13、字典和列表差异 列表中的元素表项由于元素通过序列从 0 开始递增存放,所以列表中的表项是排序的,而字典的内容的表项是不排序的,如下例子就很好的说明列表和字典的区别: ``` list1 = ['zhangsan',23,'BeiJing'] list2 = ['BeiJing','zhangsan',23] list1 == list2 False dic1 = {'name':'zhangsan','age':23,'address':'BeiJing'} dic2 = { 'age':23,'name':'zhangsan','address':'BeiJing'} dic1 == dic2 True ``` 由以上实例可以看出,当列表元素内容一致,顺序不同再对比内容时匹配不成功,同理字典值匹配成功,说明字典中元素内容不按顺序存放。 ## 21、集合 Python也包含有 集合 类型。集合是由不重复元素组成的无序的集。它的基本用法包括成员检测和消除重复元素。集合对象也支持像 联合,交集,差集,对称差分等数学运算。 ### 21.1、创建集合 可以使用大括号 { } 或者 set() 函数创建集合, 注意:创建一个空集合必须用 set() 而不是 { },因为 { } 是用来创建一个空字典。 ### 21.2、添加元素 语法格式: s.add(x) 将元素 x 添加到集合 s 中,如果元素已存在,则不进行任何操作。 s = set(('hello','world')) print(s) ``` # 向集合 s 中添加元素 s.add('!') print('添加元素后的集合是:%s' % s) # 输出结果是: 添加元素后的集合是:{'world', '!', 'hello'} ``` 除了 add() 方法可以添加元素外,还有一个方法,也可以添加元素,并且参数可以是列表,元组,字典等,语法格式如下: s.update( x ) 参数 x 可以是一个,也可以是多个,多个参数之间用逗号相隔 ``` #  1)添加列表 s = set(('hello', 'world')) s.update([1,3],[2,4]) print('添加元素后的集合是:%s' % s) 打印结果: 添加元素后的集合是:{1, 2, 3, 4, 'world', 'hello'} # 2)添加元组 s.update(('h', 'j')) print('添加元素后的集合是:%s' % s) 打印结果: 添加元素后的集合是:{1, 2, 3, 4, 'world', 'j', 'hello', 'h'} ``` ### 21.3、移除元素 语法格式: s.remove( x ) 将元素 x 从集合 s 中移除,如果元素不存在,则会发生错误。 ``` # 将元素 2 从集合中移除 s.remove(2) print('移除元素 2 后的集合是:%s' % s) # 如果移除集合中不存在的元素会报异常 # 移除集合中不存在的集合 s.remove('hi') print('移除元素后的集合是:%s' % s) #  异常信息 Traceback (most recent call last): File "test.py", line 20, in s.remove('hi') KeyError: 'hi' ``` 此外还有一个方法也是移除集合中的元素,且如果元素不存在,不会发生错误。格式如下所示: s.discard( x ) ``` > > > thisset = set(("Google", "Runoob", "Taobao")) > > > thisset.discard("Facebook") # 不存在不会发生错误 > > > print(thisset) {'Taobao', 'Google', 'Runoob'} ``` 我们也可以设置随机删除集合中的一个元素,语法格式如下: s.pop() ``` # 随机删除集合中的一个元素 print(s) s.pop() print('移除元素后的集合是:%s' % s) 输出结果: {1, 3, 4, 'world', '!', 'hello', 'h', 'j'} 移除元素后的集合是:{3, 4, 'world', '!', 'hello', 'h', 'j'} 注意:在交互模式,pop是删除集合的第一个元素(排序后的集合的第一个元素)。 ``` ### 21.4、集合运算 集合之间的运算符分别是‘-’、‘|’、‘&’、‘^’ ; 下面以两个集合之间的运算为例进行讲解: ‘-’:代表前者中包含后者中不包含的元素 ‘|’:代表两者中全部元素聚在一起去重后的结果 ‘&’:两者中都包含的元素 ‘^’:不同时包含于两个集合中的元素 ``` >>> a = set('afqwbracadaagfgbrafg') >>> b = set('rfgfgfalacazamddg') >>> a {'r', 'q', 'd', 'b', 'w', 'g', 'f', 'c', 'a'} >>> b {'r', 'd', 'g', 'f', 'l', 'z', 'c', 'm', 'a'} # 集合a中包含而集合b中不包含的元素 >>> a - b {'b', 'w', 'q'} # 集合a或b中包含的所有元素 >>> a | b {'d', 'g', 'l', 'c', 'r', 'q', 'b', 'w', 'f', 'z', 'm', 'a'} # 集合a和b中都包含了的元素 >>> a & b {'r', 'd', 'g', 'f', 'c', 'a'} # 不同时包含于a和b的元素 >>> a ^ b {'l', 'q', 'b', 'w', 'z', 'm'} ``` ### 21.5、集合推导式 和列表一样,集合也支持推导式 ``` # 判断元素是否存在 >>> a = {x for x in 'abracadabra' if x not in 'abc'} >>> a {'r', 'd'} ``` ### 21.6、集合内置方法 #### 21.6.1、difference() difference() 方法用于返回集合的差集,即返回的集合元素包含在第一个集合中,但不包含在第二个集合(方法的参数)中,返回一个新的集合。** difference() 方法语法:** set.difference(set) ``` 实例: 两个集合的差集返回一个集合,元素包含在集合 x ,但不在集合 y : # 求两个集合的差集,元素在 x 中不在 y 中 x = {"apple", "banana", "cherry"} y = {"google", "microsoft", "apple"} z = x.difference(y) print('两个集合的差集是:%s' % z) # 输出结果为: {'cherry', 'banana'} ``` #### 21.6.2、difference_update() difference_update() 方法用于移除两个集合中都存在的元素。 difference_update() 方法与 difference() 方法的区别在于 difference() 方法返回一个移除相同元素的新集合,而 difference_update() 方法是直接在原来的集合中移除元素,没有返回值。 ``` x = {"apple", "banana", "cherry"} y = {"google", "microsoft", "apple"} x.difference_update(y) print(x) 结果为: {'banana', 'cherry'} x1 = {1,2,3,4} y1 = {1,2,3} x1.difference_update(y1) print(x1) # 结果为: {4} ``` #### 21.6.3、intersection() intersection() 方法用于返回两个或更多集合中都包含的元素,即交集,返回一个新的集合。 intersection() 方法语法: ``` set.intersection(set1, set2 ... etc) **参数:** set1 -- 必需,要查找相同元素的集合 set2 -- 可选,其他要查找相同元素的集合,可以多个,多个使用逗号 , 隔开 ``` ``` # 返回两个或者多个集合的交集 x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} z = x.intersection(y) print(z) # 返回三个集合的交集 x = {"a", "b", "c"} y = {"c", "d", "e"} z = {"f", "g", "c"} result = x.intersection(y, z) print('三个集合的差集是:%s' % result) # 输出结果: {'apple'} 两个集合的差集是:{'c'} ``` #### 21.6.4、intersection_update() intersection_update() 方法用于获取两个或更多集合中都重叠的元素,即计算交集。 intersection_update() 方法不同于 intersection() 方法,因为 intersection() 方法是返回一个新的集合,而 intersection_update() 方法是在原始的集合上移除不重叠的元素。 intersection_update() 方法语法: set.intersection_update(set1, set2 ... etc) **参数** set1 -- 必需,要查找相同元素的集合 set2 -- 可选,其他要查找相同元素的集合,可以使用多个多个,多个使用逗号‘,’ 隔开 ``` 实例: # 返回一个无返回值的集合交集 x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} x.intersection_update(y) print(x) x = {"a", "b", "c"} y = {"c", "d", "e"} z = {"f", "g", "c"} x.intersection_update(y, z) print(x) # 输出结果: {'apple'} {'c'} ``` #### 21.6.5、union() union() 方法返回两个集合的并集,即包含了所有集合的元素,重复的元素只会出现一次,返回值返回一个新的集合 语法: union() 方法语法: set.union(set1, set2...) 参数 set1 -- 必需,合并的目标集合 set2 -- 可选,其他要合并的集合,可以多个,多个使用逗号 , 隔开。 实例: ``` # 合并两个集合,重复元素只会出现一次: x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} z = x.union(y) print(z) 输出结果为: {'cherry', 'runoob', 'google', 'banana', 'apple'} # 合并多个集合: 实例 1 x = {"a", "b", "c"} y = {"f", "d", "a"} z = {"c", "d", "e"} result = x.union(y, z) print(result) 输出结果为: {'c', 'd', 'f', 'e', 'b', 'a'} ``` #### 21.6.6、isdisjoint() isdisjoint() 方法用于判断两个集合是否包含相同的元素,==如果没有返回 True,否则返回 False。 语法: isdisjoint() 方法语法: set.isdisjoint(set) 实例: ``` x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} # 判断集合 y 中是否包含集合 x 中的元素,如果没有返回 True, 有则返回 False z = x.isdisjoint(y) # 结果返回 False,说明集合 y 中有和 x 中相同的元素 print(z) x = {"apple", "banana", "cherry"} y = {"google", "runoob", "baidu"} # 判断集合 y 中是否包含集合 x 中的元素,如果没有返回 True, 有则返回 False z = x.isdisjoint(y) # 结果返回 True,说明集合 y 中没有和 x 中相同的元素 print(z) 输出结果: False True ``` #### 21.6.7、issubset() issubset() 方法用于判断集合的所有元素是否都包含在指定集合中,如果是则返回 True,否则返回 False。 issubset() 方法用于判断集合的所有元素是否都包含在指定集合中,如果是则返回 True,否则返回 False。 语法: issubset() 方法语法: set.issubset(set) **参数** set -- 必需,要比查找的集合 返回值 返回布尔值,如果都包含返回 True,否则返回 False。 ``` 实例: # 判断集合 x 的所有元素是否都包含在集合 y 中: x = {"a", "b", "c"} y = {"f", "e", "d", "c", "b", "a"} z = x.issubset(y) print(z) 输出结果 # 说明 集合 x 中的元素都包含在 y 中 True 注意:必须是集合中的元素都包含在内,否则结果为false # 集合 y 中只有元素 b 和 c ,执行结果为False x = {"a", "b", "c"} y = {"f", "e", "d", "c", "b","y"} z = x.issubset(y) print(z) 结果输出; False ``` #### 21.6.8、issuperset() issuperset() 方法用于判断指定集合的所有元素是否都包含在原始的集合中,如果是则返回 True,否则返回 False。 语法: set.issuperset(set) 实例: ``` # 判断集合 y 的所有元素是否都包含在集合 x 中: x = {"f", "e", "d", "c", "b", "a"} y = {"a", "b", "c"} z = x.issuperset(y) print(z) 输出结果为: True # 如果没有全部包含返回 False: 实例 1 x = {"f", "e", "d", "c", "b"} y = {"a", "b", "c"} z = x.issuperset(y) print(z) 输出结果为: False ``` #### 21.6.9、symmetric_difference() symmetric_difference() 方法返回两个集合中不重复的元素集合,即会移除两个集合中都存在的元素,结果返回一个新的集合。 语法: set.symmetric_difference(set) 实例: ``` # 返回两个集合组成的新集合,但会移除两个集合的重复元素: x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} z = x.symmetric_difference(y) print(z) 输出结果: {'banana', 'google', 'cherry', 'runoob'} ``` #### 21.6.10、symmetric_difference_update() symmetric_difference_update() 方法移除当前集合中在另外一个指定集合相同的元素,并将另外一个指定集合中不同的元素插入到当前集合中。 语法: set.symmetric_difference_update(set) 实例: ``` # 在原始集合 x 中移除与 y 集合中的重复元素,并将不重复的元素插入到集合 x 中: x = {"apple", "banana", "cherry"} y = {"google", "runoob", "apple"} x.symmetric_difference_update(y) print(x) 输出结果: {'runoob', 'cherry', 'banana', 'google'} ``` ## 22、输入与输出 ### 22.1、格式化输出 Python 输出值的方式有两种:表达式语句和 print 函数(文件对象的输出使用 write 方法,标准文件输出可以参考 sys.stdout ,详细文档:https://docs.python.org/zh-cn/3/faq/extending.html#how-do-i-catch-the-output-from-pyerr-print-or-anything-that-prints-to-stdout-stderr)。 如果我们想要将输出的值转成字符串,可以使用 repr() 或 str() 函数来实现,其中 repr() 函数产生一个解释器易读的表达形式,str() 函数返回一个用户易读的表达形式。 如果我们不只是想打印使用空格分隔的值,而是想对输出进行格式化控制,可以采用两种方式:一种是自己处理整个字符串,另一种是采用 str.format() 方式,下面介绍下 str.format() 的使用。 ``` >>> print('{}网址:"{}!"'.format('Python技术', 'www.justdopython.com')) Python技术网址:"www.justdopython.com!" ``` 括号及其里面的字符 (称作格式化字段) 将会被 format() 中的参数替换。 在括号中的数字用于指向传入对象在 format() 中的位置,如下: ``` >>> print('{0} 和 {1}'.format('Hello', 'Python')) Hello 和 Python >>> print('{0} {1}'.format('Hello', 'Python')) Hello Python >>> print('{1} {0}'.format('Hello', 'Python')) Python Hello ``` 如果在 format() 中使用了关键字参数,那么它们的值会指向使用该名字的参数,如下: ``` >>> print('{name}网址:{site}'.format(name='Python技术', site='www.justdopython.com')) Python技术网址:www.justdopython.com ``` 位置及关键字参数可以任意的结合,如下: ``` >>> print('电商网站 {0}, {1}, {other}。'.format('淘宝', '京东', other='拼多多')) 电商网站 淘宝, 京东, 拼多多。 ``` 用 ** 标志将字典以关键字参数的方式传入,如下: ``` >>> "repr() shows quotes: {!a}; str() doesn't: {!s}".format('test1', 'test2') "repr() shows quotes: 'test1'; str() doesn't: test2" ``` 在字段后的 : 后面加一个整数会限定该字段的最小宽度,如下: ``` >>> table = {'Sjoerd': 123, 'Jack': 456, 'Dcab': 789} >>> for name, phone in table.items():print('{0:10} ==> {1:10d}'.format(name, phone)) Jack ==> 456 Dcab ==> 789 Sjoerd ==> 123 ``` 用 ** 标志将这个字典以关键字参数的方式传入,如下: ``` >>> table = {'Sjoerd': 123, 'Jack': 456, 'Dcab': 789789789789} >>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table)) Jack: 456; Sjoerd: 123; Dcab: 789789789789 ``` ### 22.2、读取键盘输入 Python 提供了 input() 内置函数从标准输入读入一行文本,默认的标准输入是键盘,input() 可以接收一个 Python 表达式作为输入,并将运算结果返回。示例如下: ``` >>> str = input("请输入:"); >>> print ("输入的内容是: ", str) 请输入:Hello Python 你输入的内容是: Hello Python ``` ### 22.3、文件读写 函数 open() 返回文件对象,通常的用法需要两个参数:open(filename, mode)。 第一个参数 filename 是要访问的文件名,第二个参数 mode 是描述如何使用该文件(可取值主要包括:'r' 读取文件;'w' 只是写入文件,已经存在的同名文件将被删掉;'a' 打开文件进行追加,自动添加到末尾;'r+' 打开文件进行读取和写入;'rb+' 以二进制格式打开一个文件用于读写...),mode 参数是可选的,默认为 'r'。 #### 22.3.1、read() 要读取文件内容,调用 read(size) ,size为可选参数。 ``` >>> f = open('tmp.txt', 'r') >>> str = f.read(5) >>> print(str) >>> f.close() Hello ``` #### 22.3.2、readline() 读取一行,换行符为 \n 。 ``` >>> f = open('tmp.txt', 'r') >>> str = f.readline() >>> print(str) >>> f.close() ``` #### 22.3.3、readlines() 读取文件中包含的所有行,可设置可选参数 size 。 ``` >>> f = open('tmp.txt', 'r') >>> str = f.readlines(1) >>> print(str) >>> f.close() ['Hello Python'] ``` #### 22.3.4、write() write(string) 将 string 的内容写入文件。 ``` >>> f = open('tmp.txt', 'w') >>> num = f.write('Hello Python') >>> print(num) >>> f.close() 12 ``` #### 22.3.5、seek() seek(offset, from_what) 改变文件当前的位置。offset 移动距离;from_what 起始位置,0 表示开头,1 表示当前位置,2 表示结尾,默认值为 0 ,即开头。 ``` >>> f = open('tmp.txt', 'rb+') >>> f.write(b'0123456789abcdef') # 移动到文件的第 6 个字节 >>> f.seek(5) >>> print(f.read()) b'56789abcdef' ``` #### 22.3.6、tell() tell() 返回文件对象当前所处的位置,它是从文件开头开始算起的字节数。 ``` >>> f = open('tmp.txt', 'r') >>> f.seek(5) >>> print(f.tell()) 5 ``` #### 22.3.7、close() 当你处理完一个文件后,调用 close() 来关闭文件并释放系统的资源。也可以使用 with 关键字处理文件对象,实现文件用完后自动关闭。 ``` >>> with open('tmp.txt', 'r') as f: ... read_data = f.read() >>> print(f.closed) True ``` ### 22.4、操作 json 格式数据 json.dumps(obj) 序列化,obj 转换为 json 格式的字符串; json.dump(obj, fp) 序列化,将 obj 转换为 json 格式的字符串,将字符串写入文件; json.loads(str) 反序列化,将 json 格式的字符串反序列化为一个 Python 对象; json.load(fp) 反序列化,从文件中读取含 json 格式的数据,将之反序列化为一个 Python 对象。 ``` >>> import json >>> data = {'id':'1', 'name':'jhon', 'age':12} >>> with open('t.json', 'w') as f: ... json.dump(data, f) >>> with open("t.json", 'r') as f: ... d = json.load( f) >>> print(d) {'id': '1', 'name': 'jhon', 'age': 12} ``` ## 23、错误和异常 在python中的错误通常的说就是一些语法错误,而异常就是程序真正执行的时候抛出的异常,程序运行期间检测到的错误被称为异常, Python 提供了 try/except语句用来捕获和处理异常。try 语句用来检测语句块中是否有错误,except 语句则用来捕获 try 语句中的异常,并进行处理,附加的 else 可以在 try 语句没有异常时执行。 语法: ``` try: statement(s) # 要检测的语句块 except exception: deal_exception_code # 如果在 try 部份引发了 'exception' 异常 except exception2, e: deal_exception2_code # 如果引发了 'exception2' 异常 else: no_exception_happend_code #如果没有异常发生 ``` try 语句的执行逻辑如下: 首先,执行 try 子句 (try 和 except 关键字之间的(多行)语句)。 如果没有异常发生,则跳过 except 子句 并完成 try 语句的执行。 如果在执行try 子句时发生了异常,则跳过该子句中剩下的部分。然后,如果异常的类型和 except 关键字后面的异常匹配,则执行 except 子句,然后继续执行 try 语句之后的代码。 如果发生的异常和 except 子句中指定的异常不匹配,则将其传递到外部的 try 语句中;如果没有找到处理程序,则它是一个 未处理异常,执行将停止并显示错误消息。 如果 try 语句执行时没有发生异常,那么将执行 else 语句后的语句(如果有 else 的话),然后控制流通过整个 try 语句。 ### 23.1 、基类 如果发生的异常和 except 子句中的类是同一个类或者是它的基类,则异常和 except 子句中的类是兼容的(但反过来则不成立 --- 列出派生类的 except 子句与基类兼容)。 实例 ``` class BException(Exception): #继承Exception基类 pass class CException(BException): #继承BException基类 pass class DException(CException): #继承CException基类 pass for cls in [BException, CException, DException]: try: raise cls() #抛出异常 except DException: print("D") except CException: print("C") except BException: print("B") #输出 B C D ``` 请注意如果 except 子句被颠倒(把 except BException 放到第一个),它将打印 B,B,B --- 因为DException类继承CException类,CException类继承BException类,将 except BException 放到第一个可以匹配这三个异常,后面的 except 就不会执行。 ### 23.2、不带异常类型的 except Python可以在所有 except 的最后加上 except 子句,这个子句可以省略异常名,以用作通配符。它可以捕获前面任何 except (如果有的话)没有捕获的所有异常。 ``` try: statement(s) # 要检测的语句块 except exception: deal_exception_code # 如果在 try 部份引发了 'exception' 异常 except : deal_all_other_exception2_code # 处理全部其它异常 else: no_exception_happend_code #如果没有异常发生 ``` 实例: ``` try: raise BException() #抛出异常 except DException: print("D") except: print("处理全部其它异常") #处理全部其它异常 #输出 处理全部其它异常 ``` ### 23.3、except 语句捕获多种异常类型 一个 try 语句可能有多个 except 子句,以指定不同异常的处理程序,最多会执行一个处理程序。处理程序只处理相应的 try 子句中发生的异常,而不处理同一 try 语句内其他处理程序中的异常。一个 except 子句可以将多个异常命名为带括号的元组。 ``` try: statement(s) # 要检测的语句块 except exception: deal_exception_code # 如果在 try 部份引发了 'exception' 异常 except (Exception1[, Exception2[,...ExceptionN]]]) : deal_all_other_exception2_code # 处理多个异常 else: no_exception_happend_code #如果没有异常发生 ``` 实例: ``` try: raise BException() #抛出异常 except (BException, DException): print("D") except: print("处理全部其它异常") #处理全部其它异常 else: print("没有异常发生") #没有异常发生 #输出 D ``` ### 23.4、try - finally 语句 finally 语句用于无论是否发生异常都将执行最后的代码。 ``` try: # <语句> finally: # <语句> #退出try时总会执行 ``` 实例: ``` try: raise BException() #抛出异常 except (BException, DException): print("D") except: print("处理全部其它异常") #处理全部其它异常 else: print("没有异常发生") #没有异常发生 finally: print("你们绕不过我,必须执行") #必须执行的代码 #输出 D 你们绕不过我,必须执行 ``` 这里注意 finally 和 else 的区别,finally 是无论是否有异常都会执行,而 else 语句只有没有异常时才会执行。也就是说如果没有异常,那么 finally 和 else 都会执行。 ### 23.5、异常的参数 except 子句可以在异常名称后面指定一个变量。这个变量和一个异常实例绑定,它的参数是一个元组,通常包含错误字符串,错误数字,错误位置,存储在 .args 中。为了方便起见,异常实例定义了__str__() ,因此可以直接打印参数而无需引用 .args。 ``` try: # 正常的操作 ...... except ExceptionType as inst: # 可以在这输出 inst 的值..... ``` 实例: ``` try: x = 1 / 0 # 除数为0 except ZeroDivisionError as err: #为异常指定变量err print("Exception") print(err.args) #打印异常的参数元组 print(err) #打印参数,因为定义了__str__() #输出 Exception ('division by zero',) division by zero ``` ### 23.6、触发异常 Python 提供了 raise 语句用于手动引发一个异常。 语法 raise [Exception [, args [, traceback]]] 参数说明 Exception:异常的类型,例如 ZeroDivisionError args:异常参数值,可选,默认值 "None" traceback:可选,用于设置是否跟踪异常对象 异常参数值可以是一个字符串,类或对象 实例 ``` def diyException(level): if level > 0: raise Exception("raise exception", level) #主动抛出一个异常,并且带有参数 print('我是不会执行的') #这行代码不会执行 try: diyException(2) #执行异常方法 except Exception as err: #捕获异常 print(err) #打印异常参数 #输出 ('raise exception', 2) ``` 为了能够捕获异常,"except"语句必须有用相同的异常来抛出类对象或者字符串。如果要捕获上面代码抛出的异常,except 语句应该如下所示: ``` #定义函数 def diyException(level): if level > 0: raise Exception("error level", level) #主动抛出一个异常,并且带有参数 print('我是不会执行的') #这行代码不会执行 try: diyException(2) #执行异常方法 except 'error level' as err: #捕获异常 print(err) #打印异常参数 #输出 Traceback (most recent call last): File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 51, in diyException(2) #执行异常方法 File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 47, in diyException raise Exception("error level", level) #主动抛出一个异常,并且带有参数 Exception: ('error level', 2) ``` 当然,我们也可以通过 traceback 来捕获异常: ``` import traceback #定义函数 def diyException(level): if level > 0: raise Exception("error level", level) #主动抛出一个异常,并且带有参数 print('我是不会执行的') #这行代码不会执行 try: diyException(2) #执行异常方法 except Exception: #捕获异常 traceback.print_exc() #输出 Traceback (most recent call last): File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 51, in diyException(2) #执行异常方法 File "/Users/cxhuan/Documents/python_workspace/stock/test.py", line 47, in diyException raise Exception("error level", level) #主动抛出一个异常,并且带有参数 Exception: ('error level', 2) ``` ### 23.7、用户自定义异常 除了使用 Python 内置的异常,我们还可以创建自己的异常类型。创建自己的异常非常简单,只需要创建一个类,并继承 Exception 类或其子类。 下面的代码创建了一个异常 DiyError 继承自 Python 内置的 RuntimeError,用于在异常触发时输出更多的信息。 ``` #自定义异常 class DiyError(RuntimeError): def __init__(self, arg): self.args = arg try: raise DiyError("my diy exception") #触发异常 except DiyError as e: print(e) ``` 定义好了之后,我们就可以在 except 语句后使用 DiyError 异常,变量 e 是用于创建 DiyError 类的实例。我们也可以通过 raise 语句手动触发这个异常。 ### 23.8、预定义的清理行为 一些对象定义了标准的清理行为,无论系统是否成功的使用了它,一旦不需要它了,那么这个标准的清理行为就会执行。 ``` for line in open("myfile.txt"): print(line, end="") ``` 上面这个例子尝试打开一个文件,然后把内容打印出来。但是有一个问题:当执行完毕后,程序没有关闭文件流,文件会保持打开状态。 关键词 with 语句就可以保证诸如文件之类的对象在使用完之后一定会正确的执行他的清理方法。 ``` with open("myfile.txt") as f: for line in f: print(line, end="") ``` 以上这段代码执行完毕后,就算在处理过程中出问题了,文件 f 总是会关闭。这里面的原理就是使用了 finally 机制,有兴趣的可以去深入了解一下。 ## 24、Python 之引用 Python 中对于变量的处理与 C 语言有着很大的不同,Python 中的变量具有一个特殊的属性:identity,即“身份标识”。这种特殊的属性也在很多地方被称为“引用”。 为了更加清晰地说明引用相关的问题,我们首先要介绍两个工具:一个Python的内置函数:id();一个运算符:is;同时还要介绍一个sys模块内的函数:getrefcount()。 ### 24.1、内置函数:id() 返回值为传入对象的“标识”。该标识是一个唯一的常数,在传入对象的生命周期内与之一一对应。生命周期没有重合的两个对象可能拥有相同的id()返回值。 一个对象的id就可以视作是其虚拟的内存地址。 ### 24.2、运算符is is的作用是比较对象的标识。 ### 24.3、sys模块函数getrefcount()函数 返回值是传入对象的引用计数。由于作为参数传入getrefcount()的时候产生了一次临时引用,因此返回的计数值一般要比预期多1。 此处的“引用计数”,在 Python中被定义为“对象被引用的次数”。一旦引用计数归零,则对象所在的内存被释放。这是 Python 内部进行自动内存管理的一个机制。 ### 24.4、python中的变量地址 对 Python 来说,变量就不再是一段固定的地址,在C语言中是固定地址,而只是 Python 中各个对象所附着的标签。理解这一点对于理解 Python 的很多特性十分重要。 实例: ``` >>> a = 10000 >>> id(a) 1823863879824 >>> a = 12345 >>> id(a) 1823863880176 ``` 这就有点儿意思了,更加神奇的是,即使赋给变量同一个常数,其得到的id也可能不同: ``` >>> a = 10000 >>> id(a) 1823863880304 >>> a = 10000 >>> id(a) 1823863879408 ``` 并不是对所有的常数都存在这种情况。以常数1为例: ``` >>> a = 1 >>> id(a) 140734357607232 >>> a = 1 >>> id(a) 140734357607232 >>> id(1) 140734357607232 ``` 常数1对应的id一直都是相同的,没有发生变化,因此变量a的id也就没有变化,这是因为Python在内存中维护了一个特定数量的常量池,对于一定范围内的数值均不再创建新的对象,而直接在这个常量池中进行分配。 假如a对应的数据类型是一个列表,那么: ``` >>> a = [1,2] >>> id(a) 2161457994952 >>> a = [1,2] >>> id(a) 2161458037448 ``` 得到的id值也是不同的,Python 中对列表对象的操作还表现出另一种特性: ``` >>> c = [1, 2, 3] >>> id(c) 2161458355400 >>> c[2] = 5 >>> c [1, 2, 5] >>> id(c) 2161458355400 >>> c.append(3) >>> c [1, 2, 5, 3] >>> id(c) 2161458355400 ``` 对于列表而言,可以通过直接操作变量本身,从而在不改变其引用的情况下改变所引用的值,如果是两个变量同时引用同一个列表,则对其中一个变量本身直接进行操作,也会影响到另一个变量的值: ``` >>> c = [1, 2, 3] >>> cc = c >>> id(c) 1823864610120 >>> id(cc) 1823864610120 ``` 显然此时的变量c和cc的id是一致的。现在改变c所引用的列表值: ``` >>> c[2] = 5 >>> cc [1, 2, 5] ``` 可以看到cc所引用的列表值也随之变化了。再看看相应地id: ``` >>> id(c) 1823864610120 >>> id(cc) 1823864610120 ``` 两个变量的id都没有发生变化。再调用append()方法: ``` >>> c.append(3) >>> c [1, 2, 5, 3] >>> cc [1, 2, 5, 3] >>> id(c) 1823864610120 >>> id(cc) 1823864610120 ``` 删除元素: ``` >>> del c[3] >>> c [1, 2, 5] >>> cc [1, 2, 5] >>> id(c) 1823864610120 >>> id(cc) 1823864610120 ``` 也就是说,对于变量本身进行的操作并不会创建新的对象,而是会直接改变原有对象的值。 相应地,对于数值进行加减乘除并将结果赋给原来的变量,都会改变变量对应的引用值: ``` >>> a = 10000 >>> id(a) 2161457772304 >>> a = a + 1 >>> a 10001 >>> id(a) 2161457772880 ``` ``` >>> li = [1, 2, 3] >>> id(li) 2161458469960 >>> li += [4] >>> id(li) 2161458469960 >>> li [1, 2, 3, 4] ``` 上面的执行结果明明进行的是“相加再赋值”操作,按理说引用发生了变化,但是为什么变量的值发生了改变,但引用却没有变? 实际上这是因为加法运算符在 Python 中存在重载的情况,对列表对象和数值对象来说,加法运算的底层实现是完全不同的,在简单的加法中,列表的运算还是创建了一个新的列表对象;但在简写的加法运算+=实现中,则并没有创建新的列表对象。这一点要十分注意。 ### 24.5、原理解析 Python 中的六个标准数据类型实际上分为两大类:可变数据和不可变数据。其中,列表、字典和集合均为“可变对象”;而数字、字符串和元组均为“不可变对象”。实际上上面演示的数值数据(即数字)和列表之间的差异正是这两种不同的数据类型导致的。 由于数字是不可变对象,我们不能够对数值本身进行任何可以改变数据值的操作。因此在 Python 中,每出现一个数值都意味着需要另外分配一个新的内存空间(常量池中的数值例外)。 ### 24.6、总结 对于列表、字典和集合这些“可变对象”,通过对变量所引用对象本身进行操作,可以只改变变量的值而不改变变量的引用;但对于数字、字符串和元组这些“不可变对象”,由于对象本身是不能够进行变值操作的,因此要想改变相应变量的值,就必须要新建对象,再把新建对象赋值给变量。 ## 25、迭代器 对于迭代器对象本身来说,需要具有`__iter__()`[2]和`__next__()`[3]两种方法,二者合称为“迭代器协议”。也就是说,只要同时具有这两种方法,Python 解释器就会认为该对象是一个迭代器;反之,只具有其中一个方法或者二者都不具有,解释器则认为该对象不是一个迭代器。 ``` >>> from collections import Iterable, Iterator, Container >>> class bothIterAndNext: ... def __iter__(self): ... pass ... def __next__(self): ... pass ... >>> isinstance(bothIterAndNext(), Iterable) # 两种方法都有的对象是可迭代的 True >>> isinstance(bothIterAndNext(), Iterator) # 两种方法都有的对象是迭代器 True >>> >>> class onlyNext: ... def __next__(self): ... pass ... >>> isinstance(onlyNext(), Iterable) # 只有方法 __next__() 是不可迭代的 False >>> isinstance(onlyNext(), Iterator) # 只有方法 __next__() 不是迭代器 False >>> >>> class onlyIter: ... def __iter__(self): ... pass ... >>> isinstance(onlyIter(), Iterable) # 只有方法 __iter__() 是可迭代的 True >>> isinstance(onlyIter(), Iterator) # 只有方法 __iter__() 不是迭代器 False ``` ### 25.1、迭代器的实质 迭代器对象本质上代表的是一个数据流,通过反复调用其方法__next__()或将其作为参数传入next()函数,即可按顺序逐个返回数据流中的每一项;直到流中不再有数据项,从而抛出一个StopIteration异常,终止迭代。 在 Python 中内置了两个函数:iter()和next(),分别用于“将参数对象转换为迭代器对象”和“从迭代器中取出下一项”。 实际上所有具有方法__iter__()的对象均被视作“可迭代的”。因为方法__iter__() 进行的操作其实就是返回一个该对象对应的迭代器,也就是说“可迭代的(iterable)”的真实含义其实是“可以被转换为迭代器(iterator)的”。而内置函数iter()也是调用对象本身具有的__iter__() 方法来实现特定对象到迭代器的转换。 相应地,内置函数next()其实是调用了对象本身的方法__next__(),而该方法执行的操作就是从对象对应的数据流中取出下一项。 因此直接调用对象的__iter__()和__next__()方法与将对象作为参数传入内置函数iter()和next()是等效的。 要注意的一点在于,对迭代器调用其本身的__iter__()方法,得到的将会是这个迭代器自身,该迭代器相关的状态都会被保留,包括该迭代器目前的迭代状态。见下述代码: ``` >>> li = [1, 2, 3] >>> li_iterator = iter(li) >>> isinstance(li, Iterator) False >>> isinstance(li_iterator, Iterator) True ``` 显然,列表li本身并不是一个迭代器,而将其传入内置函数iter()就得到了相应于列表li的迭代器li_iterator。我们调用next()函数来迭代它: ``` >>> next(li_iterator) 1 >>> next(li_iterator) 2 ``` 一切都在预料之中。我们再来将其本身作为参数传入内置函数iter(): ``` >>> li_iterator = iter(li_iterator) >>> next(li_iterator) 3 ``` 到这里跟我们希望的就有所出入了。在使用这样一个语句的时候,通常我们的目的都是得到一个新的迭代器,而非跟原先的迭代器一样的对象。 更进一步地,我们还可以发现,对迭代器调用iter()函数得到的对象不仅与原先的迭代器具有相同的状态,它们其实就是指向同一个对象: ``` >>> id(li_iterator) 2195581916440 >>> li_iterator = iter(li_iterator) >>> id(li_iterator) 2195581916440 >>> li_iterator2 = iter(li_iterator) >>> id(li_iterator2) 2195581916440 ``` 也就是说在对象本身就是一个迭代器的情况下,生成的对应迭代器的时候 Python 不会进行另外的操作,就返回这个迭代器本身作为结果。 ## 26、装饰器 装饰器(decorator),又称“装饰函数”,即一种返回值也是函数的函数,可以称之为“函数的函数”。其目的是在不对现有函数进行修改的情况下,实现额外的功能。最基本的理念来自于一种被称为“装饰模式”的设计模式。 在 Python 中,装饰器属于纯粹的“语法糖”,不使用也没关系,但是使用的话能够大大简化代码,使代码更加易读——当然,是对知道这是怎么回事儿的人而言。 想必经过一段时间的学习,大概率已经在 Python 代码中见过@这个符号。没错,这个符号正是使用装饰器的标识,也是正经的 Python 语法。 语法糖:指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。 ### 26.1、运行机制 简单来说,下面两段代码在语义上是可以划等号的(当然具体过程还是有一点微小区别的): ``` def IAmDecorator(foo): '''我是一个装饰函数''' pass @IAmDecorator def tobeDecorated(...): '''我是被装饰函数''' pass ``` 与: ``` def IAmDecorator(foo): '''我是一个装饰函数''' pass def tobeDecorated(...): '''我是被装饰函数''' pass tobeDecorated = IAmDecorator(tobeDecorated) ``` 可以看到,使用装饰器的@语法,就相当于是将具体定义的函数作为参数传入装饰器函数,而装饰器函数则经过一系列操作,返回一个新的函数,然后再将这个新的函数赋值给原先的函数名。 最终得到的是一个与我们在代码中显式定义的函数同名而异质的新函数。 而装饰函数就好像为原来的函数套了一层壳。 这里要注意一点,上述两段代码在具体执行上还是存在些微的差异。在第二段代码中,函数名tobeDecorated实际上是先指向了原函数,在经过装饰器修饰之后,才指向了新的函数;但第一段代码的执行就没有这个中间过程,直接得到的就是名为tobeDecorated的新函数。 此外,装饰函数有且只能有一个参数,即要被修饰的原函数。 ### 26.2、用法 Python 中,装饰器分为两种,分别是“函数装饰器”和“类装饰器”,其中又以“函数装饰器”最为常见,“类装饰器”则用得很少。 #### 26.2.1、函数装饰器 由于要求装饰函数返回值也为一个函数的缘故,为了在原函数的基础上对功能进行扩充,并且使得扩充的功能能够以函数的形式返回,因此需要在装饰函数的定义中再定义一个内部函数,在这个内部函数中进一步操作。最后return的对象就应该是这个内部函数对象,也只有这样才能够正确地返回一个附加了新功能的函数。 装饰函数就像一个“包装”,将原函数装在了装饰函数的内部,从而通过在原函数的基础上附加功能实现了扩展,装饰函数再将这个新的整体返回。同时对于原函数本身又不会有影响。这也是“装饰”二字的含义。 ``` def IAmDecorator(fun): def wrapper(*args, **kw): print("我真的是一个装饰器") return fun(*args, **kw) return wrapper @IAmDecorator def func(): print("我是原函数") func() 打印结果: 我真的是一个装饰器 我是原函数 ``` 内部函数参数设置为(*args, **kw) 的目的是可以接收任意参数,之所以要让wrapper能够接收任意参数,是因为我们在定义装饰器的时候并不知道会用来装饰什么函数,具体函数的参数又是什么情况;定义为“可以接收任意参数”能够极大增强代码的适应性。 只执行装饰器函数,不执行原函数,如下: ``` def IAmDecorator(fun): def wrapper(*args, **kw): print("我真的是一个装饰器") return fun return wrapper @IAmDecorator def func(): print("我是原函数") func() 打印结果: 我真的是一个装饰器 ``` 我们的装饰器目的是返回一个函数对象,返回语句的对象一定是不带参数的函数名;在内部函数中,我们是需要对原函数进行调用,因此需要带上函数参数,否则,如果内部函数的返回值还是一个函数对象,就还需要再给一组参数才能够调用原函数。 原函数没有被成功调用,只是得到了原函数对应的函数对象。只有进一步给出了下一组参数,才能够发生正确的调用。 ##### 26.2.1.1、函数属性 还应注意的是,经过装饰器的修饰,原函数的属性也发生了改变。 ``` >>> def func(): ... print("我是原函数") ... >>> func.__name__ 'func' ``` 正常来说,定义一个函数,其函数名称与对应的变量应该是一致的,这样在一些需要以变量名标识、索引函数对象时才能够避免不必要的问题。但是事情并不是那么顺利: ``` >>> @IAmDecorator ... def func(): ... print("我是原函数") ... >>> func.__name__ 'wrapper' ``` 变量名还是那个变量名,原函数还是那个原函数,但是函数名称却变成了装饰器中内部函数的名称。 在这里我们可以使用 Python 内置模块functools中的wraps工具,实现“在使用装饰器扩展函数功能的同时,保留原函数属性”这一目的。这里functools.wraps本身也是一个装饰器。运行效果如下: ``` > > > import functools >>> # 定义保留原函数属性的装饰器 ... def IAmDecorator(fun): ... @functools.wraps(fun) ... def wrapper(*args, **kw): ... print("我真的是一个装饰器") ... return fun(*args, **kw) ... return wrapper ... > > > @IAmDecorator ... def func(): ... print("我是原函数") ... > > > func.__name__ 'func' ``` #### 26.2.2、类装饰器 类装饰器的概念与函数装饰器类似,使用上语法也差不多: ``` @ClassDecorator class Foo: pass ``` 等价于 ``` class Foo: pass Foo = ClassDecorator(Foo) ``` 在定义类装饰器的时候,要保证类中存在__init__和__call__两种方法。其中__init__方法用以接收原函数或类,__call__方法用以实现装饰逻辑。 简单来讲,__init__方法负责在初始化类实例的时候,将传入的函数或类绑定到这个实例上;而__call__方法则与一般的函数装饰器差不多,连构造都没什么两样,可以认为__call__方法就是一个函数装饰器,因此不再赘述。 ## 27、命名空间 命名空间是名字和对象的映射,命名空间是通过 Python Dictionary(字典) 来实现的。 命名空间提供了一个在大型项目下避免名字冲突的方法 Python 中各个命名空间都是独立的,他们之间无任何关系 一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。 命名空间就像是计算机中的文件夹一样,同一个文件夹中的文件不可重名,但是如果两个文件从属于不同的文件夹就可以重名。 同理相同的对象名可以存在不同的命名空间中。 ### 27.1、命名空间的种类 命名空间的种类分为 3 类,命名空间的种类也体现了命名空间的生命周期。三个种类及生命周期描述如下: 1)内置名称(built-in names) Python 语言内置的名称,比如函数名 abs、char 和异常名称 BaseException、Exception 等等。 生命周期: 对于Python built-in names组成的命名空间,它在Python解释器启动的时候被创建,在解释器退出的时候才被删除; 2)全局名称(global names) 模块中定义的名称,记录了模块的变量,包括函数、类、其它导入的模块、模块级的变量和常量。 生命周期: 对于一个Python模块的global namespace,它在这个module被import的时候创建,在解释器退出的时候退出; 3)局部名称(local names) 函数中定义的名称,记录了函数的变量,包括函数的参数和局部定义的变量。(类中定义的也是) 生命周期: 对于一个函数的local namespace,它在函数每次被调用的时候创建,函数返回的时候被删除。 注意:命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束。因此,我们无法从外部命名空间访问内部命名空间的对象。例如: ``` # var1 是全局名称 var1 = 5 var4 = var2 # 这地方就报错,全局命名空间的变量无法访问局部命名空间的变量 def some_func(): # var2 是局部名称 var2 = 6 def some_inner_func(): # var3 是内嵌的局部名称 var3 = 7 ``` Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问。 ``` name1 = 'SuSan' if chr('SuSan'.__eq__(name1)): result = 'I am from China' else: result = 'I am from USA' print(result) # 结果为I am from China ``` 实例中 result 变量定义在 if 语句块中,但外部还是可以访问的。 如果将 result 定义在函数中,则它就是局部变量,外部不能访问,在代码中会报错运行出异常: ``` def names(): name2 = 'SuSan' # 在程序调用方法内部的变量报错 if ('SuSan'.__eq__(name2)): result = 'I am ' + name2 + ',' + 'I am from China' else: result = 'I am from USA' print(result) ``` name2 未定义,因为name2 是函数names() 中的局部变量,只能在函数内部调用,外部不能调用函数中的局部变量。 ### 27.2、查找变量的顺序、创建变量的顺序、销毁变量的顺序 1、查找变量 如果程序执行时去使用一个变量 hello ,那么 Python, 查找变量顺序为: 局部的命名空间 -> 全局命名空间 -> 内置命名空间 如果按照这个顺序找不到相应的变量,它将放弃查找并抛出一个 NameError 异常: 2、创建变量 python解释器启动 ->创建内建命名空间 -> 加载模块 -> 创建全局命名空间 ->函数被调用 ->创建局部命名空间 3、销毁变量 函数调用结束 -> 销毁函数对应的局部命名空间 -> python虚拟机(解释器)退出 ->销毁全局命名空间 ->销毁内建命名空间。 一个模块的引入,函数的调用,类的定义都会引入命名空间,函数中的再定义函数,类中的成员函数定义会在局部namespace中再次引入局部namespace。 ### 27.3、作用域 作用域就是一个 Python 程序可以直接访问命名空间的正文区域。 Python 程序中,直接访问一个变量,会从内到外依次访问所有的作用域直到找到,否则会报未定义的错误。 Python 中,程序的变量并不是在哪个位置都可以访问的,访问权限决定于这个变量是在哪里赋值的。 Python 中, 变量的作用域决定了在哪一部分程序可以访问哪个特定的变量名称 #### 27.3.1、作用域种类 作用域分为4类,分别如下: L(Local):最内层,包含局部变量,比如一个函数/方法内部。 E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。 G(Global):当前脚本的最外层,比如当前模块的全局变量。 B(Built-in):包含了内建的变量/关键字等,最后被搜索。 作用域规则顺序为:L->E->G->B ,如果变量在局部内找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再找不到就去内置中找。 ``` # 全局作用域 global_scope = 0 # 定义闭包函数中的局部作用域 def outer(): o_count = 1 # 闭包函数外的函数中,相对于函数 inner() 来说 作用域非局部 def inner(): local_scope = 2 # 局部作用域 ``` 以上实例展示的是全局作用域和闭包函数中的函数,以及函数中的局部作用域,对于函数 inner() 来说,outer() 中的作用域为 non-local Python 中的内建作用域(Built-in):包含了内建的变量/关键字等,最后被搜索,内建作用域是通过一个名为 builtin 的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。在Python3.0中,可以使用以下的代码来查看到底预定义了哪些变量: ``` import builtins print(dir(builtins)) ``` #### 27.3.2、global 和 nonlocal关键字 当内部作用域想修改外部作用域的变量时,就要用到global和nonlocal关键字了。 变量访问顺序: 当前作用域局部变量->外层作用域变量->再外层作用域变量->......->当前模块全局变量->pyhton内置变量 global:全局变量,当局部作用域改变全局变量用global,同时global还可以定义新的全局变量 nonlocal:外层嵌套函数的变量,nonlocal不能定义新的外层函数变量,只能改变已有的外层函数变量,同时nonlocal不能改变全局变量 1、修改全局变量 ``` num = 1 def fun1(): # 申明访问全局变量 global num # 需要使用 global 关键字声明 # 输出全局变量原始值 print(num) #  修改全局变量 num = 123 print(num) # 调用函数 fun1() # 输出修改后的全局变量值 print(num) ``` 输出结果: ``` 1 123 123 ``` 2、修改嵌套作用域 如果要修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量则需要 nonlocal 关键字 ``` # 定义函数 def outer(): # 定义变量 num = 10 #  定义嵌套函数 def inner(): nonlocal num # nonlocal关键字声明,使用函数中变量 # 修改变量值 num = 100 print(num) inner() print(num) outer() ``` 结果如下: ``` 100 100 ``` 3、global 和 nonlocal 的区别 两者的功能不同。global关键字修饰变量后标识该变量是全局变量,对该变量进行修改就是修改全局变量,而nonlocal关键字修饰变量后标识该变量是上一级函数中的局部变量,如果上一级函数中不存在该局部变量,nonlocal位置会发生错误(最上层的函数使用nonlocal修饰变量必定会报错)。 两者使用的范围不同。global关键字可以用在任何地方,包括最上层函数中和嵌套函数中,即使之前未定义该变量,global修饰后也可以直接使用,而nonlocal关键字只能用于嵌套函数中,并且外层函数中定义了相应的局部变量,否则会发生错误 ## 28、Python 到底是值传递还是引用传递 凡是对原对象操作的函数,都会影响传递的实际参数;凡是生成了新对象的操作,都不会影响传递的实际参数,如下: ``` def swap(list): list.append(4) # 对原对象操作的函数,都会影响传递的实际参数 print("in swap list is %s " % list) list_x = [1, 2, 3] swap(list_x) print("in main list is %s " % list_x) ``` 打印结果: in swap list is [1, 2, 3, 4] in main list is [1, 2, 3, 4] ``` def swap(list): list = list + [4] # 凡是生成了新对象的操作,都不会影响传递的实际参数 print("in swap list is %s " % list) list_x = [1, 2, 3] swap(list_x) print("in main list is %s " % list_x) ``` 打印结果: in swap list is [1, 2, 3, 4] in main list is [1, 2, 3] ## 29、== 操作符和 is 相信大家对于这两个操作符都不陌生。具体来说就是 == 操作符比较的是两个对象的值是否相等,而 is 操作符的含义则是二者到底是否是同一个对象,换言之,即两个对象是否指向同一块内存地址。 上面我们说过,Python 中一切皆是对象,对象包含 id(唯一身份标识),type(类型) 和 value(值) 三个要素。id 可以通过函数 id(obj) 来获取。因此 is 操作符就相当于比较两个对象的 id 是否相同,而 == 操作符则相当于比较两个对象的 value 是否相同。 ``` >>> a = 256 >>> b = 256 >>> a == b True >>> a is b True >>> a = 257 >>> b = 257 >>> a == b True >>> a is b False ``` 同样,对于 == 操作符,无论是 a,b 的值是 256 还是 257 二者都是相等的。奇怪的是同样是 is 操作,数值的大小居然对结果有影响。 事实上 a is b 为 True 的结论只适用于 -5 到 256 的数值,因为出于性能的考虑,Python 对这个范围内的数值进行了缓存。当你为整数对象赋值时(-5 到 256)并不会生成新的对象,而是使用事先创建好的缓存对象。如果超过了缓存范围,那么就会申请两块不同的内存地址,is 操作当然会返回 False。 但是,当你把同样的代码放到编辑器中去去执行时,你会惊奇的发现程序的执行结果跟我们刚才所说的缓存机制竟然是相冲突的。 不是说 -5 到 256 范围内的整数才会被缓存么。为啥这么大的数 is 操作也返回 True 了呢。 从结果来看 a 和 b 两个大数的内存地址肯定是一样的,不然 is 操作符也不会返回 True。这是因为在交互模式(就是那个黑窗口了)下每一条命令就是一个代码块,Python 逐行编译执行;在编辑器中,一个函数,一个类或者一个文件才是一个代码块。Python 会整体编译执行,因此相同值的变量只会初始化一次,第二次初始化相同值的变量时会重用旧值。 上面所说的编译是指 CPython 将源代码编译为字节码的过程。 只有当对象的值是数值或者字符串型且在缓存范围内时,a is b 才返回 True,否则当 a 和 b 是 int,str,list,tuple,set 或 dict 类型时,a is b 均返回 False。 事实上,经过测试,我发现对于有空格的字符串 Python 并不会去缓存。找了好久,终于在 stringobject.h 中找到了解释,Python 解释器采取 intern 机制来提高字符串操作效率,当内存中有相同值的字符串时就会重用,而不是生成一个新的相同值的字符串对象。但也不是说对所有的字符串均采取该 intern 机制。只有看起来像 Python 标识符的字符串才会被缓存。 另外,比较操作符 is 的效率要优于 ==,因为 is 操作符无法被重载,执行 is 操作只是对比对象的 id 而已。而 == 操作符则会递归地遍历对象的所有值,并逐一比较。 ## 30、对象的拷贝 对于顶层不可变的对象,不存在对象的拷贝,因为都是指向同一个对象,没有新的对象产生。 通过切片来实现浅拷贝: ``` >>> a = [1, 2, 3] >>> b = a[:] >>> a == b True >>> a is b False ``` 但是对于顶层不可变的对象,不存在对象的拷贝,因为都是指向同一个对象,没有新的对象产生。 ``` >>> a = (1,2,3) >>> b = tuple(a) >>> a == b True >>> a is b True ``` 如你所见,关于浅拷贝如果元素不可变的还好,没什么副作用;如果元素可变,那就要小心其副作用了。 深拷贝递归拷贝顶层对象以及它内部的子对象,因此,新对象和原来的旧对象,没有任何关联。Python 中使用 copy.deepcopy() 函数来实现对对象的深拷贝。 ``` import copy a = [1, 'hello', [1,2]] b = copy.deepcopy(a) a[0] = 100 a[2][0] = 100 print(a) print(b) ## 输出结果 [100, 'hello', [100, 2]] [1, 'hello', [1, 2]] ``` 深拷贝即拷贝了顶层对象,同时还拷贝了子对象,所以 a[2] 和 b[2] 指向的是两个不同的列表。修改 a[2][0] 后,重新指向了新的整数,但是这并不会影响到 b[2]。 ``` >>> import copy >>> a = (1,2,3) >>> b = copy.deepcopy(a) >>> a is b True >>> a = (1,2,[1,2]) >>> b = copy.deepcopy(a) >>> a is b False >>> a[2][0] = 100 >>> a (1, 2, [100, 2]) >>> b (1, 2, [1, 2]) ``` 对于不可变对象来说,如果其子对象全部不可变,那么深拷贝就和浅拷贝是一样的效果,都是指向同一个内存地址。 但是如果子对象中包含可变对象,那么深拷贝之后的对象就不再是原来的对象了,因为可变对象被重新拷贝了一份,放到例子中就是 a[2] 和 b[2] 指向的不再是同一个列表。因此,修改 a[2] 并不会影响 b[2]。 如果深拷贝的对象中存在指向自身的引用,那么会不会无限递循环呢。 答案是不会,深拷贝函数内部维护了一个字典,该字典记录了已经拷贝的对象与其 id。拷贝过程中,如果字典里已经存储了将要拷贝的对象,则会从字典直接返回,不再进行递归。 ### 30.1、总结 is 比较两个对象的 id 值是否相等,== 比较的是两个对象的值是否相等,小整数对象 [-5, 256] 会被缓存起来重复使用,is 的效率要优于 == 操作。浅拷贝是对原对象中子对象的引用,有可能会产生副作用,深拷贝会创建新的对象,不会和原对象干扰。 ## 31、线程 ### 31.1、简介 说到线程就不得不提与之相关的另一概念:进程,那么什么是进程?与线程有什么关系呢?简单来说一个运行着的应用程序就是一个进程,比如:我启动了自己手机上的酷猫音乐播放器,这就是一个进程,然后我随意点了一首歌曲进行播放,此时酷猫启动了一条线程进行音乐播放,听了一部分,我感觉歌曲还不错,于是我按下了下载按钮,此时酷猫又启动了一条线程进行音乐下载,现在酷猫同时进行着音乐播放和音乐下载,此时就出现了多线程,音乐播放线程与音乐下载线程并行运行,说到并行,你一定想到了并发吧,那并行与并发有什么区别呢?并行强调的是同一时刻,并发强调的是一段时间内。线程是进程的一个执行单元,一个进程中至少有一条线程,进程是资源分配的最小单位,线程是 CPU 调度的最小单位。 线程一般会经历新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)5 种状态,当线程被创建并启动后,并不会直接进入运行状态,也不会一直处于运行状态,CPU 可能会在多个线程之间切换,线程的状态也会在就绪和运行之间转换。 ### 31.2、Python 中的线程与进程 Python 提供了 _thread(Python3 之前名为 thread ) 和 threading 两个线程模块。_thread 是低级、原始的模块,threading 是高级模块,对 _thread 进行了封装,增强了其功能与易用性,绝大多数时候,我们只需使用 threading 模块即可。下一节我们会对 threading 模块进行详细介绍。 Python 提供了 multiprocessing 模块对多进程进行支持,它使用了与 threading 模块相似的 API 产生进程,除此之外,还增加了新的 API,用于支持跨多个输入值并行化函数的执行及跨进程分配输入数据,[详细用法可以参考官方文档](https://docs.python.org/zh-cn/3/library/multiprocessing.html) 。 ### 31.3、全局解释器锁GIL #### 31.3.1、GIL 相关概念 GIL 全称 Global Interpreter Lock(全局解释器锁),是 Python 解释器 CPython 采用的一种机制,通过该机制来控制同一时刻只有一条线程执行 Python 字节码,本质是一把全局互斥锁,将并行运行变成串行运行。 什么是 CPython 呢?我们从 Python 官方网站下载安装 Python 后,获得的官方解释器就是 CPython,因其是 C 语言开发的,故名为 CPython,是目前使用最广泛的 Python 解释器;因为我们大部分环境下使用的默认解释器就是 CPython,有些人会认为 CPython 就是 Python,进而以为 GIL 是 Python 的特性,其实 CPython 只是一种 Python 解释器,除了 CPython 解释器还有:PyPy、Psyco、Jython (也称 JPython)、IronPython 等解释器,其中 Jython 与 IronPython 分别采用 Java 与 C# 语言实现,就没有采用 GIL 机制;而 GIL 也不是 Python 特性,Python 可以完全独立于 GIL 运行。 #### 31.3.2、GIL 起源与发展 我们已经知道了 GIL 是 CPython 解释器中引入的机制,那为什么 CPython 解释器中要引入 GIL 呢?GIL 一开始出现是因为 CPython 解释器的内存管理不是线程安全的,也就是采用 GIL 这把锁解决 CPython 的线程安全问题。 随着时间的推移,计算机硬件逐渐向多核多线程方向发展,为了更加充分的利用多核 CPU 资源,各种编程语言开始对多线程进行支持,Python 也加入了其中,尽管多线程的编程方式可以提高程序的运行效率,但与此同时也带来了线程间数据一致性和状态同步的问题,解决这个问题最简单的方式就是加锁,于是 GIL 这把锁再次登场,很容易便解决了这个问题。 慢慢的越来越多的代码库开发者开始接受了这种设定,进而开始大量依赖这种特性,因为默认加了 GIL 后,Python 的多线程便是线程安全的了,开发者在实际开发无需再考虑线程安全问题,省掉了不少麻烦。 对于 CPython 解释器中的多线程程序,为了保证多线程操作安全,默认使用了 GIL 锁,保证任意时刻只有一个线程在执行,其他线程处于等待状态。 #### 31.3.3、成也 GIL,败也 GIL 以前为了解决多线程的线程操作安全问题,CPython 采用了 GIL 锁的方式,这种方式虽然解决了线程操作安全问题,但由于同一时刻只能有一条线程执行,等于主动放弃了线程并行执行的机会,因此在目前 CPython 下的多线程并不是真正意义上的多线程。 现在这种情况,我们可能会想要实现真正意义上的多线程,可不可以去掉 GIL 呢?答案是可以的,但是有一个问题:依赖这个特性的代码库太多了,现在已经是尾大不掉了,使去除 GIL 的工作变得举步维艰。 当初为了解决多线程带来的线程操作安全问题使用了 GIL,现在又发现 GIL 方式下的多线程比较低效,想要去掉 GIL,但已经到了尾大不掉的地步了,真是成也 GIL,败也 GIL。 对于 CPython 下多线程的低效问题,除了去掉 GIL,还有什么其他解决方案吗?我们来简单了解下: 1)使用无 GIL 机制的解释器;如:Jython 与 IronPython,但使用这两个解释器失去了利用 C 语言模块一些优秀特性的机会,因此这种方式还是比较小众。 2)使用 multiprocess 代替 threading;multiprocess 使用了与 threading 模块相似的 API 产生进程,不同之处是它使用了多进程而不是多线程,每个进程有自己独立的 GIL,因此不会出现进程之间的 GIL 争抢,但这种方式只对计算密集型任务有效,通过后面的示例我们也能得出这个结论。 ### 31.4、多线程实现 threading 模块通过 Thread 类提供对多线程的支持,首先,我们要导入 threading 中的类 Thread,示例如下: from threading import Thread 依赖导入了,接下来要就要创建线程了,直接创建 Thread 实例即可,示例如下: ``` # method 为线程要执行的具体方法 p1 = Thread(target=method) ``` 若要实现两条线程,再创建一个 Thread 实例即可,示例如下: ``` p2 = Thread(target=method) ``` 需要实现更多条的线程也是一个道理。线程创建好了,通过 start 方法启动即可,示例如下: ``` p1.start() p2.start() ``` 如果是多线程任务,我们可能需要等待所有线程执行完成再进行下一步操作,使用 join 方法即可。示例如下: ``` # 等待线程 p1、p2 都执行完 p1.join() p2.join() ``` #### 31.4.1、通过构造器创建线程 调用 threading.Thread 类的如下构造器创建线程: ``` threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None) group:指定该线程所属的线程组,目前该参数还未实现,为了日后扩展 ThreadGroup 类实现而保留。 target:用于 run() 方法调用的可调用对象,默认是 None,表示不需要调用任何方法。 args:是用于调用目标函数的参数元组,默认是 ()。 kwargs:是用于调用目标函数的关键字参数字典,默认是 {}。 daemon:如果 daemon 不是 None,线程将被显式的设置为守护模式,不管该线程是否是守护模式,如果是 None (默认值),线程将继承当前线程的守护模式属性。 ``` 示例如下: ``` import time import threading def work(num): print('线程名称:',threading.current_thread().getName(),'参数:',num,'开始时间:',time.strftime('%Y-%m-%d %H:%M:%S')) if __name__ == '__main__': print('主线程开始时间:',time.strftime('%Y-%m-%d %H:%M:%S')) t1 = threading.Thread(target=work,args=(3,)) t2 = threading.Thread(target=work,args=(2,)) t3 = threading.Thread(target=work,args=(1,)) t1.start() t2.start() t3.start() t1.join() t2.join() t3.join() print('主线程结束时间:', time.strftime('%Y-%m-%d %H:%M:%S')) ``` 运行结果: ``` 主线程开始时间: 2021-10-09 10:31:39 线程名称: Thread-1 参数: 3 开始时间: 2021-10-09 10:31:39 线程名称: Thread-2 参数: 2 开始时间: 2021-10-09 10:31:39 线程名称:Thread-3 参数: 1 开始时间: 2021-10-09 10:31:39 主线程结束时间: 2021-10-09 10:31:39 ``` 上述示例中实例化了三个 Thread 类的实例,并向任务函数传递不同的参数,start 方法开启线程,join 方法阻塞主线程,等待当前线程运行结束。 #### 31.4.2、通过继承方式创建线程 通过继承的方式创建线程包括如下步骤: 1)定义 Thread 类的子类,并重写该类的 run 方法; 2)创建 Thread 子类的实例,即创建线程对象; 3)调用线程对象的 start 方法来启动线程。示例如下: ``` import time import threading class MyThread(threading.Thread): def __init__(self,num): super().__init__() self.num = num def run(self): print('线程名称:', threading.current_thread().getName(), '参数:', self.num, '开始时间:', time.strftime('%Y-%m-%d %H:%M:%S')) if __name__ == '__main__': print('主线程开始时间:',time.strftime('%Y-%m-%d %H:%M:%S')) t1 = MyThread(3) t2 = MyThread(2) t3 = MyThread(1) t1.start() t2.start() t3.start() t1.join() t2.join() t3.join() print('主线程结束时间:', time.strftime('%Y-%m-%d %H:%M:%S')) ``` 结果如下: ``` 主线程开始时间: 2021-10-09 10:37:36 线程名称: Thread-1 参数: 3 开始时间: 2021-10-09 10:37:36 线程名称: Thread-2 参数: 2 开始时间: 2021-10-09 10:37:36 线程名称: Thread-3 参数: 1 开始时间: 2021-10-09 10:37:36 主线程结束时间: 2021-10-09 10:37:36 ``` 上述示例中自定义了线程类 MyThread,继承了 threading.Thread,并重写了 __init__ 方法和 run 方法。 #### 31.4.3、守护线程 守护线程(也称后台线程)是在后台运行的,它的任务是为其他线程提供服务,如 Python 解释器的垃圾回收线程就是守护线程。如果所有的前台线程都死亡了,守护线程也会自动死亡。来看个例子: ``` # 不设置守护线程 import threading def work(num): for i in range(num): print(threading.current_thread().name + " " + str(i)) t = threading.Thread(target=work, args=(10,), name='守护线程') t.start() for i in range(10): pass # 输出结果: ''' 守护线程 0 守护线程 1 守护线程 2 守护线程 3 守护线程 4 守护线程 5 守护线程 6 守护线程 7 守护线程 8 守护线程 9 ''' ``` ``` # 设置守护线程 import threading def work(num): for i in range(num): print(threading.current_thread().name + " " + str(i)) t = threading.Thread(target=work, args=(10,), name='守护线程') t.daemon = True t.start() for i in range(10): pass # 输出结果: # 守护线程 0 ``` 上述示例直观的说明了当前台线程结束,守护线程也会自动结束。 如果你设置一个线程为守护线程,就表示这个线程是不重要的,在进程退出的时候,不用等待这个线程退出;如果你的主线程在退出的时候,不用等待哪些子线程完成,那就设置这些线程为守护线程;如果你想等待子线程完成后再退出,那就什么都不用做,或者显示地将 daemon 属性设置为 false。 #### 31.4.4、线程本地数据 Python 的 threading 模块提供了 local 方法,该方法返回得到一个全局对象,不同线程使用这个对象存储的数据,其它线程是不可见的(本质上就是不同的线程使用这个对象时为其创建一个独立的字典) ,这个相当于java的ThreadLocal 来看个示例: ``` # 不使用 threading.local import threading import time num = 0 def work(): global num for i in range(10): num += 1 print(threading.current_thread().getName(), num) time.sleep(0.0001) for i in range(5): threading.Thread(target=work).start() # 输出结果: ''' Thread-1 10 Thread-2 20 Thread-3 30 Thread-4 40 Thread-5 50 ''' ``` 上面示例中 num 是全局变量,变成了公共资源,通过输出结果,我们发现子线程之间的计算结果出现了互相干扰的情况。 ``` # 使用 threading.local num = threading.local() def work(): num.x = 0 for i in range(10): num.x += 1 print(threading.current_thread().getName(), num.x) time.sleep(0.0001) for i in range(5): threading.Thread(target=work).start() # 输出结果: ''' Thread-1 10 Thread-2 10 Thread-3 10 Thread-4 10 Thread-5 10 ''' ``` 使用 threading.local 的示例中,num 是全局变量,但每个线程定义的属性 num.x 是各自线程独有的,其它线程是不可见的,因此每个线程的计算结果未出现相互干扰的情况。 #### 31.4.5、定时器 threading 模块提供了 Timer 类实现定时器功能,来看个例子: ``` # 单次执行 from threading import Timer def work(): print("Hello Python") # 5 秒后执行 work 方法 t = Timer(5, work) t.start() ``` Timer 只能控制函数在指定的时间内执行一次,如果我们需要多次重复执行,需要再进行一次调度,想要取消调度时可以使用 Timer 的 cancel 方法。来看个例子: ``` # 重复执行 count = 0 def work(): print('当前时间:', time.strftime('%Y-%m-%d %H:%M:%S')) global t, count count += 1 # 如果 count 小于 5,开始下一次调度 if count < 5: t = Timer(1, work) t.start() # 指定 2 秒后执行 work 方法 t = Timer(2, work) t.start() ``` 结果如下: ``` 当前时间: 2021-10-09 11:19:20 当前时间: 2021-10-09 11:19:21 当前时间: 2021-10-09 11:19:22 当前时间: 2021-10-09 11:19:23 当前时间: 2021-10-09 11:19:24 ``` #### 31.4.6、常用方法、属性 threading 模块提供了十分丰富的线程操作功能,它的 API 方法及属性自然也特别多,我们来看一下常用的方法和属性。 1)threading.Thread 实例的方法、属性 ``` 方法 说明 start() 启动线程活动,它在一个线程里最多只能被调用一次。 run() 表示线程活动的方法。 join(timeout=None) 等待至线程中止。 getName() 返回线程名。 setName() 设置线程名。 is_alive() 返回线程是否是活动的。 daemon 是否为守护线程的标志。 ident 线程标识符,线程尚未开始返回 None,已启动返回非零整数。 ``` 2)threading 直接调用的方法 ``` 方法 说明 active_count() 返回当前存活的线程类 Thread 对象,返回个数等于 enumerate() 返回的列表长度。 current_thread() 返回当前对应调用者的 Thread 对象。 get_ident() 返回当前线程的线程标识符,它是一个非零的整数。 enumerate() 以列表形式返回当前所有存活的 Thread 对象。 main_thread() 返回主 Thread 对象。 settrace(func) 为所有 threading 模块开始的线程设置追踪函数。 setprofile(func) 为所有 threading 模块开始的线程设置性能测试函数。 stack_size([size]) 返回创建线程时用的堆栈大小。 ``` ### 31.5、效率大比拼 #### 31.5.1、计算密集型任务 计算密集型任务的特点是要进行大量的计算,消耗 CPU 资源,比如:计算圆周率、对视频进行解码 ... 全靠 CPU 的运算能力,下面看一下单线程、多线程、多进程的实际耗时情况。 1)单线程就是一条线程,我们直接以主线程为例,来看下单线程表现如何: ``` # 计算密集型任务-单线程 import os,time def task(): ret = 0 for i in range(100000000): ret *= i if __name__ == '__main__': print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(5): task() stop = time.time() print('单程耗时 %s' % (stop - start)) # 测试结果: ''' 本机为 4 核 CPU 单线程耗时 23.19068455696106 ''' ``` 2)来看多线程表现: ``` # 计算密集型任务-多线程 from threading import Thread import os,time def task(): ret = 0 for i in range(100000000): ret *= i if __name__ == '__main__': arr = [] print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(5): p = Thread(target=task) arr.append(p) p.start() for p in arr: p.join() stop = time.time() print('多线程耗时 %s' % (stop - start)) # 测试结果: ''' 本机为 4 核 CPU 多线程耗时 25.024707317352295 ''' ``` 3)来看多进程表现: ``` # 计算密集型任务-多进程 from multiprocessing import Process import os,time def task(): ret = 0 for i in range(100000000): ret *= i if __name__ == '__main__': arr = [] print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(5): p = Process(target=task) arr.append(p) p.start() for p in arr: p.join() stop = time.time() print('计算密集型任务,多进程耗时 %s' % (stop - start)) # 输出结果 ''' 本机为 4 核 CPU 计算密集型任务,多进程耗时 14.087027311325073 ''' ``` 通过测试结果我们发现,在 CPython 下执行计算密集型任务时,多进程效率最优,多线程还不如单线程。 #### 31.5.2、 I/O 密集型任务 涉及到网络、磁盘 I/O 的任务都是 I/O 密集型任务,这类任务的特点是 CPU 消耗很少,任务的大部分时间都在等待 I/O 操作完成(因为 I/O 的速度远远低于 CPU 和内存的速度)。通过下面例子看一下耗时情况: 1)来看单线程表现: ``` # I/O 密集型任务-单线程 import os,time def task(): f = open('tmp.txt','w') if __name__ == '__main__': arr = [] print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(500): task() stop = time.time() print('I/O 密集型任务,多进程耗时 %s' % (stop - start)) # 输出结果 ''' 本机为 4 核 CPU I/O 密集型任务,单线程耗时 0.2964005470275879 ''' ``` 2)来看多线程表现: ``` # I/O 密集型任务-多线程 from threading import Thread import os,time def task(): f = open('tmp.txt','w') if __name__ == '__main__': arr = [] print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(500): p = Thread(target=task) arr.append(p) p.start() for p in arr: p.join() stop = time.time() print('I/O 密集型任务,多进程耗时 %s' % (stop - start)) # 输出结果 ''' 本机为 4 核 CPU I/O 密集型任务,多线程耗时 0.24960064888000488 ''' ``` 3)来看多进程表现: ``` # I/O 密集型任务-多进程 from multiprocessing import Process import os,time def task(): f = open('tmp.txt','w') if __name__ == '__main__': arr = [] print('本机为',os.cpu_count(),'核 CPU') start = time.time() for i in range(500): p = Process(target=task) arr.append(p) p.start() for p in arr: p.join() stop = time.time() print('I/O 密集型任务,多进程耗时 %s' % (stop - start)) # 输出结果 ''' 本机为 4 核 CPU I/O 密集型任务,多进程耗时 21.05265736579895 ''' ``` 通过 I/O 密集型任务在 CPython 下的测试结果我们发现:多线程效率优于多进程,单线程与多线程效率接近。 对于一个运行的程序来说,随着 CPU 的增加执行效率必然会有所提高,因此大多数时候,一个程序不会是纯计算或纯 I/O,所以我们只能相对的去看一个程序是计算密集型还是 I/O 密集型。