文章导读
本文记录python知识点,主要用于防止编程语言混淆,部分知识点和其他编程语言的概念及其用法完全一样就没有做记录,如果无法理解我做的文本解释,可以打开Pycharm直接跟练例子。
1、基本概念
1.1、对象及内存
①、Python中,一切皆对象。
②、每个对象由:标识(identity)、类型(type)、值(value)组成
②.1、标识用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数id(obj)可返回对象obj的标识。
②.2、类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用type(obj)获得对象的所属类型。
②.3、值表示对象所存储的数据的信息。使用print(obj)可以直接打印出值。
总结:对象的本质就是一个内存块,拥有特定的值,支持特定类型的相关操作。
实操:通过编写下面的例子来理解内存(内存包括堆内存和栈内存):
通过上图测试结果可以得到如下内存图:
通过观察上面的内存图也能印证我们之前的结论:在Python中,一切皆对象,并且对象由标识,类型 和 值 组成。
1.2、引用
概述:①、在Python中,变量也称为对象的引用(reference),变量存储的就是对象的地址。
②、变量位于栈内存中(栈内存依次存放),对象位于堆内存中(堆内存随机存放)。
注意:Python是动态类型语言(即编写代码时不需要明确的声明变量的类型,Python解释器会根据变量引用的对象,自动确定数据类型)
1.3、标识符规则
①、标识符只能由字母、数字、下划线组成;
②、区分大小写;
③、第一个字符必须是字母、下划线;
④、不能使用关键字;
⑤、以双下划线开头和结尾的名称通常有特殊含义,尽量避免这种写法,比如:__init__
是类的构造函数。
1.4、赋值
1.4.1、普通赋值
概述:普通赋值通过单个'='实现,'='左侧为变量,'='右侧为为变量赋的值;
1.4.2、链式赋值
概述:通过连接多个'='实现一次性为多个值赋同一个值;
实操:通过如下代码测试链式赋值:
a = b = c = d = e = 200
print('a='+str(a),'b='+str(b),'c='+str(c),'d='+str(d),'e='+str(e))
得到如下结果:
1.4.3、系列解包赋值
概述:虽然前面的链式赋值能同时为多个变量赋值,但是只能赋相同的值,灵活性较低,因此提出了系列解包赋值,它能同时为多个变量赋不同的值。
注意:系列解包赋值的变量和值的个数必须对应,否则会报错。
实操:为三个变量赋值,然后交换任意两个变量的值;
# 分别为变量a,b,c赋值
a,b,c=1,2,3
print("a="+str(a),"b="+str(b),"c="+str(c))
# 交换变量a和b的值
a,b=b,a
print("a="+str(a),"b="+str(b),"c="+str(c))
测试结果如下:
1.5、内置数据类型
概述:Python内置的数据类型包括整型(int),浮点型(float),布尔型(bool)和字符串型(str)。
实操:练习每一种内置数据类型。
# int型
a=1
# float型
b1=3.14
b2=314e-2 #这里采用科学计数法,e-2表示10的(-2)次方
# bool型 (注意值的首字母大写)
c1=True
c2=False
# str型 (单引号和双引号都可以)
d1='hello'
d2="world"
print("a的类型为"+str(type(a)),"b1的类型为"+str(type(b1)),\
"b2的类型为"+str(type(b2)),"\nc1的类型为"+str(type(c1)),\
"c2的类型为"+str(type(a)),"d1的类型为"+str(type(d1)),\
"d2的类型为"+str(type(d2)),)
测试结果如下:
布尔类型知识点补充:有一些特殊的值被解释器认为是为False,例如False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。除此之外的其他情况均为True。
实操:通过bool()函数验证可能被解释器认为是False的值。
print('0布尔类型的值:',bool(0))
print('空列表布尔类型的值:',bool([]))
print('空字符串的布尔类型的值:',bool(""))
print('None布尔类型的值:',bool(None))
print('False布尔类型的值:',bool(False))
print('字符串True和False转成布尔都是True:',bool("False"))
运行结果如下:
注意:需要区分bool(False)和bool("False")的区别,False是一个bool类型的值,而"False"是值为False的字符串,因此bool(False)的结果为False,而bool("False")的结果为True(值不为空的字符串为True)
1.6、运算符
运算符 | 说明 | 示例 |
---|---|---|
+ | 加法 | 3+2 |
- | 减法 | 30-5 |
* | 乘法 | 3*6 |
/ | 浮点数除法(结果为浮点数) | 6/2 |
// | 整数除法(结果为整数) | 7//2 |
% | 模(取余) | 7%4 |
** | 幂 | 2**3 |
实操:通过任意两个变量将上图的运算符实操一遍。
# 定义两个变量
a,b=3,7
# 加法运算
c=a+b
# 减法运算
d=a-b
# 乘法运算
e=a*b
# 浮点数除法运算
f=a/b
# 整数除法运算
g=a//b
# 模运算
h=a%b
# 幂运算
i=a**b
print("a+b="+str(c),"a-b="+str(d),"a*b="+str(e)\
,"a/b="+str(f),"a//b="+str(g),"a%b="+str(h)\
,"a**b="+str(i))
测试结果如下:
知识点补充之加法:
①、字符串拼接 “3”+“2”
结果是 “32”
②、列表、元组等合并 [10,20,30]+[5,10,100]
结果是[10,20,30,5,10,100]
知识点补充之乘法:
①、字符串复制 “sxt”*3
结果是 ”sxtsxtsxt”
②、列表、元组等复制 [10,20,30]*3
结果是 [10,20,30,10,20,30,10,20,30]
知识点补充之divmod():我们在使用模(%)进行计算时只能拿到余数,当我们既要拿到商,又要拿到余数时可以采用divmod(被除数,除数),divmod会返回一个格式为(商,余数)的元组,演示如下:
a,b=3,7
print(divmod(a,b))
测试结果如下:
1.7、强制类型转换
1.7.1、int()
①、浮点数直接舍去小数部分。如:int(9.456457737)
结果是:9。
②、布尔值True
转为1
,False
转为0
。 如:int(True)
结果是1。
③、字符串的内容是整数时,通过int()强制类型转换会直接转成对应整数,如果字符串的内容是浮点数或字符,用int()进行强制类型转换就会直接报错。
实操:实操上面提到的情况。
# 浮点数转整数
a=9.456457737
print(int(a))
# 布尔值转整数
b1=True
print(int(b1))
b2=False
print(int(b2))
# 字符串转整数
c1="100"
print(int(c1))
测试结果如下:
下面演示字符串的内容是浮点数和字符时通过int()进行强制类型转换的实操:
# 字符串中包含浮点数
c2="3.14"
print(int(c2))
# 字符串中包含字符
c3="1abc"
print(int(c3))
测试结果如下:
由此印证了如果字符串包括浮点数或字符,用int()进行强制类型转换就会直接报错。
知识点补充:在Python3中,int
可以存储任意大小的整数,演示如下:
num=999**9999
print(num)
运行结果如下:
因此,Python3中可以做超大数的计算,而不会造成“整数溢出”。
1.7.2、float()
①、类似于int()
,唯一的区别就是float()可以将字符串中的浮点数转换为浮点数。
②、整数和浮点数一起参与运算时,表达式结果自动转型成浮点数。
③、round(值)返回输入值四舍五入后的结果,但结果不会改变原有值,而是产生新的值。
实操:
# 将包含浮点数的字符串转换为浮点数
str_float = "3.14"
print(float(str_float))
# 整数和浮点数一起参与计算
a,b=5.15,9
print(a+b)
# 实操round()函数
num = 3.14
print("3.14四舍五入后:"+str(round(num)))
print("调用round()函数后的num:"+str(num))
运行结果如下:
1.8、逻辑运算符
运算符 | 格式 | 说明 |
---|---|---|
or 逻辑或 | x or y | x为true,则不计算y,直接返回true;x为false,则返回y |
and 逻辑与 | x and y | x为true,则返回y的值x为false,则不计算y,直接返回false |
not 逻辑非 | not x | x为true,返回false;x为false,返回true |
实操:
a=True
b=False
print("a and b:",a and b)
print("a or b:",a or b)
print("not a:",not a)
print("not b:",not b)
运行结果如下:
注意:Python不支持C,Java的自增和自减功能。
1.9、同一运算符(is/is not)
概述:is的本质是比较两个值的地址,而==的本质是调用_eq_()方法,比较两个值的值是否一样。
运算符 | 描述 |
---|---|
is | is是判断两个标识符是不是引用同一个对象 |
is not | is not 是判断两个标识符是不是引用不同对象 |
实操:
a1=2345678
a2=2345678
print(a1 is a2)
运行结果如下:
诶?此时我就开始疑惑,虽然两个值都相同,但为什么通过同一运算符比较两个值的结果是True呢?其实这里就涉及到了整数缓存问题。
整数缓存问题:
①、命令行模式下,Python仅仅对比较小的整数对象进行缓存(范围为[-5, 256])缓存起来,底层用数组实现,连续分配空间
。
②、文件模式下(Pycharm),所有数字都会被缓存,范围是:[-无穷大,+无穷大]),但是底层仍然使用数组实现
,不在[-5,256]中的数,会被缓存到链表中,不连续分配空间
。
总结:同一运算符中,is 比 == 的效率更高 , 在比较变量和None值时应该使用is进行比较。
1.10、成员运算符
概述:成员运算符用于测试序列中是否包含特定的字符/数字等信息,这里的序列包括字符串,列表,元组,集合和字典(对于字典,in
操作符用于检查键的存在性,而不是值的存在性)。
运算符 | 描述 |
---|---|
in | 如果在指定的序列中找到值返回 True,否则返回 False |
not in | 如果在指定的序列中找不到值,则返回True |
实操:
a="hello world!"
print('llo' in a)
运行结果如下:
2、字符串
①、字符串的本质是字符序列,并且Python不支持单字符,因此单个字符在Python中也是一个字符串。
②Python的 字符串是不可变的,无法对源字符串进行修改。
2.1、字符串的创建
①、通过单引号或双引号创建
str1="'双引号'创建"
str2='"单引号"创建'
print(str1,str2)
运行结果如下:
通过上面的例子可知,使用单引号和双引号的好处是可以创建本身包含引号的字符串,而不需要使用转义字符。
②、通过三引号创建
str3="""
Keep
study
habits
"""
print(str3)
运行结果如下:
通过上面的例子可知,连续三个单引号或三个双引号,能够创建多行字符串,并且创建的字符串会保留原始的格式。
知识点补充:Python允许空字符串的存在,不包含任何字符且长度为0,而len()用于计算字符串含有多少字符。
2.2、字符串的拼接/复制/打印和input()
2.2.1、字符串拼接
①、通过+号将多个字符串拼接起来
a="hello"+"World"
print(a)
运行结果如下:
注意:如果加号两边的类型不同会报如下错误:
②、将多个字符串直接放在一起实现字符串拼接
b = "keep""Study"
print(b)
运行结果如下:
2.2.2、字符串的复制
概述:字符串的复制通过*号实现。
str1="Keep study habits\n"
print(str1*5)
运行结果如下:
2.2.3、字符串打印
概述:调用print时会自动打印一个换行符。如果不想自动添加换行符。我们可以自己通过参数end = “任意字符串”来实现末尾添加任何内容:
print("hello",end="")
print("World",end="!")
运行结果如下:
2.2.4、input()
概述:input()用于获取用户输入的字符串,括号内允许给出提示信息用于提示用户输入的内容。
name = input("请输入您的用户名:")
print("欢迎您,"+name)
运行结果如下:
2.3、字符替换(replace())
概述:在进入第二章字符串时就提到字符串是不允许修改的,但难免有这样的需求,此时可以通过replace()方法替换原有字符串的字符,再创建新的字符串保存即可。
str1="helloworld"
str2=str1.replace('w','W') # 这里我们将str1的w替换为W并赋值给str2
print(str1)
print(str2)
运行结果如下:
为了让我们更改字符串的效果更逼真,还可以创建新的对象,让str1指向新的对象而不指向旧的对象:
str1="helloworld"
str1=str1.replace('w','W')
print(str1)
运行结果如下:
注意:这里并没有改变原有的字符串,而是创建了新的字符串对象,并让保存原有字符串的变量不再指向原有的字符串对象,而是指向通过replace()方法创建的新字符串对象。
知识点补充:当我们调用print()函数时,解释器自动调用了str()将非字符串的对象转成了字符串。
2.4、字符串切片
切片格式:[起始偏移量start : 终止偏移量stop : 步长step]
注意:切片遵循左闭右开原则(即: start<=取值范围<stop )
当下标为正数时演示如下:
操作和说明 | 示例 | 结果 |
---|---|---|
[:] 提取整个字符串 | “abcdef”[:] | “abcdef” |
[start:] 从start索引开始到结尾 | “abcdef”[3:] | “def” |
[:end] 从头开始知道end-1 | “abcdef”[:3] | “abc” |
[start:end] 从start到end-1 | “abcdef”[1:3] | “bc” |
[start:end:step] 从start提取到end-1,步长是step | “abcdef”[1:5:3] | “be” |
当下标为负数时演示如下:
示例 | 说明 | 结果 |
---|---|---|
"abcdefg"[-3:] | 倒数三个 | “efg” |
"abcdefg"[-6:-3] | 倒数第3个到倒数第6个(左闭右开原则)(注意:这里的步长为正数,所以是从左边取到右边) | 'bcd' |
"abcdefg"[::-1] | 步长为负,从右到左反向提取 | 'gfedcba' |
实操①:
str1="abcdefg"
print(str1[1::2])
运行结果如下:
实操②:当步长为负数时,方向是从右到左进行切片,因此起始值在右侧,结束值在左侧:
str1="abcdefg"
print(str1[-2:-7:-1])
运行结果如下:
2.5、字符串分隔与合并
2.5.1、字符串分隔(split())
概述:split()可以基于指定分隔符将字符串分隔成多个子字符串(存储到列表中)。如果不指定分隔符,则默认指定空白字符(换行符/空格/制表符)对字符串进行分隔。
实操①:不指定分隔符。
str1="Keep Study habits"
str2=str1.split()
print(str2)
运行结果如下:
实操②:指定分隔符为' t '。
str1="Keep Study habits"
str2=str1.split('t')
print(str2)
运行结果如下:
2.5.2、字符串合并(join())
join()语法:'连接符'.join(元素全为字符串的列表/元组/集合)
实操:这里演示元素全为字符串的元组进行拼接。
str1=('keep','stydy','habits')
str2='~'.join(str1)
print(str2)
运行结果如下:
概述:在前面的章节中使用了+号对字符串进行拼接,会生成新的字符串对象,因此不推荐使用+
来拼接字符串。推荐使用join
函数,因为join
函数在拼接字符串之前会计算所有字符串的长度,然后逐一拷贝,仅新建一次对象。
实操:为了验证上面的结论,我们分别用+号和join()凭借999999次"muxishiye",并打印时间。
import time
time_start_add = time.time()
a = ''
for i in range(999999):
a+="muxishiye"
time_end_add = time.time()
print("+拼接的时间为:"+str(time_end_add-time_start_add)+"秒")
lis = []
time_start_join = time.time()
for n in range(999999):
lis.append("muxishiye")
"".join(lis)
time_end_join = time.time()
print("join拼接时间为:"+str(time_end_join-time_start_join)+"秒")
运行结果如下:
2.6、字符串常用方法
2.6.1、常用的查找方法
以下是测试文本:
str1="""君不见,黄河之水天上来,奔流到海不复回。
君不见,高堂明镜悲白发,朝如青丝暮成雪。
人生得意须尽欢,莫使金樽空对月。
天生我材必有用,千金散尽还复来。
烹羊宰牛且为乐,会须一饮三百杯。
岑夫子,丹丘生,将进酒,君莫停。
与君歌一曲,请君为我倾耳听。
钟鼓馔玉不足贵,但愿长醉不复醒。
古来圣贤皆寂寞,惟有饮者留其名。
陈王昔时宴平乐,斗酒十千恣欢谑。
主人何为言少钱,径须沽取对君酌。
五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。"""
方法和使用示例 | 说明 | 结果 |
---|---|---|
len(str1) | 字符串包含多少个字符 | 217 |
str1.startswith("君不见") | 以指定字符串开头 | True |
str1.endswith("万古愁。") | 以指定字符串结尾 | True |
str1.find("黄河之水") | 第一次出现指定字符串的位置 | 4 |
str1.rfind("君不见") | 最后一次出现指定字符串的位置 | 21 |
str1.count("酒") | 指定字符串出现了几次 | 3 |
str1.isalnum() | 所有字符全是字母或数字 | False |
2.6.2、去除首尾信息(strip())
概述:通过strip()同时去除字符串首尾指定信息。通过lstrip()去除字符串左边指定信息,rstrip()去除字符串右边指定信息。
实操:熟悉strip(),lstrip()和rstrip()方法。
str_mine = "~Keep study habits~"
str_strip = str_mine.strip("~")
print(str_strip)
str_lstrip = str_mine.lstrip("~")
print(str_lstrip)
str_rstrip = str_mine.rstrip("~")
print(str_rstrip)
运行结果如下:
2.6.3、大小写转换(capitalize() title() upper() lower() swapcase())
首先准备测试素材:
a = "The habit of learning plays a pivotal role"
示例 | 说明 | 结果 |
---|---|---|
a.capitalize() | 产生新的字符串,首字母大写 | "The habit of learning plays a pivotal role" |
a.title() | 产生新的字符串,每个单词都首字母大写 | "The Habit Of Learning Plays A Pivotal Role" |
a.upper() | 产生新的字符串,所有字符全转成大写 | "THE HABIT OF LEARNING PLAYS A PIVOTAL ROLE" |
a.lower() | 产生新的字符串,所有字符全转成小写 | "the habit of learning plays a pivotal role" |
a.swapcase() | 产生新的,所有字母大小写转换 | "tHE HABIT OF LEARNING PLAYS A PIVOTAL ROLE" |
实操:将上面的示例进行实操。
a = "The habit of learning plays a pivotal role"
print(a.capitalize())
print(a.title())
print(a.upper())
print(a.lower())
print(a.swapcase())
运行结果如下:
2.6.4、格式排版(center() rjust() ljust())
概述:格式排版通过center(),ljust()和rjust()三个函数实现,ljust()表示左对齐,rjust()表示右对齐,center()表示居中对齐。
格式:要显示的字符串.center(宽度,填充的字符)
实操:总共包括16个字符,将muxi左对齐/右对齐/居中显示,其他空缺的字符用~填充。
a='muxi'
print(a.center(16,'~'))
print(a.ljust(16,'~'))
print(a.rjust(16,'~'))
运行结果如下:
2.6.5、特征判断
概述:
isalnum():检测字符串是否为字母或数字。
isalpha():检测字符串是否由字母或汉字组成。
isdigit():检测字符串是否只由数字组成。
isspace():检测字符串是否为空白符。
isupper():检测字符串是否为大写字母。
islower():检测字符串是否为小写字母。
实操1:
# 这里说明浮点数也不认为是数字,因为包含小数点
num1 = "3.14"
print(num1.isdigit())
num2 = "314"
print(num2.isdigit())
测试结果如下:
实操2:
str1 = "hello沐曦可期"
print(str1.isalpha())
运行结果如下:
2.7、字符串格式化(format())
2.7.1、format()基本用法 (熟悉)
概述:format()函数可以接受不限个数的参数,位置可以不按顺序,配合{}和数字的用法很像C语言中的%d,%c等格式化字符串。
实操1:
a="用户名:{0}\n联系电话:{1}"
b=a.format("沐曦可期",119)
print(b)
运行结果如下:
实操2:
a="用户名:{name}\n联系电话:{tel}"
b=a.format(name="沐曦可期",tel=120)
print(b)
运行结果如下:
2.7.2、填充与对齐(了解)
概述:这里的填充与对齐是format()基本用法与2.6.4章节中的格式排版相结合的产物。
①.填充常跟对齐一起使用
②.^
、<
、>
分别是居中、左对齐、右对齐,后面带宽度
③.:
号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充
实操:
a="用户名为{0},联系电话是{1:~^16}".format("沐曦可期","10086")
print(a)
运行结果如下:
2.7.3、数字格式化(参考)
概述:浮点数通过f进行格式化,整数通过d进行格式化。
数字 | 格式 | 输出 | 描述 |
---|---|---|---|
3.1415 | {:.2f} | 3.14 | 保留小数点后两位 |
3.1415 | {:+.2f} | 3.14 | 带符号保留小数点后两位 |
2.66378 | {:.0f} | 3 | 不带小数 |
5 | {:0>2d} | 05 | 数字补零 (填充左边, 宽度为2) |
5 | {:x<4d} | 5xxx | 数字补x (填充右边, 宽度为4) |
10 | {:x<4d} | 10xx | 数字补x (填充右边, 宽度为4) |
10000000 | {:,} | 10,000,000 | 以逗号分隔的数字格式 |
0.3 | {:.2%} | 30.00% | 百分比格式 |
100000000 | {:.2e} | 1.00E+08 | 指数记法 |
13 | {:10d} | 13 | 右对齐 (默认, 宽度为10) |
13 | {:<10d} | 13 | 左对齐 (宽度为10) |
13 | {:^10d} | 13 | 中间对齐 (宽度为10) |
实操:
a="我是{0},我的余额有{1:.3f}".format("沐曦可期",648.1288812888)
print(a)
运行结果如下:
2.8、可变字符串
概述:在之前的章节中多次提到字符串是不允许修改的,但某些业务一定要在原地址的基础上对字符串进行修改,而不是创建一个新的字符串对象,可以通过io.StringIO对象或array模块在定义字符串时就定义一个可变字符串,从而实现对字符串的修改。
实操:定义一个字符串,将下标2~4的字符替换为'~'。
import io
a="muxikeqi"
# aio才是可变字符串
aio=io.StringIO(a)
print(aio)
print("修改前:"+aio.getvalue())
# 指针移动到下标2
print(aio.seek(2))
# 进行写操作,会覆盖原有内容(有点类似按到键盘上的Ins键后在文本中进行输入的效果)
aio.write("~~~")
print("修改后:"+aio.getvalue())
运行结果如下:
2.9、类型转换参考表
int(x [,base]) | 将x转换为一个整数 |
long(x [,base] ) | 将x转换为一个长整数 |
float(x) | 将x转换到一个浮点数 |
complex(real[,imag]) | 创建一个复数 |
str(x) | 将对象 x 转换为字符串 |
repr(x) | 将对象 x 转换为表达式字符串 |
eval(str) | 用来计算在字符串中的有效Python表达式,并返回一个对象 |
Complex(A) | 将参数转换为复数型 |
tuple(s) | 将序列 s 转换为一个元组 |
list(s) | 将序列 s 转换为一个列表 |
set(s) | 转换为可变集合 |
dict(d) | 创建一个字典。d 必须是一个序列 (key,value)元组 |
frozenset(s) | 转换为不可变集合 |
chr(x) | 将一个整数转换为一个字符 |
unichr(x) | 将一个整数转换为Unicode字符 |
ord(x) | 将一个字符转换为它的整数值 |
hex(x) | 将一个整数转换为一个十六进制字符串 |
oct(x) | 将一个整数转换为一个八进制字符串 |
3、序列
序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放多个值的连续内存空间。Python中的序列包括字符串,列表,元组,字典和集合。
3.1、列表
3.1.1、列表简介
①、列表用于存储任意类型和数目的数据集合。arr = [1,2,"a",True]
②、列表是内置可变序列,是包含多个元素的有序连续的内存空间。
③列表的标准语法格式:a = [1,2,3,4],中括号包括的
1,2,3,4是列表a的元素。
④、Python的列表大小可变,根据需要随时增加或缩小。(不同于字符串不允许修改)
列表的方法汇总(参考):
方法 | 要点 | 描述 |
---|---|---|
list.append(x) | 增加元素 | 将元素x增加到列表list尾部 |
list.extend(aList) | 增加元素 | 将列表aList所有元素加到列表list尾部 |
list.insert(index,x) | 增加元素 | 在列表list指定位置index处插入元素x |
list.remove(x) | 删除元素 | 在列表list中删除首次出现的指定元素x |
list.pop([index]) | 删除元素 | 删除并返回列表list指定为止index处的元素,默认是最后一个元素 |
list.clear() | 删除所有元素 | 删除列表所有元素,并不是删除列表对象 |
list.index(x) | 访问元素 | 返回第一个x的索引位置,若不存在x元素抛出异常 |
list.count(x) | 计数 | 返回指定元素x在列表list中出现的次数 |
len(list) | 列表长度 | 返回列表中包含元素的个数 |
list.reverse() | 翻转列表 | 所有元素原地翻转 |
list.sort() | 排序 | 所有元素原地排序 |
list.copy() | 浅拷贝 | 返回列表对象的浅拷贝 |
3.1.2、列表的创建
①、利用[ ]创建列表
# 创建一个空列表
arr1=[]
# 通过[]创建一个包含内容的列表
arr2=[1,2,"yeah",True]
print("arr1:",arr1)
print("arr2:",arr2)
运行结果如下:
②、通过list方法创建列表
概述:list()可以将任何可迭代的数据转化成列表。
# 创建一个列表对象
arr3=list()
print("arr3:",arr3)
# 通过list()包含字符串创建列表
arr4=list("hello,muxikeqi")
print("arr4:",arr4)
# list()结合range()创建整数列表
arr5=list(range(2,8))
print("arr5:",arr5)
运行结果如下:
③、通过range()创建整数列表
概述:range()方法能快捷地创建出整数列表。
格式:range([start],end,[step]) (起始下标和步长为可选选项,结束下标不能省略,并且range()方法和字符串的切片一样遵循左闭右开原则)
实操1:创建一个列表,其内容是1到10之间的奇数。
arr6=list(range(1,11,2)) # 因为遵循左闭右开原则所以不包含11
print("arr6:",arr6)
运行结果如下:
实操2:由于range()和字符串切片规则一样,所以range()的步长也支持负数,起始下标一定在前面,结束下标在后面,当步长为负数时,就是从大的数往小的数取值,下面的例子方便理解:
arr7=list(range(50,10,-5)) # 从50向10取值,步长为5,由于左闭右开原则,不包含10
print("arr7:",arr7)
运行结果如下:
从上面的例子不难发现,即使我们的步长为负,range()也遵循左闭右开的原则。
④、通过推导式生成列表
概述:由于这里需要用到for循环和if,所以这里只做简单案例。
# 将5~9各个数平方后再保存到列表中
arr8=[i**2 for i in range(5,10)]
print("arr8:",arr8)
# 将1~50中所有5的倍数的值平方后保存到列表中
arr9=[j**2 for j in range(1,51) if j%5==0]
print("arr9:",arr9)
运行结果如下:
3.1.3、为列表添加元素
概述:当列表有添加/删除元素的需求时,推荐从列表的尾部添加/删除元素,有利于提高列表效率(如果是在列表的某一个位置添加一个元素,原列表从这个插入的位置开始,所有的元素都会向后移动一位,大大的降低了列表的效率,而从尾部添加/删除一个元素就不需要考虑移动其他元素的负担)
①、append()方法添加元素
概述:该方法直接在指定列表的尾部添加指定的内容,它修改了原有列表对象,效率最高所以推荐使用。
arr1=[1,2,3,4,5]
arr1.append("muxikeqi")
print("arr1:",arr1)
运行结果如下:
②、+运算符添加元素
概述:+运算符虽然能达到添加元素的效果,但实际是创建了一个新的列表对象并指向新的列表对象(在实际练习的过程中发现使用+=不能替代+号,+=会直接添加指定的列表内容,并且不会创建新的列表对象)。
arr2=["muxi","keqi"]
print(arr2)
print("arr2修改前的id:",id(arr2))
# 注意:使用arr2+=["keep","study"]的区别是不会创建新的列表对象(两个id是相同的)
arr2=arr2+["keep","study"]
print("arr2通过加运算符修改后的id:",id(arr2))
print(arr2)
运行结果如下:
③、extend()方法添加其他列表的元素
概述:extend()用于将指定列表的元素添加到本列表的尾部,所有操作都在本列表的id上操作,不创建新的列表对象。
arr3=["muxi","keqi"]
arr4=["keep","study"]
print("arr3操作前的id:",id(arr3))
arr3.extend(arr4)
print("arr3通过extend()方法添加元素后:",arr3)
print("arr3操作后的id:",id(arr3))
运行结果如下:
④、insert插入元素
概述:insert()方法就是在指定下标添加指定列表元素,该方法会让插入位置后面所有的元素进行移动,影响处理速度。类似发生这种移动的函数还有:remove()、pop()和del()方法,它们在删除非尾部元素时也会发生操作指定位置后面元素的移动。
arr5=['muxi', 'keqi', 'study', 'habits']
arr5.insert(2,"keep")
print("arr5:",arr5)
运行结果如下:
⑤、乘法扩展
概述:列表的乘法扩展类似于字符串的复制(注意:与+运算符添加元素一样,虽然*与*=的实现效果一样,但是它们的运行逻辑并不一样,*会创建新的列表对象并指向新的列表对象(id会发生改变),而*=不会创建新的列表对象(id不会改变))。
arr6=["muxi","keqi"]
print("乘法扩展前:",id(arr6))
# 与arr6*=6的区别是arr6=arr6*6创建了新的列表对象(id不一样)
arr6=arr6*6
print("乘法扩展后的arr6:",arr6)
print("乘法扩展后:",id(arr6))
运行结果如下:
3.1.4、删除列表元素
概述:删除元素的本质还是拷贝,例如列表arr包含100个元素,现在要删除第50个元素,那么从第51个元素开始到最后一个元素都会被拷贝到自身前一个元素的位置上,因此,被删除元素后面的元素越多,执行的效率就越低。
①、del()方法删除元素
概述:del()方法用于删除指定列表的指定元素。
实操:将arr列表中的第一个元素删除。
arr=["No","study","habit"]
del(arr[0])
print("执行完del()方法后:",arr)
运行结果如下:
②、pop()方法删除元素
概述:pop()方法用于删除并返回指定列表的指定下标元素,如果没有指定删除列表的下标,默认删除列表的最后一个元素。
# 通过列表推导式创建列表
arr=[i for i in range(1,100) if i%7==0]
print("创建好的列表:",arr)
# 删除第一个元素
arr.pop(0)
print("删除第一个元素:",arr)
# 删除最后一个元素
arr.pop()
print("删除最后一个元素:",arr)
运行结果如下:
③、remove()方法删除元素
概述:remove()方法用于删除指定列表的指定内容,当列表中存在多个元素的值与指定内容匹配时只会删除列表中第一个匹配的元素,如果列表中不存在指定的内容就会抛出异常。
arr=[int(i/2) for i in range(1,100) if i%7==0]
# 查看创建的列表
print(arr)
arr.remove(3)
print("删除存在的元素后:",arr)
# 删除不存在的元素
arr.remove(1)
运行结果如下:
由于上面的例子不能很好的演示删除带有多个相同值的列表情况,下面再补一个例子:
arr=[20,30,20,40]
arr.remove(20)
print(arr)
运行结果如下:
3.1.5、index(),count(),len()方法及成员资格判断
①、index()方法获取指定元素下标
定义:index()方法可以获取并返回指定内容在列表中首次出现的索引,并且能规定搜索的范围。
语法:列表名.index( "寻找的值" , [start,[end]] ) (start和end可有可无,根据自己的需要决定)
arr=[1,2,3,4,5]
arr*=3
print("arr列表:",arr)
# 通过index()查找2的索引
print("2的索引为:",arr.index(2))
# 通过index()指定范围并查找2
print("从索引4开始查找2的索引为:",arr.index(2,4))
运行结果如下:
②、count()方法获取指定元素的出现次数
概述:count()方法用于统计指定字符串在序列中出现的次数。
# 通过list()方法结合乘法扩展创建列表
arr=list("muxi")*4
print("创建的列表为:",arr)
print("m在列表中出现的次数是:",arr.count('m'))
运行结果如下:
③、len()方法
定义:len()方法用于获取列表长度(列表包含多少个元素)。
arr=list("keepStudy")*2
print("创建的arr内容:",arr)
print("arr的长度为:",len(arr))
运行结果如下:
④、成员资格判断
概述:当遇到判断列表中是否存在指定元素的需求时,可以通过上面学到的count()方法实现,也可以通过更加简洁的关键字in来实现。
# in表示存在,not in表示不存在
arr=list("learn")*2
print("列表arr:",arr)
print("'a' in arr?","a" in arr)
print("'l' not in arr?","l" not in arr)
运行结果如下:
3.1.6、Slice切片操作
列表的切片操作和字符串的切片写法一样(都遵循左闭右开原则)。
操作和说明 | 示例 | 结果 |
---|---|---|
[:] 提取整个列表 | [1,2,3][:] | [1,2,3] |
[start:] 从start索引开始到结尾 | [1,2,3][1:] | [2,3] |
[:end] 从头开始知道end-1 | [1,2,3][:2] | [1,2] |
[start:end] 从start到end-1 | [1,2,3,4,5][1:3] | [2,3] |
(start没有写默认就是从切片开始的地方开始(主要是区分步长是正还是负,是正数列表就从左往右进行切片,反之从右往左切片),end没写默认就是到最后一个元素(也要区分步长的正负,与start相同),step没写默认就是步长为1) | [1,2,3,4,5,6,7][:6:2] | [1, 3, 5] |
步长为正的巩固练习:
arr=list(range(1,10))
print(arr)
# 由于左闭右开原则,所以-3还要加1才是倒数第三个元素(真实开发中直接写-2即可,这里是方便大家理解)
print("arr从头开始到倒数第三个元素的内容为:",arr[:(-3+1)])
运行结果如下:
步长为负的巩固练习:
arr=list(range(1,10))
print(arr)
print("倒序输出列表中的奇数元素:",arr[-1::-2])
运行结果如下:
知识点补充:进行切片操作且步长为正时,起始偏移量和终止偏移量不在[0,字符串长度-1]
这个范围,也不会报错。起始偏移量小于0
则会当做0
,终止偏移量大于“长度-1”
会被当成”长度-1”。
步长为正且切片超出范围的实操:
arr=list(range(1,10))
print("创建的列表为:",arr)
print("当步长为正且切片超过列表范围时:",arr[-2000:999:])
运行结果如下:
步长为负且切片超出范围的实操:
arr=list(range(1,10))
print("创建的列表为:",arr)
print("当步长为负且切片超过列表范围时:",arr[100:-200:-1])
运行结果如下:
3.1.7、列表的遍历/排序/max,min,sum
①、列表的遍历
概述:列表的遍历通过for循环实现。
arr=list("keepStudy")
print("创建的列表arr内容如下:",arr)
print("通过for循环对arr进行遍历打印:")
for i in arr:
print(i,end="\t")
运行结果如下:
②、列表的排序
②.1、在原列表的基础上进行排序
概述:在原列表上实现正序、反序的功能主要是通过sort()方法实现的(sort()方法默认实现升序,如果需要降序只需要在sort()的括号中添加如下参数:reverse=True);如果需要乱序,需要使用random包中的shuffle()方法。
正反序的案例演示:
需要注意的是,不能直接将arr.sort()和random.shuffle()直接写在内置函数print()中,否则得到的结果就为None。
arr=[2,5,8,4,7,0,6]
print("列表创建后的id:",id(arr))
# 实现正序操作
arr.sort()
print("列表正序后的id:",id(arr))
# 测试过程中发现不能直接将arr.sort()直接写在print语句中
print("arr正序后的结果:",arr)
# 实现反序操作(只需添加reverse=True的参数即可)
arr.sort(reverse=True)
print("列表反序后的id:",id(arr))
print("arr反序后的结果:",arr)
运行结果如下:
乱序的案例演示:
import random
arr=[0, 2, 4, 5, 6, 7, 8]
random.shuffle(arr)
print("乱序后的arr为:",arr)
运行结果如下:
②.2、在新列表的基础上进行排序
概述:在新列表的基础上进行排序主要是通过内置函数sorted()实现,该方法会返回新的列表(所以可以直接将方法写在内置函数print()中),不会修改原列表内容。
arr=[8, 2, 6, 4, 0, 5, 7]
print("创建新的数组对象后升序排列的arr为:",sorted(arr))
print("创建新的数组对象后降序排列的arr为:",sorted(arr,reverse=True))
运行结果如下:
③、内置函数reversed()
概述:该内置函数支持进行逆序排列,与列表对象reverse()方法不同的是,内置函数reversed()不对原列表做任何修改,只返回一个逆序排列的迭代器对象。
arr=[8, 2, 6, 4, 0, 5, 7]
print(reversed(arr))
运行结果如下:
④、max(),min()和sum()
概述:max()用于查找列表中的最大值,min()用于查找列表中的最小值,sum()用于计算列表中元素的和。
arr=[8, 2, 6, 4, 0, 5, 7]
print("arr的最大值为:",max(arr))
print("arr的最小值为:",min(arr))
print("arr所有元素的和为:",sum(arr))
运行结果如下:
3.1.8、二维列表
概述:二维列表能够存储二维、表格的数据。
姓名 | 年龄 | 薪资 | 城市 |
---|---|---|---|
张三 | 21 | 3000 | 北京 |
李四 | 22 | 3500 | 上海 |
王五 | 23 | 4000 | 广州 |
通过如下程序存储上面表格的数据:
arr=[
["张三",21,3000,"北京"],
["李四",22,3500,"上海"],
["王五",23,4000,"广州"]
]
# 打印二维列表
print(arr)
# 打印指定元素"上海"
print("打印指定内容:",arr[1][3])
运行结果如下:
二维列表与for循环相结合的练习:该案例用到了for循环的嵌套和if判断语句,初学者可以照着敲一遍找找感觉,后面的章节还会细说。
arr=[
["张三",21,3000,"北京"],
["李四",22,3500,"上海"],
["王五",23,4000,"广州"]
]
for i in range(3):
for j in range(4):
print(arr[i][j],end="\t")
if j==3:
print("\n")
运行结果如下:
3.2、元组(tuple)
3.2.1、元组简介
概述:在前一个章节中有提到列表属于可变序列,允许修改列表中的元素,而本章节的元组属于不可变序列,不能修改元组中的元素,但元组和列表都属于序列,因此相比前一个章节本章节只需要学习创建元组,删除元组,元素访问和元素的计数方法即可。
元组支持的操作:索引访问,切片操作,连接操作,成员关系操作,比较运算操作和计数。
3.2.2、元组的创建
①、通过()创建元组
概述:通过该方式创建列表时可以省略小括号,如果元组只有一个元素,元素后面一定要加英文逗号。
tup1=(1,2,3)
print("tup1:",tup1)
tup2=1,2,3,4
print("tup2:",tup2)
tup3=1,
print("tup3:",tup3)
运行结果如下:
②、通过tuple()方法创建元组
tup1=tuple("muxikeqi")
print("tup1:",tup1)
tup2=tuple(range(7))
print("tup2:",tup2)
tup3=tuple(list(range(5)))
print("tup3:",tup3)
运行结果如下:
总结:tuple()可以接收列表,字符串及其它序列类型,迭代器等数据生成元组。
3.2.3、元组访问,计数,排序和类型转换
概述:元组的元素访问,index()方法,count()方法和切片操作都和列表一样。
tup=tuple("muxikeqi")
print("创建的元组:",tup)
# 打印元组tup的第一个元素
print("tup的第一个元素为:",tup[0])
# index()方法展示 (和列表一样,只返回第一个匹配到的索引)
print("获取元组中'i'的索引为:",tup.index("i"))
# count()方法展示
print("tup包含字符串'i'的次数为:",tup.count("i"))
# 元组切片展示(包括正/负步长)
print("tup去掉两端各一个元素后:",tup[1:-1])
print("倒叙并且每隔一个元素就打印tup:",tup[-1::-2])
打印结果如下:
但是,元组与列表的本质区别是元组不允许修改元素内容:
tup=tuple(range(10,0,-2))
print("元组tup的元素为:",tup)
tup[0]="元组的内容不允许修改"
运行结果如下:
当需要对元组元素排序时,在列表章节的学习中我们学习了在原列表的基础上排序和在新列表的基础上排序两种方式,结合我们多次提到元组不允许修改元素内容,因此,元组只支持在新列表的基础上排序,但尴尬的是元组没有sorted()方法,因此元组使用sorted()方法排序后的结果类型是列表。
tup=tuple("keep123789Study")
print("元组tup的元素为:",tup)
# tup[0]="元组的内容不允许修改"
print("tup使用sorted()后:",sorted(tup))
print("元组tup使用sorted()方法后的类型:",type(sorted(tup)))
print("tup通过sorted()方法后的内容为:",tup)
运行结果如下:
虽然通过sorted()方法将元组内容转换成了列表,我们也能将列表转换为元组,可通过zip(列表1,列表3,......)方法实现,但是使用zip()方法时,只会将多个列表对应位置的元素组合成为元组,但如果各个迭代器的元素个数不一致时,则返回列表长度最短的对象长度相同:
list1=[1,2,3]
list2=[4,5,6]
list3=[7,8,9,10]
print("执行zip()方法的结果",zip(list1,list2,list3))
print("将object类型的zip(list1,list2,list3)转换为tuple类型:",tuple(zip(list1,list2,list3)))
运行结果如下:
通过观察运行结果,可以发现将object类型的zip(list1,list2,list3)转换为tuple类型后会是一个二维元组。
zip(list1,list2,list3)不仅可以转换成元组,也可以转换成列表。
list1=[1,2,3]
list2=[4,5,6]
list3=[7,8,9,10]
print("将object类型的zip(list1,list2,list3)转换为tuple类型:",tuple(zip(list1,list2,list3)))
print("将object类型的zip(list1,list2,list3)转换为list类型:",list(zip(list1,list2,list3)))
运行结果如下:
3.2.4、生成器推导式创建元组
概述:在3.1.4章节中的pop()方法练习中利用了推导式创建了特定内容的列表,但元组的推导式创建元组与列表的推导式创建列表存在一定差异:
①、元组的生成器推导式创建在形式上与列表类似,但是元组的生成器推导式用小括号创建,而列表是用中括号。
②、生成器推导式生成的不是列表也不是元组,而是一个生成器对象。
③、可以参考上一章节的zip(),虽然zip()的结果是一个对象,但可以通过类型转换将对象转换成我们需要的列表或元组,因此可以将生成器对象转化成列表或者元组(但是只能转一次,如果转两次得到的结果将为空)。也可以使用生成器对象的__next__()
方法进行遍历,或者直接作为迭代器对象来使用。不管什么方式使用,元素访问结束后,如果需要重新访问其中的元素,必须重新创建该生成器对象。
实操1:主要是测试用生成器推导式创建元组后多次强制类型转换的结果。
tup1=(x for x in range(6))
print("通过推导式创建后的内容为:",tup1)
print("对tup1做第一次强制类型转换:",tuple(tup1))
print("对tup1再次做强制类型转换:",list(tup1))
print("对tup1再次做强制类型转换:",tuple(tup1))
运行结果如下:
通过上面的例子不难发现,通过生成器推导式创建元组时,除第一次强制类型转换外,后面的强制类型转换都为空(无论只转为列表还是元组的结果都一样)。
实操2:__next__()方法和上面的强制类型转换有相似之处(只能调用一次,但调用的次数超出了元组的范围时就会报错)
# (10, 12, 14, 16, 18)
tup3=(y for y in range(10,20,2))
# tup3只有5个元素,但我们这里调用了六次__next__()方法,观察查出tup3范围后的情况
print(tup3.__next__())
print(tup3.__next__())
print(tup3.__next__())
print(tup3.__next__())
print(tup3.__next__())
print(tup3.__next__())
运行结果如下:
实操3:在测试过程中发现__next__()方法和生成器推导式创建完成后做强制类型转换同时出现时只有写在前面的方法会得到预期的结果,因为这两个方法都是移动内存指向的指针实现,由于只能指向一次,所以写在前面的方法已经将指针移动了,后面的方法只能从前面方法结束时指针的位置开始操作:
# (5,7,9)
tup4=(x for x in range(5,10,2))
# 由于tup4有三个元素,所以这里先调用三次__next__()方法
print(tup4.__next__())
print(tup4.__next__())
print(tup4.__next__())
# 紧接着我们将tup4转换为元组和列表
tup5=tuple(tup4)
lis=list(tup4)
print("将tup4转换为tuple:",tup5)
print("将tup4转换为列表",lis)
运行结果如下:
元组总结:
①元组的核心特点是:不可变序列。
②元组的访问和处理速度比列表快。
③元组和整数,字符串可以作为字典的键,列表不能作为字典的键使用(因为列表允许修改,所以值是会变的,而字典的键要求不可变)。
3.3、字典
3.3.1、字典概述
概述:
①、字典是无序可变的键值对序列。
②、字典中每一对键值对就是一个元素。
③、每个键值对包含"键对象"和"值对象",键值对之间用逗号隔开。
④、可以通过键对象获取,修改,删除对应的值对象。
⑤、前面学习的列表,元组,字符串都是通过下标(索引)获取对应元素,而字典是通过键对象寻找对应的值对象。
⑥、不变的数据才有资格充当键对象,例如:整数,浮点数,字符串和元组;而列表,字典和集合这类可变序列就不能充当键对象。
⑦、键对象不能重复,而值对象可以是任意内容且可以重复出现,如果同名的键对象出现,只会使用最后一个同名的键值对。
3.3.2、字典的创建
创建方式一:通过{ }直接创建字典
dic={"name":"muxikeqi","telephone":110,tuple(range(1,4)):list(range(5,9))}
print("创建的字典内容为:",dic)
运行结果如下:
创建方式二:通过dict()方法创建字典
注意:①、在字符串创建的过程中,如果是在{ }中间创建,键值对中间用 ' : ' 连接,如果在dict()方法中且没有被{ }包裹,则键值对中间用 ' = ' 连接,且键对象为字符串时不需要用引号修饰。
②、如果要通过dict()方法创建键对象为元组,整数,浮点数类型的字典时,需要将键对象为元组,整数和浮点数的键值对用{ }包裹起来。
# 通过dict()方法创建键对象为字符串的字典
dict2=dict(name="muxikeqi",age=88,lis=list("12345"))
print("创建后的字典内容:",dict2)
# 通过dict()方法创建键对象有元组,整数,浮点数的情况
dc_key_tup=dict({tuple(range(5)):list("123"),1:(3,4,5),3.14:"Π"},name="muxi",age=88)
print("dc_key_tup的内容为:",dc_key_tup)
# 使用dict()方法包裹列表,列表包裹多个元组,每个元组就是一个键值对
dic=dict([("name","muxi"),("age",88),(tuple(range(5)),list(range(6,10)))])
print("dic的值为:",dic)
运行结果如下:
提示:在上面的例子中,在使用dict()方法的前提下,将键对象为元组,整数,浮点数的键值对用{}包裹了起来,其实键对象为字符串的键值对也可以放在{ }中被包裹,创建效果一样,上面的例子是为了区分哪些类型的键对象一定要用{ }包裹才不会报错。
创建方式三:通过zip()方法创建字典,该方法利用了3.2.3章节中学习的zip()方法简化了dict()方法通过列表包裹多个元组的方式创建字典。
k=["name",3.15,tuple(range(5))]
v=["muxi",88,3.14]
dic=dict(zip(k,v))
print(dic)
运行结果如下:
创建方式四:通过字典的fromkeys()方法创建值为空,只指定键对象的字典
dic=dict.fromkeys(["name","age",tuple(range(6))])
运行结果如下:
3.3.3、字典元素的访问
①、通过 [键] 访问字典中的值,如果键不存在就会报错。
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print("字典dic中name的值为:",dic["name"])
print("字典dic中age的值为:",dic["age"])
print("字典dic中telephone的值为:",dic["telephone"])
# 获取不存在的元素
print("sex的值为:",dic["sex"])
运行结果如下:
②、通过get()方法获得 "值" 。优点是指定键不存在时返回None;也可以设定指定键不存在时默认返回的对象。
格式:字典名.get[键,当键不存在时返回的内容] ,方括号中的键一定要有,键不存在时返回的内容可有可无,不写默认为None,写了默认返回你指定的内容。
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print("字典dic的age值为:",dic.get("age"))
print("查找dic中不存在的值且不指定返回值:",dic.get("sex"))
print("查找dic中不存在的值且指定返回值:",dic.get("sex","boy"))
运行结果如下:
③、通过items()方法获取字典所有的键值对。
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print(dic.items())
运行结果如下:
④、通过keys()方法和values()方法分别获取所有的键对象和所有的值对象。
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print("dic所有的键为:",dic.keys())
print("dic所有的值为:",dic.values())
运行结果如下:
⑤、len()方法获取字典元素的个数(每一对键值对就是一个元素)
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print("dic的元素个数为:",len(dic))
运行结果如下:
⑥、使用关键字in检测字典中是否存在指定键
dic=dict(name="muxikeqi",age=88.8,telephone=120)
print("检测name是否在字典dic中:","name" in dic)
print("检测不存在的元素是否在字典dic中:","不存在的元素" in dic)
运行结果如下:
3.3.4、字典的增删改
~~字典元素的更改~~
①、 字典名[键]=值 如果指定的键不存在,该键值对就会成为该字典的新元素;如果指定键存在,就会覆盖原有键对象的值。
dic=dict(name="muxikeqi",age=88.8,telephone=120)
# 为dic添加不存在的元素
dic["sex"]="man"
# 为dic添加已经存在的元素值
dic["age"]=77.7
print("修改后的dic内容为:",dic)
运行结果如下:
②、使用update()方法将新值的所有键值对添加到旧字典中,如果key值重复,则直接覆盖。
语法:字典名1 . update(字典名2)
注意:update()方法不要直接放在打印语句中,执行完update()方法后会返回一个None,所以打印完成后得到的结果为None,因此先执行玩update()方法,再在打印语句中打印字典名1即可。
dic1=dict(name="muxikeqi",age=88.8,telephone=120)
dic2=dict(sex="man",mail="123@gmail.com",telephone=666666)
# 执行update()方法
dic1.update(dic2)
# 打印update()后的dic1
print("将dic2的内容添加到dic1后:",dic1)
# 被添加的字典不会受影响
print("被添加的dic2:",dic2)
运行结果如下:
~~字典元素的删除~~
①、clear()删除所有元素
dic = dict(name="muxi",age=88,telephone=114)
print("调用clear()方法前:",dic)
dic.clear()
print("调用clear()方法后:",dic)
运行结果如下:
②、del()删除指定元素
dic = dict(name="muxi",age=88,telephone=114)
print("调用del()方法删除telephone键前:",dic)
del(dic["telephone"])
print("调用del()方法删除telephone键后:",dic)
运行结果如下:
③、通过pop()方法删除指定键值对,并且会返回对应的值对象
dic = dict(name="muxi",age=88,telephone=114)
print("创建后的dic包括如下元素:",dic)
age_muxi = dic.pop("age")
print("对dic调用pop()方法后:",dic)
print("被删除的指定元素值为:",age_muxi)
运行结果如下:
④、通过popitem()随机删除并返回对应键值对
dic = dict(name="muxi",age=88,telephone=114)
print("创建后的dic包括如下元素:",dic)
dic_popitem1=dic.popitem()
print("使用一次popitem()方法后:",dic)
print("随机删除的键值对为:",dic_popitem1)
dic_popitem2=dic.popitem()
print("使用两次popitem()方法后:",dic)
print("随机删除的键值对为:",dic_popitem2)
运行结果如下:
知识点补充:字典是“无序可变序列”,因此没有 第一个元素 或 最后一个元素 的概念;popitem 弹出随机的键值对,因为字典并没有"最后的元素"或者其他有关顺序的概念。若想一个接一个地移除并处理键值对,此方法非常有效(因为不用首先获取键的列表)。
3.3.5、序列解包
①、序列解包可以用于元组、列表、字典。序列解包可以让我们方便的对多个变量赋值。
x,y,z = (10,20,30)
(a,b,c) = (5,6,7)
[e,f,g] = [1,2,3]
print("x:",x,"\ty:",y,"\tz:",z)
print("a:",a,"\tb:",b,"\tc:",c)
print("e:",e,"\tf:",f,"\tg:",g)
运行结果如下:
②、序列解包用于字典时,默认是对“键”进行操作; 如果需要对键值对操作,则需要使用items();如果需要对“值”进行操作,则需要使用values();
# 对字典进行序列解包的案例
s = {'name':"muxikeqi","age":"999","tel":"123"} # 创建了一个字典
# 默认对键进行序列解包
a,b,c = s
print("a:",a," b:",b," c:",c)
# 对字典的值进行序列解包
val_a,val_b,val_c = s.values()
print("val_a:",val_a," val_b:",val_b," val_c:",val_c)
# 对字典的元素(键值对)进行序列解包
k_v_a, k_v_b, k_v_c = s.items() # 使用.items()返回的是元组
print("k_v_a:",k_v_a," k_v_b:",k_v_b," k_v_c:",k_v_c)
# 上面通过s.items将字典s的一个元素赋值给了k_v_a,可以通过下标分别获得键和值
print("k_v_a的第一个元素:",k_v_a[0],"\tk_v_a的第二个元素:",k_v_a[1])
运行结果如下:
注意:对字典进行序列解包时,被赋值的变量个数一定要小于或等于字典元素的个数(字典中每一对键值对就是一个元素),否则会报错。
3.3.6、字典&列表案例之存储/读取表格
概述:存储表格数据的思路非常简单,首先用字典将每一行的信息存储起来,再将所有的字典放进一个列表中即可,而数据的读取主要通过get("键名")获取,为了能高效获取多个键,下面的案例中配合了循环获取。
# 字典&列表案例之存储表格数据
# r1,r2,r3代表每一行的数据
r1 = {"name":"张三","age":"22","salary":"3000","city":"北京"}
r2 = {"name":"李四","age":"23","salary":"3500","city":"上海"}
r3 = {"name":"王五","age":"24","salary":"4000","city":"广州"}
# 将上面每一行的数据放进一个列表存储
table = [r1,r2,r3]
print("table:",table)
# 通过循环获取表中所有的姓名
print("打印表中所有的姓名:") # 这里的提示信息打印放在循环外是因为我们不需要多次打印这个提示信息
for i in range(len(table)):
print(table[i].get("name"))
# 通过循环打印表中的多列信息,不同列的信息之间用逗号连接
print("打印表中的多列信息:")
for j in range(len(table)):
print(table[j].get("name"),table[j].get("age"),table[j].get("city"),table[j].get("salary"))
运行结果如下:
3.3.7、字典内存分析
~字典存储键值对的过程~
概述:字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做bucket
。每个bucket
有两部分:一个是键对象的引用,一个是值对象的引用。
由于,所有bucket
结构和大小一致,我们可以通过偏移量来读取指定bucket
。
接下来我们分析一个简单的字典代码:
a = {}
a["name"]="muxikeqi"
假设字典a对象创建完后,内存为数组分配了长度为8的空间:
我们要把”name”=”muxikeqi”这个键值对放到字典对象a中,首先第一步需要计算键”name”的散列值。Python中可以通过hash()来计算。
print(bin(hash("name")))
由于数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即“011”,十进制是数字3。我们查看偏移量3,对应的bucket是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边3位作为偏移量,即“101”,十进制是数字5。再查看偏移量为5的bucket是否为空。直到找到为空的bucket将键值对放进去。(键对象存储的是变量a的对象地址,值对象存储的是"muxikeqi"的字符串地址)
扩容:由于字典对象的核心是散列表,而散列表是一个稀疏数组,所以python会根据散列表的拥挤程度扩容。“扩容”指的是:创造更大的数组,将原有内容拷贝到新数组中。一般散列表的空间被占用接近2/3时就会扩容。
~字典取键值对的过程~
对字典进行取值操作时通常使用get()方法实现。
a.get("name")
当调用a.get(“name”)时,就是根据键“name”查找到“键值对”,从而找到值对象"muxikeqi"。我们仍然要首先计算"name"对象的散列值:
bin(hash("name"))
和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。 假设数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即110
,十进制是数字6。我们查看偏移量6,对应的bucket
是否为空。如果为空,则返回None
。如果不为空,则将这个bucket
的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回None,具体流程如下:
提示:同一个键值无论存取都在相同的地址,但是每次运行后的hash地址是会发生变化的,演示如下:
a = {}
a["name"] = "张三"
print(bin(hash("name")))
a.get("name")
print(bin(hash("name")))
第一次运行结果如下:
第二次运行结果如下:
实操总结:通过运行截图能够清晰的发现多次运行后hash地址的变化和同一个键值无论存取都在相同的地址。
小结总结:
①、字典在内存中开销巨大,典型的空间换时间(字典对象的核心是散列表,必然造成空间浪费)。
②、键查询速度很快。
③、向字典里面添加新键值对可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字典的同时进行字典的修改
④、键必须是可散列的(不可变的)
4.1、数字、字符串、元组,都是可散列的
4.2、自定义对象需要满足下面三个条件(面向对象章节会补充上面没有说到的知识):
4.2.1、满足hash()函数
4.2.2、支持通过__eq__()
方法检测相等性
4.2.3、若a==b
为真,则hash(a)==hash(b)
也为真
3.4、集合
概述:集合是无序可变,元素不能重复。实际上,集合底层是字典实现,集合的所有元素都是字典中的“键对象”,因此是不能重复的且唯一的。
3.4.1、集合的创建和删除
①、用{ }创建集合对象,并使用add()方法添加元素
ass = {2,3,4,"keep",(1,2,3)}
print(ass)
ass.add("study")
print(ass)
运行结果如下:
②、使用set(),将列表、元组等可迭代对象转成集合。如果原来数据存在重复数据,则只保留一个
lis = [1,2,3,4,3,4]
print(set(lis))
运行结果如下:
③、remove()删除指定元素;clear()清空整个集合
a = {1,2,3,4,5}
a.remove(2)
print("通过remove()删除元素2后的集合a:",a)
a.clear()
print("通过clear()删除所有元素后的集合a:",a)
运行结果如下:
与数学中的集合概念一样,Python对集合也提供了并集、交集、差集等运算。示例如下:
a = {1,2,3,"a"}
b = {"a","b","c"}
print("a与b的并集为:",a|b)
print("a与b的并集为:",a.union(b))
print("a与b的交集为:",a&b)
print("a与b的交集为:",a.intersection(b))
print("a与b的差集为:",a-b)
print("a与b的差集为:",a.difference(b))
运行结果如下:
4、控制语句
概述:控制语句是把前面学习的语句组合成能完成一定功能的小逻辑模块。控制语句包括顺序,选择和循环。
顺序结构:哪条语句在前面就先执行哪条语句,然后再执行紧跟着的语句。
条件结构:如果满足条件就执行对应的语句,如果不满足条件就跳过对应的语句。
循环结构:首先判断是否满足条件,如果条件满足就重复执行对应的语句直到条件不满足。
4.1、选择结构
4.1.1、单分支选择结构
语法:
if 条件表达式:
语句/语句块
条件表达式:可以是逻辑表达式、关系表达式、算术表达式等等。
实操:用户通过键盘输入任意一个数字通过if单分支语句与数字9进行比较,比较完成后打印比较结果。
num = input("输入任意一个数字与9比较大小:")
# 这里对num做强制类型转换是因为input()语句会将键盘输入的值转换为String类型
if int(num) < 9:
print("您输入的数字是:",num)
print(num,"小于9")
if int(num) >= 9:
print("您输入的数字是:", num)
print(num,"大于或等于9")
运行结果如下:
※ 在选择和循环结构中,条件表达式的值为False
的情况如下:
False、0、0.0、空值None、空序列对象(空列表、空元组、空集合、空字典、空字符 串)、空range对象、空迭代对象。
其他情况,均为True
。因此,Python所有的合法表达式都可以看做条件表达式,甚至包括函数调用的表达式。
温馨提示:Python中,条件表达式不能出现赋值操作符 =
,避免了其他语言中经常误将关系运算符==
写作赋值运算符 =
带来的困扰。
4.1.2、双分支选择结构
①、if...else...结构
语法:
if 条件表达式:
语句1/语句块1
else:
语句2/语句块2
实操:用户通过键盘输入任意一个数字通过if双分支语句与数字9进行比较,比较完成后打印比较结果。
num = input("输入任意一个数字与9比较大小:")
if int(num) < 9:
print(num,"小于9")
else:
print(num,"大于或等于9")
运行结果如下:
②、三元运算符
语法:
条件为真时的值 if (条件表达式) else 条件为假时的值
实操:用户从键盘输入任何一个数字与10进行比较。
num = input("请输入一个数字")
# if后面的条件成立时结果为num,否则结果为else后的内容
print( num if int(num)<10 else "数字太大")
运行结果如下:
4.1.3、多分支选择结构
语法:
if 条件表达式1 :
语句1/语句块1
elif 条件表达式2:
语句2/语句块2
...
elif 条件表达式n :
语句n/语句块n
[else:
语句n+1/语句块n+1
]
提示:方括号中的else部分的语句为可选。
实操:输入一个学生成绩,将成绩进行等级转换,转换规则为:小于60分为不及格,60~79为及格,80~89为良好,90~100为优秀。
score = input("请输入成绩:")
if int(score)<60 :
grad="不及格"
elif 60<=int(score)<80:
grad="及格"
elif 80<=int(score)<90:
grad="良好"
else:
grad="优秀"
print("成绩:{0}\t等级为:{1}".format(score, grad))
运行结果如下:
注意:多分支结构按顺序进行判断,如果进入了其中一个分支,后面的分支就不会触发,例如,在上面的例子中,我们输入了66,满足了条件60<=int(score)<80,就不会再触发后面的else分支,所以多分支语句不能随意颠倒顺序。
4.1.4、选择结构的嵌套
概述:选择结构支持嵌套,需要注意缩进。
实操:输入一个学生成绩,要求输入的成绩范围为0~100,将成绩进行等级转换,转换规则为:小于60分为不及格,60~79为及格,80~89为良好,90~100为优秀。
score = input("请输入成绩:")
if 0<=int(score)<=100:
if int(score)<60 :
grad="不及格"
elif 60<=int(score)<80:
grad="及格"
elif 80<=int(score)<90:
grad="良好"
else:
grad="优秀"
print("成绩:{0}\t等级为:{1}".format(score,grad))
else:
print("~请输入0~100之间的数字~")
运行结果如下:
4.2、循环结构
概述:循环结构用来重复执行一条或多条语句。如果符合条件,则反复执行循环体里的语句。在每次执行完后都会判断一次条件是否为True,如果为True则重复执行循环体里的语句。
4.2.1、while()循环结构
语法:
while 条件表达式:
循环体语句
实操1:利用while循环打印0~10的数字。
# 给变量一个初始值
num = 0
while num<=10:
print(num,end="\t")
# 注意:python不支持num++的操作
num+=1
运行结果如下:
实操2:利用while循环计算1~100之和。
# num为加上的值 num = 1 # s为累加和 s = 0 while num <=100: # 对和做累加 s+=num # 通过循环对num做累加 num+=1 print("1~100之和为:",s)
运行结果如下:
4.2.2、for()循环
概述:for循环通常用于可迭代对象的遍历。
for 变量 in 可迭代对象:
循环体语句
Python包含以下几种可迭代对象:
①、序列。包含:字符串、列表、元组、字典、集合。
②、迭代器对象(iterator)
③、生成器函数(generator)
④、文件对象
温馨提示:for语句中的变量名是临时变量名。
实操1:通过for循环将字符串中的每个字母打印出来。
str_m = "keep study habit"
for i in str_m:
print(i,end="\t")
运行结果如下:
实操2:通过for语句遍历字典的键,值和键值对。
dic = dict(name="muxikeqi",age=88,tel=110)
print("dic:")
# for循环默认对键进行操作
for i in dic:
print(i,end=" ")
print("\n\ndic.keys():")
# 通过keys()方法指定对键进行操作
for i in dic.keys():
print(i,end=" ")
print("\n\ndic.values():")
# 通过values()方法指定对值进行操作
for i in dic.values():
print(i,end=" ")
print("\n\ndic.items():")
# 通过items()方法指定对键值对进行操作
for i in dic.items():
print(i,end=" ")
运行结果如下:
※ range对象
range对象是一个迭代器对象,用来产生指定范围的数字序列。语法如下:
range(start, end [,step])
range对象遵循左闭右开原则,若没有填写start,默认从0开始。step
是可选的步长,默认为1。
实操:计算1~100的 奇数和 与 偶数和。
sum_odd = 0
sum_eve = 0
for i in range(1,101,2):
sum_odd+=i
for j in range(2,101,2):
sum_eve+=j
print("1~100的奇数和为:"+str(sum_odd))
print("1~100的偶数和为:"+str(sum_eve))
运行结果如下:
4.2.3、循环嵌套
概述:一个循环体内可以嵌入另一个循环,一般称为“嵌套循环”,或者“多重循环”。
实操1:利用循环打印一个表格,表格格式为五行五列,每行的数字都相同,第一行从0开始,每行依次加1.
num = 0
for i in range(1,6):
for j in range(1,6):
print(num,end="\t")
print("\n")
num+=1
运行结果如下:
案例解析:学过其他语言的同学都知道,循环嵌套时,外层的循环每循环一次,内层的循环就要循环一整轮,利用这个特性,我们将外层的for循环控制每行的输出,内层的循环控制打印的列数,内层循环每执行完一轮就通过num+=1为我们打印的变量加一,使打印的内容每行都加一。
实操2:利用循环嵌套打印9*9乘法表。
案例分析:通过观察上面的9*9乘法表可只9*9乘法表由 被乘数*乘数=积 的格式组成的,并且每列的被乘数都一样,并且逐列递加,每行的乘数都一样,因此我们可以考虑外层循环控制乘数,内层循环控制被乘数做递加,由于被乘数和乘数都是有规律的递加,因此递加的任务可以交给range对象完成。
代码实现如下:
for i in range(1,10):
for j in range(1,i+1):
print("{}*{}={}".format(j,i,i*j),end="\t")
print("\n")
4.2.4、break语句
概述:break语句可用于while和for循环,用来结束整个循环。当有嵌套循环时,break语句只能跳出最近一层的循环。
实操:写一个死循环,当键盘输入q时就会退出死循环。
while 1:
get_user = input("请输入任意字符,当输入q时将退出循环程序:")
if get_user.upper() == "Q":
print("已退出循环!")
break
运行结果如下:
4.2.5、continue语句
概述:continue语句用于结束本次循环,继续下一次。多个循环嵌套时,continue也是应用于最近的一层循环。
实操:输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员工的数量和薪资明细,以及平均薪资。
# 用列表存储所有的工资
alary = []
# 储存员工数量
worker = 0
# 存储总工资
sum_all = 0
print("~欢迎进入工资录入系统,录入完成后按Q键退出系统即可~")
# 让录入能一直进行
while 1:
get_input_salary = input("请输入工资:")
# 录入完成就退出整个程序
if get_input_salary.upper() == "Q":
break
# 薪资小于0则重新输入
if float(get_input_salary)<=0.0:
continue
alary.append(float(get_input_salary))
worker+=1
print("员工总人数为:{}".format(worker))
print("员工薪资为:{}".format(alary))
# 循环用于对所有工资做累加
for i in range(len(alary)):
sum_all += alary[i]
print("公司平均薪资为:{}".format(sum_all/worker))
运行结果如下:
4.2.6、else语句
概述:while、for循环可以附带一个else语句(可选)。如果for、while语句没有被break语句结束,则会执行else子句,否则不执行。
语法:
while 条件表达式:
循环体
else:
语句块
或者:
for 变量 in 可迭代对象:
循环体
else:
语句块
实操:员工一共4人。录入这4位员工的薪资。全部录入后,打印提示“您已经全部录入4名员工的薪资”。
salary = []
print("~欢迎进入工资录入系统,录入完成后将自动退出系统~")
for i in range(4):
get_input_salary = input("请输入薪资:")
salary.append(float(get_input_salary))
else:
print("您已经录入所有员工的薪资~")
print("工资表{}".format(salary))
运行结果如下:
4.2.7、循环代码优化
编写循环时,遵守下面三个原则可以大大提高运行效率:
①、尽量减少循环内部不必要的计算;
②、嵌套循环中,尽量减少内层循环的计算,尽可能向外提;
③、局部变量查询较快,尽量使用局部变量;
附加优化手段:
①、连接多个字符串,使用join()而不使用+ (前面有解释过,用+连接多个字符串会创建新的字符串,而join()不会创建新的字符串)
②、列表进行元素插入和删除,尽量在列表尾部操作
4.3、zip()并行迭代
概述:通过zip()函数对多个序列进行并行迭代,zip()函数在最短序列“用完”时就会停止。
实操:测试zip()并行迭代
name = ["孙悟空","猪八戒","沙和尚","唐僧"]
age = [500,358,429]
tel = [110,120,119,122]
for zip_name,zip_age,zip_tel in zip(name,age,tel):
print("name:{}\tage:{}\ttel:{}".format(zip_name,zip_age,zip_tel))
运行结果如下:
4.4、推导式创建序列
概述:推导式是从一个或者多个迭代器快速创建序列的一种方法。它可以将循环和条件判断结合,从而避免冗长的代码。
4.4.1、列表推导式
语法:
[表达式 for item in 可迭代对象]
or
[表达式 for item in 可迭代对象 if 条件]
实操:结合上一小节学习的zip()并行迭代和列表推导式实现创建列表嵌套元组的数据结构并给该结构中填入任意数据。
cells = [(row,col) for row,col in zip(range(10),range(20,50))]
for cell in cells:
print(cell)
运行结果如下:
4.4.2、字典推导式
语法:
{key_expression: value_expression for 表达式 in 可迭代对象 [if 条件]}
提示:字典推导式类似于列表推导式,字典推导也可以增加if条件判断、多个for循环。
实操1:练习通过字典推导式创建一个字典。
city = ["北京","上海","广州","深圳"]
dic = {id:city for id,city in zip(range(4),city)}
print(dic)
运行结果如下:
实操2:利用字典推导式统计不同字符在字符串中出现的次数。
str_test = "muxikeqi please keep study habits!"
dic = ({s:str_test.count(s) for s in str_test})
print(dic)
运行结果如下:
提示:该例利用了字典的键对象不能重复的特性。
4.4.3、集合推导式
语法:
{表达式 for item in 可迭代对象 }
或者:{表达式 for item in 可迭代对象 if 条件判断}
实操:利用集合推导式创建一个集合。
ass = {k for k in range(100) if k%5==0}
print(ass)
运行结果如下:
4.4.4、生成器推导式(不直接生成元组)
概述:如果想通过生成器推导式生成元组需要通过 (表达式 for item in 可迭代对象)生成一个生成器对象,再通过循环才能拿到生成器对象中的元组数据,详细流程如下:
首先我们直接打印通过 (表达式 for item in 可迭代对象) 创建的生成器对象:
print((x for x in range(1,100) if x%9==0))
运行结果如下:
接下来我们将生成器对象用变量保存,再通过for循环将生成器对象中的数据取出来:
date = (x for x in range(1,100) if x%9==0)
for i in date:
print(i,end="\t")
运行结果如下:
注意:一个生成器只能运行一次。第一次迭代可以得到数据,第二次迭代发现数据已经没有了。
※ 我们在上面代码的基础上再写一个for循环遍历迭代器对象,结果如下:
date = (x for x in range(1,100) if x%9==0)
for i in date:
print(i,end="\t")
for i in date:
print(i,end="\t")
由结果可知,第二次遍历时并没有获取到迭代器数据。
5、函数和内存分析
概述:
①、函数是可重用的程序代码块。
②、函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。
提示:在编写函数时,函数体中的代码写法和我们前面讲述的基本一致,只是对代码实现了封装,并增加了函数调用、传递参数、返回计算结果等内容。
5.1、函数入门
概述:
①、一个程序由一个一个的任务组成;函数就是代表一个任务或者一个功能(function)。
②、函数是代码复用的通用机制。
Python函数分类:内置函数,标准库函数,第三方库函数和自定义函数。
内置函数:str(),list(), len() 等这些都是内置函数,我们可以拿来直接使用。
标准库函数:我们可以通过import
语句导入库,然后使用其中定义的函数。
第三方库函数:Python社区也提供了很多高质量的库。下载安装这些库后,也是通过import
语句导入,然后可以使用这些第三方库的函数。
自定义函数:用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。
语法:
def 函数名 ([参数列表]) :
函数体/若干语句
实操:定义一个函数,用于计算三个数的和,调用函数后会返回运算的结果。
def sum_def(a,b,c):
sum_return = a+b+c
# 通过return语句将结果返回给函数调用处
return sum_return
# 调用函数并直接打印结果
print("三个值之和为:{}".format(sum_def(1,2,3)))
运行结果如下:
总结:
1、使用def
来定义函数,然后就是一个空格和函数名称;Python执行def
时,会创建一个函数对象,并绑定到函数名变量上。
2、参数列表
①、圆括号内是形式参数列表,有多个参数则使用逗号隔开;
②、定义时的形式参数不需要声明类型,也不需要指定函数返回值类型;
③、调用时的实际参数必须与形参列表 一 一 对应。
3、return 返回值
①、如果函数体中包含return
语句,则结束函数执行并返回值;
②、如果函数体中不包含return
语句,则返回None
值。
4、调用函数之前,必须要先定义函数,即先创建函数对象再调用def
①、内置函数对象会自动创建;
②、标准库和第三方库函数,通过import
导入模块时,会执行模块中的def语句。
5.2、形式参数和实际参数
概述:
①、定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
②、调用时的实际参数必须与形参列表一一对应
实操:定义一个函数用于比较两个值的大小,返回较大的一个值。
def more_big(val1,val2):
# 利用三元运算符比较形式参数并将较大的值赋值给val
val = val1 if val1>val2 else val2
# 将val返回给函数调用处
return val
# 对num1和num2进行赋值
num1 = 4
num2 = 5
# 这里的函数调用用到的实参是变量,也可以直接填入常量,一定要注意与形参的个数对应
print("{}与{}进行比较,{}更大".format(num1,num2,more_big(num1,num2)))
运行结果如下:
说明:在定义more_big函数时括号中的参数就是形式参数,它们的作用就是接收函数调用时传入的参数并将这些参数拿到接收到参数的函数体内使用。在最后一行的打印语句中more_big(num1,num2)就是函数的调用,num1和num2就是实际参数,我们在倒数第而三行对num1和num2赋过值。
5.3、返回值
要点:
①、如果函数体中包含return
语句,则结束函数执行并返回值
②、如果函数体中不包含return
语句,则返回None
值
③、如果需要返回多个值,使用列表、元组、字典、集合将多个值“存起来”即可
实操:定义一个函数,要求返回值是一个列表。
def return_lis(n):
lis1 = "keep"*n
lis2 = "study"*n
lis3 = "habits"*n
return [lis1,lis2,lis3]
print("return_lis(n)函数的返回结果为{}".format(return_lis(3)))
运行结果如下:
5.4、函数内存分析
概述:在本文的第一节就有提到"Python中一切皆对象"的观点。实际上,执行def
定义函数后,系统就创建了相应的函数对象。
# 用zhengshu指向内置函数int
zhengshu = int
print(type(zhengshu("234")))
在上面的例子中我们用变量zhengshu指向内置函数int,使得变量zhengshu指向了内置函数的地址。
下面的例子效果也一样,接下来分析下面的例子:
def print_star(n):
print("*"*n)
print_star(3)
print("print_star函数的地址为{}".format(id(print_star)))
# 将abc指向print_star函数的地址
abc = print_star
abc(5)
print("abc的内存地址为{}".format(id(abc)))
运行结果如下:
上面的内存图为:
5.5、浅拷贝和深拷贝
浅拷贝(copy):拷贝对象,但不拷贝子对象的内容,只是拷贝子对象的引用。
深拷贝(deepcopy):拷贝对象,并且会连子对象的内存也全部(递归)拷贝一份,对子对象的修改不会影响源对象
实操:创建两个函数分别用于测试浅拷贝和深拷贝。
# 导入copy模块
import copy
def test_copy():
a = [1,2,[3,4]]
b = copy.copy(a)
print("a:{}".format(a))
print("b:{}".format(b))
b.append(5)
b[2].append(6)
print("浅拷贝:")
print("a:{}".format(a))
print("b:{}".format(b))
def test_deepCopy():
c = [1, 2, [3, 4]]
d = copy.deepcopy(c)
print("a:{}".format(c))
print("b:{}".format(d))
d.append(5)
d[2].append(6)
print("深拷贝:")
print("a:{}".format(c))
print("b:{}".format(d))
test_copy()
test_deepCopy()
运行结果如下:
浅拷贝内存图如下:
深拷贝内存图如下:
5.6、不可变对象含可变子对象
概述:传递不可变对象时。不可变对象里面包含的子对象是可变的。如果方法内修改了这个可变对象,源对象也会发生变化。
实操:定义一个不可变序列包含可变子序列的结构传递给任意一个变量,再定义一个函数,将先去定义的变量传入函数中,查看传入后的变量地址是否发生变化,再对传入变量的可变序列内容进行修改,打印修改后的内容和地址是否发生变化,调用完函数后再查看起初定义的变量内容是否发生变化。
# 定义一个不可变序列包含可变序列的结构并赋值给t_l
t_l = (1,2,[3,4])
# 打印t_l的id
print("t_l的id:{}".format(id(t_l)))
# 定义一个测试函数
def _t_l(s):
# 打印传入的s地址是否发生变化
print("s的id:{}".format(id(s)))
# 对s可变序列部分进行修改
s[2][0]="插入的信息"
# 打印修改后的s
print("s:{}".format(s))
print("s的id:{}".format(id(s)))
# 调用测试函数
_t_l(t_l)
# 测试函数执行后打印t_l的内容,观察内容是否会发生变化
print("t_l:{}".format(t_l))
运行结果如下:
上方程序的内存图如下:
5.7、可变参数
概述:可变参数指的是 " 可变数量的参数 "。分两种情况:
①、*param (一个星号),将多个参数收集到一个“元组”对象中。
②、**param (两个星号),将多个参数收集到一个“字典”对象中。
注意:可变参数一般都放在最后。
实操1:熟悉可变参数*param。
# 定义测试函数
def var_param(a,b,*c):
print(a,b,c)
# 调用测试函数
var_param(1,2,3,4,5,"牛逼","123")
运行结果如下:
实操2:熟悉可变参数**param。
# 定义测试函数
def var_param2(a,b,**c):
print(a,b,c)
# 调用函数
var_param2("a","b",name="muxikeqi",age=1024,salary=0.01)
运行结果如下:
5.8、lambda表达式和匿名函数
概述:
①、lambda表达式可以用来声明匿名函数。lambda函数是一种简单的、在同一行中定义函数的方法。lambda函数实际生成了一个函数对象。
②、lambda表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。
语法:
lambda arg1,arg2,arg3... : <表达式>
实操1:熟悉lambda表达式和匿名函数。
# 定义匿名函数
f = lambda a,b,c:a+b+c
# 调用匿名函数
print("执行匿名函数的结果为{}".format(f(1,2,3)))
运行结果如下:
实操2:lambda表达式和匿名函数进阶。
# 定义匿名函数(一个列表中包含三个函数对象)
f = [lambda a:a**2, lambda b:b**3, lambda c:c**3]
# 调用列表中的匿名函数(方括号是找下标,根据不同下标找不同的匿名函数,小括号是传入实参)
print(f[0](1),f[1](2),f[2](3))
运行结果如下:
5.9、eval()函数
功能:将字符串str当成有效的表达式来求值并返回计算结果。
语法:
eval(source[, globals[, locals]]) -> value
语法说明:source:一个Python表达式或函数compile()返回的代码对象。
globals:可选参数,必须是dictionary。
locals:可选参数,任意映射对象。
注意:eval()会将字符串当做语句来执行,因此存在被注入的安全隐患。比如:字符串中含有删除文件的语句。如果重要文件被删除,造成的损失是无法挽回的,因此,需要谨慎使用eval()!!!
实操:eval()入门案例。
s = "print('执行力eval()')"
eval(s)
运行结果如下:
5.10、递归函数
概述:递归(recursion)是一种常见的算法思路,在很多算法中都会用到。递归的基本思想就是“自己调用自己”。
递归函数指在函数体内部直接或间接的自己调用自己。每个递归函数必须包含两个部分:
终止条件:表示递归什么时候结束。一般用于返回值,不再调用自己。
递归步骤:把第n步的值和第n-1步相关联。
注意:递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力,因此需要尽量少使用。
实操1:为测试函数传入一个大于1的参数,如果参数大于1就拿自身减一并且做递归,如果参数等于1就结束递归。
def recur(n):
print("start:{}".format(n))
if n<=1:
print("~over~")
else:
# 进行递归操作
recur(n-1)
print("end:{}".format(n))
# 调用测试函数
recur(3)
运行结果如下:
结合上面的运行截图,内存图如下:
实操2:利用递归计算任意值的阶乘(例如5的阶乘:5!= 5*4*3*2*1)。
num = int(input("请输入要计算的阶乘:"))
def factorial(n):
if n==1:
return 1
else:
return n*factorial(n-1)
print("{}的阶乘为:{}".format(num,factorial(num)))
运行结果如下:
5.11、嵌套函数
概述:嵌套函数就是在函数内部定义的函数。
注意:嵌套函数无法在被嵌套的函数体外被调用,只能在被嵌套的函数体内被调用。
实操:练习入门嵌套函数。
def outer():
print("outer running")
def inner():
print("inner running")
# 只能在outer()函数体内调用inner()函数
inner()
# 无法在outer()函数体外调用inner()函数,如果调用会报错
# inner()
outer()
运行结果如下:
使用嵌套函数的场景:
①、封装 - 数据隐藏(外部无法访问“嵌套函数”)。
②、遵循DRY(Don’t Repeat Yourself) 原则。
③、避免出现重复代码。
④、闭包。
实操2:设计一个能正确输出中英文名的函数,函数内使用嵌套函数输出,通过条件判断选择参数的正确位置。
def printName(isEnglish,familyname,name):
# 嵌套函数
def innerPrint(a,b):
print("{} {}".format(a,b))
# 通过判断决定参数的位置
if isEnglish:
innerPrint(name,familyname)
else:
innerPrint(familyname,name)
printName(1,"Justin","Sofia")
运行结果如下:
5.12、nonlocal和global关键字
概述:nonlocal关键字用在内部函数中,声明外层的局部变量;global关键字用在函数内声明全局变量,然后再使用全局变量。
实操:通过下面的例子理解nonlocal和global关键字。
# 定义全局变量val_a
val_a = 1
def outer():
val_b = 2
def inner():
nonlocal val_b
print("进入内部函数后的val_b:{}".format(val_b))
# 对val_b进行修改
val_b = "被修改的val_b"
global val_a
val_a = "被修改的val_a"
# 调用内部函数
inner()
# 查看val_b的值是否有变化
print("调用内部函数后的val_b:{}".format(val_b))
# 调用外部函数
outer()
# 查看val_a是否有变化
print("调用外部函数后的val_a:{}".format(val_a))
运行结果如下:
5.13、LEGB规则
概述:Python在查找“名称”时,是按照LEGB规则查找的:local => Enclosed => Global => Built in
local:指函数或者类的方法内部。
Enclosed:指嵌套函数(一个函数包裹另一个函数,闭包)。
Global:指模块中的全局变量。
Build in:指Python为自己保留的特殊名称。
说明:如果某个name映射在局部local命名空间中没有找到,接下来就会在闭包作用域enclosed进行搜索,如果闭包作用域也没有找到,Python就会到全局global命名空间中进行查找,最后会在内建build in命名空间搜索 (如果一个名称在所有命名空间中都没有找到,就会产生一个NameError)
6、面向对象
6.1、面向对象简介
概述:Python完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。
①、面向对象(Object oriented Programming,OOP)编程的思想主要是针对大型软件设计而来的。
②、面向对象编程使程序的扩展性更强、可读性更好,使编程可以像搭积木一样简单。
③、面向对象编程将数据和操作数据相关的方法封装到对象中,组织代码和数据的方式更加接近人的思维,从而大大提高了编程的效率。
注:Python支持面向过程、面向对象、函数式编程等多种编程范式。
6.2、类的定义
概述:类可以看做是一个模版,系统根据类的定义来造出对象,对象是类的具体实体,一般称为“类的实例”。一个类包含属性和方法,当不同对象调用同一个类时,不同的对象保存各自的属性,但不同对象使用的方法都是同一个方法。
注意:Python中一切皆对象,因此类也叫类对象。
语法:
class 类名:
类体
类定义的要点:
①、类名必须符合“标识符”的规则;一般规定,首字母大写,多个单词使用“驼峰原则”。
②、类体中我们可以定义属性和方法
③、属性用来描述数据,方法(即函数)用来描述这些数据相关的操作
实操:类的创建及其属性方法的调用。
# 创建的类对象
class Student:
# 构造函数(self指当前指向类对象的对象)
def __init__(self,name,age,score):
self.name = name
self.age = age
self.score = score
# 定义类的方法
def printScore(self):
print("{}的分数为{}".format(self.name,self.score))
# 定义对象(默认调用构造方法)
s1 = Student("muxikeqi",1024,404)
# 查看是否为类对象的属性赋值成功
print(s1.name,s1.age,s1.score)
# 通过对象调用对应的方法
s1.printScore()
运行结果如下:
注:在前面的章节中我们有提到过Python对象分为三个部分:id,type 和 value。相较于其他类型的对象,类对象显著的区别在于它的value包含了很多内容(例如:构造方法中的属性和类对象中的方法)
6.3、构造方法 __init__和__new__方法
概述:对于初始化对象,我们需要定义构造函数__init__()
方法。构造方法用于执行“实例对象的初始化工作”,即对象创建后,初始化当前对象的相关属性,无返回值。
要点:
①、构造方法的名称固定为__init__
②、构造方法的第一个参数固定为self,self指的就是刚刚创建好的实例化对象。
③、构造方法一般用于初始化实例对象的实例属性,演示如下:
def __init__(self,name,age,score):
self.name = name
self. Age = age
④、通过 类名(参数列表) 调用构造函数,调用后,将创建好的对象返回给相应的变量,例如:
# 这里的s其实是对创建好的对象的引用
s = Student("muxikeqi",1024)
⑤、__new__方法用于创建对象,但我们一般无需重定义该方法。
⑥、如果我们在类对象中没有定义构造方法__init__,系统会提供一个默认的__init__方法,如果定义了一个带参数的__init__方法,系统将不再创建默认的__init__方法。
6.4、实例属性和实例方法
6.4.1、实例属性
概述:实例属性是从属于实例对象的属性,也称为“实例变量”。
要点:①、实例属性一般在构造方法__init__中通过 self.实例属性名=初始值 定义。
②、在同类的其他方法中通过 self.实例属性名 调用对象的实例属性。
③、创建实例对象后通过实例对象访问/修改属性值。
obj1 = 类名() #创建并初始化对象
obj1.实例属性名 = 值 #可以为已有属性赋值,也可以添加新的属性及其对应的值
6.4.2、实例方法
概述:是从属于实例对象的方法。
实例方法语法:
def 方法名(self [, 形参列表]):
函数体
调用实例方法语法:
对象.方法名([实参列表]) #不用填self对应的参数,因为self对应的就是对象
补充:
dir(对象) :获取对象的所有属性和方法。
对象.__dict__ :获取对象的属性字典。
pass :空语句。
isinstance(对象,类型) :判断对象是不是指定的类型。
6.5、类属性
概述:类属性是从属于“类对象”的属性,也称为“类变量”。由于,类属性从属于类对象,可以被所有实例对象共享。
语法:
class 类名:
类变量名= 初始值
实操:熟悉类属性的定义和调用,巩固构造方法和实例方法的书写。
class Student:
count = 0 # 类属性
# 用于初始化的构造方法
def __init__(self,name,age):
self.name = name;
self.age = age;
# 每调用一次构造方法就加一,即每创建一个对象就加一
Student.count +=1 # 通过类名.类属性名调用类属性
# 实例方法
def printAge(self):
print("{}现在{}岁".format(self.name,self.age))
# 创建对象
s1 = Student("muxikeqi","404")
s2 = Student("keep","1024")
# 通过对象调用类方法
s1.printAge()
# 查看Student类创建了几个对象
print("通过Student类创建了{}个对象".format(Student.count))
运行结果如下:
上述代码的内存图如下:
6.6、类方法和静态方法
6.6.1、类方法
概述:类方法是从属于“类对象”的方法。类方法通过装饰器@classmethod
来定义,格式如下:
@classmethod
def 类方法名(cls [,形参列表]) :
方法体
要点:①、@classmethod必须位于方法上面一行。
②、小括号中的cls必须有,cls指类对象本身。
③、调用类方法的格式:类名.类方法名(参数列表)(注意:在参数列表中不用指定cls,和self是一个道理。)
④、类方法中不能访问实例属性和实例方法。
⑤、子类方法继承父类方法时,传入的cls是子类对象而非父类对象。
实操:熟悉类方法的定义和调用。
# 定义类对象
class Student:
# 类属性
name = "muxikeqi"
# 类方法
@classmethod
def printName(cls):
print(cls.name)
# 调用类对象的类方法
Student.printName()
运行结果如下:
6.6.2、静态方法
概述:Python中允许定义与“类对象”无关的方法,称为“静态方法”。“静态方法”和在模块中定义普通函数没有区别,只不过“静态方法”放到了“类的名字空间里面”,需要通过“类名.方法名”调用。
语法:
@staticmethod
def 静态方法名([形参列表]) :
方法体
要点:①、@staticmethod必须位于静态方法的上一行。
②、调用静态方法的格式:类名.方法名(参数列表)
③、静态方法不能访问实例属性和实例方法。
实操:熟悉静态方法的定义和调用。
# 定义类对象
class Student:
# 定义静态方法
@staticmethod
def printInfo(name,age):
print("~调用了静态方法~")
print("{}:{}".format(name,age))
# 调用静态方法
Student.printInfo("muxikeqi",404)
运行结果如下:
6.7、__del__ (析构函数)和垃圾回收机制
概述:①、__del__称为"析构方法",用于实现对象被销毁时所需的操作。例如释放对象占用的资源。
②、Python实现自动的垃圾回收,当对象没有被引用时(引用计数为0),由垃圾回收器调用__del__ 。
③、Python支持通过__del__语句删除对象,从而保证调用了析构方法。
④、系统会自动提供__del__方法,一般不需要自定义析构方法。
实操:了解__del__函数。
# 定义类对象
class Student:
# 重写析构方法
def __del__(self):
print("销毁了如下对象:{}".format(self))
# 通过类对象创建两个实例对象
s1 = Student()
s2 = Student()
# 在程序结束之前手动删除实例对象s1
del s1
# 执行完程序后打印结束程序的提示
print("程序结束!")
运行结果如下:
6.8、__call__方法和可调用对象
概述:①、Python 中,凡是可以将 () 直接应用到自身并执行,都称为可调用对象。
②、可调用对象包括自定义的函数、Python 内置函数、以及本节所讲的实例对象__call__。
③、定义了__call__的对象,称为“可调用对象”,即该对象可以像函数一样被调用。
④、该方法使得实例对象可以像调用普通函数那样,以“对象名()”的形式使用。
实操:了解__call__方法的作用。
# 定义类对象
class Student:
# 空语句
pass
# 创建实例对象
s1 = Student()
# 通过实例对象()直接调用
s1()
运行结果如下:
由此可知,类对象中没有__call__方法的实例对象不能通过实例对象()的形式直接调用,因此,为类对象加了__call__方法。
# 定义类对象
class Student:
# 重写__call__方法
def __call__(self,name,age):
print("调用了__call__方法")
print("{}:{}".format(name,age))
# 创建实例对象
s1 = Student()
# 由于类对象中有__call__方法,因此可以通过实例对象()调用
s1("muxikeqi","404")
运行结果如下:
6.9、方法没有重载&方法的动态性
6.9.1、方法没有重载
概述:如果我们在类体中定义了多个重名的方法,只有最后一个方法有效。(建议不要使用重名的方法)
知识补充:①、在其他语言(比如:Java)中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含3个部分:方法名、参数数量、参数类型。
②、Python中,方法的的参数没有声明类型(调用时确定参数的类型),但参数的数量也可以由可变参数控制。因此,Python中是没有方法的重载的。
实操:通过案例了解Python的方法没有重载(写在后面的同名方法会直接将前面的同名方法直接覆盖,即使参数不一样)
class Student:
def printName(self, name):
print(name)
def printName(self):
pass
p1 = Student()
p1.printName("Muxikeqi")
运行结果如下:
案例解析:即使我们通过参数指定了我们想要调用的是前面的方法,但由于Python的方法没有重载,因此默认执行最后的同名方法,而上面的例子中最后的同名方法是不需要传递任何参数的,但我们为他传入了一个参数,因此报错。
6.9.2、方法的动态性
概述:Python是动态语言,我们可以动态的为类添加新的方法,或者动态的修改类的已有的方法。
实操:通过练习熟悉方法的动态性。
# 定义了一个类对象
class Student:
def study(self):
print("好好学习")
# 不要省略形参,因为类对象中的实例方法需要传递参数self,否则会报错
def relax(s):
print("好好放松")
# 不要省略形参,因为类对象中的实例方法需要传递参数self,否则会报错
def studyPls(s):
print("成谜学习")
# 无论是增加实例方法还是修改实例方法,赋值的都是函数对象而不是函数,因此需要省略小括号
# 为类对象新增实例方法
Student.relax = relax
# 修改类对象的实例方法
Student.study = studyPls
# 创建实例对象
s1 = Student()
# 测试修改的方法和新增的方法
s1.study()
s1.relax()
运行结果如下:
6.10、私有属性和私有方法
概述:Python对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。
要点:①、Python约定,两个下划线开头的属性是私有的(private)。其他为公共的(public)。
②、类内部可以访问私有属性(方法)
③、类外部不能直接访问私有属性(方法)
④、类外部可以通过_类名__私有属性(或方法)名
访问私有属性(或方法)
注意:方法本质上也是属性,只不过是可以通过()执行而已。
实操:通过实操理解上面的要点,首先测试私有类属性。
class Student:
# 定义了一个私有属性
# Python解释器解释私有属性实际上是将 __属性名 转换成了 _类名__属性名
__name = "muxikeqi"
print(Student.__name)
运行结果如下:
通过上面的例子可以看出无法通过类名.属性名的方式调用私有属性,修改如下:
class Student:
# 定义了一个私有属性
# Python解释器解释私有属性实际上是将__name转成了_类名__属性名
__name = "muxikeqi"
print(Student._Student__name)
运行结果如下:
接下来测试私有实例属性:
# 定义一个类对象
class Student:
# 定义构造方法
def __init__(self,name,age):
# 定义私有实例属性
self.__name = name
self.__age = age
# 定义测试私有属性的实例方法
def printInfo(self):
print("实例方法通过类名.属性名调用了私有实例属性")
print("姓名:{},年龄:{}".format(self.__name,self.__age))
print("----------------------------------")
# 创建实例对象
s1 = Student("muxikeqi",110)
s1.printInfo()
# 通过实例对象获取实例对象的私有属性
print("通过实例对象获取私有实例属性__name:{},__age:{}".format(s1._Student__name,s1._Student__age))
运行结果如下:
接下来测试私有方法的定义和调用:
class printInfo:
def __info(self):
print("执行了私有方法info")
s1 = printInfo()
# 调用私有方法与调用实例方法的区别就在于前面要加_类名,与私有属性的调用方法一样
s1._printInfo__info()
运行结果如下:
6.11、@property装饰器
概述:①、@property可以将一个方法的调用方式变成“属性调用”。
②、@property主要用于帮助我们处理属性的读操作、写操作。
③、对于某一属性我们可以通过 实例对象.属性名=值 的写法操作,但这种做法不安全,推荐使用装饰器@property进行读操作,通过@函数名.setter指定同名函数进行写操作。
注意:如果我们想利用@property控制读写操作,那么需要定义两个同名的函数,一个接收参数,另一个不接收参数。没有接收实际参数的函数用于控制读操作,函数的上一行用@property指定;接收实际参数的函数用于控制写操作,函数的上一行用@函数名.setter进行指定。
实操:熟悉@property装饰器的操作。
class Student:
# 定义构造函数
def __init__(self,name,age):
self.name = name
# 这里将age设置为了私有属性,不允许外部直接修改
self.__age = age
# 指定@property的函数在调用时可省略小括号,但不允许修改其中的值
@property
# 该函数控制年龄的获取
def age(self):
print("年龄为{}".format(self.__age))
return self.__age
# 指定@函数名.setter的函数在调用时可省略小括号,允许修改其中的值
@age.setter
# 结合判断对年龄进行修改
def age(self,in_age):
if 18<in_age<60:
self.__age = in_age
print("年龄为{}".format(self.__age))
else:
print("请输入18~60的年龄!")
s1 = Student("s1",20)
s2 = Student("s2",30)
s3 = Student("s3",1024)
# 没有传入参数,说明调用的是指定了@property的函数
s3.age
# 等号后面的50就是传入的参数
# 语义解析:由于传入了50,所以找到名为age且带一个形参的函数,由于我们将实参修改为了50,符合函数中if的判断条件,因此进入了if的分支
s3.age = 50
# 再次测试年龄不在18~60的值
s1.age = 200
运行结果如下:
案例总结:①、@property与@函数名.setter组合使用,分别控制读写操作。
②、无论是被@property指定还是被@函数名.setter指定,调用对应函数时都可以像调用属性一样不写小括号。
③、控制读操作的函数和控制写操作的函数需要同名,两个函数的区别在于参数的接收。
6.12、属性和方法命名的总结
_xxx : 保护成员,不能用 from module import * 导入,只有类对象和子类对象能访问这些成员。
__xxx__ : 系统定义的特殊成员。
__xxx : 类中的私有成员,只有类对象自己能访问,子类对象也不能访问。(由于python没有严格的意义的私有成员,因此可以在类外部通过对象名._类名__xxx进行调用)
类编码风格:
①、类名首字母大写,多个单词之间采用驼峰原则。
②、实例名、模块名采用小写,多个单词之间采用下划线隔开。
③、每个类,应紧跟“文档字符串”,说明这个类的作用。
④、可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类。
6.12、None对象
概述:①、None是一个特殊的常量,表示变量没有指向任何对象。
②、JAVA中用null表示空值,而Python表示空值。、
③、Python万物皆对象,因此None也是一个对象,有属于自己的类型:NoneType。
④、可以将None赋值给任意变量,但不能创建任意None类型的对象。
注意:None不是False,None不是0,None不是空字符串。None和任何其他的数据类型比较永远返回False。
实操1:将None与0和False进行比较。
a = None
if a==0 or a==False:
print("None 与 0 or False 相等")
else:
print("None 与 0 or False 不相等")
运行结果如下:
实操2:用if进行判断时,空列表[ ]、空字典{ }、空元组( )、0等一系列代表空和无的对象会被转换成False。
a=[];
b=();
c={};
d="";
e=0;
f=None
if (not a) and (not b) and (not c) and (not d) and (not e) and (not f):
print("if判断时,空列表[]、空字符串、0、None等代表空和无的对象会被转换成False")
运行结果如下:
实操3:用 == 和 is 进行判断时,空列表、空元组、空字符串,空字典不会自动转成False,而0会自动转换成False。
a=[];
b=();
c={};
d="";
e=0;
if (a==False or b==False or c==False or d==False or a is False or b is False or c is False or d is False):
print("==时,空列表、空字符串不是False!") #不会执行
if(e==False):
print("==时,0会转成False")
# is操作符用于检查对象的身份而不是值,False和0是不同的对象,它们在内存中的地址不一样
if(e is False):
print("用is判断时,0会转成False")
运行结果如下:
6.13、面向对象的三大特征
概述:Python是面向对象的语言,支持面向对象编程的三大特性:继承、封装(隐藏)、多态。
封装:封装就是隐藏对象的属性和实现细节,只对外提供必要的方法。相当于将“细节封装起来”,只对外暴露“相关调用方法”。
通过前面学习的“私有属性、私有方法”的方式,实现“封装”。Python追求简洁的语法,没有严格的语法级别的“访问控制符”,更多的是依靠程序员自觉实现。
继承:继承可以让子类具有父类的特性,提高了代码的重用性。从设计上是一种增量进化,原有父类设计不变的情况下,可以增加新的功能,或者改进已有的算法。
多态:多态是指同一个方法调用由于对象不同会产生不同的行为。生活中这样的例子比比皆是:同样是休息方法,人不同休息方法不同。张三休息是睡觉,李四休息是玩游戏,程序员休息是“敲几行代码”。
6.13.1、构造方法的继承
概述:①、继承是面向对象编程的三大特征之一。继承让我们更加容易实现类的扩展。实现代码的重用,不用再重新发明轮子;
②、如果一个新类继承自一个设计好的类,就直接具备了已有类的特征,就大大降低了工作难度。已有的类,我们称为“父类或者基类”,新的类,我们称为“子类或者派生类”。
语法(Python支持多重继承,一个子类可以继承多个父类):
class 子类类名(父类1[,父类2,...]):
类体
注意:如果在类定义中没有指定父类(前面学习的关于对象的例子都是),则默认父类是object类
。也就是说,object是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__() 。
子类构造方法的情况汇总:
①、如果子类没有定义构造方法,子类会自动调用父类的构造方法。
②、如果子类定义了构造方法,子类就会使用自己定义的构造方法。
③、如果子类定义了构造方法,并且想继承父类的构造方法,可以使用super关键字,也可以使用父类名.init(self,参数列表)调用。
实操1:通过案例了解子类没有自己定义构造方法的情况。
# 定义父类Person
class Person:
def __init__(self,name,age):
print("创建了Person类")
self.name = name
self.age = age
def print_info(self):
print("{}的年龄为{}。".format(self.name,self.age))
# 定义子类Child,继承父类Person的属性和方法
class Child(Person):
# 子类的内容为空语句,没有任何实际的内容
pass
# 创建子类Child的实例对象
c = Child("muxikeqi",22)
# 通过实例对象c调用父类的实例方法
c.print_info()
运行结果如下:
实操2:通过案例了解子类自己定义构造方法的情况(只修改了子类的内容)。
# 定义父类Person
class Person:
def __init__(self,name,age):
print("创建了Person类")
self.name = name
self.age = age
def print_info(self):
print("{}的年龄为{}。".format(self.name,self.age))
# 定义子类Child,继承父类Person的属性和方法
class Child(Person):
# 子类的内容为空语句,没有任何实际的内容
def __init__(self,name,age,city):
print("创建了Child类")
self.name = name
self.age = age
self.city = city
# 创建子类Child的实例对象
c = Child("child_muxikeqi",22,"北京")
# 通过实例对象c调用父类的实例方法
c.print_info()
运行结果如下:
实操3:通过案例了解子类创建了构造方法并继承父类的继承方法的情况。(在前一个案例的基础上修改子类构造方法即可)
# 定义父类Person
class Person:
def __init__(self,name,age):
print("创建了Person类")
self.name = name
self.age = age
def print_info(self):
print("{}的年龄为{}。".format(self.name,self.age))
# 定义子类Child,继承父类Person的属性和方法
class Child(Person):
# 子类的内容为空语句,没有任何实际的内容
def __init__(self,name,age,city):
# 方法一:通过super继承父类的构造方法
super(Child,self).__init__(name,age)
# 方法二:通过父类名.__init__(self,参数列表)方法继承父类的构造方法
# Person.__init__(self,name,age)
print("创建了Child类")
self.city = city
# 创建子类Child的实例对象
c = Child("child_muxikeqi",22,"北京")
# 通过实例对象c调用父类的实例方法
c.print_info()
运行结果如下:
实操总结:当子类自己定义了构造方法时,有两种方式可以调用父类的构造方法,一是super(),二是父类名.init(self,参数列表)方法,案例3中将两种方式都进行了演示。
6.13.2、类成员的继承和重写
成员继承的概述:子类除构造方法之外,所有的一切都继承(包括私有属性和私有方法)。
方法重写的概述:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为“重写”。
实操1:通过案例了解子类继承父类的私有属性。
# 定义父类Person
class Person:
def __init__(self,name,age):
print("创建了Person类")
# 父类的属性都为私有属性
self.__name = name
self.__age = age
def print_info(self):
print("{}的年龄为{}。".format(self.__name,self.__age))
# 定义子类Child,继承父类Person的属性和方法
class Child(Person):
# 子类的内容为空语句,没有任何实际的内容
def __init__(self,name,age,city):
print("创建了Child类")
# 方法一:通过super继承父类的构造方法
# super(Child,self).__init__(name,age)
# 方法二:通过父类名.__init__(self,参数列表)方法继承父类的构造方法
Person.__init__(self,name,age)
self. City = city
# 创建子类Child的实例对象
c = Child("child_muxikeqi",22,"北京")
# 通过实例对象c调用父类的实例方法
c.print_info()
运行结果如下:
实操2:通过案例了解方法的重写。
# 创建父类Person
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
def get_info(self):
print("父类的get_info方法")
# 创建子类Child
class Child(Person):
# 重写父类中的get_info方法
def get_info(self):
print("我是子类重写后的get_info方法")
# 创建实例对象c1
c1 = Child("muxikeqi",18)
# 调用c1的get_info方法
c1.get_info()
运行结果如下:
查看类的层次继承结构:通过类的方法mro()或类的属性__mro__就可以输出这个类的继承层次结构。
案例3:通过实例了解类方法mro()的用法。
class A:pass
class B(A):pass
class C(B):pass
print(C.mro())
运行结果如下:
注意:__main__.C表示执行当前代码的模块。
6.13.3、object根类
概述:object类是所有类的父类,因此所有的类都有object类的属性和方法。
知识补充:为了深入学习对象,先学习内置函数dir(),他可以让我们方便的看到指定对象所有的属性。
实操:通过案例学习dir()的使用。
# 创建父类Person
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
def get_info(self):
pass
# 创建object()根对象的实例并查看其所有属性
obj = object()
print(dir(obj))
# 创建Person的实例p并查看其所有属性
p = Person("muxikeqi",88)
print(dir(obj))
# 验证方法的本质是属性
print(p.get_info)
print(type(p.get_info))
运行结果如下:
总结:①、Person类作为object的子类,因此Person类拥有object类的所有属性。
②、例子中的实例方法get_info虽然是方法,实际上也是属性(因为我们通过调用属性的方式调用了get_info方法)
③、在Pycharm中通过alt+7可以发现Person类自身拥有四个属性、方法:__init__(self,name,age),get_info(self),age,name。
6.13.4、重写__str__()方法
概述:①、object有一个__str__()方法,用于返回一个对于“对象的描述”。可用于强制类型转换的内置函数str(对象),实际调用的就是__str__() 。
②、print()方法实际上也是调用__str__()方法,帮助我们查看对象信息,__str__()也可用于去重。
实操:通过该案例理解__str__()方法。
# 创建类对象
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
# 重写__str__()方法
def __str__(self):
print("调用了重写后的__str__()方法")
return "姓名:{},年龄:{}".format(self.name,self.age)
# 创建实例对象p
p =Person("沐曦可期",88)
print(p)
str(p)
运行结果如下:
结果分析:在倒数第二行的pring(p)中,由于print()方法实际调用的就是__str__()方法,因此调用了我们重写后的__str__()方法,打印了第一行的文本;由于我们重写__str__()方法时将"姓名:{},年龄:{}".format(self.name,self.age)返回给了实例对象p,因此执行print(p)时打印了结果的第二行文本;由于用于强制类型转换的内置函数str()实际调用的也是__str__()方法,所以str(p)时也调用了我们重写后的__str__()方法。
6.13.5、多重继承
概述:Python支持多重继承,一个子类可以有多个“直接父类”。这样,就具备了“多个父类”的特点。但是由于,这样会被“类的整体层次”搞得异常复杂,所以尽量避免使用。
实操:通过实例了解多重继承。
class A:
def a(self):
print("a")
class B:
def b(self):
print("b")
class C(A,B):
def c(self):
print("c")
c1 = C()
c1.a()
c1.b()
c1.c()
运行结果如下;
案例分析:该案例的继承层次结构为C同时继承A和B,而A和B的父类都为objecet。
6.13.6、MRO方法解析顺序
概述:①、Python支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将“从左向右”按顺序搜索。
②、MRO(Method Resolution Order):方法解析顺序。 我们可以通过mro()方法获得“类的层次结构”,方法解析顺序也是按照这个“类的层次结构”寻找的。
实操:通过案例了解MRO方法解析顺序。
class AAA:
def a(self):
print("a")
def say(self):
print("say A!")
class BBB:
def b(self):
print("b")
def say(self):
print("say B!")
class CCC(AAA,BBB):
def c(self):
print("cc")
c = CCC()
print(CCC.mro()) #打印类的层次结构
c.say() #解释器寻找方法是“从左到右”的方式寻找,此时会执行A类中的say()
运行结果如下:
6.13.7、super()获得父类定义
概述:在子类中,如果想要获得父类的方法时,我们可以通过super()来做。super()代表父类的定义,不是父类对象。
super()语法:
# 调用构造方法
super(子类名称,self).__init__(参数列表) #参数列表不包括self,前面已经传入过self,否则会报错
# 调用父类中的实例方法
super().方法名()
实操:通过案例学习如何利用super()获取父类的属性和方法。
# 定义父类
class Teacher:
def __init__(self,name):
self.__name = name
def print_info(self):
print("调用了父类的print_info方法\n姓名:{}".format(self.__name))
# 定义子类
class Student(Teacher):
def __init__(self,name,age):
super(Student,self).__init__(name)
self.age = age
# 重写父类的print_info方法
def print_info(self):
# 这里使用Teacher.print_into(self)的效果是一样的
# 获取父类中print_info方法中的内容
super().print_info()
# 由于父类中name为私有属性,所以调用时要通过_父类名__属性名调用
print("调用了子类中的print_info方法\n姓名:{},年龄:{}".format(self._Teacher__name,self.age))
# 创建实例对象s1
s1 = Student("沐曦可期",1024)
# 通过实例对象s1调用print_info方法
s1.print_info()
运行结果如下:
6.13.8、多态
概述:多态(polymorphism)是指同一个方法调用由于对象不同可能会产生不同的行为。
注意:①、多态是方法的多态,属性没有多态。
②、多态存在的两个必要条件:继承和方法重写。
实操:通过实操了解多态。
#多态
class Animal:
def shout(self):
print("动物叫了一声")
class Dog(Animal):
def shout(self):
print("汪汪汪")
class Cat(Animal):
def shout(self):
print("喵喵喵")
def animalShout(a):
a.shout() #传入的对象不同,shout方法对应的实际行为也不同。
animalShout(Dog())
animalShout(Cat())
运行结果如下:
6.13.9、特殊方法和运算符重载
常见特殊方法:
方法 | 说明 | 例子 |
---|---|---|
__init__ | 构造方法 | 对象创建和初始化:p = Person() |
__del__ | 析构方法 | 对象回收 |
__repr__ ,__str__ | 打印,转换 | print(a) |
__call__ | 函数调用 | a() |
__getattr__ | 点号运算 | a.xxx |
__setattr__ | 属性赋值 | a.xxx = value |
__getitem__ | 索引运算 | a[key] |
__setitem__ | 索引赋值 | a[key]=value |
__len__ | 长度 | len(a) |
运算符实际对应的方法:
运算符 | 特殊方法 | 说明 |
---|---|---|
+ | __add__ | 加法 |
- | __sub__ | 减法 |
< <= == | __lt__ __le__ __eq__ | 比较运算符 |
> >= != | __gt__ __ge__ __ne__ | 比较运算符 |
| ^ & | __or__ __xor__ __and__ | 或、异或、与 |
<< >> | __lshift__ __rshift__ | 左移、右移 |
* / % // | __mul__ __truediv__ __mod__ __floordiv__ | 乘、浮点除、模运算(取余)、整数除 |
** | __pow__ | 指数运算 |
6.13.10、特殊属性
概述:Python对象中包含了很多双下划线开始和结束的属性,这些是特殊属性,有特殊用法。
特殊属性 | 含义 |
---|---|
obj.__dict__ | 对象的属性字典 |
obj.__class__ | 对象所属的类 |
class.__bases__ | 表示类的父类(多继承时,多个父类放到一个元组中) |
class.__base __ | 类的父类 |
class.__mro __ | 类层次结构 |
class.__subclasses__() | 子类列表 |
实操:通过案例了解特殊属性。
class A:
pass
class B:
pass
class C_class(B,A):
def __init__(self,n):
self.n = n
def cc(self):
print("Cccc")
c = C_class(3)
print("c.__dict__:",c.__dict__)
print("c.__class__:",c.__class__)
print("C_class.__bases__:",C_class.__bases__) # 打印父类
print("C_class.mro():",C_class.mro()) # 打印整个层次结构
print("A.__subclasses__():",A.__subclasses__())
运行结果如下:
6.13.11、对象的浅拷贝和深拷贝
浅拷贝:拷贝时,拷贝源对象,但对象包含的子对象内容不拷贝。
深拷贝:拷贝时,拷贝源对象,也递归拷贝对象中包含的子对象
提示:①、Python拷贝一般都是浅拷贝。
②、深拷贝需要使用copy模块的deepcopy函数,递归拷贝对象中包含的子对象。
实操:通过实操了解对象的浅拷贝和深拷贝。
import copy
class MobilePhone:
def __init__(self,cpu):
self.cpu = cpu
class cpu_class:
pass
# 创建cpu_class类的实例对象c
c = cpu_class()
# 创建MobilePhone类的实例对象,并将实例对象c作为参数传入进去
m = MobilePhone(c)
print("----浅拷贝-------")
m2 = copy.copy(m) # 对实例对象m进行浅拷贝
print("m:",id(m))
print("m2:",id(m2))
print("m的cpu:",id(m.cpu))
print("m2的cpu:",id(m2.cpu)) # m2和m都指向同一个属性
print("----深拷贝--------")
m3 = copy.deepcopy(m) # 对实例对象m进行深拷贝
print("m:",id(m))
print("m3:",id(m3))
print("m的cpu:",id(m.cpu))
print("m3的cpu:",id(m3.cpu)) # m和m3并没有指向同一属性
运行结果如下:
6.13.12、组合
概述:除了继承,“组合”也能实现代码的复用!“组合”核心是“将父类对象作为子类的属性”。
实操:通过案例了解组合的用法。
class screen:
def show(self):
print("显示画面中...")
class machine:
def run(self):
print("主机运行中...")
class computer:
def __init__(self,machine,screen):
self.machine = machine
self.screen = screen
# 创建实例对象
s = screen()
m = machine()
# 创建实例对象c时将上面创建的两个实例对象作为参数传入
c = computer(m,s)
c.screen.show()
c.machine.run()
运行结果如下:
总结:①、is-a关系,我们可以使用“继承”。从而实现子类拥有的父类的方法和属性。is-a关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。
②、has-a关系,我们可以使用“组合”,也能实现一个类拥有另一个类的方法和属性。has-a
关系指的是这样的关系:手机拥有CPU。 MobilePhone has a CPU
6.14、设计模式
6.14.1、工厂模式
概述:工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。
实操:通过实操了解工程模式。
class Banana:pass
class Apple:pass
class Pear:pass
class FruitFactory:
def creat_Fruit(self,name):
if name=="banana":
return Banana()
elif name=="apple":
return Apple()
elif name=="pear":
return Pear()
else:
return "没有收录的水果,无法创建"
# 创建工厂的实例对象
factory = FruitFactory()
# 通过工厂的实例对象再创建对应的水果对象
f1 = factory.creat_Fruit("banana")
f2 = factory.creat_Fruit("apple")
# 打印水果对象的信息
print(f1)
print(f2)
运行结果如下:
6.14.2、单例模式
概述:①、单例模式(Singleton Pattern)的核心作用是确保一个类只有一个实例,并且提供一个访问该实例的全局访问点。
②、单例模式只生成一个实例对象,减少了对系统资源的开销。当一个对象的产生需要比较多的资源,如读取配置文件、产生其他依赖对象时,可以产生一个“单例对象”,然后永久驻留内存中,从而极大的降低开销。
提示:单例模式有多种实现的方式,这里推荐重写__new__()方法。
实操:通过案例了解单例模式。
class MySingleton:
# 定义类属性
__obj = None
__init_flag = True
# 重写__new__()方法,参数中的cls表示类本身
def __new__(cls, *args, **kwargs):
# 如果没有创建对象就会进入该分支
if cls.__obj == None:
# 如果没有创建实例对象,就通过object.__new__()方法创建一个新的实例
cls.__obj = object.__new__(cls)
# 返回唯一的实例
return cls.__obj
# 构造方法
def __init__(self):
# 由于类属性__init_flag的初始值为True,所以第一次会进入构造方法的分支
if MySingleton.__init_flag:
print("初始化了第一个对象")
# 这一步是单例模式的关键,只有首次能进入
MySingleton.__init_flag = False
# 验证单例模式
# 创建第一个对象
m1 = MySingleton()
print(m1)
# 创建第二个对象
m2 = MySingleton()
print(m2)
运行结果如下:
6.14.3、工厂模式和单例模式相结合
概述:设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现。
实操:通过案例了解工厂模式和单例模式的结合。
class Banana:pass
class Apple:pass
class Pear:pass
class FruitFactory:
# 定义类属性
__obj = None
__init_flag = True
# 重写__new__()方法,参数中的cls表示类本身
def __new__(cls, *args, **kwargs):
# 如果没有创建对象就会进入该分支
if cls.__obj == None:
# 如果没有创建实例对象,就通过object.__new__()方法创建一个新的实例
cls.__obj = object.__new__(cls)
# 返回唯一的实例
return cls.__obj
# 构造方法
def __init__(self):
# 由于类属性__init_flag的初始值为True,所以第一次会进入构造方法的分支
if FruitFactory.__init_flag:
# 这一步是单例模式的关键,只有首次能进入
FruitFactory.__init_flag = False
def creat_Fruit(self,name):
if name=="banana":
return Banana()
elif name=="apple":
return Apple()
elif name=="pear":
return Pear()
else:
return "没有收录的水果,无法创建"
# 创建工厂的实例对象
factory1 = FruitFactory()
print(factory1)
# 再次创建一个工厂的实例对象
factory2 = FruitFactory()
print(factory2)
运行结果如下: