前言

本文依据C语言中文网,结合官方文档,W3cSchool,菜鸟教程,再搭配自己学习道路上的经验编写,仅供个人参考学习。


​ 作者—结了冰的可乐

第一章 Python编程基础

2.1 Python注释

2.2 Python缩进规则

2.3 Python标识符命名规范

标识符的命名需要遵循以下几点规则:

(1) 标识符由数字、字母和下划线组成,但不能以数字开头

(2) 标识符命名不能和关键字相同

(3) 标识符严格区分字母的大小写

(4) 在Python中,以下划线开头的标识符具有特殊含义:

  • 以单划线开头的标识符(_width),表示不能直接访问的类属性,不能通过 from\...import导入

  • 以双划线开头的标识符(__add),表示类的私有成员

  • 以双划线开头和结尾的标识符(__init__),是专用标识符

所以如非特殊需要,一般不使用下划线做为标识符的开头,此外在不同场景中,也有额外的规则需要遵守:

  • 当标识符用作模块名时,应尽量短小,并且全部使用小写字母,可以使用下划线分割多个字母,例如 game_miangame_register 等。

  • 当标识符用作包的名称时,应尽量短小,也全部使用小写字母,不推荐使用下划线,例如 com.mrcom.mr.book 等。

  • 当标识符用作类名时,应采用单词首字母大写的形式。例如,定义一个图书类,可以命名为 Book

  • 模块内部的类名,可以采用 “下划线+首字母大写” 的形式,如 \_Book;

  • 函数名、类中的属性名和方法名,应全部使用小写字母,多个单词之间可以用下划线分割;

  • 常量命名应全部使用大写字母,单词之间可以用下划线分割;

2.4 Python关键字

import keyword
a = keyword.kwlist
print(a)
===================================================
['False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

2.5 Python内置函数

官方文档

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gSN24Whl-1651584436771)(D:\learn\learn.assets\image-20220217175511570.png)]

第二章 变量类型和运算符

变量在 Python 内部是有类型的,比如 intfloat 等,但是我们在编程时无需关注变量类型,所有的变量都无需提前声明,赋值后就能使用。另外,可以将不同类型的数据赋值给同一个变量,所以变量的类型是可以改变的。

2.1 变量的定义和使用

变量(Variable)可以看成一个小箱子,专门用来"盛装"程序中的数据。每个变量都拥有独一无二的名字,通过变量的名字就能找到变量中的数据。 从底层看,程序中的数据最终都要放到内存(内存条)中,变量其实就是这块内存的名字。 和变量相对应的是常量,它们都是用来"盛装"数据的小箱子,不同的是:变量保存的数据可以被多次修改,而常量一旦保存某个数据之后就不能修改了。

Python变量的赋值

使用=做为赋值运算符,集体格式为name = value,这里要注意name要遵循[Python标识符命名规范](#_2.3 Python标识符命名规范)。

Python变量的使用

在使用的时候只要知道变量名即可,并且几乎在所有地方都可以使用变量。

Python是弱类型语言

弱语言有以下两个主要特点:

  1. 变量无须声明 就可以直接赋值,

  2. 变量的数据类型可以直接改变

但是弱类型不是没有类型,只是说在书写代码时不需要可以关注,但是代码底层实现的时候依然是有类型的,可以使用type()这个内置函数查看。

a = 123
print(type(a))
=====================================
<class 'int'>

2.2 int详解

Python的int包括正整数,零和负整数,并且int不分类型(例如:longshort等)。

整数的不同进制

f1 = 12.5
print("f1Value: ", f1)
print("f1Type: ", type(f1))f2 = 0.34557808421257003
print("f2Value: ", f2)
print("f2Type: ", type(f2))f3 = 0.0000000000000000000000000847
print("f3Value: ", f3)
print("f3Type: ", type(f3))f4 = 345679745132456787324523453.45006
print("f4Value: ", f4)
print("f4Type: ", type(f4))f5 = 12e4
print("f5Value: ", f5)
print("f5Type: ", type(f5))f6 = 12.3 * 0.1
print("f6Value: ", f6)
print("f6Type: ", type(f6))

2.3 float详解

Python中小数的表达方式分为十进制表示和指数形式表示,例如2.1E5 = 2.1×105,其中 2.1 是尾数,5是指数。

2.4 complex详解

使用的具体格式为a+bj

c1 = 12 + 0.2j
print("c1Value: ", c1)
print("c1Type", type(c1))
c2 = 6 - 1.2j
print("c2Value: ", c2)
#对复数进行简单计算
print("c1+c2: ", c1+c2)
print("c1*c2: ", c1*c2)

2.5 Python字符串

可以使用''或者""实现。字符串的内容不作限制,可以为任何内容。

处理字符串当中的引号

当字符串内容中出现引号的时候如果不作处理,Python救市出现解析错误。所以有以下两种处理方案。

  • 对引号进行转义
    在引号前面加上\(反斜杠)就可以进行转义。
str1 = 'I\' a student'
  • 使用不同的引号
str1 = "I\' a student\"

字符串的换行

Python不是格式自由的语言,它对程序的换行、缩进都有严格的要求。当字符串过长想换行输入时就需要在一行的末尾加上\。

str1 = "I\' astudent"

长字符串

使用三个单引号或者双引号可以对多行内容进行注释,这其实是 Python 长字符串的写法。所谓长字符串,就是可以直接换行(不用加反斜杠\)书写的字符串。

在长字符串当中无论使用单引号还是双引号都不会导致解析错误。

原始字符串

前面提到了反斜杠\,又称转义字符。具体会在后面具体解释。转义字符有时候使用起来会出现麻烦,例如在输入Windows路径D:\learn这样的字符串。由于\的特殊性,所以我们需要对每个\进行转义,就是这样D:\\learn

但是这种写法会在不经意间出错,所以有了原始字符串。在普通字符串前面加上r就变成了原始字符串。这时候\就不会被当作是转义字符。

a = r'i\'m a student'
b = r"D:\learn"

普通格式的原始字符串在出现引号时仍然需要对其进行转义,但是和普通字符串不同的是此时用于转义的反斜杠会成为字符串内容的一部分。

需要注意的是,Python 原始字符串中的反斜杠仍然会对引号进行转义,因此原始字符串的结尾处不能是反斜杠,否则字符串结尾处的引号会被转义,导致字符串不能正确结束。

2.6 Python的Bytes

不常用,略过。

2.7 Python Bool类型

true/false

2.8 Python input()函数

这是Python的内置函数,用于从控制台读取输入的内容。input()函数总是以字符串的形式处理输入的内容,所以可以输入任何的内容。

str = input(tipmsg)

tipmsg表示提示信息,会显示在控制台上,告诉用户应该输入什么内容;如果不写tipmsg,就不会有任何提示信息。

str1 = input("请输入:")
print(str1)

2.9 Python print()函数用法

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

sep–用来间隔对象,默认是一个空格

end–设定用什么来结尾,默认是换行符\n

file–要写入的文件对象

flush–输出是否被缓存通常决定于file,但是如果flush的关键字为True,流将会强制刷新

2.10 Python格式化字符串

print()函数使用以%开头的转换说明符对各种类型的数据进行格式化输出,具体请看下表。

转换说明符 解释
%d、%i 转换为带符号的十进制整数
%o 转换为带符号的八进制整数
%x、%X 转换为带符号的十六进制整数
%e 转化为科学计数法表示的浮点数(e 小写)
%E 转化为科学计数法表示的浮点数(E 大写)
%f、%F 转化为十进制浮点数
%g 智能选择使用 %f 或 %e 格式
%G 智能选择使用 %F 或 %E 格式
%c 格式化字符及其 ASCII 码
%r 使用 repr() 函数将表达式转换为字符串
%s 使用 str() 函数将表达式转换为字符串

指定最小输出宽度

当使用上表中的转换说明符时,可以使用下面的格式指定最小输出宽度(至少占用多少个字符的位置):

  • %10d 表示输出的整数宽度至少为 10;
  • %20s 表示输出的字符串宽度至少为 20。

请看下面的演示:

n = 1234567
print("n(10):%10d." % n)
print("n(5):%5d." % n)
url = "http://c.biancheng.net/python/"
print("url(35):%35s." % url)
print("url(20):%20s." % url)

运行结果:

n(10):   1234567.
n(5):1234567.
url(35):     http://c.biancheng.net/python/.
url(20):http://c.biancheng.net/python/.

从运行结果可以发现,对于整数和字符串,当数据的实际宽度小于指定宽度时,会在左侧以空格补齐;当数据的实际宽度大于指定宽度时,会按照数据的实际宽度输出。

你看,这里指定的只是最小宽度,当数据的实际宽度足够时,指定的宽度就没有实际意义了。

指定对齐方式

默认情况下,print() 输出的数据总是右对齐的。也就是说,当数据不够宽时,数据总是靠右边输出,而在左边补充空格以达到指定的宽度。Python 允许在最小宽度之前增加一个标志来改变对齐方式,Python 支持的标志如下:

标志 说明
- 指定左对齐
+ 表示输出的数字总要带着符号;正数带+,负数带-
0 表示宽度不足时补充 0,而不是补充空格。

几点说明:

  • 对于整数,指定左对齐时,在右边补 0 是没有效果的,因为这样会改变整数的值。

  • 对于小数,以上三个标志可以同时存在。

  • 对于字符串,只能使用-标志,因为符号对于字符串没有意义,而补 0 会改变字符串的值。

n = 123456
#%09d 表示最小宽度为9,左边补0
print("n(09):%09d" % n)
#%+9d 表示最小宽度为9,带上符号
print("n(+9):%+9d" % n)f = 140.5
#%-+010f 表示最小宽度为10,左对齐,带上符号
print("f(-+0):%-+010f" % f)s = "Hello"
#%-10s 表示最小宽度为10,左对齐
print("s(-10):%-10s." % s)

指定小数精度

对于小数(浮点数),print() 还允许指定小数点后的数字位数,也即指定小数的输出精度。

精度值需要放在最小宽度之后,中间用点号.隔开;也可以不写最小宽度,只写精度。具体格式如下:

%m.nf
%.nf
f = 3.141592653
#最小宽度为8,小数点后保留3位
print("%8.3f" % f)
#最小宽度为8,小数点后保留3位,左边补0
print("%08.3f" % f)
#最小宽度为8,小数点后保留3位,左边补0,带符号
print("%+08.3f" % f)

2.11 Python转义字符

转义字符 说明
\n 换行符,将光标位置移到下一行开头。
\r 回车符,将光标位置移到本行开头。
\t 水平制表符,也即 Tab 键,一般相当于四个空格。
\a 蜂鸣器响铃。注意不是喇叭发声,现在的计算机很多都不带蜂鸣器了,所以响铃不一定有效。
\b 退格(Backspace),将光标位置移到前一列。
\ 反斜线
单引号
" 双引号
\ 在字符串行尾的续行符,即一行未完,转到下一行继续写。
#使用\t排版
str1 = '网站\t\t域名\t\t\t年龄\t\t价值'
str2 = 'C语言中文网\tc.biancheng.net\t\t8\t\t500W'
str3 = '百度\t\twww.baidu.com\t\t20\t\t500000W'
print(str1)
print(str2)
print(str3)print("--------------------")# \n在输出时换行,\在书写字符串时换行
info = "Python教程:http://c.biancheng.net/python/\n\
C++教程:http://c.biancheng.net/cplus/\n\
Linux教程:http://c.biancheng.net/linux_tutorial/"
print(info)
网站        域名                年龄    价值
C语言中文网 c.biancheng.net     8       500W
百度        www.baidu.com       20      500000W
--------------------
Python教程:http://c.biancheng.net/python/
C++教程:http://c.biancheng.net/cplus/
Linux教程:http://c.biancheng.net/linux_tutorial/

2.12 Python数据类型转换

函 数 作 用
int(x) 将 x 转换成整数类型
float(x) 将 x 转换成浮点数类型
complex(real,[,imag]) 创建一个复数
str(x) 将 x 转换为字符串
repr(x) 将 x 转换为表达式字符串
eval(str) 计算在字符串中的有效 Python 表达式,并返回一个对象
chr(x) 将整数 x 转换为一个字符
ord(x) 将一个字符 x 转换为它对应的整数值
hex(x) 将一个整数 x 转换为一个十六进制字符串
oct(x) 将一个整数 x 转换为一个八进制的字符串

2.13 运算符

第三章 列表、元组、集合、字典

序列就是一块可以存放多个值的连续空间,这些值按照一定的顺序排列,可以同过索引找到每个值的所在位置。

在Python中,序列包括字符串、列表、元组、字典和集合

  • 序列支持索引、切片、相加、相乘、检查元素是否在序列中

  • 特殊的是,集合和字典不支持索引、切片、相加和相乘的操作

3.1 序列初步认识

序列索引


str="C语言中文网"
print(str[0],"==",str[-6])
print(str[5],"==",str[-1])=============================
C == C
网 == 网

序列切片

切片可以访问序列中某一范围内的元素sname[start : end : step],其中step表示步长

str="C语言中文网"
#取索引区间为[0,2]之间(不包括索引2处的字符)的字符串
print(str[:2])
#隔 1 个字符取一个字符,区间是整个字符串
print(str[::2])
#取整个字符串,此时 [] 中只需一个冒号即可
print(str[:])====================================
C语
C言文
C语言中文网

序列相加

序列支持类型相同的序列相加,但是不会去除重复的元素。

str="c.biancheng.net"
print("C语言"+"中文网:"+str)====================================
C语言中文网:c.biancheng.net

序列相乘

Python 中,使用数字 n 乘以一个序列会生成新的序列,其内容为原来序列被重复 n 次的结果。例如:

str="C语言中文网"
print(str*3)============================
C语言中文网C语言中文网C语言中文网

注:

列表类型在进行乘法运算的时候,还可以初始化指定长度列表。

list1 = [None,True]*5
print(list1)===========================
[None, True, None, True, None, True, None, True, None, True]

检查元素是否包含在序列当中

可以使用in关键字实现检查某元素是否为序列的成员。value in sequence

str="c.biancheng.net"
print('c'in str)==========================
False

与之相反的还有not in

和序列相关的内置函数

函数 功能
len() 计算序列的长度,即返回序列中包含多少个元素。
max() 找出序列中的最大元素。
min() 找出序列中的最小元素。
list() 将序列转换为列表。
str() 将序列转换为字符串。
sum() 计算元素和。注意,对序列使用 sum() 函数时,做加和操作的必须都是数字,不能是字符或字符串,否则该函数将抛出异常,因为解释器无法判定是要做连接操作(+ 运算符可以连接两个序列),还是做加和操作。
sorted() 对元素进行排序。
reversed() 反向序列中的元素。⚠️:返回的是一个反转的迭代器
enumerate() 将序列组合为一个索引序列,多用在 for 循环中。

reversed函数用法—reversed(seq)

reversed()返回的是一个反转的迭代器。

seq是要转换的序列,可以是元组,字符串,列表和区间(range)。

a = range(20)
print(list(reversed(a)))=================================
[19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

用法演示

from os import sepa = [1, 2, 3]
b = reversed(a)print(a, b, list(b), sep=";")====================================
[1, 2, 3];<list_reverseiterator object at 0x00000252711685B0>;[3, 2, 1]

enumerate函数用法–enumerate(seq,[start=0])

enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。

语法:

seq–一个序列、迭代器回着其他支持迭代的对象

start–下标的起始位置

返回值:

返回enumerate(枚举)对象

示例:

import enuma = [“一”,”二“, ”三“]
b = list(enumerate(a))
c = enumerate(a)print(a, b, c, sep=";")================================
['一', '二', '三'];[(0, '一'), (1, '二'), (2, '三')];<enumerate object at 0x000002E2A133AA00>

普通for循环

i = 0
seq = ['one','two','three']
for element in seq:print(i,seq[i])i += 1===============================================
0 one
1 two
2 three

for循环使用enumerate

seq = ['one', 'two', 'three']
for element in enumerate(seq, start=1):print(element)====================================
(1, 'one')
(2, 'two')
(3, 'three')

3.2 Python list详解

从形式上看,列表就是使用[]把所有元素包含在里面,如下所示:[elemenr1,element2,...]

element没有限制,只要是Python支持的就行

创建列表

1)直接使用[]创建列表

num = [1, 2, 3, 4, 5, 6, 7]
name = ["C语言中文网", "http://c.biancheng.net"]
program = ["C语言", "Python", "Java"]

2)使用list()创建列表

list()是Python提供的内置函数,可以将其他数据类型转换为列表,例如:

#将字符串转换成列表
list1 = list("hello")
print(list1)
#将元组转换成列表
tuple1 = ('Python', 'Java', 'C++', 'JavaScript')
list2 = list(tuple1)
print(list2)
#将字典转换成列表
dict1 = {'a':100, 'b':42, 'c':9}
list3 = list(dict1)
print(list3)
#将区间转换成列表
range1 = range(1, 6)
list4 = list(range1)
print(list4)
#创建空列表
print(list())===============================
['h', 'e', 'l', 'l', 'o']
['Python', 'Java', 'C++', 'JavaScript']
['a', 'b', 'c']
[1, 2, 3, 4, 5]
[]

访问列表元素

使用index就可以访问元素。

删除列表

可以使用del关键字,del listname

list添加元素的三种方法

前面提过序列有相加的操作,使用+运算符可以将多个序列连接起来。

a = [1, 2, 3]
b = [4, 5, 6]
c = a+b
print(c)============================
[1, 2, 3, 4, 5, 6]

1)append()方法添加元素

append用于在列表的末尾添加元素,用法如下:

list.append(obj)

obj–表示添加到末尾的数据,可以是单个元素,也可以是列表、元组等。

l = ['Python', 'C++', 'Java']
#追加元素
l.append('PHP')
print(l)
#追加元组,整个元组被当成一个元素
t = ('JavaScript', 'C#', 'Go')
l.append(t)
print(l)
#追加列表,整个列表也被当成一个元素
l.append(['Ruby', 'SQL'])
print(l)=======================================
['Python', 'C++', 'Java', 'PHP']
['Python', 'C++', 'Java', 'PHP', ('JavaScript', 'C#', 'Go')]
['Python', 'C++', 'Java', 'PHP', ('JavaScript', 'C#', 'Go'), ['Ruby', 'SQL']]

2)extend()方法添加元素

extend()不会把列表和元组当成一个整体,而是把它们包含的元素逐个添加到列表中。

list.extend()

obj— 表示到添加到列表末尾的数据,它可以是单个元素,也可以是列表、元组等,但不能是单个的数字。

nums = [0, 1, 2, 3, 4, 5, 6]
b = [1,2]
c = nums.extend(b)print(nums)=====================================
[0, 1, 2, 3, 4, 5, 6, 1, 2]

3)insert()方法插入元素

insert()可以在列表中间的某个位置插入元素。

list.insert(index,obj)

l = ['Python', 'C++', 'Java']
#插入元素
l.insert(1, 'C')
print(l)
#插入元组,整个元祖被当成一个元素
t = ('C#', 'Go')
l.insert(2, t)
print(l)
#插入列表,整个列表被当成一个元素
l.insert(3, ['Ruby', 'SQL'])
print(l)
#插入字符串,整个字符串被当成一个元素
l.insert(0, "http://c.biancheng.net")
print(l)===========================================
['Python', 'C', 'C++', 'Java']
['Python', 'C', ('C#', 'Go'), 'C++', 'Java']
['Python', 'C', ('C#', 'Go'), ['Ruby', 'SQL'], 'C++', 'Java']
['http://c.biancheng.net', 'Python', 'C', ('C#', 'Go'), ['Ruby', 'SQL'], 'C++', 'Java']

list删除元素的四种方法

在 Python列表中删除元素主要分为以下 3 种场景:

  • 根据目标元素所在位置的索引进行删除,可以使用 del 关键字或者 pop() 方法;
  • 根据元素本身的值进行删除,可使用列表(list类型)提供的 remove() 方法;
  • 将列表中所有元素全部删除,可使用列表(list类型)提供的 clear() 方法。

del:根据索引值删除元素

del listname[index]

或者

del listname[start:end]

其中,start 表示起始索引,end 表示结束索引。del 会删除从索引 start 到 end 之间的元素,不包括 end 位置的元素。

pop:根据索引值删除据元素

listname.pop(index)

pop会返回弹出元素的值

nums = [40, 36, 89, 2, 36, 100, 7]
nums.pop(3)
print(nums)
nums.pop()
print(nums)========================
[40, 36, 89, 36, 100, 7]
[40, 36, 89, 36, 100]

remove():根据元素进行删除

需要注意的是,remove() 方法只会删除第一个和指定值相同的元素,而且必须保证该元素是存在的,否则会引发 ValueError 错误。

remove() 方法使用示例:

nums = [40, 36, 89, 2, 36, 100, 7]
#第一次删除36
nums.remove(36)
print(nums)
#第二次删除36
nums.remove(36)
print(nums)
#删除78
nums.remove(78)
print(nums)======================================
[40, 89, 2, 36, 100, 7]
[40, 89, 2, 100, 7]
Traceback (most recent call last):File "d:\learn\main.py", line 9, in <module>nums.remove(78)
ValueError: list.remove(x): x not in list

clear():删除列表所有元素

url = list("http://c.biancheng.net/python/")
url.clear()
print(url)=====================
[]

list修改元素

Python修改元素既可以每次修改单个元素,也可以修改多个元素。

修改单个元素

easy

l = [1,2,4]
l[2]=3
print(l)================
[1, 2, 3]

修改一组元素

Python 支持通过切片语法给一组元素赋值。在进行这种操作时,如果不指定步长(step 参数),Python 就不要求新赋值的元素个数与原来的元素个数相同;这意味,该操作既可以为列表添加元素,也可以为列表删除元素。

nums = [1,2,3,4,5]nums[1: 4] = []
print(nums)====================
[1,5]

如果对空切片(slice)赋值,就相当于插入一组新的元素:

nums = [0,1, 2, 3, 4, 5]nums[4:4] = '34'
print(nums)=======================
[0, 1, 2, 3, '3', '4', 4, 5]

但是在使用切片时候,必须是可迭代的,不然会抛出这样的错误:can only assign an iterable

nums = [0,1, 2, 3, 4, 5]
nums[4:4] = 34
print(nums)======================
Traceback (most recent call last):File "d:\learn\main.py", line 3, in <module>nums[4:4] = 34
TypeError: can only assign an iterabl

使用切片语法时也可以指定步长(step 参数),但这个时候就要求所赋值的新元素的个数与原有元素的个数相同,例如:

nums = [0, 1, 2, 3, 4, 5, 6]nums[1: 6: 2] = ['a', 'b', 'c']
print(nums)===============================
[0, 'a', 2, 'b', 4, 'c', 6]

list查找元素

index()方法

该方法就是查找某个元素在列表的索引。如果不存在就会导致ValueError,所以查找之前最好使用count()判断一下。

listname.index(obj, start, end)

nums = [0, 1, 2, 3, 4, 5, 6]
nums[1: 6: 2] = ['a', 'b', 'c']print(nums)
print(nums.count(0))
print(index(0))===============================
[0, 'a', 2, 'b', 4, 'c', 6]
1
0

count()方法

list使用技巧和注意事项

前面章节介绍了很多关于 list 列表的操作函数,有很多操作函数的功能非常相似。例如,增加元素功能的函数有 append()extend(),删除元素功能的有 clear()remove()pop()del 关键字。

list添加元素的区别

前文提到过的方法:

  • +

  • append()

  • insert()

  • extend()

    tt = 'hello'list1 = [1,4,tt,3.4,"yes",[1,2]]
    print(list1,id(list1))
    print("1.----------------")list3 = [6,7]
    l2 = list1 + list3
    print(l2,id(l2))
    print("2.----------------")l2 = list1.extend(list3)
    print(l2,id(l2))
    print(list1,id(list1))print("3.----------------")
    l2 = list1.append(list3)
    print(l2,id(l2))
    print(list1,id(list1))=================================
    [1, 4, 'hello', 3.4, 'yes', [1, 2]] 2251638471496
    1.----------------
    [1, 4, 'hello', 3.4, 'yes', [1, 2], 6, 7] 2251645237064
    2.----------------
    None 1792287952
    [1, 4, 'hello', 3.4, 'yes', [1, 2], 6, 7] 2251638471496
    3.----------------
    None 1792287952
    [1, 4, 'hello', 3.4, 'yes', [1, 2], 6, 7, [6, 7]] 2251638471496
    
    1. 使用+连接的列表,是重新生成的列表
    2. extend()处理后得到的是None,所以extend()没有返回值,不能使用链式表达式。就是说extend()绝不能放在等式的右侧。
    3. extend 处理之后, list1 的内容与使用+号生成的 l2 是一样的。但 list1 的地址在操作前后并没有变化,这表明 extend 的处理仅仅是改变了 list1,而没有重新创建一个 list。从这个角度来看,extend 的效率要高于“+”号。
    4. append是将整个list做为一个整体加入到末尾。

list删除元素

del使用的时候需要注意,删除的是变量还是数据。下面代码进行演示

tt = 'hello'
#定义一个包含多个类型的 list
list1 = [1,4,tt,3.4,"yes",[1,2]]
l2 = list1
print(id(l2),id(list1))
del list1
print(l2)
print(list1)========================
1785149533440 1785149533440
[1, 4, 'hello', 3.4, 'yes', [1, 2]]
Traceback (most recent call last):File "d:/learn/main.py", line 8, in <module>print(list1)
NameError: name 'list1' is not defined

由输出结果看得出来:

  • list1l2的内存地址是一样的。
  • 删除了list1之后,l2仍然能打印。说明l2指向的内存空间依旧存在,del仅仅删除了变量list1,没有删除指定的数据。

所以除了删除变量其他都是删除数据,下面代码进行演示:

tt = 'hello'
#定义一个包含多个类型的 list
list1 = [1,4,tt,3.4,"yes",[1,2]]
l2 = list1
l3 = l2
del l2[:]
print(l2)
print(l3)=========================
[]
[]

输出结果可以看到,l2l3执行同样的内存地址,当l2被清空之后,l3的内容也被清空了。

个人理解:

  • 删除了变量直接就是del listname
  • 删除数据要具体说明删除了什么数据,譬如:del listname[start:end]
  • 这个理解还是有点片面,应该从内存地址层面理解

在实际过程中,即使使用del删除了删除了指定的变量,且该变量所指的内存没有被其他变量占用,此内存空间也不会被系统回收,只会被标记为无效内存.

为了系统能够回收这些无效内存,就需要使用gc库中得collect()函数,比如:

import gc
list1 = 'hello'
list2 = "world"del list1
gc.collect  #执行回收内存地址

rang()快速初始化列表

本节需要先了解最基本的Python循环结构

实际场景中,经常需要存储一组数字。列表就非常适合这个工作,而且Python也提供了range()函数.

range()函数能够快速生成一系列数字.譬如:

<span id = “bar">zhejiu

for i in range(1,5):print(i) =================================
1
2
3
4

range(1,5)不包含5

额外需要注意的是range()函数返回值并不是列表类型:

>>> type(range(1,5))============================
<class "range">

可以看到range()返回值的类型是rangelist,但是可以借助lsit来得到列表。

range也可以指定步长

在实际使用的时候range()函数通常搭配Python循环结构、推导式使用。

list实现栈和队列

栈和队列是两种数据额结构,内部都是按照固定顺序来存放变量的,二者的区别在于:

  • 队列先存放最先取出,即“先进先出”
  • 栈是最后存入最先取出,即“后进先出”

实现队列

实现方法是,定义一个list变量,存入数据使用insert()方法,设置第一个参数为0,,这样就可以保证每次后插入的数据都在最前面。读取数据的时候使用pop()将最后一个元素弹出,譬如:

list1 = []
list1.insert(0,1)
list1.insert(0,2)
list1.insert(0,3)
print(list1)
print(list1.pop())  #取出第一个元素
print(list1.pop())  #取出第二个元素
print(list1.pop())  #取出第三个元素==========================================
[3, 2, 1]
1
2
3

list实现栈

实现栈的思路就是保证新插入元素的位置在末尾,所以就可以使用append()函数添加元素,读取数据还是用pop(),譬如:

#定义一个空 list 当做栈
stack = []
stack.append(1)
stack.append(2)
stack.append("hello")
print(stack)
print("取一个元素:",stack.pop())
print("取一个元素:",stack.pop())
print("取一个元素:",stack.pop())=====================================
[1, 2, 'hello']
取一个元素: hello
取一个元素: 2
取一个元素: 1

collections实现栈和队列

在实际使用的时候,前面提到的使用list实现队列的方法效率不高。因为每次在list的开头插入新元素,都会使得所有元素都向后移动一个位置。

更加高效的方法是使用标准库collections模块中得deque结构体。它被设计成在两端存取都很快的特殊list,用来实现栈和队列的功能。

from collections import dequequeueAndStack = deque()
queueAndStack.append(1)
queueAndStack.append(2)
queueAndStack.append("hello")
print(list(queueAndStack))# 实现队列功能,从队列中取一个元素,根据先进先出原则,这里应输出 1
print(queueAndStack.popleft())
# 实现栈功能,从栈里取一个元素,根据后进先出原则,这里应输出 hello
print(queueAndStack.pop())
# 再次打印列表
print(list(queueAndStack))================================
[1, 2, 'hello']
1
hello
[2]

3.2 Python 元组详解

元组和列表相似,其中的元素也是按照特定的顺序排列而成。但是二者之间也有不同,列表是可以修改的,二元组一旦创建,它的元素就不可以修改。

从形式上看,元素的元素被()包围,相邻元素用,隔开。中间包含的元素也不做限制。

创建元组

  1. 使用()直接创建
num = (1,2,3)
a = 1,2,3

在Python中,元组的小括号并不是必须的,只要将元素用逗号隔开,Python就是视其为元组。

  1. 使用tuple()函数创建

内置的tuple()函数可以将其他类型的数据转换为元组。用法:tuple(data)。data可以为字符串,元组,range对象等。如果data没有给出就会默认创建一个空元组。

访问元组元素

和列表一样同样使用index访问。不重复阐述。

修改元组

元组虽然是不可变序列,但是元组包含的元素如果是可变序列,就可以修改这个可变序列达到改变元组的目的。

同时也可以使用+连接两个元组。

删除元祖

del关键字

3.3 Python元组和列表区别

二者之间最大的区别就是列表创建后还可以修改元素,而元组不可以。

这样的差异势必会影响二者之间的存储方式,譬如:

listdemo = []
a = listdemo.__sizeof__()tupledemo = ()
b = tupledemo.__sizeof__()print(a, b)==========================
40,24

由输出结果可以看到,虽然二者都是空的,但是list占用了50字节,元组占用了24字节。这是由于列表是动态的,需要存储指针来指向对应的元素(占用了8字节)。剩余的8字节,由于列表是可变的所以需要额外存储已经分配的长度大小。

虽然列表很强大,但是元组也必不可缺。从存储差异上可以引申出结论,即元组更加轻量,所以总体上来说,元组的性能速度要更好。

python会在后台对一些静态资源做一些资源缓存。因为垃圾回收的存在,如果一些变量不被使用了,Python就会回收这些变量所占的内存。

但是对于一些静态变量,比如元组。如果他不被使用并且占空间不大的时候,Python会暂存这部分内存。这样当下次在创建同样大小的元组时,Python就不会再想操作系统发出请求寻找内存,而是直接分配之前缓存的内存空间,这样就可以大大加快。

总的来说,元组的功能确实不够强大,但是不妨碍它依旧是很重要的序列之一。元组的不可替代性可以体现在以下场景:

  • 元组作为很对内置函数和序列类型方法的返回值存在。也就是说,在使用某些函数或者方法时,它的返回值回事元组类型。
  • 元组的访问和处理速度要更快于列表。
  • 元组可以在映射(和集合的成员)中当做“键”存在,而列表不可以。

3.4 Python dict详解

Python的dict是无序、可变的序列。它的元素以键值对的方式存在。列表和元组都是有序的,它们的元组在底层都是挨着放的。

字典类型是Python中唯一的映射类型。映射是数学中的术语,可以简单的理解为元素之间的相互对应关系。,即通过一个元素,可以找到唯一一个对应的元素。

主要特征 解释
通过键而不是索引来读取元素 字典类型有时候称为关联数组或者散列表(hash)。它是通过键将一系列的值联系起来的,这样就可以通过键从字典中获取指定的项,但不能通过索引
字典是任意类型数据的无序集合 字典中的元素是无序的。
字典是可变的,并且可以嵌套
字典的键必须唯一 字典中的键不可以出现多次,否则只会保留最后一个键值对
字典中的键必须是不可变的 子弟阿忠的每个键值对的键必须是不可变的,只能是数字、字符串或者元组,不能是列表

创建字典

创建字典方法有很多,下面一一介绍:

  1. 使用{}创建字典

语法格式为:dictname={key1:value1,...}

  1. 通过fromkeys()创建字典

具体格式为:dictname = dict.fromkeys(list,value=None),list参数表示字典中所有键组成的列表;value参数表示默认值,不写就为None。

dictname = dict.fromkeys([1,2,3],[1,2,3])
print(dictname)=========================
{1: [1, 2, 3], 2: [1, 2, 3], 3: [1, 2, 3]}
  1. 通过dict()映射函数创建字典

通过dict()函数创建字典的方法有很多,这里只说一些常见的方法:

创建格式 注意事项
a = dict(str1=value1,…) str是字符串类型的键,value表示键对应的值。使用这个方法时字符串不能带引号。如果dict()不传入任何参数,就相当于创建了一个空字典,字符串不可以是纯数字
a=[(“1”,1),…];a=[[“1”,1],…];a=((“1”,1),…);a=([“1”,1],…) 向dict()函数传入列表或者元组,而他们的元素有各自可以使包含两个元素的列表或者元组,其中第一个元素作为键,第二个作为值
keys=[‘1’,…],value=[1,…],a = dict(zip(keys,value)) 通过应该用dict()函数和zip()函数,可将前两个列表转换为相对应的字典

访问字典

和前面提到的元组和列表不同,字典是通过key来访问对应的值。因为在字典中的元素是无序的,每个元素的位置不固定,所以字典也不能使用切片的方式一次访问多个元素。

keys = [1,2,3]
values = [1,2,3]a = dict(zip(keys,values))
print(a,a[1],sep=";")===========================
{1: 1, 2: 2, 3: 3};1

键必须是存在的,否则会抛出异常

除了上面这种方式,python更推荐使用dict类型提供的get()方法来获取指定键对应的值。当指定的键不存在是get()方法不会抛出异常。

keys = [1,2,3]
values = [1,2,3]a = dict(zip(keys,values))
print(a,a[1],a.get(4),sep=";")===============================
{1: 1, 2: 2, 3: 3};1;None

如果想明确的提示用户该键不存在,那么可以指定第二个参数,譬如a.get(4,'该键不存在')

删除字典

使用del关键字即可。

dict基本操作

python中常见的字典操作有以下几种:

  • 向现有的字典中添加新的键值对
  • 修改现有字典中的键值对
  • 从现有字典中删除键值对
  • 判断现有字典是否有指定的键值对。

添加键值对

为字典添加新的键值对很简单,直接给不存在的key复制即可,dictname[key]=value

a = {1: 1}
a[2] = 2print(a)====================
{1: 1, 2: 2}

修改键值对

这里说的修改只能修改value,字典的key事实不能修改的。

字典中的key是唯一的,所以如果新添加的键与原有的键相同,那么键对应的值就会被新的值替换掉。

a = {1: 1}
a[1] = 2
print(a)===================
{1:2}

删除键值对

还是可以使用del语句。

a = {1: 1}
del a[1]
print(a)================
{}

判断键值对是否存在

可以通过in或者not in判断字典中是否存在指定的键。

对于dict而言,in或者not in都是基于key判断的。

a = {1: 1}
del a[1]
print(1 in a)================
False

字典完全方法攻略

dir(dict)可以查看dict包含了哪些方法。['clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values'],这些方法中fromkeys和get在前面提过了。

keys()、values()、items()

将这三个方法放在一起介绍是因为他们都用来获取字典中得特定数据:

  • keys()方法用于返回字典的所有key
  • values()方法用于返回所有键对应的value
  • items()用于返回字典中的所有键值对
a = {"1": 1}print(a.keys(), a.values(), a.items())===========================================
dict_keys(['1']) dict_values([1]) dict_items([('1', 1)])

由输出结果可以看到,这三个方法返回值得类型是dict_keys、dict_values、dict_items。Python3不希望用户直接操作这几个类型的返回值。

但是如果想使用这三种数据类型,Python提供了两种方案:

  1. 使用lit()函数,将它们转换为列表

    a = {"1": 1}print(list(a.keys()))===========================
    ['1']
    
  2. 使用for循环遍历它们的返回值

    a = {"1": 1, "2": 2, "3": 3}for i in a.keys():print(i, end=" ")========================
    1 2 3
    

copy()方法

这个方法返回一个字典的拷贝,即返回一个具有相同键值对的新字典。

a = {"1": 1, "2": 2, "3": 3, "4": [1,2,3]}
b = a.copy()print(a, b, sep="--|--")=========================
{'1': 1, '2': 2, '3': 3, '4': [1,2,3]}--|--{'1': 1, '2': 2, '3': 3, '4': [1,2,3]}

注意,copy()遵循的拷贝原理,既有深拷贝,也有浅拷贝

copy()方法只会对最表层的键值对进行深拷贝。也就是说,它会再申请一块内存来存放{“1”: 1, “2”: 2, “3”: 3,”4”:[]};但是对于某些列表类型的值来说,此方法做的是浅拷贝,也就是说b中的[1,2,3]不是独有的,是和a共有。

a = {"1": 1, "2": 2, "3": 3, "4": [1, 2, 3]}
b = a.copy()
print(a, b, sep="--|--")
a["5"] = 5  # b已经提前复制了a,所以再向a中添加键值对不会影响b
print(a, b, sep="--|--")
a["4"].remove(1)  # 列表是a,b共有的所以删除a的也会影响b的
print(a, b, sep="--|--")===============================
{'1': 1, '2': 2, '3': 3, '4': [1, 2, 3]}--|--{'1': 1, '2': 2, '3': 3, '4': [1, 2, 3]}
{'1': 1, '2': 2, '3': 3, '4': [1, 2, 3], '5': 5}--|--{'1': 1, '2': 2, '3': 3, '4': [1, 2, 3]}
{'1': 1, '2': 2, '3': 3, '4': [2, 3], '5': 5}--|--{'1': 1, '2': 2, '3': 3, '4': [2, 3]}

update()方法

此方法可以使用一个字典所包含的键值对来更新已有的字典。

在执行才此方法的时候,如果被更新的字典里已包含对应的键值对,那么原value就会被更新;如果被更新的字典里不包含对应的键值对,则该键值对就会被添加进去。

a = {"1": 1, "2": 2, "3": 3, "4": [1, 2, 3]}
b = {"1": 2, "5": 5}
a.update(b)print(a)================
{'1': 2, '2': 2, '3': 3, '4': [1, 2, 3], '5': 5}

pop()和popitem()方法

pop()和items()都是用来删除字典的键值对,不同的是,pop()用来删除指定的键值对,而popitems()从字典中移除并返回一个 (键, 值) 对。 键值对会按 LIFO 的顺序被返回

dictname.pop(key)
dictname.popitem()
a = {"1": 1, "2": 2, "3": 3, "4": [1, 2, 3]}
a.pop("1")
a.popitem()
print(a)==============
{'2': 2, '3': 3}

python 3.7之前popitem()删除的是一个随机项。

setdefault()方法

这个方法用来返回某个key对应的value,语法格式:dictname.setdefault(key,defaultvalue)。defultvalue表示默认值(可以不写,不写就是返回None)。

当指定的key不存在时,setdefault()会先为这个不存在的key设置一个默认的defaultvalue,然后再返回defaultvalue。

也就是说setdefault()总能返回指定的key对应的value:

  • 如果key存在,就直接返回key对应的value
  • 如果key不能存在,那么先为这个不存在的key设置默认的defaultvalue,然后再返回这个key对应的defaultvalue
a = {"1": 1, "2": 2, "3": 3, "4": [1, 2, 3]}
b = a.setdefault('1')
c = a.setdefault('5', "不存在")print(b, c)====================
1 不存在

3.5 使用字典格式化字符串

使用字典对字符串进行格式化输出,具体方法是:在字符串模板中按key指定变量,然后通过字典为字符串模板中的key设置值。

b = '第一个元素是:%(1)s,第二个元素是:%(2)s'
a = {'1': 1, '2': 2}print(b%a)====================
第一个元素是:1,第二个元素是:2

注意:b圆括号里1不要打引号,否则会报错

b = '第一个元素是:%(1)s,第二个元素是:%(2)s'
a = {("1"): 1, '2': 2}print(b%a)

注意和上面程序的联系

个人理解:这个key不管是什么类型的,一定要用做引号的嵌套。

3.6 set集合

跟数学中的集合一样,集合里面的元素都是唯一的。

a = {1, 2, 3, 1}
print(a, type(a), sep="\n")

元素只能是不可变数据类型,否则会抛出TypeError

# a = {1, 2, 3, [1, 2, 3]} 列表是可变数据类型
b = {1, 2, 3, (1, 2, 3)}

创建集合

1){}直接创建

a = {1,2,3}

2)set创建集合

set()是Python的内置函数,可以将字符串、列表、元组、range()对象等可迭代对象转换成集合。

set()创建后的集合是无序的不重复的元素集。

官方文档要求参数必须是可迭代的。之前说过可迭代对象包括序列、字典和迭代器,但是实际使用中要注意以下内容:

先看两段代码:

a = set([1, 2, 3])
print(a)=====================a = set([1, 2, [3]])
print(a)

第二段代码的列表里内嵌了一个列表(或者字典、集合),运行了之后会抛出错误:TypeError: unhashable type: 'set'。抛出的错误可以看出需要将里面的元素换成hashable。hashable可以简单的理解为不可变。具体内容参考下面链接内容:Python中的hashable和immutable。

集合的基本操作

  1. 添加元素

    add():setname.add(element)

  2. 删除元素

    setname.remove(element)

  3. 交集、并集、差集运算

    运算操作 Python运算符 含义 例子
    交集 & 取两集合公共的元素 >>> set1 & set2 {3}
    并集 | 取两集合全部的元素 >>> set1 | set2 {1,2,3,4,5}
    差集 - 取一个集合中另一集合没有的元素 >>> set1 - set2 {1,2} >>> set2 - set1 {4,5}
    对称差集 ^ 取集合 A 和 B 中不属于 A&B 的元素 >>> set1 ^ set2 {1,2,4,5}

set方法集合

  • add()

  • clear()

  • copy()

    这里的copy()都是深拷贝。

    a = (1,2,3)
    b = {a,4,5,6}print(id(a),id(b))
    del a
    print(b)=============
    2734035390848 2734034731072
    {(1, 2, 3), 4, 5, 6}
    
  • difference()

    将set1中有而set2中没有的元素给set3

    a = {1,2,3}
    b = {2,3,4}c = set1.difference(set2)
    
  • difference_update()

    从set1中删除于set2相同的元素

    a = {1,2,3}
    b = {2,3,4}c = a.difference_update(b)
    
  • discard()

    set1.discard(element)

  • intersection()

    取set1和set2的交集传给set3

  • intersection_update()

    取set1和set2的交集,并传递给set1

  • isdisjoint()

    判断set1和set2有无交集,返回true或者FALSE

  • issubset()

    判断set1是否是set2的子集

  • issuperset()

    判断set2是否是set1的子集

  • pop()

    移除set1的元素并返回该元素

  • remove()

    移除一个指定的元素。这个方法和discard()的不同点是:remove()移除一个不存在的元素会报错,而discard()不会。

  • symmetric_difference()

  • symmertric_difference_update()

  • union()

    取set1和set2的并集,并传递给set3

  • update()

    添加新的元素或者集合到set1中

frozenset集合

set是一个可变序列,其中的元素可以改变,但是frozenset()集合是个不可变序列。所以set集合中支持改变集合本身的方法frozenset()都不支持。

3.7 深层了解字典和集合

字典和集合是进行过性能高度优化的数据结构,特别是对于查找、添加和删除操作。本节将结合实例介绍它们在具体场景下的性能表现,以及与列表等其他数据结构的对比。

例如,有一个存储产品信息(产品 ID、名称和价格)的列表,现在的需求是,借助某件产品的ID找出其价格。则实现代码如下:

def find_product_price(products, product_id):for id, price in products:if id == product_id:return pricereturn None
products = [(111, 100),(222, 30),(333, 150)
]
print('The price of product 222 is {}'.format(find_product_price(products, 222)))

运行结果为:

The price of product 222 is 30

在上面程序的基础上,如果列表有 n 个元素,因为查找的过程需要遍历列表,那么最坏情况下的时间复杂度就为 O(n)。即使先对列表进行排序,再使用二分查找算法,也需要 O(logn) 的时间复杂度,更何况列表的排序还需要 O(nlogn) 的时间。

但如果用字典来存储这些数据,那么查找就会非常便捷高效,只需 O(1) 的时间复杂度就可以完成,因为可以直接通过键的哈希值,找到其对应的值,而不需要对字典做遍历操作,实现代码如下:

products = {111: 100,222: 30,333: 150
}
print('The price of product 222 is {}'.format(products[222]))

运行结果为:

The price of product 222 is 30

有些读者可能对时间复杂度并没有直观的认识,没关系,再给大家列举一个实例。下面的代码中,初始化了含有 100,000 个元素的产品,并分别计算出了使用列表和集合来统计产品价格数量的运行时间:

#统计时间需要用到 time 模块中的函数,了解即可
import time
def find_unique_price_using_list(products):unique_price_list = []for _, price in products: # Aif price not in unique_price_list: #Bunique_price_list.append(price)return len(unique_price_list)
id = [x for x in range(0, 100000)]
price = [x for x in range(200000, 300000)]
products = list(zip(id, price))
# 计算列表版本的时间
start_using_list = time.perf_counter()
find_unique_price_using_list(products)
end_using_list = time.perf_counter()
print("time elapse using list: {}".format(end_using_list - start_using_list))
#使用集合完成同样的工作
def find_unique_price_using_set(products):unique_price_set = set()for _, price in products:unique_price_set.add(price)return len(unique_price_set)
# 计算集合版本的时间
start_using_set = time.perf_counter()
find_unique_price_using_set(products)
end_using_set = time.perf_counter()
print("time elapse using set: {}".format(end_using_set - start_using_set))

运行结果为:

time elapse using list: 51.57102079999993

time elapse using set: 0.0074425000000246655

可以看到,仅仅十万的数据量,两者的速度差异就如此之大。而往往企业的后台数据都有上亿乃至十亿数量级,因此如果使用了不合适的数据结构,很容易造成服务器的崩溃,不但影响用户体验,并且会给公司带来巨大的财产损失。

那么,字典和集合为什么能如此高效,特别是查找、插入和删除操作呢?

字典和集合的工作原理

字典和集合能如此高效,和它们内部的数据结构密不可分。不同于其他数据结构,字典和集合的内部结构都是一张哈希表:

  • 对于字典而言,这张表存储了哈希值(hash)、键和值这 3 个元素。
  • 而对集合来说,哈希表内只存储单一的元素。

对于之前版本的 Python 来说,它的哈希表结构如下所示:

  | 哈希值 (hash)  键 (key)  值 (value)
. |           ...
0 |    hash0      key0    value0
. |           ...
1 |    hash1      key1    value1
. |           ...
2 |    hash2      key2    value2
. |           ...

这种结构的弊端是,随着哈希表的扩张,它会变得越来越稀疏。比如,有这样一个字典:

{‘name’: ‘mike’, ‘dob’: ‘1999-01-01’, ‘gender’: ‘male’}

那么它会存储为类似下面的形式:

entries = [
['--', '--', '--']
[-230273521, 'dob', '1999-01-01'],
['--', '--', '--'],
['--', '--', '--'],
[1231236123, 'name', 'mike'],
['--', '--', '--'],
[9371539127, 'gender', 'male']
]

显然,这样非常浪费存储空间。为了提高存储空间的利用率,现在的哈希表除了字典本身的结构,会把索引和哈希值、键、值单独分开,也就是采用如下这种结构:

Indices
----------------------------------------------------
None | index | None | None | index | None | index ...
----------------------------------------------------Entries
--------------------
hash0   key0  value0
---------------------
hash1   key1  value1
---------------------
hash2   key2  value2
---------------------...
---------------------

在此基础上,上面的字典在新哈希表结构下的存储形式为:

indices = [None, 1, None, None, 0, None, 2]
entries = [
[1231236123, 'name', 'mike'],
[-230273521, 'dob', '1999-01-01'],
[9371539127, 'gender', 'male']
]

通过对比可以发现,空间利用率得到很大的提高。

清楚了具体的设计结构,接下来再分析一下如何使用哈希表完成对数据的插入、查找和删除操作。

哈希表插入数据

当向字典中插入数据时,Python 会首先根据键(key)计算出对应的哈希值(通过 hash(key) 函数),而向集合中插入数据时,Python会根据该元素本身计算对应的哈希值(通过 hash(valuse) 函数)。

例如:

dic = {"name":1}print(hash("name"))setDemo = {1}print(hash(1))

运行结果为:

8230115042008314683
1

得到哈希值(例如为 hash)之后,再结合字典或集合要存储数据的个数(例如 n),就可以得到该元素应该插入到哈希表中的位置(比如,可以用 hash%n 的方式)。

如果哈希表中此位置是空的,那么此元素就可以直接插入其中;反之,如果此位置已被其他元素占用,那么 Python 会比较这两个元素的哈希值和键是否相等:

  • 如果相等,则表明该元素已经存在,再比较他们的值,不相等就进行更新;
  • 如果不相等,这种情况称为哈希冲突(即两个元素的键不同,但求得的哈希值相同)。这种情况下,Python 会使用开放定址法、再哈希法等继续寻找哈希表中空余的位置,直到找到位置。

具体遇到哈希冲突时,各解决方法的具体含义可阅读《哈希表详解》一节做详细了解。

哈希表查找数据

在哈希表中查找数据,和插入操作类似,Python 会根据哈希值,找到该元素应该存储到哈希表中的位置,然后和该位置的元素比较其哈希值和键(集合直接比较元素值):

  • 如果相等,则证明找到;
  • 反之,则证明当初存储该元素时,遇到了哈希冲突,需要继续使用当初解决哈希冲突的方法进行查找,直到找到该元素或者找到空位为止。

这里的找到空位,表示哈希表中没有存储目标元素。

哈希表删除元素

对于删除操作,Python 会暂时对这个位置的元素赋于一个特殊的值,等到重新调整哈希表的大小时,再将其删除。

需要注意的是,哈希冲突的发生往往会降低字典和集合操作的速度。因此,为了保证其高效性,字典和集合内的哈希表,通常会保证其至少留有 1/3 的剩余空间。随着元素的不停插入,当剩余空间小于 1/3 时,Python 会重新获取更大的内存空间,扩充哈希表,与此同时,表内所有的元素位置都会被重新排放。

虽然哈希冲突和哈希表大小的调整,都会导致速度减缓,但是这种情况发生的次数极少。所以,平均情况下,仍能保证插入、查找和删除的时间复杂度为 O(1)

3.8 Python深拷贝和浅拷贝

先通过代码理解二者不同。

Python浅拷贝

list1 = [1, 2, 3]
list2 = list(list1)print(list2)
print(list1 == list2)
print(list1 is list2)set1 = set([1, 2, 3])
set2 = set(set1)
print(set2)
print(set1 == set2)
print(set1 is set2)

运行结果:

[1, 2, 3]
True
False
{1, 2, 3}
True
False

上面的程序后缀是2的都是对后缀是1的浅拷贝。

对于可变序列,浅拷贝也可以通过切片来实现:

list1 = [1, 2, 3]
list2 = list1[:]print(list2)
print(list1 == list2)
print(list1 is list2)

运行结果:

[1, 2, 3]
True
False

除此之外,Python还提供了copy.cpoy()函数,适用于任何的数据类型:

import copylist1 = [1, 2, 3]
list2 = copy.copy(list1)print(list2, list1 == list2, list1 is list2, sep="--|--")
[1, 2, 3]--|--True--|--False

不过对于元组来说,使用tuple()或者切片操作来说,不会创建一份浅拷贝,相反它会一个指向相同元组的引用:

tuple1 = (1, 2, 3)
tuple2 = tuple(tuple1)
tuple3 = tuple1[:]print(tuple2, tuple3)
print(tuple1 == tuple2, tuple1 == tuple3)
print(tuple1 is tuple2, tuple1 is tuple3)
(1, 2, 3) (1, 2, 3)
True True
True True

浅拷贝指的是重新分配一块内存,创建一个新的对象,但里面的元素是原对象各个子对象的引用。

对于浅拷贝,如果原对象的元素不可变,那倒无所谓,但是如果元素是可变的,浅拷贝就会出现问题,参见以下代码:

a = [[1, 2], (4, 5)]
b = list(a)a.append(3)
print(a, b, sep="--|--")a[0].append(3)
print(a, b, sep="--|--")a[1] += (6,)
print(a, b, sep="--|--")
[[1, 2], (4, 5), 3]--|--[[1, 2], (4, 5)]
[[1, 2, 3], (4, 5), 3]--|--[[1, 2, 3], (4, 5)]
[[1, 2, 3], (4, 5, 6), 3]--|--[[1, 2, 3], (4, 5)]

这段程序首先是创建了一个a序列,a包括了一个列表和元组。然后对a进行了浅拷贝赋予了b。

程序第二段是对a加了3这个新元素。这个操作不会对b造成影响,这是因为a,b作为整体是两个不同的对象,不共享内存地址。

程序第三段是对a里面的列表新添了元素3.因为b是对a的浅拷贝,所以b也会同时发生变化。

程序第四段是对a里面的元组进行了拼接。b并没有对拼接后的新元组进行引用,所以b不发生改变。

个人理解:

append()是直接在原列表末尾添加,不会产生新列表

元组的+操作不会改变原来的元组,而是产生新的元组

所以这就是为什么上一段程序改变了列表和元祖后,只有列表的操作会对b产生影响。

Python深拷贝

要想完整的拷贝一个对象,就需要使用深拷贝。深拷贝就是重新分配一块内存,创建一个新对象,并将原对象中的元素以递归的方式,通过创建新得子对象拷贝到新对象中。所以新对象和原对象没有任何关系。

Python提供了copy.deepcopy()进行深拷贝。下面的程序是将上一段程序的浅拷贝换成了深拷贝:

import copya = [[1, 2], (4, 5)]
b = copy.deepcopy(a)a.append(3)
print(a, b, sep="--|--")a[0].append(3)
print(a, b, sep="--|--")a[1] += (6,)
print(a, b, sep="--|--")
[[1, 2], (4, 5), 3]--|--[[1, 2], (4, 5)]
[[1, 2, 3], (4, 5), 3]--|--[[1, 2], (4, 5)]
[[1, 2, 3], (4, 5, 6), 3]--|--[[1, 2], (4, 5)]

通过运行结果可以看到,无论a怎么变化,都不会影响到b。

但是深拷贝也不是完美的。如果被拷贝对象存在指向自身的引用,那么程序很容易陷入无限循环。

import copya = [1, 2, 3]
a.append(a)
b = copy.deepcopy(a)print(a, b, sep="--|--")
[1, 2, 3, [...]]--|--[1, 2, 3, [...]]

这段程序中列表a存在指向自身的引用,所以这是一个无限嵌套的列表。但是当深拷贝到b后,程序没有出现栈溢出的现象。

这是因为深拷贝函数deepcopy()函数会维护一个字典,记录已经拷贝的对象和其ID。拷贝过程中,如果字典里已经存储了将要拷贝的对象,那么就从字典里直接返回。

第四章 字符串常用方法

本章节所讲的字符串方法是字符串方法特有的。

4.1 字符串拼接

在Python中字符串拼接很简单,可以直接将两个字符串写在一起,例如:

strname = "123"456"
print(strname)

运行结果:

123456

需要注意的是,这样的方法只能用于拼接字符串,如果是变量的拼接就需要使用+

字符串和数字拼接

Python不允许直接拼接字符串和数字,所以需要先将数字转换为字符串。可以借助str()repr()实现。

str()repr()区别:

  • str()是将数据转换成适合人类阅读阅读的字符串形式;
  • repr()是将数据转换成适合解释器阅读的字符串形式。
s = "结了冰的可乐"
s_str = str(s)
s_repr = repr(s)print(type(s_str),s_str)
print(type(s_repr),s_repr)
<class 'str'> 结了冰的可乐
<class 'str'> '结了冰的可乐'

s自身就是一个字符串,但是仍然使用了str()repr()对其进行了转换。从运行结果可以看出,str()保留了字符串最原始的样子,repr()使用引号将字符串包围了起来。这就是Python字符串的表达形式。

另外在Python的交互式编程中输入一个表达式,Python会自动使用repr()函数处理该表达式。

4.2 截取字符串

通过索引进行操作。

4.3 len( )函数

len( )函数除了可以获得字符串的长度,也可以获得字符串占用的字节数。在Python中不同的字符占的字节数不同,数字,字母,小数点,下划线和空格,各占一个字节。而一个汉字可能占2~4个字节,具体占的字节数要看采用的编码方式。例如,采用GBK/GB2312编码中占用2个字节,而在UTF-8中一般占用3个字节。

我们可以使用encode()方法实现将字符串编码,在获取它的字节数。默认采用UTF-8:

str1 = "一二三_123"
print(len(str1.encode()))
print(len(str1.encode("gbk")))
13
10

4.4 split( ):分割字符串

Python中除了可以使用一些内建的函数获取字符串的信息外(如len( )),字符串本身也提供了一些方法使用。

注意:

这里的方法指的是字符串本身提供的,由于涉及到类和对象的知识,初学者不必深究,只需要知道具体方法的具体用法即可。

split()方法可以实现将一个字符串按照指定的方式分割成多个子串,这些子串会被保存到列表中(不包含分隔符),作为返回值反馈回来,具体用法如下:

str.split(sep,maxsplit)
  1. str:表示要分割的字符串;
  2. sep:用于指定分隔符。这个参数默认为None,表示所有的空字符;
  3. maxsplit:表示分割的次数
str1 = "1-2-3"print(str1.split(sep="-",maxsplit=3))
['1', '2', '3']

4.5 join( ):合并字符串

join()split()的逆方法,用来将列表\元组中包含的多个字符串连接成一个字符串。

使用join()方法合并字符串时,会将列表或者元组中多个字符串采用固定的分隔符连接在一起。例如:2022-3-26就可以看做是使用-当做分隔符将列表['2022','3','26']连接在一起的。

join()的语法格式:newstr = str.join(iterable)

a = ["1", "2", "3"]
str1 = ".".join(a)
print(str1)
=========
1.2.3

4.6 count():统计出现次数

count 方法用于检索指定字符串在另一字符串中出现的次数,如果检索的字符串不存在,则返回 0,否则返回出现的次数。

count 方法的语法格式如下:

str.count(sub[,start[,end]])

此方法中,各参数的具体含义如下:

  • str:表示原字符串

  • sub:表示要检索的字符串

  • start:指定检索的起始位置,也就是从什么位置开始检测。如果不指定,默认从头开始检索

  • end:指定检索的终止位置,如果不指定,则表示一直检索到结尾。

4.7 find():检测是否包含某字符串

find() 方法用于检索字符串中是否包含目标字符串,如果包含,则返回第一次出现该字符串的索引;反之,则返回 -1。

find() 方法的语法格式如下:

str.find(sub[,start[,end]])

此格式中各参数的含义如下:

  1. str:表示原字符串;
  2. sub:表示要检索的目标字符串;
  3. start:表示开始检索的起始位置。如果不指定,则默认从头开始检索;
  4. end:表示结束检索的结束位置。如果不指定,则默认一直检索到结尾。

python还提供了另一个方法rfind(),二者最大的区别就是rfind()是从字符串右边开始检索。

4.8 index():检测字符串中是否包含某字符串

同 find() 方法类似,index() 方法也可以用于检索是否包含指定的字符串,不同之处在于,当指定的字符串不存在时,index() 方法会抛出异常。

index() 方法的语法格式如下:

str.index(sub[,start[,end]])

此格式中各参数的含义分别是:

  1. str:表示原字符串;
  2. sub:表示要检索的子字符串;S
  3. start:表示检索开始的起始位置,如果不指定,默认从头开始检索;
  4. end:表示检索的结束位置,如果不指定,默认一直检索到结尾。

index()也有对应的rindex()

4.9 字符串对齐方法

Python提供了三种字符串对齐的方法,分别是:ljust()rjust()center()

  1. ljust( )

    此方法的功能是向字符串的右侧填充指定的字符,达到左对齐的效果。

    语法格式为:s.ljust(width[,fillchar])

    width:包括s本身在内,字符串总共的长度

    fillchar:指定用来填充的字符,默认使用空格

    这个方法不会改变原来的s

  2. rjust( )

    与上一个相反,不多赘述。

  3. center( )

    此方法功能是让文本居中

4.10 startwith( )和endwith( )

  1. startwith( )

    此方法是用来检测字符串是否以指定字符串开头的,如果是就返回True

    语法格式为:s.startwith(sub[,start[,end]])

  2. endwith( )

    与前一个方法效果相反,不多赘述。

4.11 字符串大小写转换

  1. title( )

    这个方法就是将每个单词的首字母大写,其他全变为小写。

    a = "qWerRT"
    b = a.title()
    print(a, b)
    

    运行结果为:

    qWerRT Qwerrt
    
  2. lower( )

    此方法是将字符串中的所有字母都变为小写。

  3. upper( )

    此方法是将字符串中的所有字母都变为大写。

4.11 去除字符串中的空格

  1. strip( )

    删除字符串左右两边的空格或特殊字符串

  2. lstrip( )

    只删除字符串左边的空格或者特殊字符串

  3. rstrip( )

    只删除字符串右边的空格或者特殊字符串

这里的特殊字符串指的是:制表符(\t)、回车符(\r)、换行符(\n)等。

因为字符串时不可变数据类型,所以这三个方法是返回字符串的副本。

a = "  qWerRT"
b = a.lstrip()
print(a, b)

运行结果:

 qWerRT qWerRT

4.12 format( )格式化输出字符串

语法格式如下:

str.format(args)

args:用于指定进行格式转换的项,如果有多项就用逗号隔开。

学习 format() 方法的难点,在于搞清楚 str 显示样式的书写格式。在创建显示样式模板时,需要使用{}来指定占位符,其完整的语法格式为:

{ [index][ : [ [fill] align] [sign] [#] [width] [.precision] [type] ] }

注意,格式中用 [] 括起来的参数都是可选参数,即可以使用,也可以不使用。各个参数含义如下:

  • index:指定后边设置的格式要作用到 args 中第几个数据,数据的索引值从 0 开始。如果省略此选项,则会根据 args 中数据的先后顺序自动分配。
  • fill:指定空白处填充的字符。注意,当填充字符为逗号(,)且作用于整数或浮点数时,该整数(或浮点数)会以逗号分隔的形式输出,例如(1000000会输出 1,000,000)。
  • align:指定数据的对齐方式。

C语言中文网对此方法解释比较难以理解,更好的解释可以看菜鸟教程+官方文档。

4.13 encode( )和decode( )

4.14 dir( )和help( )

Python dir() 函数用来列出某个类或者某个模块中的全部内容,包括变量、方法、函数和类等,它的用法为:

dir(obj)

Python help() 函数用来查看某个函数或者模块的帮助文档,它的用法为:

help(obj)

在 Python 标准库中,以__开头和结尾的方法都是私有的,不能在类的外部调用。

第五章 流程控制

Python程序也可以分为3大块,即顺序结构、选择结构和循环结构。

5.1 if else详解

Python 中的 if else 语句可以细分为三种形式,分别是 if 语句、if else 语句和 if elif else 语句,它们的语法和执行流程如表所示。

语法格式 执行流程
if 表达式: 代码块
if 表达式: 代码块 1 else: 代码块 2
if 表达式 1: 代码块 1 elif 表达式 2: 代码块 2 elif 表达式 3: 代码块 3 …//其它elif语句 else: 代码块 n

5.2 pass语句

5.3 assert断言函数

Python assert 语句,又称断言语句,可以看做是功能缩小版的 if 语句,它用于判断某个表达式的值,如果值为真,则程序可以继续往下执行;反之,Python 解释器会报 AssertionError 错误。

assert 语句的语法结构为:

assert 表达式

有读者可能会问,明明 assert 会令程序崩溃,为什么还要使用它呢?这是因为,与其让程序在晚些时候崩溃,不如在错误条件出现时,就直接让程序崩溃,这有利于我们对程序排错,提高程序的健壮性。

因此,assert 语句通常用于检查用户的输入是否符合规定,还经常用作程序初期测试和调试过程中的辅助工具。

mathmark = int(input())
#断言数学考试分数是否位于正常范围内
assert 0 <= mathmark <= 100
#只有当 mathmark 位于 [0,100]范围内,程序才会继续执行
print("数学考试分数为:",mathmark)

运行该程序,测试数据如下:

90
数学考试分数为: 90

再次执行该程序,测试数据为:

159
Traceback (most recent call last):File "D:/learn/main2.py", line 3, in <module>assert 0 <= mathmark <= 100
AssertionError

5.4 合理使用assert

讲完了 assert 的基本语法之后,本节通过一些实际应用的例子,给大家演示一下 assert 在 Python 中的用法,并明确 assert 的使用场景。

第一个例子,假设 C 语言中文网想做 VIP 促销活动,准备进行打折,现需要写一个 apply_discount() 函数,要求是,向该函数传入原来的价格和折扣力度,该函数返回打折后的价格。

apply_discount() 大致应该写成如下这样:

#price 为原价,discount 为折扣力度
def apply_discount(price, discount):updated_price = price * (1 - discount)    assert 0 <= updated_price <= price, '折扣价应在 0 和原价之间'    return updated_price

可以看到,在计算新价格的后面,添加了一个 assert 语句,用来检查折后价格,这里要求新折扣价格必须大于等于 0、小于等于原来的价格,否则就抛出异常。

我们可以试着输入几组数,来验证一下这个功能:

print(apply_discount(100,0.2))
print(apply_discount(100,1.1))

运行结果为:

80.0
Traceback (most recent call last):File "C:\Users\mengma\Desktop\demo.py", line 7, in <module>print(apply_discount(100,1.1))File "C:\Users\mengma\Desktop\demo.py", line 4, in apply_discountassert 0 <= updated_price <= price, '折扣价应在 0 和原价之间'
AssertionError: 折扣价应在 0 和原价之间

可以看到,当 discount 是 0.2 时,输出 80 没有问题,但是当 discount 为 1.1 时,程序便抛出下面 AssertionError 异常。

这样一来,如果开发人员修改相关的代码,或者是加入新的功能,导致 discount 数值异常时,只要运行程序就很容易能发现问题,这也从侧面印证了前面多讲的,assert 的加入可以有效预防程序漏洞,提高程序的健壮性。

另外,在实际工作中,assert 还有一些很常见的用法,例如:

def func(input):assert isinstance(input, list), '输入内容必须是列表'# 下面的操作都是基于前提:input 必须是 listif len(input) == 1:...elif len(input) == 2:...else:...

上面代码中,func() 函数中的所有操作都基于输入必须是列表这个前提。所以很有必要在开头加一句 assert 的检查,防止程序出错。

以上给大家介绍了 2 个有关 assert 的使用场景,很多读者可能觉得,assert 的作用和 if 语句非常接近,那么他们之间是否可以相互替代呢?

要注意,前面讲过,assert 的检查是可以被关闭的,比如在命令行模式下运行 Python 程序时,加入 -O 选项就可以使程序中的 assert 失效。一旦 assert 失效,其包含的语句也就不会被执行。

还是拿 C 语言中文网用户来说,只有 VIP 用户才可以阅读 VIP 文章,我们可以设计如下这个函数来模式判断用户身份的功能:

def login_user_identity(user_id):#凭借用户 id 判断该用户是否为 VIP 用户assert user_is_Vip(user_id) "用户必须是VIP用户,才能阅读VIP文章"read()

此代码从代码功能角度上看,并没有问题,但在实际场景中,基本上没人会这么写,因为一旦 assert 失效,则就造成任何用户都可以阅读 VIP 文章,这显然是不合理的。

所以正确的做法是,使用 if 条件语句替代 assert 语句进行相关的检查,并合理抛出异常:

def login_user_identity(user_id):#凭借用户 id 判断该用户是否为 VIP 用户if not user_is_Vip(user_id):raise Exception("用户必须是VIP用户,才能阅读VIP文章")read()

总之,不能滥用 assert,很多情况下,程序中出现的不同情况都是意料之中的,需要用不同的方案去处理,有时用条件语句进行判断更为合适,而对于程序中可能出现的一些异常,要记得用 try except 语句处理(后续章节会做详细介绍)。

5.5 for、while循环

5.6 循环结构中的else

Python中不管是for循环还是while循环,其后都可以紧跟一个else模块,它的作用是当跳出循环时,程序会最先执行else代码块中的代码。

add = "http://c.biancheng.net/python/"
i = 0
while i < len(add):print(add[i], end="")i = i + 1
else:print("\n执行 else 代码块")

5.7 循环嵌套及其用法

for和while循环支持互相嵌套和自身嵌套。当2个甚至多个循环相互嵌套时,位于外层的循环结构称为外层循环或外循环,位于内层的反之。

5.8 嵌套循环实现冒泡排序

冒泡排序实现的思想遵循以下几个步骤:

  1. 比较相邻的元素,如果第一个比第二个大就交换位置;
  2. 从第一对一直到最后一对,完成步骤1后,所有元素中的最大元素就放到了最后面
  3. 将循环缩短,出去最后一个元素,在重复之前的步骤
import timea = [5, 8, 4, 1]start = time.perf_counter()for i in range(len(a) - 1):for j in range(len(a) - i - 1):if a[j] > a[j + 1]:a[j], a[j + 1] = a[j + 1], a[j]end = time.perf_counter()
print(a, end - start)

5.9 break跳出循环

用户在使用for或者while循环的时,在某些场景下希望提前结束循环。Python提供了2种强制离开当前循环的方法:

  • 使用continue语句,可以跳过执行本次循环体中剩余的代码,转而执行下一次循环的代码;
  • 使用break语句,可以完全终止当前循环。

break

break语句可以立即终止当前循环的执行,跳出当前的循环结构。

a = "结了冰的可乐"for i in a:print(i)if len(a) == 6:break

运行结果

前面提及到for循环后面也可以跟else。这种情况下,如果使用了break语句,就不会执行else后面的代码。

a = "结了冰的可乐"for i in a:print(i)if len(a) == 6:break
else:print("执行了else")

运行结果

continue

continue只会终止本次循环中剩下的代码,直接从下一次循环开始执行。

a = "结了冰的可乐,可乐结了冰"for i in a:if i == ",":print("\n")continueprint(i, end="")

运行结果

结了冰的可乐可乐结了冰

5.10 Python推导式

  • 列表推导式
  • 元组推导式
  • 字典推导式
  • 集合推导式

推导式又称解析器,是Python独有的特性。使用推导式可以快速生成列表、元组、字典和集合。

1.列表推导式

列表推导式可以利用range区间、元组、列表、字典和集合等数据类型,快速生成一个满足指定要求的列表。

列表推导式语法格式如下:

[表达式 for 迭代变量 in 可迭代对象 if[ 条件表达式]]

通过上面的表达式,读者会感觉到列表推导式和for循环存在某些关联。其实出去可以省略的if部分,其余部分的含义以及执行顺序和for循环完全一样。表达式其实就是for循环中的循环体。

for 迭代变量 in 可迭代对象:表达式

对于读者初学来说可以认为列表推导式只是对for循环语句做了一点简单的变形,并用[]括起来而已。只不过不同之处在于,列表推导式最终会在循环过程中,计算表达式得到的一系列值组成的一个列表。

实例

a = range(10)a_list = [x * x for x in a]print(a_list)

运行结果

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

如果在里面再加上if条件语句,那么推导式只对符合条件的元素进行迭代。

a = range(4)a_list = [x * x for x in a if x == 2]print(a_list)

运行结果

4

此外,列表推导式还可以多个循环,就像嵌套循环一样

a = range(10)a_list = [(i,j) for i in a for j in a ]print(a_list)

运行结果

[(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3)]

从运行结果可以看出,其等效于:

a_list = []
for i in range(4):for j in range(4):a_list.append((i,j))

同样可以加入if条件语句。

2.元组推导式

元组推导式可以利用range区间、元组、列表、字典和集合等数据类型,快速生成一个符合指定需求的元组。

元组表达式的语法格式为:

(表达式 for 迭代变量 in 可迭代对象 [if 条件表达式])

元组推导式在创建及用法上大都和列表推导式类似,但是一点需要注意的是,元组推导式的返回结果并不是一个元组,而是一个生成器对象。

a = (i for i in range(10))print(a, type(a))

运行结果

<generator object <genexpr> at 0x000002089B417D60> <class 'generator'>

如果想使用元组推导式得到一个新元组,有以下三种方法:

  1. 使用tuple()函数

  2. 使用for循环遍历

  3. 使用__next__()方法遍历

    a = (i for i in range(3))print(a.__next__())
    print(a.__next__())
    print(a.__next__())b = tuple(a)
    print(b)
    

    运行结果:

    0
    1
    2
    ()
    

    由结果可以看出使用了__next__()方法遍历了生成器对象之后,原生成器对象将消失。

    其实对于for循环遍历来说也是如此。

3.字典推导式、集合推导式

二者之所以放在一起说是因为其推导式完全一致。

{ 表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] }

下面阐释二者之间的区别以及如何区分。

# 字典推导式a = ["结了冰的可乐", "可乐鸡翅"]
b = {key: len(key) for key in a}
print(b)

运行结果

{'结了冰的可乐': 6, '可乐鸡翅': 4}
# 元组推导式a = [1, 2, 3, 4, 4]b = {x for x in a}print(b)

运行结果

{1, 2, 3, 4}

5.11 zip函数及其用法

zip()函数是python的内置函数之一,可以将多个序列(列表、元组、字典、集合和range()区间构成的列表)“压缩”成一个zip对象。其实就是将这些序列中的对应位置元素重新组合,生成一个新的元组。

语法格式为:

zip(iterable)

# zip函数示例a = range(0, 10)
b = range(10, 20)c = zip(a, b)
print(c)for i in c:print(i)

运行结果

<zip object at 0x0000022F0559FB80>
(0, 10)
(1, 11)
(2, 12)
(3, 13)
(4, 14)
(5, 15)
(6, 16)
(7, 17)
(8, 18)
(9, 19)

但zip压缩多个序列的时候,如果序列的元素个数不一致,则按照最短的为准。

zip在创建字典这一小节里有提过

5.12 reversed函数

reversed函数是python的内置函数之一,可以给给定的序列(包括列表、元组、字典、集合和range区间)防返回一个逆序列的迭代器。

语法格式为:

reversed(seq)

# reversed函数示例a = [i for i in reversed(range(10))]print(a)

运行结果

[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

reversed()函数进行逆序操作并不会改变原来序列中的元素顺序

5.13 sorted函数

sorted函数是python的内置函数之一,可以对序列(列表、元组、字典、集合和字符串)进行排序

range区间属实是没必要排序

语法格式为:
list = sorted(iterable,key=None, reverse = False)

  • iterable:表示指定的序列
  • key:自定义排序规则
  • reverse:默认False(升序)
# 对列表进行排序a = [5, 3, 4, 2, 1]
print(sorted(a))

运行结果

[1, 2, 3, 4, 5]
chars=['http://c.biancheng.net',\'http://c.biancheng.net/python/',\'http://c.biancheng.net/shell/',\'http://c.biancheng.net/java/',\'http://c.biancheng.net/golang/']
#默认排序
print(sorted(chars))
#自定义按照字符串长度排序
print(sorted(chars,key=lambda x:len(x)))

运行结果

['http://c.biancheng.net','http://c.biancheng.net/golang/','http://c.biancheng.net/java/','http://c.biancheng.net/python/','http://c.biancheng.net/shell/']
['http://c.biancheng.net','http://c.biancheng.net/java/','http://c.biancheng.net/shell/','http://c.biancheng.net/python/','http://c.biancheng.net/golang/']

此程序中的lambda表示式,会在后面介绍

第六章 函数和lambda表达式

函数就是一段封装好的代码,能够重复使用。函数 是可以提前保存起来的,并起一个特有的名字,函数还可以接收数据,并根据数据的不同做出不同的操作。

6.1 函数定义与调用

在前面的章节里读者也接触过很多的内置函数,比如input()、print()、range()等等。

除了内置函数,python还支持自定义函数,达到一次编写,多次调用的目的。

比如len()函数可以获得字符串的长度,所以除此之外我们也可以用for循环得到

# 获取字符串长度i = 0
a = "12345"for x in a:i += 1
print(i)

获取字符串的长度是一个很常用的功能,如果每次都要写这样重复的代码不仅费时费力,还容易出错,所以Python支持将常用的代码以固定的格式封装成一个独立的模块,只要知道模块的名字就可以重复使用,这个模块就叫做函数。

其实函数就是有一段特定功能,可以重复使用的代码,这段代码提前被编写好,并且起了个名字。

# 获得字符串长度的函数my_strdef my_str(str):n = 0for i in str:n += 1return nlength = my_str("123456")
print(length)

运行结果

6

python和其他语言相同的是,python的函数支持接收多个参数,不同的是,python支持返回多个值。

读者通过上一段代码不难看出定义函数的语法格式为:

def 函数名(参数列表)://实现特定功能的代码[return [返回值]]

[ ]表示可省略

  • 函数名:原则上建议用户使用的函数名最好能体现函数的功能;
  • 形参列表:设置该函数接收的参数个数,用逗号隔开;
  • return 返回值:用于设置该函数的返回值。
# 定义了一个空函数
def pass_pass():pass# 定义一个比较字符串大小的函数
def str_max(str1,str2):str = str1 if str1>str2 else str2return str

另外值得一提的是,python支持return语句直接返回一个表达式的值。

def str_max(str1,str2):return str = str1 if str1>str2 else str2

前面的章节说过,通过help()可以查看某个函数的说明文档。其实函数的说明文档本质就是一段字符串,位置要放在函数内部,所有代码的最前面。

def str_max(str1, str2):"""比较两个字符串的长度:param str1::param str2::return: 返回最长的字符串"""str_max_ = str1 if str1 > str2 else str2return str_max_help(str_max)

运行结果

Help on function str_max in module __main__:str_max(str1, str2)比较两个字符串的长度:param str1::param str2::return: 返回最长的字符串进程已结束,退出代码0

6.2 函数值的传递和引用(含形参和实参)

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

在使用函数的时候,经常会用到形式参数和实际参数,二者之间的区别是:

  1. 形式参数:在定义函数时,函数名后面括号中的参数就是形参
  2. 实际参数:在调用函数时,函数名后面括号中的参数就是实参
def demo(obj):passdemo(a)

这里的obj就是形参,a就是实参。

接下来就要思考实参传递给形参的方式是什么这个问题

在Python中,根据实参的不同类型,传递方式有两种:

  1. 值传递:适合于实参类型是不可变数据类型(字符串,元组,数字)
  2. 引用(地址)传递:适合于实参是可变数据类型(列表、字典、集合)

二者之间的区别是,函数参数进行值传递后,若形参的值发生变化,不会影响实参的值,而进行引用传递后,改变形参的值,实参也会发生改变。

# 形参和实参比较
def demo(obj):obj += objprint("形参是:", obj)print("----值传递----")
a = '结了冰的可乐'
print("a的值为:", a)
demo(a)
print("a的实参为:", a)print("----引用传递----")
b = ['结了冰的可乐']
print("b的值为:", b)
demo(b)
print("b的实参为:", b)

运行结果

----值传递----
a的值为: 结了冰的可乐
形参是: 结了冰的可乐结了冰的可乐
a的实参为: 结了冰的可乐
----引用传递----
b的值为: ['结了冰的可乐']
形参是: ['结了冰的可乐', '结了冰的可乐']
b的实参为: ['结了冰的可乐', '结了冰的可乐']

分析结果可知,在执行值传递的时,改变了形参的值,实参不变;在执行引用传递时,改变了形参的值,也改变了实参的值。

6.3 参数传递机制

初学者可跳过

本节将围绕值传递和引用传递,深度剖析它们的底层实现。

Python 函数参数的值传递,其本质就是将实际参数值复制一份,将其副本传给形参。这意味着,采用值传递方式的函数中,无论其内部对参数值进行如何修改,都不会影响函数外部的实参。

下面程序演示了函数参数进行值传递的效果:

def swap(a , b) :'''下面代码实现a、b变量的值交换'''a, b = b, aprint("swap函数里,a =", a, " b =", b)
a = 6
b = 9
swap(a , b)
print("函数外部 a =", a ," b =", b)

运行上面程序,将看到如下运行结果:

swap函数里,a = 9  b = 6
函数外部 a = 6  b = 9

从上面的运行结果来看,在 swap() 函数里,经过交换形参 a 和 b 的值,它们的值分别变成了 9 和 6,但函数外部变量 a 和 b 的值依然是 6 和 9。这也证实了,swap() 函数的参数传递机制,采用的是值传递,函数内部使用的形参 a 和 b,和实参 a、b 没有任何关系。

swap() 函数中形参 a 和 b,各自分别是实参 a、b 的复制品。

如果读者依旧不是很理解,下面通过示意图来说明上面程序的执行过程。

上面程序开始定义了 a、b 两个局部变量,这两个变量在内存中的存储示意图如图 所示。

当程序执行 swap() 函数时,系统进入 swap() 函数,并将主程序中的 a、b 变量作为参数值传入 swap() 函数,但传入 swap() 函数的只是 a、b 的副本,而不是 a、b 本身。进入 swap() 函数后,系统中产生了 4 个变量,这 4 个变量在内存中的存储示意图如图所示。

当在主程序中调用 swap() 函数时,系统分别为主程序和 swap() 函数分配两块栈区,用于保存它们的局部变量。将主程序中的 a、b 变量作为参数值传入 swap() 函数,实际上是在 swap() 函数栈区中重新产生了两个变量 a、b,并将主程序栈区中 a、b 变量的值分别赋值给 swap() 函数栈区中的 a、b 参数(就是对 swap() 函数的 a、b 两个变量进行初始化)。此时,系统存在两个 a 变量、两个 b 变量,只是存在于不同的栈区中而己。

程序在 swap() 函数中交换 a、b 两个变量的值,实际上是对图 2 中灰色区域的 a、b 变量进行交换。交换结束后,输出 swap() 函数中 a、b 变量的值,可以看到 a 的值为 9,b 的值为 6,此时在内存中的存储示意图如图所示。

对比可以看到两个示意图中主程序栈区中 a、b 的值并未有任何改变,程序改变的只是 swap() 函数栈区中 a、b 的值。这就是值传递的实质:当系统开始执行函数时,系统对形参执行初始化,就是把实参变量的值赋给函数的形参变量,在函数中操作的并不是实际的实参变量。

如果实际参数的数据类型是可变对象(列表、字典),则函数参数的传递方式将采用引用传递方式。

下面程序示范了引用传递参数的效果:

def swap(dw):# 下面代码实现dw的a、b两个元素的值交换dw['a'], dw['b'] = dw['b'], dw['a']print("swap函数里,a =", dw['a'], " b =", dw['b'])dw = {'a': 6, 'b': 9}
swap(dw)
print("外部 dw 字典中,a =", dw['a']," b =",dw['b'])

运行上面程序,将看到如下运行结果:

swap 函数里,a = 9 b = 6
外部 dw 字典中,a = 9 b = 6

从上面的运行结果来看,在 swap() 函数里,dw 字典的 a、b 两个元素的值被交换成功。不仅如此,当 swap() 函数执行结束后,主程序中 dw 字典的 a、b 两个元素的值也被交换了。

注意,这里这很容易造成一种错觉,读者可能认为,在此 swap() 函数中,使用 dw 字典,就是外界的 dw 字典本身,而不是他的复制品。这只是一种错觉,实际上,引用传递的底层实现,依旧使用的是值传递的方式。下面还是结合示意图来说明程序的执行过程。

程序开始创建了一个字典对象,并定义了一个 dw 引用变量(其实就是一个指针)指向字典对象,这意味着此时内存中有两个东西:对象本身和指向该对象的引用变量。此时在系统内存中的存储示意图如图所示:

接下来主程序开始调用 swap() 函数,在调用 swap() 函数时,dw 变量作为参数传入 swap() 函数,这里依然采用值传递方式:把主程序中 dw 变量的值赋给 swap() 函数的 dw 形参,从而完成 swap() 函数的 dw 参数的初始化。值得指出的是,主程序中的 dw 是一个引用变量(也就是一个指针),它保存了字典对象的地址值,当把 dw 的值赋给 swap() 函数的 dw 参数后,就是让 swap() 函数的 dw 参数也保存这个地址值,即也会引用到同一个字典对象。下图显示了 dw 字典传入 swap() 函数后的存储示意图。

从图 来看,这种参数传递方式是不折不扣的值传递方式,系统一样复制了dw 的副本传入 swap() 函数。但由于 dw 只是一个引用变量,因此系统复制的是 dw 变量,并未复制字典本身。

当程序在 swap() 函数中操作 dw 参数时,由于 dw 只是一个引用变量,故实际操作的还是字典对象。此时,不管是操作主程序中的 dw 变量,还是操作 swap() 函数里的 dw 参数,其实操作的都是它们共同引用的字典对象,它们引用的是同一个字典对象。因此,当在 swap() 函数中交换 dw 参数所引用字典对象的 a、b 两个元素的值后,可以看到在主程序中 dw 变量所引用字典对象的 a、b 两个元素的值也被交换了。

为了更好地证明主程序中的 dw 和 swap() 函数中的 dw 是两个变量,在 swap() 函数的最后一行增加如下代码:

#把dw 直接赋值为None,让它不再指向任何对象
dw = None

运行上面代码,结果是 swap() 函数中的 dw 变量不再指向任何对象,程序其他地方没有任何改变。主程序调用 swap() 函数后,再次访问 dw 变量的 a、b 两个元素,依然可以输出 9、6。可见,主程序中的 dw 变量没有受到任何影响。实际上,当在 swap() 函数中增加“dw =None”代码后,在内存中的存储示意图如图 6所示。

从图 6来看,把 swap() 函数中的 dw 赋值为 None 后,在 swap() 函数中失去了对字典对象的引用,不可再访问该字典对象。但主程序中的 dw 变量不受任何影响,依然可以引用该字典对象,所以依然可以输出字典对象的 a、b 元素的值。

通过上面介绍可以得出如下两个结论:

  1. 不管什么类型的参数,在 Python 函数中对参数直接使用“=”符号赋值是没用的,直接使用“=”符号赋值并不能改变参数。
  2. 如果需要让函数修改某些数据,则可以通过把这些数据包装成列表、字典等可变对象,然后把列表、字典等可变对象作为参数传入函数,在函数中通过列表、字典的方法修改它们,这样才能改变这些数据。

6.4 位置参数

位置参数又称必备参数,必须按照正确的的顺序将实参传入到函数中,也就是说,再调用函数时传入实参的数量和位置都必须于定义函数时保持一致。

实参和形参数量必须一致

在调用函数时,指定的实参的数量和位置都必须和形参保持一致。否则会异常TypeError并提示缺少必要的位置参数。

def nomean(a, b):a += 1b += 1return a, bnomean(1)
Traceback (most recent call last):File "E:/learn/main2.py", line 6, in <module>nomean(1)
TypeError: nomean() missing 1 required positional argument: 'b'

上面列举的时少传了参数,读者自行尝试多传了参数的情况。

实参和形参的位置必须一致

在调用函数的时候,传入的实际参数必须和形式参数位置保持一致,否则会产生一下两种错误:

  1. 抛出TypeError异常

    但是实参和形参的数据类型不同,且两种类型不能相互转换的时候,就会抛出TypeError异常

    def demo(a, b):return a * a * bdemo("1", 2)
    

    运行结果

    Traceback (most recent call last):File "E:/learn/main2.py", line 5, in <module>demo("1", 2)File "E:/learn/main2.py", line 2, in demoreturn a * a * b
    TypeError: can't multiply sequence by non-int of type 'str'
    
  2. 产生的结果与预期不符

    调用函数时,如果指定的实际参数和形式参数位置不一致,但是数据类型相同,那么程序不会抛出异常,只不过会导致运行结果和预期不符合。

    def demo(a, b):return a * a * ba = demo(1, 2)
    b = demo(2, 1)
    print("正确的结果是", a)
    print("错误的结果是", b)
    

    运行结果

    正确的结果是 2
    错误的结果是 4
    

6.5 关键字参数

目前为止,我们使用函数所用的参数都是位置参数,即传入函数的实际参数和形式参数的数量和位置要对应。本节将介绍的关键字参数,就可以避免牢记参数位置的麻烦。

关键字参数是指使用形参的名字来确定输入的参数值。通过此方式指定实参的时候不需要与形参位置完全一致,只需要将参数名写正确即可。

def demo(a, b):return a * a * ba = demo(a=1, b=2)
b = demo(b=2, a=1)
print("正确的顺序是", a)
print("错误的顺序是", b)

运行结果

正确的顺序是 2
错误的顺序是 2

除此之外也可以使用位置参数和关键字参数混用的方式,但是关键字参数必须要位于所有的位置参数之后。

6.6 默认参数

我们知道,在调用函数的时候如果不指定某个参数,python就会抛出异常。为了解决这问题,python允许为参数设置默认值。

语法格式为:

def 函数名(..., 形参名, 形参名=默认值):代码块

注意使用此方法的时候,有默认值的参数必须要位于没有默认值参数后面

def demo(aa, bb=3):return aa * aa * bba = demo(1)
b = demo(1, 2)
print("默认的输出结果:", a)
print("指定参数的输出结果:", b)

运行结果

默认的输出结果: 3
指定参数的输出结果: 2

当然此方法也可以结合关键字参数一起使用,请读者自行编写程序检验。

这个时候有的读者会产生疑问,对于自己定义的函数可以轻松的知道哪个参数由默认值,但是如果使用Python提供的内置函数,又或者是第三方提供的函数怎么能够知道参数是否有默认值呢。

为此在Python中可以使用函数名.(__defaults__)查看函数的默认值参数,返回值是一个元组。

def demo(aa, bb=3):return aa * aa * bbprint(demo.__defaults__)

运行结果

(3,)

6.7 如何传入任意个参数

python在定义函数的时候也可以使用可变参数,即允许定义参数个数可变的参数。当调用这样的函数时,就可以向其传入任意多个参数。

Python定义可变参数有以下两种方式:

  1. 可变参数:形参前添加一个"*"

    语法格式为:

    *args

    def demo(aa, *bb):print(aa)print("bb:", bb)for s in bb:print(s)demo('12', "34", "56")
    

    运行结果

    12
    bb: ('34', '56')
    34
    56
    

    当然可变参数不一定非要是第一个,但是必须以关键字参数的形式给普通参数传值,上面的代码可以修改为:

    def demo(*aa, bb):print(aa)print("bb:", bb)for s in bb:print(s)demo('12', "34", bb="56")
    

    运行结果

    ('12', '34')
    bb: 56
    5
    6
    
  2. 可变参数:形参前添加两个"*"

    这种形式的语法格式为:

    **kwargs

    **kwargs:表示创建一个名为kwargs的空字典,该字典可以接收任意多个以关键字参数赋值的实际参数。

    # 定义了支持参数收集的函数
    def dis_str(aa, *bb, **cc):print(aa)print(bb)print(cc)# 调用函数
    dis_str("1","2","3",四是="4",五是="5",六是="6")
    

    运行结果

    1
    ('2', '3')
    {'四是': '4', '五是': '5', '六是': '6'}
    

    从运行结果可以看出,第一个参数传递给了aa;第2、3个参数传递给了bb;最后两个关键字参数传递给了cc,并以字典的形式接收。

    无论是*args还是**args,都可以不给他们传值,分别默认为空元组和空字典。所以上面的代码也可以写成:

    # 定义了支持参数收集的函数
    def dis_str(aa, *bb, **cc):print(aa)print(bb)print(cc)# 调用函数
    dis_str(aa = "12")
    

    运行结果为

    12
    ()
    {}

多个*,**形参是不允许的

6.8 逆向参数详解(进阶)

这一小节也可以叫做如何使用序列中的元素给函数传递参数或者参数解包。

在前面章节介绍了,Python是支持定义具有可变参数的函数,即可以接收任意多个参数。非关键字参数(*args)会集中存储到元组参数和关键字参数(**kwargs)会集中存储到字典参数中,这个过程称为参数收集。

不仅如此,python还支持逆向参数收集,即直接将列表、元组、字典作为参数,python会将其拆分,把其中存储的元素按照次序分给函数中的各个形参。

在逆向传参的时候,python规定,当传入列表或者元组时,其名称前需要带一个*号,字典前需要带2个**号。

尤其需要注意的是函数中参数的数量必须与序列中元素的数量保持一致,这样一一对应,才可能解包成功

def demo(aa, bb):print("aa:", aa)print("bb", bb)a = ["12", "34"]
demo(*a)

运行结果

aa: 12
bb 34
def demo(aa, bb):print("aa:", aa)print("bb:", bb)a = {"aa": 12, "bb": 34}
demo(**a)

运行结果

aa: 12
bb: 34

需要注意的是,此时字典的key必须要和形参的名称一致。

此外,以逆向参数收集的方式,还可以给拥有可变参数的函数传参;

在这个地方作者发现了一个好玩的地方,C语言中文网没有明确写出,这里进行粗略的演示。

首先要明确解包的时候,函数中的参数个数要和序列中的元素个数相同。

接下来看几段代码

# 这是C语言中文网的代码
def dis_str(name, *add):print("name:", name)print("add:", add)data = ["http://c.biancheng.net/python/", \"http://c.biancheng.net/shell/", \"http://c.biancheng.net/golang/"]
# 使用逆向参数收集方式传值
dis_str("Python教程", *data)
-------------------------------------
运行结果:
name: Python教程
add: ('http://c.biancheng.net/python/', 'http://c.biancheng.net/shell/', 'http://c.biancheng.net/golang/')

上面这段代码没有任何问题,但是接下来作者在使用字典序列进行参数解包的时候发现了点问题,这也是C语言中文网没有提及到的地方,接下来进行解释。

字典进行参数解包的时候,进行调用的时实参前面要加**

def demo(a, *b):print(a)print(b)map_a = {"aa": 'a', "bb": 'b'}
demo('1', **map_a)

这一段代码乍一看上去没得问题,只不过把上一段代码的列表换成了字典。但是运行的时候就出现了问题

Traceback (most recent call last):File "E:/learn/main1.py", line 7, in <module>demo('1', **map_a)
TypeError: demo() got an unexpected keyword argument 'aa'

如果用户尝试把map_a里的aa删掉同样也会报错

Traceback (most recent call last):File "E:/learn/main1.py", line 7, in <module>demo('1', **map_a)
TypeError: demo() got an unexpected keyword argument 'bb'

到这里作者就很迷惑了,始终找不到问题出错在哪,在查找了资料后找到了症结。

正确的代码应该这么写:

def demo(a, **b):print(a)print(b)map_a = {"bb": 'b'} # 这里把aa加上也可以
demo('1', **map_a)

所以如果形参是**的话,在调用函数进行参数解包的时候实参必须是**

再次强调,如果使用逆向参数收集的方式,必须注意 * 号的添加。以逆向收集列表为例,如果传参时其列表名前不带 * 号,则 Python 解释器会将整个列表作为参数传递给一个参数。

6.9 None及其用法

在Python中存在一个特殊的常量None。和False不一样,None不表示0,而是没有,也就是空值。

但是空值也不代表空对象,他有自己的数据类型。

print(None is "")print(None is [])print(type(None))

运行结果

False
False
<class 'NoneType'>

None是NoneType数据类型的唯一值,也可以理解为,我们不可以创建其他NoneType类型的变量,但是可以将None赋值给其他任何变量。

除此之外,None常用于assert、判断以及函数无返回值的时候。比如print()函数,它的返回值就是None。

对于没有return语句的函数,python都会在末尾加上return None,使得不带值的return语句,就返回None。

def demo(a):print(a)return Noneaa = '结了冰的可乐'
aaa = demo(aa)
print(aaa)

运行结果

结了冰的可乐
None

6.10 return返回值

python中,在创建函数的时候,可以使用return语句指定返回值,返回值可以是任意数据类型。不过需要注意的是,return语句在同一函数中可以出现多次,但是只要有一个得到了执行,会使得函数结束执行。

def demo(a):if a > 10:return Trueelse:return Falseaa = 12
print(demo(aa))

运行结果

True

6.11 函数返回多个值

通常情况下一个函数只有一个返回值,但是python函数可以以返回列表或者元组的形式,将要返回的多个值保存到其中,间接实现返回多个值。

所以要实现返回多个值,有以下两种方式:

  1. 将要返回的值存储到一个列表或者元组,然后将其返回
  2. 函数直接返回多个值,之间用,隔开,Python会自动将多个值封装到一个元组中
def demo1(a):a1 = areturn a1def demo2(b):return b[1], b[2]c = [1, 2, 3]
print(demo1(c))
print(demo2(c))

运行结果

[1, 2, 3]
(2, 3)

在此基础上,我们可以利用 Python 提供的序列解包功能,之间使用对应数量的变量,直接接收函数返回列表或元组中的多个值。

def demo(a):a1 = areturn a1c = [1, 2, 3]
d, e, f = demo(c)
print(d, e, f, sep="__")

运行结果

1__2__3

6.12 partial偏函数

简单的理解偏函数,它是对原始函数的二次封装,时间现有函数的部分参数预先绑定为指定值,从而的一个新函数,该函数称为偏函数。相较于原函数,偏函数具有较少的可变参数,降低了函数调用的难度。

定义偏函数需要使用partial关键字,语法格式为:

偏函数名 = partial(func, *args, *kwargs)

其中,func指的是要二次封装的原函数。

*args,**kwargs就是6.7小节的内容。

from functools import partialdef demo(name, gender):print("name", name)print("gender", gender)demo_partial = partial(demo, name="结了冰的可乐")
demo_partial(gender="male")

第9行的那么也可以省略不写。

运行结果

name 结了冰的可乐
gender male

必须以关键字参数给gender传参。

6.13 函数递归

一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。执行递归函数将反复调用其自身,没调用一次就进入新的一层。当最内层函数执行完毕后,再一层层的有里往外退出。

已知有一个数列:f(0) =1, f(1) = 4, f(n+2) = 2*f(n+1)+f(n),其中n是大于0的证书,求f(10)。这个问题就可以使用递归得到很好的解决。

def fn(n):if n == 0:return 1elif n == 1:return 4else:return 2 * fn(n - 1) + fn(n - 2)print(fn(10))

在上面的fn函数中再次调用了fn函数,这就是函数递归。注意在fn函数中调用fn的形式。对于fn(10)来说等于2*fn(9)+fn(8),其中fn(9)又等于fn(8)+fn(7),之后以此类推。

仔细观察可以发现,一个递归函数不断调用其自身,必须在某个时刻的返回值是确定的,即不再调用其自身。否则这个递归就变成了无穷递归,类似于死循环。

递归一定要朝已知方向进行。

例如:如果把上面的表达式改为:f(20) = 1,f(21) = 4,f(n+2) = 2*f(n+1)+f(n)。那么函数体应该改为:

def fn(n):if n == 20:return 1elif n == 21:return 4else:return fn(n + 2) - 2 * fn(n + 1)print(fn(10))

运行结果

-3771

6.14 变量作用域

作用域可以理解为变量的有效范围。有些变量可以在整段代码随意使用,有些变量只能在函数内部使用。

1.局部变量

在函数内部定义的变量,它的作用域也仅限于函数内部。

要知道,当函数被执行时,Python 会为其分配一块临时的存储空间,所有在函数内部定义的变量,都会存储在这块空间中。而在函数执行完毕后,这块临时存储空间随即会被释放并回收,该空间中存储的变量自然也就无法再被使用。

def demo():a = "结了冰的可乐"print("inside", a)demo()
print('outside', add)

运行结果

Traceback (most recent call last):File "E:/learn/main.py", line 14, in <module>print('outside', add)
NameError: name 'add' is not defined

函数的参数也是局部变量只能局部使用。

2.全局变量

除了定义在函数内部的变量之外的变量都是全局变量。全局变量的作用域是整段程序。

定义全局变量的方式分为2种:

①:直接在函数体外定义变量

②:在函数体内定义全局变量。使用global关键字对变量进行修饰。

def demo(a):global aaaa = areturn aaprint(demo(22), aa)

运行结果

22 22

使用global关键字修饰变量时,不能直接给变量赋值。

3.获得指定作用域中的变量

在某些特殊的场景中,我们需要获得某个作用域内的所有变量,python提供了以下三种方式:

  1. globals( )函数

    globals()是python的内置函数,,它可以返回一个包含全局范围内所有变量的字典,该字典的每个键值对,键为变量名,值为变量值

    def demo():a = 12return Noneaa = 12print(globals())
    

    运行结果:

    {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001F4429D0880>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'E:/Test/Learn.py', '__cached__': None, 'demo': <function demo at 0x000001F4429B61F0>, 'aa': 12}
    

    从运行结果可以看出,返回的变量很多,这些都是Python主程序内置的,暂时不做理会。

    通过该字典还可以访问指定变量,甚至于可以修改它的值,例如,在上面程序加上:

    print(globals()['aa'])
    globals()["aa'] = "12"
    print(aa)
    
  2. locals( )函数

    locals()也是Python的内置函数之一,此函数可以得到包含当前作用域内所有的变量字典。当前作用域指的是,在函数内部调用,就获得包含所有局部变量的字典,在全局范围内调用就和globals()一样

    def demo(a):global aaaa = aprint(locals())return Noneprint(demo(22))
    

    运行结果

    {'a': 22}
    None
    

    需要注意的是,locals()也可以和globals()一样,通过访问指定键的值,但是无法改变

    def demo():a = 12print(locals()['a'])locals()["a"] = 11print(a)return None
    demo()
    
  3. vars(object)

    vars()也是python的内置函数,其可以返回一个指定object对象范围内的所有变量组成的字典。如果不传入object参数,就和locals()作用相同。

    初学者没有学过类和对象,建议先跳过,等学完类和对象在回过头来学习

挖个坑在这里

6.15 如何在函数中使用同名的全局变量

全局变量在程序中的任何位置都可以被访问和修改。但是当函数中定义了和全局变量同名的局部变量的时候,那么在当前函数中无论访问和修改,操作的都是局部变量,不是全局变量。

举个实例代码理解:

a = '12'def demo():print(a)return Nonedemo()

运行结果:

12
a = '12'def demo():print(a)a = '11'return Nonedemo()

运行结果:

  File "main.py", line 15, in <module>demo()File "main.py", line 11, in demoprint(a)
UnboundLocalError: local variable 'a' referenced before assignment

Python在语法规定中,在函数内部中对不存在的变量赋值时,默认就是重新定义新的局部变量。所以上面一段程序就是新定义了一个a的局部变量。由于这个局部变量和全局变量同名,所以局部a变量就会遮蔽全局的a变量,再加上局部a变量在print之后才被初始化,违反了先定义后使用的原则,所以会报错。

那么如果想访问被遮蔽甚至是修改,可以采取以下两种方法:

  1. 直接访问被遮蔽的全局变量。如果希望程序能够依然访问全局变量a,且在函数中重新定义局部变量a,可以通过globals()实现:

    a = '12'def demo():print(globals()['a'])globals()['a'] = "11"a = '13'return Nonedemo()
    print(a)
    

    运行结果:

    12
    11
    
  2. 在函数中声明全局变量。为了避免在函数中对全局变量赋值(不是重新定义局部变量),可以使用global()

    a = '12'def demo():global aprint(a)a = "11"return Nonedemo()
    print(a)
    

    增加了global之后,a就变成了全局变量,a = ‘11’就只是对全局变量赋值,而不是重新定义局部变量。

6.16局部函数

函数内部定义的函数称为内部函数。

def demo1():def demo2():print("12")demo2()
demo1()

运行结果:

12

跟全局函数返回其局部变量,就可以扩大该变量的作用于一样。通过将局部函数所在的函数作为函数的返回值,也可以扩大函数的使用范围。

def outdef():def indef():print("nxl")return indef# 调用全局函数
a = outdef()
# 调用全局函数中的局部函数
a()

运行结果:

def outdef():def indef():print("nxl")return indef# 调用全局函数
a = outdef()
# 调用全局函数中的局部函数
a()

因此局部函数的作用域可以概括为;如果所在函数没有返回局部函数,则局部函数的可用范围仅限于所在函数内部;如果所在函数将局部函数作为返回值,则局部函数的作用域就会扩大,既可以在局部函数内部使用,也可以在所在函数的作用域中使用。

以上一段程序中的indef和outdef为例,如果ourdef不将indef作为返回值,则indef只能在outdef内部使用;反之indef既可以在outdef内部使用,也可以在outdef作用域内使用,也就是全局范围。

另外局部函数和全局函数也会发生遮蔽问题。但是前面说的方法就不适用于解决此问题。这里可以使用nonlocal关键字。

def outdef():a = 'out'def indef():nonlocal aprint(a)a = "in"indef()outdef()

运行结果:

out

3.17函数高级用法

python函数还可以赋值,作为其他函数的参数和其他函数的返回值。

首先,python允许直接将函数赋值给其他变量。

def indef():print("nxl")a = indef()
a
=======================
a = indef
a()

好奇这个括号的作用。包括前面的程序(注释含调用全局函数和调用全局函数的局部函数)

将函数以参数的形式传入其他函数

def add(a, b):return a + bdef multi(a, b):return a * bdef my_def(a, b, dis):return dis(a, b)# 求两个数的和
print(my_def(3, 4, add))
# 求两个数的乘积
print(my_def(3, 4, multi))

通过上面的程序可以看出,通过使用函数作为参数,可以在函数调用的时候动态传入参数。

还可以将函数的返回值设为函数

def outdef():def indef():print("nxl")return indef# 调用全局函数
a = outdef()
# 调用全局函数中的局部函数
a()

3.18闭包函数

当一个函数的返回值是另外一个函数,而返回的那个函数如果调用了其父函数内部的其它变量,如果返回的这个函数在外部被执行,就产生了闭包。

更好的闭包解释

def f1():n = 999def f2():print(n)return f2result = f1()
result()

这个f2就是闭包函数。

闭包的__closure__属性

闭包函数比普通函数多了一个__closure__属性,该属性记录着自由变量的地址。当闭包函数被调用的时候系统就会根据该地址找到自由变量,完成整体函数的调用。

自由变量是指未绑定到本地作用域的变量。如果自由变量绑定的值是可变的,变量仍然可以在封闭包中操作。如果是不可变的(数字、字符串等。),在封闭包中重新绑定自由变量会出错。

3.19 lambda(匿名函数)

lanbda表达式又称匿名函数,,常用来表示内部仅包含1行表达式的函数。如果一个函数的函数体仅有一行表达式。就可以使用lambda表达式代替。

name = lambda[list]:表达式

定义lambda表达式必须使用lambda关键字;[list]是可选参数,等同于定义函数时的参数列表;value是表达式的名称。

def name(list):return 表达式
name(list)
def add(x, y):return x + yprint(add(3, 4))===========================
a = lambda x, y: x + y
print(a(3, 4))

这两段程序的运行结果一致。

lambda表达式可以这么理解:

  • 对于单行表达式,使用lambda表达式可以让代码更加简洁;
  • 对于不需要多次使用的函数,lambda表达式在使用完后会立即释放,提高程序执行的性能。

3.20 函数式编程

所谓函数式编程就是代码中的每一块都是不可变的,都是由纯函数的形式组成。这里的纯函数指的是函数本身相互独立、互不影响,对于相同的输入总会有相同的输出。

除此之外,函数式编程还可以将函数本身作为参数传递给另一个函数,还允许返回一个函数。

def demo(list1):new_list = []for item in list1:new_list.append(item * 2)return new_listprint(demo([1, 2, 3]))

函数式编程主要是其纯函数和不可变的特性使得程序鲁棒性更好。但是限制多,难写。

map( )函数

语法格式为:

map(fuction,iterable)

  • function是要传入的函数,可以使任何函数;
  • iterable:表示一个或者多个可迭代对象。

map函数的作用是对可迭代对象中的每个元素都调用指定的函数,并返回一个map对象。

返回是map对象,不能直接打印,但是可以用for循环或者list()函数

a = [1, 2, 3]b = map(lambda x: x * 2, a)
for i in b:print(i, end=" ")

运行结果:

2 4 6

附录

Python入门--上相关推荐

  1. Python入门(上)

    Python 入门(上) 1.简介 (官网) 2.变量.运算符.数据类型 注释: #表示注释,作用于整行: 2)''' ''' 或者 """ ""&q ...

  2. 微软推出Python入门课,登上GitHub趋势榜第一(附视频)

    来源:新智元 本文约900字,建议阅读10分钟. 本文带你看视频轻松学习python课程! [ 导读 ] 微软针对 Python 初学者,推出了一套免费的教程视频.这套课程最大的特定是轻松简洁,一上线 ...

  3. python截图保存到内存卡_Python画月饼,云上过中秋,天池Python入门案例系列赛开启...

    阿里云天池推出了一个Python入门案例系列教程,在此之前他们还推出了一个Python基础训练营. 在天池龙珠计划Python训练营中,天池给学习者详细的介绍了Python的基础和进阶知识,根据学习者 ...

  4. Python语言快速入门上

    目录 1.前言 2.变量和常量 1)Python对象模型 2)Python变量 二.运算符和表达式 [运算符和表达式] [位运算符] [逻辑运算符] [成员运算符] [身份运算符] [常用内置函数] ...

  5. 重磅发布!微软推出Python入门课,登上GitHub趋势榜第一

    点击上方"码农突围",马上关注,每天早上8:50准时推送 真爱,请置顶或星标 新智元报道        来源:microsoft 编辑:肖琴 [导读]微软针对 Python 初学者 ...

  6. python画中秋的月亮_Python画月饼,云上过中秋,天池Python入门案例系列赛开启

    原标题:Python画月饼,云上过中秋,天池Python入门案例系列赛开启 阿里云天池推出了一个Python入门案例系列教程,在此之前他们还推出了一个Python基础训练营. 在天池龙珠计划Pytho ...

  7. python入门课程-Coursera上Python课程(公开课)汇总

    原标题:Coursera上Python课程(公开课)汇总 Python是深度学习时代的语言,Coursera上有很多Python课程,从Python入门到精通,从Python基础语法到应用Python ...

  8. python入门到精通需要学多久-史上最详细python学习路线-从入门到精通,只需5个月时间...

    针对Python的初学者,从无到有的Python语言如何入门,主要包括了:Python的简介,如何下载Python,如何安装Python,如何使用终端.Shell,IDE等各种开发环境进行Python ...

  9. python入门只需20分钟-史上最详细python学习路线-从入门到精通,只需5个月时间...

    针对Python的初学者,从无到有的Python语言如何入门,主要包括了:Python的简介,如何下载Python,如何安装Python,如何使用终端.Shell,IDE等各种开发环境进行Python ...

最新文章

  1. 单片机会被淘汰吗?单片机现在还有用吗?
  2. c#排序算法(待续)
  3. MySQL主从复制虽好,能完美解决数据库单点问题吗?
  4. 【求助】windows server 2003 64位下面网卡IP总是默认为动态分派IP
  5. 二维列表的len_基于Voronoi二维多晶体的衍生脚本操作
  6. Android Studio两模块间getLaunchIntentForPackage跳转,出现intent为null,已解决
  7. python100例详解-Python类和实例详解
  8. Android仿微信源码下载
  9. 把docx格式的word文档转换为txt文件
  10. [HDOJ4699]Editor
  11. 计算机文字输入程序,电脑“造字”,其实很简单
  12. 燕山大学联通新卡绑定校园网
  13. CSS学习笔记 10.字体
  14. android视频适配与裁剪
  15. RK3399 4K 带宽不足[drm:vop_isr] ERROR POST_BUF_EMPTY irq err
  16. 阿里云Linux下python3的安装及环境配置(详细教程)
  17. 修改mysql8.0中数据库的名字
  18. RESTful介绍和使用教程
  19. 电商后台设计——搜索
  20. 常见数据结构-栈-队列-数组-链表-哈希表

热门文章

  1. Fedora 16 安装HP USB打印机--HP K209G
  2. 2022 ACM Fellow名单出炉!北大梅宏、CMU邢波、腾讯俞栋等13位华人入选
  3. HVR日志分析简单应用
  4. ffmpeg 最简单的方式添加特效字幕
  5. Android TextView循环滚动弹幕效果
  6. 2019 年创业亏 800 万元,可以给到你的一些建议
  7. java-鸡生蛋?蛋生鸡?
  8. linux 耳机设备文件,Linux 声卡设置(喇叭和耳机同时出声的解决)
  9. 城市交通查询系统的设计与实现
  10. 一张图 综合交通 解决方案_构建区域综合交通枢纽 京津冀将形成“一张图”