Python入门
编程基本概念
Python程序的构成
- Python程序由模块组成。一个模块对应python源文件,一般后缀名是:.py
- 模块由语句组成。运行Python程序时,按照模块中语句的顺序依次执行
- 语句是Python程序的构造单元,用于创建对象、变量赋值、调用函数、控制语句等
代码的组织和缩进
很多编程语言通过字符(例如:花括号{})、关键字(例如:begain/end)来划分代码块。同时,在配合代码的缩进增加可读性。“龟叔”设计Python语言时,直接通过缩进来组织代码块。“缩进”成为了Python语法强制的规定。缩进时,几个空格都是允许的,但是数目必须统一。通常采用“四个空格”表示一个缩进。
同时,也要避免将“tab制表符”或者tab与空格混合的缩进风格。目前,常用的编辑器一般设置成:tab制表符就是4个空格
使用\
行连接符
一行程序长度是没有限制的,但是为了可读性更强,通常将一行比较长的程序分为多行。这是,我们可以使用\
行连接符,把它放在行结束的地方。Python解释器仍然将它们解释为同一行。
a = [10,20,30,40,\
50,60,70,\
80,90,100]
b = 'abcdefg\
hijklmn\
opqrst\
uvwxyz'
print(a)
print(b)
对象
- Python中,一切皆对象。
- 每个对象由:标识(identity)、类型(type)、value(值)组成
- 标识用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数
id(obj)
可返回对象obj
的标识。- 类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用
type(obj)
获得对象的所属类型。- 值表示对象所存储的数据的信息。使用
print(obj)
可以直接打印出值。
对象的本质就是:
一个内存块,拥有特定的值,支持特定类型的相关操作
a=3
print(a)
print(id(a))
print(type(a))
b='我爱你'
print(b)
print(id(b))
print(type(b))
内存示意图
引用
在Python中,变量也称为:对象的引用(reference)。变量存储的就是对象的地址。
变量通过地址引用了“对象”。
变量位于:栈内存(压栈出栈等细节,后续再介绍)。
对象位于:堆内存。
如下源代码对应的内存图:
a=3
b="我爱你"
Python是动态类型语言:变量不需要显式声明类型。根据变量引用的对象,Python解释器自动确定数据类型
标识符规则
基本用法
**标识符规则:用于变量、函数、类、模块等的名称。**标识符有如下特定的规则:
-
区分大小写。如:
sxt
和SXT
是不同的 -
第一个字符必须是字母、下划线。其后的字符是:字母、数字、下划线
-
不能使用关键字。比如:
if
、or
、while
等 -
以双下划线开头和结尾的名称通常有特殊含义,尽量避免这种写法。比如:
__init__
是类的构造函数
Python标识符命名规则
变量和简单赋值语句
变量的声明和赋值
变量的声明和赋值:用于将一个变量绑定到一个对象上,格式: 变量名 = 表达式
最简单的表达式就是字面量。比如: a = 123
。运行过程中,解释器先运行右边的表达式,生成一个代表表达式运算结果的对象;然后,将这个对象地址赋值给左边的变量。
⚠️变量在使用前必须先被初始化(先被赋值)
删除变量和垃圾回收机制
- 可以通过del语句删除不再使用的变量。
- 如果对象没有变量引用,就会被垃圾回收器回收,清空内存空间。
【操作】删除变量示例
a=123
del a
print(a)
报错如下:
常量
Python不支持常量,即没有语法规则限制改变一个常量的值。我们只能约定常量的命名规则,以及在程序的逻辑上不对常量的值作出修改。
MAX_SPEED = 120
print(MAX_SPEED) # 输出120
MAX_SPEED = 140 # 实际是可以改的。只能逻辑上不做修改。
print(MAX_SPEED) # 输出140
链式赋值
系列解包赋值
系列数据赋值给对应相同个数的变量(个数必须保持一致)
a,b,c=4,5,6
相当于: a=4;b=5;c=6
【操作】使用系列解包赋值实现变量值交换
a,b=1,2
a,b=b,a # 变量值互换
print(a,b)
使用系列解包复制可以轻松实现变量值交换
最基本内置数据类型
python中变量没有类型,但是对象都有类型,python中最基本的内置数据类型:
-
整型
int
整数, 2345 , 10 , 50
-
浮点型
float
小数, 3.14 或者科学计数法 314e-2
-
布尔型
bool
表示真假,仅包含: True 、 False
-
字符串型
str
由字符组成的序列。 “abc” , ‘sxt’ , “尚学堂” , “百战程序员”
数字和基本运算符
整数
Python的整数可以无限大,任意大
三种进制
使用int()实现类型转换
- 浮点数直接舍去小数部分。如:
int(9.9)
结果是:9
- 布尔值
True
转为1
,False
转为0
。 如: int(True)
结果是1
- 字符串符合整数格式(浮点数格式不行)则直接转成对应整数,否则报错
自动转型
整数和浮点数混合运算时,表达式结果自动转型成浮点数。比如: 2+8.0
的结果是 10.0
整数可以任意大
Python2中, int
是32位,可以存储从 -2147483648
到 2147483647
的整数(约±21亿)。Long类型是64位,可以存储:-263–263-1之间的数值。
Python3中, int
可以存储任意大小的整数, long
被取消。
Python3中可以做超大数的计算,而不会造成“整数溢出”,这也是Python特别适合科学运算的特点
浮点数 float
- 浮点数用科学计数法表示。比如:
3.14
,表示成:314E-2
或者314e-2
- 这些数字在内存中也是按照科学计数法存储。
类型转换和四舍五入
- 类似于
int()
,我们也可以使用float()
将其他类型转化成浮点数。 - 整数和浮点数混合运算时,表达式结果自动转型成浮点数。比如:
2+8.0
的结果是10.0
round(value)
可以返回四舍五入的值。但不会改变原有值,而是产生新的值
round(value))可以返回四舍五入的值。但不会改变原有值,而是产生新的值
增强型赋值运算符
运算符 +
、 -
、 *
, /
、 //
、 **
和 %
和赋值符 =
结合可以构成“增强型赋值运算符”。
⚠️注意:
- “+=”中间不能加空格!
- 结合的是右侧整个表达式:
y *= x+2 # 相当于:y = y*(x+2) 而不是:y = y*x+2
时间的表示
python中可以通过time.time()
获得当前时刻,返回的值是以秒为单位,带微秒(1/1000毫秒)精度的浮点值。例如:1635063628.5632517
import time
b = int(time.time())
totalMinutes = b//60
totalHours = totalMinutes//60
totalDays = totalHours//24
totalYears = totalDays//365 #忽略闰年情况
布尔值
Python2中没有布尔值,直接用数字 0
表示 False
, 用数字 1
表示True
。
Python3中,把 True
和 False
定义成了关键字,但他们的本质还是 1
和0
,甚至可以和数字相加。
在Python语言底层,会将布尔值
True
看作1
,将布尔值False
看作0
,尽管从表面上看,True
和1
、False
和0
是完全不同的两个值,但实际上,它们是相同的。
在Python语言中有一些特殊的布尔类型值为
False
,例如False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。其他情况,均为True。
a = True
b = 3
c = a+b #c的值是:4
print(c)
print('空字符串的布尔类型的值:',bool(""))
#False
print('空列表布尔类型的值:',bool([]))
#False
print('None布尔类型的值:',bool(None))
#False
print('0布尔类型的值:',bool(0))
#False
print('字符串True和False转成布尔都是True:',bool("False"))
#True
运算符
逻辑运算符
#测试逻辑运算符
a,b,c=10,20,30
print((a<b) and (b<c)) # and并且 输出结果是True
print((a>b) or (b>c)) # or或者 输出结果是False
print(not(b<c)) # not非 输出结果是False
比较运算符
所有比较运算符返回 1
表示真,返回 0
表示假。这分别与特殊变量True
和 False
等价。
以下假设变量 a为15 ,变量 b为30 :
a = 4
print(a<=30)
# 关系运算符可以连用
if(3<a<10):
print("a在3和10之间")
关系运算符可以连用 如: 2<a<10
位运算符
按位运算符是把数字看作二进制来进行计算的。Python中的按位运算法则如表所示。
a = 0b11001
b = 0b01000
print(bin(a|b)) # bin()可以将数字转成二进制表示'0b11001'
print(bin(a&b)) # 与
print(bin(a^b)) # 异或
print(3<<2) # 左移1位相当于乘以2.左移两位相当于:3*4
print(20>>1) # 右移移位相当于除以2
加法操作补充
- 数字相加
3+2
结果是5
- 字符串拼接
"3"+"2"
结果是"2"
- 列表、元组等合并
[10,20,30]+[5,10,100]
结果是[10,20,30,5,10,100]
乘法操作补充
- 数字相乘
3*2
结果是6
- 字符串复制
"sxt" * 3
结果是"sxtsxtsxt"
- 列表、元组等复制
[10,20,30] * 3
结果是[10,20,30,10,20,30,10,20,30]
字符串拼接 "3"+"4"
结果 "34"
列表,元组等合并 [1,2,3]+[4,5,6]
结果 [1,2,3,4,5,6]
字符串复制 "abc"* 3
结果 "abcabcabc"
列表,元组等复制 [1,2,3] * 3
结果 [1,2,3,1,2,3,1,2,3]
增强赋值运算符(补充)
复合赋值可以让程序更加精炼,提高效率。
⚠️与C和JAVA不一样,Python不支持自增(++)和自减(–)
同一运算符
同一运算符用于比较两个对象的存储单元,实际比较的是对象的地址。
is
与 ==
区别:
is
用于判断两个变量引用对象是否为同一个,既比较对象的地址。==
用于判断引用变量引用对象的值是否相等,默认调用对象的__eq__()
方法。
总结
is
比较两个对象的id
值是否相等,是否指向同一个内存地址==
比较的是两个对象的内容是否相等,值是否相等is
运算符比==
效率高,在变量和None
进行比较时,应该使用is
a=20
b=20
c=30
print("a和b是同一个对象",a is b) # 执行结果:True
print("a和c是同一个对象",a is c) # 执行结果False
print("a和c不是同一个对象",a is not c) # 执行结果True
【操作】同一运算符测试
a = 1000
b = 1000
a == b # True
a is b # 命令行下是False。 文件下执行是True
c = 10
d = 10
c is d # True
整数缓存问题
- 命令行模式下,Python仅仅对比较小的整数对象进行缓存(范围为
[-5, 256]
)缓存起来[C语言底层用数组实现,连续分配空间,便于查找 ]
,而并非是所有整数对象。 - 文件模式下,所有数字都会被缓存,范围是:
[-无穷大,+无穷大]
- 缓存实现:
[-5,256]
仍然底层用数组实现 ;不在[-5,256]
出现的数,缓存到链表中,不连续分配空间 。
成员运算符
成员运算符测试实例中包含了一系列的成员,包括字符串,列表或元组。
a = "python"
b = "py"
print(b in a) #True
c = [10,20,30]
print(10 not in c) #False
运算符优先级问题
如下优先级,从高到低。
#测试运算符优先级
a,b,c,d=20,10,15,5
e=(a+b)*c/d # 30*15/5
print('(a+b)*c/d的执行结果:',e)
e=(a+b)*(c/d) # 30*(15/5)
print('(a+b)*(c/d)的执行结果:',e)
e=a+(b*c)/d # 20+150/5
print('a+(b*c)/d的执行结果:',e)
实际使用中,记住如下简单的规则即可,复杂的表达式一定要使用小括号组织。
- 乘除优先加减
- 位运算和算术运算>比较运算符>赋值运算符>逻辑运算符
基本运算符总结
序列
序列的本质和内存结构
序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放多个值的连续的内存空间。比如一个整数序列[10,20,30,40]
,示意表示:
由于Python3中一切皆对象,在内存中实际是按照如下方式存储的:
序列中存储的是整数对象的地址,而不是整数对象的值
字符串
字符串基本特点
- 字符串的本质是:字符序列。
- Python不支持单字符类型,单字符也是作为一个字符串使用的。
⚠️Python的字符串是不可变的,我们无法对原字符串做任何修改。但,可以将字符串的一部分复制到新创建的字符串,达到“看起来修改”的效果。
字符串的编码
Python3直接支持Unicode,可以表示世界上任何书面语言的字符。Python3的字符默认就是16位Unicode编码,ASCII码是Unicode编码的子集。
使用内置函数ord()可以把字符转换成对应的Unicode码;
使用内置函数chr()可以把十进制数字转换成对应的字符。
>>> ord('A') #65
>>> ord('高') #39640
>>> chr(66) #'B'
>>> ord('淇') #28103
引号创建字符串
我们可以通过单引号或双引号创建字符串。例如: a='abc'
b="sxt"
使用两种引号的好处是可以创建本身就包含引号的字符串,而不用使用转义字符。例如:
a = "I'm a teacher!"
print(a) # I'm a teacher!
b = 'my_name is "TOM"'
print(b) # my_name is "TOM"
连续三个单引号或三个双引号,可以帮助我们创建多行字符串。在长字符串中会保留原始的格式。例如:
s='''
I
Love
Python
'''
print(s)
空字符串和len()函数
Python允许空字符串的存在,不包含任何字符且长度为0。例如:
c = ''
print(len(c)) # 结果:0
len()用于计算字符串含有多少字符。例如:
d = 'abc尚学堂'
len(d) # 结果:6
转义字符
我们可以使用 +特殊字符 ,实现某些难以用字符表示的效果。比如:换行等。常见的转义字符有这些:
【操作】测试转义字符的使用
a = 'I\nlove\nU'
print(a)
print('aabb\\cc')
"""
输出:
I
love
U
aabb\cc
"""
字符串拼接
- 可以使用
+
将多个字符串拼接起来。例如:'aa' +'bb'
结果是'aabb'
- 如果
+
两边都是字符串,则拼接。 - 如果
+
两边都是数字,则加法运算 - 如果
+
两边类型不同,则抛出异常
- 如果
- 可以将多个字面字符串直接放到一起实现拼接。例如:
'aa' 'bb'
结果是'aabb'
【操作】字符串拼接操作
a = 'sxt'+'gaoqi' #结果是:'sxtgaoqi'
b = 'sxt''gaoqi' #结果是:'sxtgaoqi'
字符串复制
使用*
可以实现字符串复制
a = 'Sxt'*3 # 结果:'SxtSxtSxt'
不换行打印
我们前面调用print时,会自动打印一个换行符。有时,我们不想换行,不想自动添加换行符。我们可以自己通过参数end = "任意字符串"
。实现末尾添加任何内容:
print("sxt",end=' ')
print("sxt",end='##')
print("sxt")
"""
sxt sxt##sxt
"""
从控制台读取字符串
我们可以使用input()从控制台读取键盘输入的内容。
myname = input("请输入名字:")
print("您的名字是:"+myname)
replace()
实现字符串替换
字符串是不可变的,我们通过[]可以获取字符串指定位置的字符,但是我们不能改变字符串。我们尝试改变字符串中某个字符,
发现报错了:
>>> a = 'abcdefghijklmnopqrstuvwxyz'
>>> a
'abcdefghijklmnopqrstuvwxyz'
>>> a[3]='高'
Traceback (most recent call last):
File "<pyshell#94>", line 1, in <module>
a[3]='高'
TypeError: 'str' object does not support item
assignment
字符串不可改变。但是,我们确实有时候需要替换某些字符。这时,只能通过创建新的字符串来实现。
>>> a = 'abcdefghijklmnopqrstuvwxyz'
>>> a
'abcdefghijklmnopqrstuvwxyz'
>>> a = a.replace('c','高')
'ab高defghijklmnopqrstuvwxyz'
整个过程中,实际上我们是创建了新的字符串对象,并指向了变量a,而不是修改了以前的字符串。 内存图如下:
str()
实现数字转型字符串
str()可以帮助我们将其他数据类型转换为字符串。例如:
a = str(5.20) # 结果是:a = '5.20'
b = str(3.14e2) # 结果是:b = '314.0'
c = str(True) # 结果是:c = 'True'
当我们调用print()函数时,解释器自动调用了str()将非字符串的对象转成了字符串。
使用[]
提取字符
字符串的本质就是字符序列,我们可以通过在字符串后面添加[]
,在[]
里面指定偏移量,可以提取该位置的单个字符。
-
正向搜索:
最左侧第一个字符,偏移量是
0
,第二个偏移量是1
,以此类推。直到len(str)-1
为止。 -
反向搜索:
最右侧第一个字符,偏移量是
-1
,倒数第二个偏移量是-2
,以此类推,直到-len(str)
为止。
【操作】使用[]
提取字符串中的字符
>>> a = 'abcdefghijklmnopqrstuvwxyz'
>>> a
'abcdefghijklmnopqrstuvwxyz'
>>> a[0]
'a'
>>> a[3]
'd'
>>> a[26-1]
'z'
>>> a[-1]
'z'
>>> a[-26]
'a'
>>> a[-30]
Traceback (most recent call last):
File "<pyshell#91>", line 1, in <module>
a[-30]
IndexError: string index out of range
字符串切片slice
操作
切片slice操作可以让我们快速的提取子字符串。标准格式为:
[起始偏移量start: 终止偏移量end: 步长step]
典型操作(三个量为正数的情况)如下:包头不包尾
其他操作(三个量为负数)的情况:
字符串逆序 [::-1]
切片操作时,起始偏移量和终止偏移量不在[0,字符串长度-1]
这个范围,也不会报错。起始偏移量小于0则会当做0,终止偏移量大于“长度-1”会被当成-1。例如:
>>> "abcdefg"[3:50]
'defg'
我们发现正常输出了结果,没有报错。
split()
分割和join()
合并
split()
可以基于指定分隔符将字符串分隔成多个子字符串(存储到列表中)。如果不指定分隔符,则默认使用空白字符(换行符/空格/制表符)。示例代码如下:
>>> a = "to be or not to be"
>>> a.split()
['to', 'be', 'or', 'not', 'to', 'be']
>>> a.split('be')
['to ', ' or not to ', '']
join()
的作用和split()
作用刚好相反,用于将一系列子字符串连接起来。示例代码如下:
>>> a = ['sxt','sxt100','sxt200']
>>> '*'.join(a)
'sxt*sxt100*sxt200'
拼接字符串要点:
使用字符串拼接符
+
,会生成新的字符串对象,因此不推荐使用+
来拼接字符串。推荐使用join
函数,因为join
函数在拼接字符串之前会计算所有字符串的长度,然后逐一拷贝,仅新建一次对象。
join拼接字符串效率高
字符串驻留机制
字符串驻留:常量字符串只保留一次
c = "dd#"
d = "dd#"
print(c is d) #True
字符串比较和同一性
我们可以直接使用 ==
!=
对字符串进行比较,是否含有相同的字符。
我们使用 is
not is
,判断两个对象是否同一个对象。比较的是对象的地址,即 id(obj1)
是否和 id(obj2)
相等。
== 和!=比较是否含有相同字符
is和not is判断是否同一对象
成员操作符判断子字符串
in
not in
关键字,判断某个字符(子字符串)是否存在于字符串中。
"ab" in "abcdefg" #true
字符串常用方法汇总
常用查找方法
去除首尾信息
我们可以通过strip()
去除字符串首尾指定信息。通过lstrip()
去除字符串左边指定信息,rstrip()
去除字符串右边指定信息。
【操作】去除字符串首尾信息
>>> "*s*x*t*".strip("*")
's*x*t'
>>> "*s*x*t*".lstrip("*")
's*x*t*'
>>> "*s*x*t*".rstrip("*")
'*s*x*t'
>>> " s xt ".strip()
's xt'
大小写转换
编程中关于字符串大小写转换的情况,经常遇到。我们将相关方法汇总到这里。为了方便学习,先设定一个测试变量:
a = "gaoqi love programming, love SXT"
格式排版
center()
、 ljust()
、 rjust()
这三个函数用于对字符串实现排版。示例如下:
>>> a="SXT"
>>> a.center(10,"*")
'***SXT****'
>>> a.center(10)
' SXT '
>>> a.ljust(10,"*")
'SXT*******'
特征判断方法
- isalnum() 是否为字母或数字
- isalpha() 检测字符串是否只由字母组成(含汉字)
- isdigit() 检测字符串是否只由数字组成
- isspace()检测是否为空白符
- isupper() 是否为大写字母
- islower() 是否为小写字母
>>> "sxt100".isalnum()
True
>>> "sxt尚学堂".isalpha()
True
>>> "234.3".isdigit()
False
>>> "23423".isdigit()
True
>>> "aB".isupper()
False
>>> "A".isupper()
True
>>> "\t\n".isspace()
True
字符串的格式化
format()
基本用法
基本语法是通过 {}
和 : 来代替以前的 %
。
format()
函数可以接受不限个数的参数,位置可以不按顺序。
>>> a = "名字是:{0},年龄是:{1}"
>>> a.format("杰克",18)
'名字是:杰克,年龄是:18'
>>> a.format("高希希",6)
'名字是:高希希,年龄是:6'
>>> b = "名字是:{0},年龄是{1}。{0}是个好小伙"
>>> b.format("杰克",18)
'名字是:杰克,年龄是18。杰克是个好小伙'
>>> c = "名字是{name},年龄是{age}"
>>> c.format(age=19,name='杰克')
'名字是杰克,年龄是19'
我们可以通过{索引}/{参数名},直接映射参数值,实现对字符串的格式化,非常方便。
填充与对齐
- 填充常跟对齐一起使用
^
、<
、>
分别是居中、左对齐、右对齐,后面带宽度:
号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充
>>> "{:*>8}".format("245")
'*****245'
>>> "我是{0},我喜欢数字{1:*^8}".format("杰克","666")
'我是杰克,我喜欢数字**666***'
数字格式化
浮点数通过 f
,整数通过 d
进行需要的格式化。案例如下:
>>> a = "我是{0},我的存款有{1:.2f}"
>>> a.format("高淇",3888.234342)
'我是高淇,我的存款有3888.23'
可变字符串
- Python中,字符串属于不可变对象,不支持原地修改,如果需要修改其中的值,只能创建新的字符串对象。
- 确实需要原地修改字符串,可以使用
io.StringIO
对象或array
模块
使用io.StringIO
可以将字符串变为可变字符串
import io
s = "hello, sxt"
sio = io.StringIO(s) # 可变字符串
print(sio)
v1 = sio.getvalue()
print("v1:",v1)
char7 = sio.seek(7) # 指针知道索引7这个位置
sio.write("gaoqi")
v2 = sio.getvalue()
print("v2:",v2)
类型转换总结
与C++、Java等高级程序设计语言一样,Python语言同样也支持数据类型转换。
#类型转换
#转换为int
print('int()默认情况下为:', int())
print('str字符型转换为int:', int('010'))
print('float浮点型转换为int:', int(234.23))
#十进制数10,对应的2进制,8进制,10进制,16进制分别是:1010,12,10,0xa
print('int(\'0xa\', 16) = ', int('0xa', 16))
print('int(\'10\', 10) = ', int('10', 10))
print('int(\'12\', 8) = ', int('12', 8))
print('int(\'1010\', 2) = ', int('1010', 2))
#转换为float
print('float()默认情况下为:', float())
print('str字符型转换为float:',
float('123.01'))
print('int浮点型转换为float:', float(32))
#转换为complex
print('创建一个复数(实部+虚部):', complex(12,43))
print('创建一个复数(实部+虚部):', complex(12))
#转换为str字符串
print('str()默认情况下为:', str())
print('float型转换为str:', str(232.33))
print('int转换为str:', str(32))
lists = ['a', 'b', 'e', 'c', 'd', 'a']
print('列表list转换为str:', ''.join(lists))
#转换为list
strs = 'hongten'
print('序列strs转换为list:', list(strs))
#转换为tuple
print('列表list转换为tuple:', tuple(lists))
#字符和整数之间的转换
print('整数转换为字符chr:', chr(67))
print('字符chr转换为整数:', ord('C'))
print('整数转16进制数:', hex(12))
print('整数转8进制数:', oct(12))
列表
列表简介
-
列表:用于存储任意数目、任意类型的数据集合。
-
列表是内置可变序列,是包含多个元素的有序连续的内存空间。列表的标准语法格式:
a = [10,20,30,40]
其中,10,20,30,40这些称为:列表a的元素。
-
列表中的元素可以各不相同,可以是任意类型。比如:
a = [10,20,'abc',True]
-
Python的列表大小可变,根据需要随时增加或缩小。
列表对象的常用方法
字符串和列表都是序列类型,一个字符串是一个字符序列,一个列表是任何元素的序列。我们前面学习的很多字符串的方法,在列表中也有类似的用法,几乎一模一样。
创建列表的4种方式
基本语法 []
创建
a = [10,20,'gaoqi','sxt']
b = [] # 创建一个空的列表对象
print(a)
list()
创建
使用list()
可以将任何可迭代的数据转化成列表。
a = list() #创建一个空的列表对象
b = list(range(10))
# 结果:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
c = list("gaoqi,sxt")
# 结果:['g', 'a', 'o', 'q', 'i', ',', 's', 'x', 't']
range()
创建整数列表
range()
可以帮助我们非常方便的创建整数列表,这在开发中及其有用。语法格式为:
range([start,] end [,step])
start参数:可选,表示起始数字。默认是0
end参数:必选,表示结尾数字。
step参数:可选,表示步长,默认为1
⚠️python3中
range()
返回的是一个range对象,而不是列表。我们需要通过list()
方法将其转换成列表对象。
a = list(range(3,15,2)) # 结果:[3, 5, 7, 9, 11, 13]
b = list(range(15,9,-1)) # 结果:[15, 14, 13, 12, 11, 10]
c = list(range(3,-4,-1)) # 结果:[3, 2, 1, 0, -1, -2, -3]
print(a,b,c)
推导式生成列表
(简介一下,重点在for循环后讲)
使用列表推导式可以非常方便的创建列表,在开发中经常使用。
# 循环创建多个元素 [0, 2, 4, 6, 8]
a = [x*2 for x in range(5)]
# 通过if过滤元素[0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198]
b = [x*2 for x in range(100) if x%9==0]
print(a,b)
增加列表元素的5种方式
当列表增加和删除元素时,列表会自动进行内存管理,大大减少了程序员的负担。但这个特点涉及列表元素的大量移动,效率较低。
⚠️除非必要,我们一般只在列表的尾部添加元素或删除元素,这会大大提高列表的操作效率。
append()
方法
原地修改列表对象,是真正的列表尾部添加新的元素,速度最快,推荐使用。
a = [20,40]
a.append(80)
print(a) # 结果:[20, 40, 80]
+
运算符操作
并不是真正的尾部添加元素,而是创建新的列表对象;将原列表的元素和新列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用。
a = [20,40]
print(id(a))
a = a+[50]
print(id(a)) # 两次地址不一样,创建了新的对象
通过如上测试,我们发现变量a的地址发生了变化。也就是创建了新的列表对象。
extend()
方法
将目标列表的所有元素添加到本列表的尾部,属于原地操作,不创建新的列表对象。
a = [20,40]
print(id(a))
b = [50,60]
a.extend(b) # 原对象修改
print(id(a))
a = a+b # 产生新对象
print(id(a))
insert()
插入元素
使用 insert()
方法可以将指定的元素插入到列表对象的任意制定位置。这样会让插入位置后面所有的元素进行移动,会影响处理速度。涉及大量元素时,尽量避免使用。类似发生这种移动的函数还有:remove()
、 pop()
、 del()
,它们在删除非尾部元素时也会发生操作位置后面元素的移动。
a = [10,20,30]
a.insert(2,100)
print(a) # 结果:[10, 20, 100, 30]
乘法扩展
使用乘法扩展列表,生成一个新列表,新列表元素是原列表元素的多次重复。
a = ['sxt',100]
b = a*3
print(a) # 结果:['sxt', 100]
print(b) # 结果:['sxt', 100, 'sxt', 100, 'sxt', 100]
适用于乘法操作的,还有:字符串、元组。例如:
c = 'sxt'
d = c*3 # 结果:'sxtsxtsxt'
列表元素的删除
del 删除
删除列表指定位置的元素。
del()传的是索引
a = [100,200,888,300,400]
del a[2]
print(a) # 结果:[100,200,300,400]
pop()
方法
pop()删除并返回指定位置元素,如果未指定位置则默认操作列表最后一个元素。
pop()传的是空或索引
a = [10,20,30,40,50]
b1 = a.pop() # 结果:b1=50
print(a,b1) # 结果:[10, 20, 30, 40] 50
b2 = a.pop(1)
print(a,b2) # 结果:[10, 30, 40],20
remove()
方法
删除首次出现的指定元素,若不存在该元素抛出异常。
remove() 传的是元素
a = [10,20,30,40,50,20,30,20,30]
a.remove(20) # [10, 30, 40, 50, 20, 30, 20, 30]
a.remove(100) # 报错:ValueError:list.remove(x): x not in list
列表元素访问和计数
通过索引直接访问元素
我们可以通过索引直接访问元素。索引的区间在 [0, 列表长度-1]
这个范围。超过这个范围则会抛出异常。
a = [10,20,30,40,50,20,30,20,30]
print(a[2]) # 结果:30
print(a[10]) # 报错:IndexError: list index out of range
index()
获得指定元素在列表中首次出现的索引
index()
可以获取指定元素首次出现的索引位置。语法是: index(value,[start,[end]])
。其中, start
和 end
指定了搜索的范围。
>>> a = [10,20,30,40,50,20,30,20,30]
>>> a.index(20) # 结果:1
>>> a.index(20,3) # 结果:5 从索引位置3开始往后搜索的第一个20
>>> a.index(30,5,7) # 结果:6 从索引位置5到7这个区间,第一次出现30元素的位置
count()
获得指定元素在列表中出现的次数
count()
可以返回指定元素在列表中出现的次数。
>>> a = [10,20,30,40,50,20,30,20,30]
>>> a.count(20)
3
len()
返回列表长度
len()
返回列表长度,即列表中包含元素的个数。
>>> a = [10,20,30]
>>> len(a)
3
成员资格判断
判断列表中是否存在指定的元素,我们可以使用 count()
方法,返回0则表示不存在,返回大于0则表示存在。但是,一般我们会使用更加简洁的 in
关键字来判断,直接返回 True
或 False
>>> a = [10,20,30,40,50,20,30,20,30]
>>> 20 in a
True
>>> 100 not in a
True
>>> 30 not in a
False
切片操作
类似字符串的切片操作,对于列表的切片操作和字符串类似。
切片是Python序列及其重要的操作,适用于列表、元组、字符串等等。
切片slice
操作可以让我们快速提取子列表或修改。标准格式为:
[起始偏移量start:终止偏移量end[:步长step]]
包头不包尾
典型操作(三个量为正数的情况)如下:
其他操作(三个量为负数)的情况:
切片操作时,起始偏移量和终止偏移量不在
[0,字符串长度-1]
这个范围,也不会报错。起始偏移量 小于0 则会当做 0 ,终止偏移量大于"长度-1" 会被当成 “长度-1” 。例如:[10,20,30,40][1:30]
结果: [20, 30, 40]
我们发现正常输出了结果,没有报错。
列表的遍历
a = [10,20,30,40]
for obj in a: # obj是临时变量名称,随意起
print(obj)
复制列表所有的元素到新列表对象
如下代码实现列表元素的复制了吗?
list1 = [30,40,50] list2 = list1
只是将list2也指向了列表对象,也就是说list2和list2持有地址值是相同的,列表对象本身的元素并没有复制。
我们可以通过如下简单方式,实现列表元素内容的复制:
list1 = [30,40,50]
list2 = [] + list1 #生成了新列表对象
注:我们后面也会学习copy模块,使用浅复制或深复制实现我们的复制操作
列表排序
修改原列表,不建新列表的排序
使用sort()
>>> a = [20,10,30,40]
>>> id(a)
46017416
>>> a.sort() # 默认是升序排列
>>> a
[10, 20, 30, 40]
>>> a = [10,20,30,40]
>>> a.sort(reverse=True) # 降序排列
>>> a
[40, 30, 20, 10]
>>> import random
>>> random.shuffle(a) # 打乱顺序
>>> a
[20, 40, 30, 10]
建新列表的排序
我们也可以通过内置函数sorted()
进行排序,这个方法返回新列表,不对原列表做修改。
>>> a = [20,10,30,40]
>>> id(a)
46016008
>>> b = sorted(a) # 默认升序
>>> b
[10, 20, 30, 40]
>>> id(b)
45907848
>>> c = sorted(a,reverse=True) # 降序
>>> c
[40, 30, 20, 10]
通过上面操作,我们可以看出,生成的列表对象b和c都是完全新的列表对象。
reversed()
返回迭代器
内置函数reversed()
也支持进行逆序排列,与列表对象reverse()
方法不同的是,内置函数reversed()
不对原列表做任何修改,只是返回一个逆序排列的迭代器对象。
>>> a = [20,10,30,40]
>>> c = reversed(a)
>>> c
<list_reverseiterator object at 0x0000000002BCCEB8>
>>> list(c)
[40, 30, 10, 20]
>>> list(c)
[]
我们打印输出c发现提示是:list_reverseiterator
。也就是一个迭代对象。同时,我们使用list(c)
进行输出,发现只能使用一次。第一次输出了元素,第二次为空。那是因为迭代对象在第一次时已经遍历结束了,第二次不能再使用。
列表相关的其他内置函数汇总
max和min
用于返回列表中最大和最小值。
>>> a = [3,10,20,15,9]
>>> max(a)
20
>>> min(a)
3
sum
对数值型列表的所有元素进行求和操作,对非数值型列表运算则会报错。
>>> a = [3,10,20,15,9]
>>> sum(a)
57
多维列表
二维列表
一维列表可以帮助我们存储一维、线性的数据。
二维列表可以帮助我们存储二维、表格的数据。例如下表的数据:
嵌套循环打印二维列表所有的数据:
a = [
["高小一",18,30000,"北京"],
["高小二",19,20000,"上海"],
["高小一",20,10000,"深圳"],
]
for m in range(3):
for n in range(4):
print(a[m][n],end="\t")
print() #打印完一行,换行
元组tuple
元组不可变序列,不能修改元组中的元素
- 列表属于可变序列,可以任意修改列表中的元素。
- 元组属于不可变序列,不能修改元组中的元素。
因此,元组没有增加元素、修改元素、删除元素相关的方法。
因此,我们只需学元组的创建和删除,元素的访问和计数即可。元组支持如下操作:
- 索引访问
- 切片操作
- 连接操作
- 成员关系操作
- 比较运算操作
- 计数:元组长度
len()
、最大值max()
、最小值min()
、求和sum()
等
元组的创建
通过()创建元组
小括号可以省略。
a = (10,20,30) 或者 a = 10,20,30
如果元组只有一个元素,则必须后面加逗号。这是因为解释器会把(1)
解释为整数1
,(1,)
解释为元组。
a = 10,
>>> a = (1)
>>> type(a)
<class 'int'>
>>> a = (1,) # 或者 a = 1,
>>> type(a)
<class 'tuple'>
通过tuple()创建元组
tuple(可迭代的对象)
a = tuple() # 创建一个空元组对象
b = tuple("abc")
c = tuple(range(3))
d = tuple([2,3,4])
总结:
tuple()
可以接收列表、字符串、其他序列类型、迭代器等生成元组。list()
可以接收元组、字符串、其他序列类型、迭代器等生成列表。
元组的元素访问和计数
元组的元素不能修改
>>> a = (20,10,30,9,8)
>>> a[3]=33
Traceback (most recent call last):
File "<pyshell#313>", line 1, in
<module>
a[3]=33
TypeError: 'tuple' object does not support item assignment
元组的元素访问、index()、count()、切片等操作,和列表一样。
>>> a = (20,10,30,9,8)
>>> a[1]
10
>>> a[1:3]
(10, 30)
>>> a[:4]
(20, 10, 30, 9)
列表关于排序的方法list.sort()
是修改原列表对象,元组没有该方法。如果要对元组排序,只能使用内置函数sorted(tupleObj)
,并生成新元组的对象。(与列表的sorted(listObj)
一样)
a = (20,10,30,9,8)
b = sorted(a) # b是新对象,内容是:[8, 9, 10, 20, 30]
zip
zip(列表1,列表2,...)
将多个列表对应位置的元素组合成为元组,并返回这个zip对象。
⚠️如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同
a = [10,20,30]
b = [40,50,60]
c = [70,80,90,100]
d = zip(a,b,c)
print(d) # zip object
e = list(d) # 列表:[(10, 40, 70), (20, 50, 80), (30, 60, 90)]
print(e)
生成器推导式创建元组
- 从形式上看,生成器推导式与列表推导式类似,只是生成器推导式使用小括号。
- 列表推导式直接生成列表对象,生成器推导式生成的不是列表也不是元组,而是一个生成器对象。
- 我们可以通过生成器对象,转化成列表或者元组。也可以使用生成器对象的
__next__()
方法进行遍历,或者直接作为迭代器对象来使用。不管什么方式使用,元素访问结束后,如果需要重新访问其中的元素,必须重新创建该生成器对象。
【操作】生成器的使用测试
# 列表推导式: [0, 2, 4, 6, 8]
# a = [x*2 for x in range(5)]
# print(a)
s = (x*2 for x in range(5))
print(s) # <generator object <genexpr> at 0x0000021C80BE2880>
b = tuple(s)
print(b) # (0, 2, 4, 6, 8)
c = tuple(s)
print(c) # ()
s2 = (x for x in range(3))
print(s2.__next__()) #0
print(s2.__next__()) #1
print(s2.__next__()) #2
print(s2.__next__()) #报错:StopIteration
元组总结
- 元组的核心特点是:不可变序列。
- 元组的访问和处理速度比列表快。
- 与整数和字符串一样,元组可以作为字典的键,列表则永远不能作为字典的键使用。
字典
字典是“键值对”的**无序可变序列**,字典中的每个元素都是一个“键值对”,包含:“键对象”和“值对象”。可以通过“键对象”实现快速获取、删除、更新对应的“值对象”。
一个典型的字典的定义方式:
a = {'name':'gaoqi', 'age':18, 'job':'programmer'}
列表中我们通过“下标数字”找到对应的对象。字典中通过“键对象”找到对应的“值对象”。
- “键”是任意的不可变数据,比如:整数、浮点数、字符串、元组。
- 但是:列表、字典、集合这些可变对象,不能作为“键”。
- 并且“键”不可重复。
- “值”可以是任意的数据,并且可重复。
字典的创建
-
我们可以通过
{}
、dict()
来创建字典对象。a = {'name':'gaoqi','age':18,'job':'programmer'} b = dict(name='gaoqi',age=18,job='programmer') a = dict([("name","gaoqi"),("age",18)]) c = {} # 空的字典对象 d = dict() # 空的字典对象
-
通过
zip()
创建字典对象k = ['name','age','job'] v = ['gaoqi',18,'teacher'] d = dict(zip(k,v)) print(d) # {'name': 'gaoqi', 'age': 18, 'job': 'techer'}
-
通过
fromkeys
创建值为空的字典f = dict.fromkeys(['name','age','job']) print(f) #结果:{'name': None, 'age': None, 'job': None}
字典元素的访问
-
通过
[键]
获得“值”。若键不存在,则抛出异常。a = {'name':'gaoqi','age':18,'job':'programmer'} b = a['name'] print(b)
-
通过
get()
方法获得“值”。❤️推荐使用。优点是:指定键不存在,返回None;也可以设定指定键不存在时默认返回的对象。推荐使用
get()
获取“值对象”a = {'name':'gaoqi','age':18,'job':'programmer'} b = a.get('name') c = a.get('gender','不存在') print(b) print(c)
-
列出所有的键值对
a = {'name':'gaoqi','age':18,'job':'programmer'}
b = a.items()
print(b) # dict_items([('name', 'gaoqi'),('age', 18), ('job', 'programmer')])
- 列出所有的键,列出所有的值
a = {'name':'gaoqi','age':18,'job':'programmer'}
k = a.keys()
v = a.values()
print(k) # dict_keys(['name', 'age','job'])
print(v) # dict_values(['gaoqi', 18,'programmer'])
-
len()
键值对的个数a = {'name':'gaoqi','age':18,'job':'programmer'} num = len(a) print(num) #3
-
检测一个“键”是否在字典中
a = {'name':'gaoqi','age':18,'job':'programmer'} print("name" in a) #True
字典元素的添加、修改、删除
-
给字典新增“键值对”。如果“键”已经存在,则覆盖旧的键值对;如果“键”不存在,则新增“键值对”
a = {'name':'gaoqi','age':18,'job':'programmer'} a['address']='西三旗1号院' a['age']=16 print(a) #{'name': 'gaoqi', 'age': 16, 'job':'programmer', 'address': '西三旗1号院'}
-
使用
update()
将新字典中所有键值对全部添加到旧字典对象上。如果key
有重复,则直接覆盖a = {'name':'gaoqi','age':18,'job':'programmer'} b = {'name':'gaoxixi','money':1000,'gender':'男的'} a.update(b) print(a) # {'name': 'gaoxixi', 'age': 18, 'job':'programmer', 'money': 1000, 'gender': '男的'}
-
字典中元素的删除,可以使用
del()
方法;或者clear()
删除所有键值对;pop()
删除指定键值对,并返回对应的“值对象”a = {'name':'gaoqi','age':18,'job':'programmer'} del(a['name']) print(a) # {'age': 18, 'job':'programmer'} age = a.pop('age') print(age) # 18
-
popitem()
:随机删除和返回该键值对。字典是“无序可变序列”,因此没有第一个元素、最后一个元素的概念;popitem
弹出随机的项,因为字典并没有"最后的元素"或者其他有关顺序的概念。若想一个接一个地移除并处理项,这个方法就非常有效(因为不用首先获取键的列表)a = {'name':'gaoqi','age':18,'job':'programmer'} r1 = a.popitem() r2 = a.popitem() r3 = a.popitem() print(a) #{}
序列解包
序列解包可以用于元组、列表、字典。序列解包可以让我们方便的对多个变量赋值。
x,y,z=(20,30,10)
(a,b,c)=(9,8,10)
[m,n,p]=[10,20,30]
序列解包用于字典时,默认是对“键”进行操作; 如果需要对键值对操作,则需要使用items()
;如果需要对“值”进行操作,则需要使用values()
;
items()对键值进行操作返回的是元组,可以通过索引获得键和值
s = {'name':'gaoqi','age':18,'job':'teacher'}
name,age,job=s # 默认对键进行操作
print(name) # name
name,age,job=s.items() # 对键值对进行操作
print(name) # ('name', 'gaoqi')
name,age,job=s.values() # 对值进行操作
print(name) # gaoqi
表格数据使用字典和列表存储和访问
r1 = {"name":"高小一","age":18,"salary":30000,"city":"北京"}
r2 = {"name":"高小二","age":19,"salary":20000,"city":"上海"}
r3 = {"name":"高小五","age":20,"salary":10000,"city":"深圳"}
tb = [r1,r2,r3]
# 获得第二行的人的薪资
print(tb[1].get("salary"))
# 打印表中所有的的薪资
for i in range(len(tb)): # i -->0,1,2
print(tb[i].get("salary"))
# 打印表的所有数据
for i in range(len(tb)):
print(tb[i].get("name"),tb[i].get("age"),tb[i].get("salary"),tb[i].get("city"))
字典核心底层原理(重要)
字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket
。每个 bucket
有两部分:一个是键对象的引用,一个是值对象的引用。
由于,所有 bucket
结构和大小一致,我们可以通过偏移量来读取指定bucket
。
将一个键值对放进字典的底层过程
a = {}
a["name"]="gaoqi"
假设字典a对象创建完后,数组长度为8:
我们要把"name"="gaoqi"
这个键值对放到字典对象a中,首先第一步需要计算键"name"
的散列值。Python中可以通过hash()
来计算。
>>> bin(hash("name"))
'-0b1010111101001110110101100100101'
由于数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即"101"
,十进制是数字5。我们查看偏移量5,对应的bucket
是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边3位作为偏移量,即"100"
,十进制是数字4。再查看偏移量为4的bucket
是否为空。直到找到为空的bucket
将键值对放进去。流程图如下:
扩容
- python会根据散列表的拥挤程度扩容。“扩容”指的是:创造更大的数组,将原有内容拷贝到新数组中。
- 接近2/3时,数组就会扩容。
根据键查找“键值对”的底层过程
明白了,一个键值对是如何存储到数组中的,根据键对象取到值对象,理解起来就简单了。
>>> a.get("name")
'gaoqi'
当调用a.get("name")
,就是根据键"name"
查找到"键值对"
,从而找到值对象"gaoqi"
。
我们仍然要首先计算"name"
对象的散列值:
>>> bin(hash("name"))
'-0b1010111101001110110101100100101'
和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。 假设数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即 101
,十进制是数字5。我们查看偏移量5,对应的 bucket
是否为空。如果为空,则返回 None
。如果不为空,则将这个 bucket
的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回None
。流程图如下:
用法总结:
字典在内存中开销巨大,典型的空间换时间。
键查询速度很快
往字典里面添加新键值对可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字典的同时进行字典的修改
键必须可散列
数字、字符串、元组,都是可散列的
自定义对象需要支持下面三点:(面向对象章节中再展开说)
支持
hash()
函数支持通过
__eq__()
方法检测相等性若
a==b
为真,则hash(a)==hash(b)
也为真
集合
集合是无序可变,元素不能重复。实际上,集合底层是字典实现,集合的所有元素都是字典中的“键对象”,因此是不能重复的且唯一的。
集合创建和删除
-
使用
{}
创建集合对象,并使用add()
方法添加元素a = {3,5,7} a.add(9) # {9, 3, 5, 7}
-
使用
set()
,将列表、元组等可迭代对象转成集合。如果原来数据存在重复数据,则只保留一个a = ['a','b','c','b'] b = set(a) # {'b', 'a', 'c'}
-
remove()
删除指定元素;clear()
清空整个集合a = {10,20,30,40,50} a.remove(20) # {10, 50, 40,30}
集合相关操作
像数学中概念一样,Python对集合也提供了并集、交集、差集等运算。我们给出示例:
>>> a = {1,3,'sxt'}
>>> b = {'he','it','sxt'}
>>> a|b # 并集
{1, 3, 'sxt', 'he', 'it'}
>>> a&b # 交集
{'sxt'}
>>> a-b # 差集
{1, 3}
>>> a.union(b) # 并集
{1, 3, 'sxt', 'he', 'it'}
>>> a.intersection(b) # 交集
{'sxt'}
>>> a.difference(b) # 差集
{1, 3}
控制语句
控制语句和逻辑思维
控制语句:把语句组合成能完成一定功能的小逻辑模块。
分为三类:顺序、选择和循环。
学会控制语句,是真正跨入编程界的“门槛”,是成为“程序猿”的“门票”。
- “顺序结构”代表
“先执行a,再执行b”
的逻辑。比如,先找个女朋友,再给女朋友打电话;先订婚,再结婚; - “条件判断结构”代表
“如果…,则…”
的逻辑。比如,如果女朋友来电,则迅速接电话;如果看到红灯,则停车; - “循环结构”代表
“如果…,则重复执行…”
的逻辑。比如,如果没打通女朋友电话,则再继续打一次; 如果没找到喜欢的人,则再继续找
很神奇的是,三种流程控制语句就能表示所有的事情!
选择结构(条件判断结构)
选择结构通过判断条件是否成立,来决定执行哪个分支。选择结构有多种形式,分为:单分支、双分支、多分支。
单分支选择结构
if语句单分支结构的语法形式如下:
if 条件表达式:
语句/语句块
- 条件表达式:可以是逻辑表达式、关系表达式、算术表达式等等。
- 语句/语句块:可以是一条语句,也可以是多条语句。多条语句,缩进必须对齐一致
条件表达式详解
在选择和循环结构中,条件表达式的值为 False
的情况如下:
False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。
其他情况,均为 True
。这么看来,Python所有的合法表达式都可以看做条件表达式,甚至包括函数调用的表达式。
【操作】测试各种条件表达式
if 3: # 整数作为条件表达式
print("ok")
a = [] # 列表作为条件表达式,由于为空列表,是False
if a:
print("空列表,False")
s = "False" # 非空字符串,是True
if s:
print("非空字符串,是True")
c = 9
if 3<c<20:
print("3<c<20")
if 3<c and c<20:
print("3<c and c<20")
if True: # 布尔值
print("True")
⚠️条件表达式中,不能有赋值操作符
=
在Python中,条件表达式不能出现赋值操作符
=
,避免了其他语言中经常误将关系运算符==
写作赋值运算符=
带来的困扰。如下代码将会报语法错误:
if 3 < c and (c=20): # 直接报语法错误! print("赋值符不能出现在条件表达式中")
双分支选择结构
双分支结构的语法格式如下:
if 条件表达式:
语句1/语句块1
else:
语句2/语句块2
【操作】输入一个数字,小于10,则打印该数字;大于10,则打印“数字太大”
num = input("输入一个数字:")
if int(num)<10:
print(num)
else:
print("数字太大")
三元条件运算符
条件为真时的值 if (条件表达式) else 条件为假时的值
上一个案例代码,可以用三元条件运算符实现:
num = input("输入一个数字:")
print(num + "<10" if(int(num) < 10) else str(num) + ">10")
可以看到,这种写法更加简洁,易读。
多分支选择结构
多分支选择结构的语法格式如下:
if 条件表达式1 :
语句1/语句块1
elif 条件表达式2:
语句2/语句块2
...
elif 条件表达式n :
语句n/语句块n
[else:
语句n+1/语句块n+1
]
⚠️多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序
【操作】输入一个学生的成绩,将其转化成简单描述:不及格(小于60)、及格(60-79)、良好(80-89)、优秀(90-100)
方法1(使用完整的条件表达)
score = int(input("请输入分数"))
grade = ''
if(score<60):
grade = "不及格"
if(60<=score<80):
grade = "及格"
if(80<=score<90):
grade = "良好"
if(90<=score<=100):
grade = "优秀"
print("分数是{0},等级是{1}".format(score,grade))
上面的每个分支都使用了独立的、完整的判断,顺序可以随意挪动,而不影响程序运行。
方法2(利用多分支结构)
'''
### 多分支选择结构
'''
score = input("输入学生分数:")
grade = ''
if 0 <= int(score) < 60:
grade = "不及格"
elif int(score) < 80: # 60<=score<=80 多分支之间具有逻辑关系
grade = "及格"
elif int(score) < 90:
grade = "良好"
elif int(score) <= 100:
grade = "优秀"
else:
grade = "成绩输入错误"
print("分数是{0},等级是{1}".format(score, grade))
⚠️多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序
选择结构的嵌套
选择结构可以嵌套,使用时一定要注意控制好不同级别代码块的缩进量,因为缩进量决定了代码的从属关系。
【操作】输入一个分数。分数在0-100之间。90以上是A,80以上是B,70以上是C,60以上是D。60以下是E
score = int(input("输入一个0-100之间的数字:"))
grade = ''
if score > 100 or score < 0:
score = int(input("输入错误,请重新输入0-100之间的数字:"))
else:
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'E'
print("分数是{0},等级是{1}".format(score, grade))
# 更少的代码方法
score = int(input("输入一个0-100之间的数字:"))
grade = 'ABCDE'
if score > 100 or score < 0:
score = int(input("输入错误,请重新输入0-100之间的数字:"))
else:
num = score // 10
if num < 6:
num = 5
print("分数是{0},等级是{1}".format(score, grade[9-num]))
循环结构
循环结构用来重复执行一条或多条语句。表达这样的逻辑:如果符合条件,则反复执行循环体里的语句。在每次执行完后都会判断一次条件是否为True
,如果为True
则重复执行循环体里的语句。图示如下:
循环体里面的语句至少应该包含改变条件表达式的语句,以使循环趋于结束;否则,就会变成一个死循环。
while循环
while循环的语法格式如下:
while 条件表达式:
循环体语句
【操作】利用while循环,计算1-100之间数字的累加和;计算1-100之间偶数的累加和,计算1-100之间奇数的累加和
num = 0
sum_all = 0 # 1-100所有数的累加和
while num<=100:
sum_all += num
num += 1 # 迭代,改变条件表达式,使循环趋于结束
print("1-100所有数的累加和",sum_all)
for循环和可迭代对象遍历
for循环通常用于可迭代对象的遍历。for循环的语法格式如下:
for 变量 in 可迭代对象:
循环体语句
【操作】遍历一个元组或列表
for x in (20, 30, 50):
print(x*3)
可迭代对象
Python包含以下几种可迭代对象:
- 序列。包含:字符串、列表、元组、字典、集合
- 迭代器对象(iterator)
- 生成器函数(generator)
- 文件对象
我们已经在前面学习了序列、字典等知识,迭代器对象和生成器函数将在后面进行详解。接下来,我们通过循环来遍历这几种类型的数据:
【操作】遍历字符串中的字符
for temp in "weqwewe":
print(x)
【操作】遍历字典
d = {"name": "haha", "age": 12, "sex": "male"}
for x in d: # 遍历字典所以key
print(x)
for x in d.keys(): # 遍历字典所有的key
print(x)
for x in d.values(): # 遍历字典所有的value
print(x)
for x in d.items(): # 遍历字典所有键值对
print(x)
range对象
range对象
是一个迭代器对象,用来产生指定范围的数字序列。格式为:
range(start, end [,step])
生成的数值序列从 start
开始到 end
结束(⚠️不包含 end ,包头不包尾)。若没有填写 start
,则默认从0开始。 step
是可选的步长,默认为1。如下是几种典型示例:
for i in range(10)
产生序列:0 1 2 3 4 5 6 7 8 9
for i in range(3,10)
产生序列:3 4 5 6 7 8 9
for i in range(3,10,2)
产生序列:3 5 7 9
【操作】利用for循环,计算1-100之间数字的累加和;计算1-100之间偶数的累加和,计算1-100之间奇数的累加和。
for x in range(3, 10, 2): # start=3 end=10 step=2 包头不包尾
print(x, end='\t')
# 计算1-100累加和,奇数累加和,偶数累加和
sum_all = 0
sum_even = 0
sum_odd = 0
for x in range(101):
sum_all += x
if x % 2 == 0:
sum_even += x
else:
sum_odd += x
print("1-100累加和{0},奇数累加和{1},偶数累加和{2}".format(sum_all, sum_odd, sum_even))
嵌套循环
for x in range(5):
for y in range(5):
print(x, end="\t")
print()
嵌套循环练习
【操作】利用嵌套循环打印九九乘法表
# 九九乘法表
for m in range(1, 10):
for n in range(1, m+1):
print("{0}*{1}={2}".format(m, n, m*n), end="\t")
print()
'''
1*1=1
2*1=2 2*2=4
3*1=3 3*2=6 3*3=9
4*1=4 4*2=8 4*3=12 4*4=16
5*1=5 5*2=10 5*3=15 5*4=20 5*5=25
6*1=6 6*2=12 6*3=18 6*4=24 6*5=30 6*6=36
7*1=7 7*2=14 7*3=21 7*4=28 7*5=35 7*6=42 7*7=49
8*1=8 8*2=16 8*3=24 8*4=32 8*5=40 8*6=48 8*7=56 8*8=64
9*1=9 9*2=18 9*3=27 9*4=36 9*5=45 9*6=54 9*7=63 9*8=72 9*9=81
'''
【操作】用列表和字典存储下表信息,并打印出表中工资高于15000的数据
# 用列表和字典存储下表信息,并打印出表中工资高于15000的数据
r1 = dict(name="hycs", age=18, salary=10000, city="shanghai")
r2 = dict(name="sdaw", age=28, salary=20000, city="beijing")
r3 = dict(name="e1qe", age=38, salary=30000, city="nanjing")
tb = [r1, r2, r3]
for x in tb:
if x.get("salary")>15000:
print(x)
break语句
break语句可用于while和for循环,用来结束整个循环。当有嵌套循环时,break语句只能跳出最近一层的循环。
【操作】使用break语句结束循环
while True:
a = input("请输入一个字符(输入Q或q结束)")
if a.upper()=='Q':
print("循环结束,退出")
break
else:
print(a)
continue语句
continue语句用于结束本次循环,继续下一次。多个循环嵌套时,continue也是应用于最近的一层循环。
【操作】要求输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员工的数量和薪资明细,以及平均薪资
# 要求输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员王的数量和薪资明细,以及平均薪资
empNum = 0
salarySum = 0
salarys = []
while True:
s = input("请输入员工薪资(按Q或q退出)")
if s.upper() == "Q":
break
if float(s) < 0:
print("无效录入,请重新录入")
continue
print("录入成功")
empNum += 1
salarySum += float(s)
salarys.append(float(s))
print("员工数", format(empNum))
print("录入薪资:", salarys)
print("总薪资:", salarySum)
print("平均薪资", salarySum / empNum)
else语句
while
、for
循环可以附带一个else
语句(可选)。如果for
、while
语句没有被break
语句结束,则会执行else
子句,否则不执行。语法格式如下:
while 条件表达式:
循环体
else:
语句块
或者:
for 变量 in 可迭代对象:
循环体
else:
语句块
【操作】员工一共4人。录入这4位员工的薪资。全部录入后,打印提示“您已经全部录入4名员工的薪资”。最后,打印输出录入的薪资和平均薪资
salarySum = 0
salarys = []
for i in range(4):
s = input("请输入员工薪资(按Q或q退出)")
if s.upper() == "Q":
break
if float(s) < 0:
print("无效录入,请重新录入")
continue
print("录入成功")
salarySum += float(s)
salarys.append(float(s))
else:
print("全部录入成功")
print("录入薪资:", salarys)
print("总薪资:", salarySum)
print("平均薪资", salarySum / 4)
"""
请输入员工薪资(按Q或q退出)40
录入成功
请输入员工薪资(按Q或q退出)50
录入成功
请输入员工薪资(按Q或q退出)60
录入成功
请输入员工薪资(按Q或q退出)70
录入成功
全部录入成功
录入薪资: [40.0, 50.0, 60.0, 70.0]
总薪资: 220.0
平均薪资 55.0
"""
循环代码优化技巧
虽然计算机越来越快,空间也越来越大,我们仍然要在性能问题上“斤斤计较”。编写循环时,遵守下面三个原则可以大大提高运行效率,避免不必要的低效计算:
- 尽量减少循环内部不必要的计算
- 嵌套循环中,尽量减少内层循环的计算,尽可能向外提
- 局部变量查询较快,尽量使用局部变量
其他优化手段
- 连接多个字符串或列表,使用join()或append()而不使用+ +会创建新的字符串,join不会
- 列表进行元素插入和删除,尽量在列表尾部操作
zip()并行送代多个序列
我们可以通过zip()函数对多个序列进行并行迭代,zip()函数在最短序列“用完”时就会停止。
names = ("haohao", "hehe", "huaiyue")
ages = (18, 19, 20)
jobs = ("IT", "TEACHER", "POLICE")
# 使用zip并行迭代多个序列
for name, age, job in zip(names, ages, jobs):
print("{0}--{1}--{2}".format(name, age, job))
# 不使用zip也可以并使迭代多个序列
for i in range(min(len(names), len(ages), len(jobs))):
print("{0}--{1}--{2}".format(names[i], ages[i], jobs[i]))
推导式创建序列
推导式是从一个或者多个迭代器快速创建序列的一种方法。它可以将循环和条件判断结合,从而避免冗长的代码。
❤️推导式是典型的Python风格,会使用它,代表你已经超过Python初学者的水平。
列表推导式
列表推导式生成列表对象,语法如下:
[表达式 for item in 可迭代对象 ]
或者:
{表达式 for item in 可迭代对象 if 条件判断}
[x for x in range(1,5)] #[1, 2, 3, 4]
[x*2 for x in range(1,5)] #[2, 4, 6, 8]
[x*2 for x in range(1,20) if x%5==0 ] #[10,20, 30]
[a for a in "abcdefg"] #['a', 'b', 'c','d', 'e', 'f', 'g']
# 可以使用两个循环,使用zip并行迭代
cells = [(row,col) for row,col in zip(range(1,10),range(101,110))]
print(cells)
a = [x for x in range(1, 10) if x % 2 == 0]
print(a)
cells = [(row, column) for row, column in zip(range(1, 10), range(101, 110))]
print(cells)
'''
[2, 4, 6, 8]
[(1, 101), (2, 102), (3, 103), (4, 104), (5, 105), (6, 106), (7, 107), (8, 108), (9, 109)]
'''
字典推导式
字典的推导式生成字典对象,格式如下:
{key_expression: value_expression for 表达式 in 可迭代对象}
类似于列表推导式,字典推导也可以增加if条件判断、多个for循环。
values = ["北京","上海","深圳","广州"]
cities = {id*100:city for id,city in zip(range(1,5),values)}
print(cities)
生成字典对象:
{100: '北京', 200: '上海', 300: '深圳', 400:'广州'}
【操作】统计文本中字符出现的次数:
# 统计字数
my_text = 'i love python, me too'
char_count = {c: my_text.count(c) for c in my_text}
print(char_count)
'''
{'i': 1, ' ': 4, 'l': 1, 'o': 4, 'v': 1, 'e': 2, 'p': 1, 'y': 1, 't': 2, 'h': 1, 'n': 1, ',': 1, 'm': 1}
'''
集合推导式
集合推导式生成集合,和列表推导式的语法格式类似:
{表达式 for item in 可迭代对象 }
或者:
{表达式 for item in 可迭代对象 if 条件判断}
>>> {x for x in range(1,100) if x%9==0}
{99, 36, 72, 9, 45, 81, 18, 54, 90, 27, 63}
生成器推导式(不直接生成元组)
很多同学可能会问:“都有推导式,元组有没有?”,能不能用小括号呢?
>>> (x for x in range(1,100) if x%9==0)
<generator object <genexpr> at 0x0000000002BD3048>
我们发现提示的是“一个生成器对象”。显然,元组是没有推导式的。
一个生成器只能运行一次。第一次迭代可以得到数据,第二次迭代发现数据已经没有了。
gnt = (x for x in range(1,100) if x%9==0)
for x in gnt:
print(x,end=' ')
for x in gnt:
print(x,end=' ')
综合练习
绘制不同颜色的同心圆
# 绘制不同颜色的同心圆
import turtle
p = turtle.Pen() # 画笔对象
radius = [x * 10 for x in range(1, 11)]
my_color = ["red", "yellow", "black", "green"]
p.width(4)
for r, i in zip(radius, range(len(radius))): # (10,0),(20,1)
p.penup()
p.goto(0, -r)
p.pendown()
p.color(my_color[i % len(my_color)])
p.circle(r)
turtle.done() # 程序执行完毕,窗口还在
函数和内存底层分析
函数是可重用的程序代码块。
函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。
在编写函数时,函数体中的代码写法和我们前面讲述的基本一致,只是对代码实现了封装,并增加了函数调用、传递参数、返回计算结果等内容。
⚠️为了让大家更容易理解,掌握的更深刻。我们也要深入内存底层进行分析。绝大多数语言内存底层都是高度相似的,这样大家掌握了这些内容也便于以后学习其他语言。
函数简介
函数(function)的基本概念
- 一个程序由一个一个的任务组成;函数就是代表一个任务或者一个功能(function)。
- 函数是代码复用的通用机制
Python函数的分类
Python函数分为如下几类:
-
内置函数
我们前面使用的
str()
、list()
、len()
等这些都是内置函数,我们可以拿来直接使用。 -
标准库函数
我们可以通过 import 语句导入库,然后使用其中定义的函数
-
第三方库函数
Python社区也提供了很多高质量的库。下载安装这些库后,也是通过 import 语句导入,然后可以使用这些第三方库的函数
-
用户自定义函数
用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。今天我们学习的就是如何自定义函数。
函数的定义和调用
核心要点
Python中,定义函数的语法如下:
def 函数名 ([参数列表]) :
'''文档字符串'''
函数体/若干语句
简单定义一个函数:
def add(a,b,c):
'''完成三个数的加法,并返回他们的和'''
sum = a+b+c
print("{0}、{1}、{2}三个数的和是:{3}".format(a,b,c,sum))
return sum
add(10,20,30)
add(30,40,50)
要点:
- 我们使用
def
来定义函数,然后就是一个空格和函数名称;- Python执行
def
时,会创建一个函数对象,并绑定到函数名变量上。
- Python执行
- 参数列表
- 圆括号内是形式参数列表,有多个参数则使用逗号隔开
- 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
- 调用时的实际参数必须与形参列表一一对应
return
返回值- 如果函数体中包含
return
语句,则结束函数执行并返回值; - 如果函数体中不包含
return
语句,则返回None
值。
- 如果函数体中包含
- 调用函数之前,必须要先定义函数,即先调用 def 创建函数对象
- 内置函数对象会自动创建
- 标准库和第三方库函数,通过
import
导入模块时,会执行模块中的def
语句
形参和实参
形参和实参的要点:
- 圆括号内是形式参数列表,有多个参数则使用逗号隔开
- 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
- 调用时的实际参数必须与形参列表一一对应
【操作】定义一个函数,实现两个数的比较,并返回较大的值
def printMax(a,b):
'''实现两个数的比较,并返回较大的值'''
if a>b:
print(a,'较大值')
return a
else:
print(b,'较大值')
return b
printMax(10,20)
printMax(30,5)
上面的 printMax
函数中,在定义时写的 printMax(a,b)
。 a 和 b 称为“形式参数”,简称“形参”。也就是说,形式参数是在定义函数时使用的。 形式参数的命名要符合“标识符”命名规则。在调用函数时,传递的参数称为“实际参数”,简称“实参”。上面代码中, printMax(10,20)
, 10 和 20 就是实际参数。
文档字符串(函数的注释)
程序的可读性最重要,一般建议在函数体开始的部分附上函数定义说明,这就是“文档字符串”,也有人成为“函数的注释”。我们通过三
个单引号或者三个双引号来实现,中间可以加入多行文字进行说明。
【操作】测试文档字符串的使用
我们调用 help(函数名)
可打印输出函数的文档字符串。
我们也可以通过 函数名.__doc__
直接获取到函数的文档字符串,自己进行打印。
def printMax(a, b):
"""
求两个数较大的数
:param a: 一个参数
:param b: 一个参数
:return: 返回较大的数
"""
help(printMax)
print("print(printMax.__doc__) ", printMax.__doc__)
"""
Help on function printMax in module __main__:
printMax(a, b)
求两个数较大的数
:param a: 一个参数
:param b: 一个参数
:return: 返回较大的数
"""
"""
print(printMax.__doc__)
求两个数较大的数
:param a: 一个参数
:param b: 一个参数
:return: 返回较大的数
"""
返回值详解
return
返回值要点:
- 如果函数体中包含
return
语句,则结束函数执行并返回值 - 如果函数体中不包含
return
语句,则返回None
值 - 要返回多个值,使用列表、元组、字典、集合将多个值“存起来”即可
def printShape(n):
"""
返回一个列表
:param n:
:return:
"""
s1 = "#" * n
s2 = "$" * n
return [s1, s2]
s = printShape(5)
print(s)
"""
['#####', '$$$$$']
"""
函数也是对象,内存底层分析
Python中,”一切都是对象”。实际上,执行def定义函数后,系统就创建了相应的函数对象。
def print_star(n):
print("*"*n)
print(print_star)
print(id(print_star))
c = print_star
c(3)
"""
<function print_star at 0x0000000002BB8620>
45844000
"""
- 显然,我们可以看出变量
c
和print_star
都是指向了同一个函数对象。因此,执行c(3)
和执行print_star(3)
的效果是完全一致的。 - Python中,圆括号意味着调用函数。在没有圆括号的情况下,Python会把函数当做普通对象。
与此核心原理类似,我们也可以做如下操作:
zhengshu = int
zhengshu("234")
显然,我们将内置函数对象 int() 赋值给了变量 zhengshu ,这样zhengshu 和 int 都是指向了同一个内置函数对象。
⚠️当然,此处仅限于原理性讲解,实际开发中没必要这么做。
变量的作用域(全局变量和局部变量)
变量起作用的范围称为变量的作用域,不同作用域内同名变量之间互不影响。变量分为:全局变量、局部变量。
全局变量
- 在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
- 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
- 要在函数内改变全局变量的值,使用
global
声明一下
局部变量
- 在函数体中(包含形式参数)声明的变量。
- 局部变量的引用比全局变量快,优先考虑使用
- 如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量
【操作】 输出局部变量和全局变量
a = 100
def f1(a, b, c):
print(a, b, c)
print(locals()) # 打印输出的局部变量
print("#" * 20)
print(globals()) # 打印输出的全局变量
f1(2, 3, 4)
"""
2 3 4
{'a': 2, 'b': 3, 'c': 4}
####################
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000023373C06CD0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\2022百战Python\\Python基础\\函数和内存分析\\practice.py', '__cached__': None, 'a': 100, 'f1': <function f1 at 0x0000023374387E50>}
"""
局部变量和全局变量效率测试
局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是在循环的时候。
在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量提高运行速度。
参数的传递
函数的参数传递本质上就是:从实参到形参的赋值操作。Python中“一切皆对象”,所有的赋值操作都是“引用的赋值”。所以,Python中参数的传递都是“引用传递”,不是“值传递”。
具体操作时分为两类:
- 对“可变对象”进行“写操作”,直接作用于原对象本身。
- 对“不可变对象”进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。
⚠️
可变对象有:
字典、列表、集合、自定义的对象等
不可变对象有:
数字、字符串、元组、function等
传递可变对象的引用
传递参数是可变对象(例如:列表
、字典
、集合
、自定义的其他可变对象等),实际传递的还是对象的引用。在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。
【操作】参数传递:传递可变对象的引用
b = [10,20]
def f2(m):
print("m:",id(m)) # b和m是同一个对象
m.append(30) # 由于m是可变对象,不创建对象拷贝,直接修改这个对象
f2(b)
print("b:",id(b))
print(b)
"""
m: 45765960
b: 45765960
[10, 20, 30]
"""
传递不可变对象的引用
传递参数是不可变对象(例如: int
、 float
、字符串
、元组
、布尔值
),实际传递的还是对象的引用。在”赋值操作”时,由于不可变对象无法修改,系统会新创建一个对象。
【操作】参数传递:传递不可变对象的引用
a = 100
def f1(n):
print("n:",id(n)) # 传递进来的是a对象的地址
n = n+200 # 由于a是不可变对象,因此创建新的对象n
print("n:",id(n)) #n 已经变成了新的对象
print(n)
f1(a)
print("a:",id(a))
"""
n: 1663816464
n: 46608592
300
a: 1663816464
"""
显然,通过 id
值我们可以看到 n
和 a
一开始是同一个对象。给n
赋值后,n
是新的对象。
浅拷贝和深拷贝
- 浅拷贝:拷贝对象,但不拷贝子对象的内容,只是拷贝子对象的引用。
- 深拷贝:拷贝对象,并且会连子对象的内存也全部(递归)拷贝一份,对子对象的修改不会影响源对象
传递不可变对象包含的子对象是可变的情况
传递不可变对象时,不可变对象里面包含的子对象是可变的。则方法内修改了这个可变对象,源对象也发生了变化。
a = (10,20,[5,6])
print("a:",id(a))
def test01(m):
print("m:",id(m))
m[2][0] = 888
print(m)
print("m:",id(m))
test01(a)
print(a)
"""
a: 41611632
m: 41611632
(10, 20, [888, 6])
m: 41611632
(10, 20, [888, 6])
"""
参数的几种类型
位置参数
函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。按位置传递的参数,称为:“位置参数”。
【操作】测试位置参数
def f1(a,b,c):
print(a,b,c)
f1(2,3,4)
f1(2,3) # 报错,位置参数不匹配
"""
2 3 4
Traceback (most recent call last):
File "E:\PythonExec\if_test01.py", line 5,
in <module>
f1(2,3)
TypeError: f1() missing 1 required positional
argument: 'c
"""
默认值参数
我们可以为某些参数设置默认值,这样这些参数在传递时就是可选的。称为“默认值参数”。默认值参数放到位置参数后面。
【操作】测试默认值参数
def f1(a,b,c=10,d=20): # 默认值参数必须位于普通位置参数后面
print(a,b,c,d)
f1(8,9)
f1(8,9,19)
f1(8,9,19,29)
"""
8 9 10 20
8 9 19 20
8 9 19 29
"""
命名参数(关键字参数)
我们也可以按照形参的名称传递参数,称为“命名参数”,也称“关键字参数”。
def f1(a,b,c):
print(a,b,c)
f1(8,9,19) # 位置参数
f1(c=10,a=20,b=30) # 命名参数
"""
8 9 19
20 30 10
"""
可变参数
可变参数指的是“可变数量的参数”。分两种情况:
*param
(一个星号),将多个参数收集到一个“元组”对象中。**param
(两个星号),将多个参数收集到一个“字典”对象中。
【操作】测试可变参数处理(元组、字典两种方式)
def f1(a,b,*c):
print(a,b,c)
f1(8,9,19,20)
def f2(a,b,**c):
print(a,b,c)
f2(8,9,name='gaoqi',age=18)
def f3(a,b,*c,**d):
print(a,b,c,d)
f3(8,9,20,30,name='gaoqi',age=18)
"""
8 9 (19, 20)
8 9 {'name': 'gaoqi', 'age': 18}
8 9 (20, 30) {'name': 'gaoqi', 'age': 18}
"""
强制命名参数
在带星号的“可变参数”后面增加新的参数,必须在调用的时候“强制命名参数”。
def f1(*a,b,c):
print(a,b,c)
#f1(2,3,4) # 会报错。由于a是可变参数,将2,3,4全部收集。造成b和c没有赋值。
f1(2,b=3,c=4)
lambda表达式和匿名函数
lambda
表达式可以用来声明匿名函数。 lambda
函数是一种简单的、在同一行中定义函数的方法。 lambda
函数实际生成了一个函数对象。
lambda
表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。
lambda
表达式的基本语法如下:
lambda arg1,arg2,arg3... : <表达式>
arg1
arg2
arg3
为函数的参数。<表达式>
相当于函数体。运算结果是:表达式的运算结果。
【操作】lambda表达式使用
f = lambda a, b, c: a + b + c
print(f)
print(id(f))
print(type(f))
print(f(1, 2, 3))
"""
<function <lambda> at 0x000001AF907F7E50>
1853555179088
<class 'function'>
6
"""
g = [lambda a: a * 2, lambda b: b * 4, lambda c: c * 8]
print(g[0](1), g[1](2), g[2](3))
"""
2 8 24
"""
eval()函数
功能:将字符串 str
当成有效的表达式来求值并返回计算结果。
语法: eval(source[, globals[, locals]]) -> value
参数:
source
:一个Python表达式或函数compile()
返回的代码对象globals
:可选。必须是dictionary
locals
:可选。任意映射对象
⚠️
eval函数
会将字符串当做语句来执行,因此会被注入安全隐患。比如:字符串中含有删除文件的语句。那就麻烦大了。因此,使用时候,要慎重!!!
s = "print('abcd')"
eval(s) # eval会将字符串当成语句来执行
"""abcd"""
a = 10
b = 20
c = eval("a+b")
print(c)
"""30"""
dict1 = dict(a=100, b=200)
d = eval("a+b", dict1)
print(d)
"""300"""
递归函数
- 递归(recursion)是一种常见的算法思路,在很多算法中都会用到。比如:深度优先搜索(DFS:Depth First Search)等。
- 递归的基本思想就是“自己调用自己”
递归函数指的是:自己调用自己的函数,在函数体内部直接或间接的自己调用自己。每个递归函数必须包含两个部分:
-
终止条件
表示递归什么时候结束。一般用于返回值,不再调用自己。
-
递归步骤
把第n步的值和第n-1步相关联。
⚠️递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时,谨慎使用。
def my_recursion(n):
print("start:" + str(n))
if n == 1:
print("recursion over!")
else:
my_recursion(n - 1)
print("end:" + str(n))
my_recursion(3)
"""
start:3
start:2
start:1
recursion over!
end:1
end:2
end:3
"""
【操作】 使用递归函数计算阶乘(factorial)
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1)
print(factorial(5))
嵌套函数(内部函数)
嵌套函数:在函数内部定义的函数!
一般在什么情况下使用嵌套函数?
-
封装-数据隐藏
外部无法访问“嵌套函数”。
-
贯彻DRY(Don’t Repeat Yourself)原则
-
嵌套函数,可以让我们在函数内部避免重复代码。
-
闭包(后面会讲解)
nonlocal和global关键字
nonlocal
用来在内部函数中,声明外层的局部变量。
global
函数内声明全局变量,然后才使用全局变量
LEGB规则
Python在查找“名称”时,是按照LEGB规则查找的:
Local
指的就是函数或者类的方法内部
Enclosed
指的是嵌套函数(一个函数包裹另一个函数,闭包)
Global
指的是模块中的全局变量
Built in
指的是Python为自己保留的特殊名称
如果某个
name
映射在局部local
命名空间中没有找到,接下来就会在闭包作用域enclosed
进行搜索,如果闭包作用域也没有找到,Python就会到全局global
命名空间中进行查找,最后会在内建built-in
命名空间搜索 (如果一个名称在所有命名空间中都没有找到,就会产生一个NameError
)
面向对象
面向对象简介
Python完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。
Python中,一切皆对象。我们在前面学习的数据类型、函数等,都是对象。
- 面向对象(Object oriented Programming,OOP)编程的思想主要是针对大型软件设计而来的。
- 面向对象编程使程序的扩展性更强、可读性更好,使编程可以像搭积木一样简单。
- 面向对象编程将数据和操作数据相关的方法封装到对象中,组织代码和数据的方式更加接近人的思维,从而大大提高了编程的效率。
❤️Python支持面向过程、面向对象、函数式编程等多种编程范式。
面向过程和面向对象思想
面向过程和面向对象的区别
面向过程和面向对象都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。
C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。
面向过程是什么?
面向过程适合简单、不需要协作的事务,重点关注如何执行。面向过程时,我们首先思考“怎么按步骤实现?”。
比如,如何开车?我们很容易就列出实现步骤:
面向对象是什么?
面向对象(Oriented-Object)思想更契合人的思维模式。我们首先思考的是”怎么设计这个事物?”。比如思考造车,我们就会先思考“车怎么设计?”,而不是“怎么按步骤造车的问题”。这就是思维方式的转变。天然的,我们就会从“车由什么组成”开始思考:
为了协作,我们找轮胎厂完成制造轮胎的步骤,发动机厂完成制造发动机的步骤;这样,发现大家可以同时进行车的制造,最终进行组装,大大提高了效率。具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开执行者、离不开面向过程!
面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。但是,具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。
我们干万不要把面向过程和面向对象对立起来。他们是相辅相成的。面向对象离不开面向过程!
面向对象和面向过程总结
①都是解决问题的思维方式,都是代码组织的方式。
②面向过程是一种“执行者思维”,解决简单问题可以使用面向过程
③面向对象是一种“设计者思维”,解决复杂、需要协作的问题可以使用面向对象
面向对象离不开面向过程:
- 宏观上:通过面向对象进行整体设计
- 微观上:执行和处理数据,仍然是面向过程
对象进化的小故事
类的定义
类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。
类:我们叫做
class
。对象:我们叫做object
,instance
(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。
我们把对象比作一个“饼干”,类就是制造这个饼干的“模具”。
属性和方法
我们通过类定义数据类型的属性(数据)和方法(行为),也就是说,“类将行为和状态打包在一起”。
从一个类创建对象时,每个对象会共享这个类的行为(类中定义的方法),但会有自己的属性值(不共享状态)。更具体一点:“方法代码是共享的,属性数据不共享”。
Python中,“一切皆对象”。类也称为“类对像",类的实例也称为“实例对象”。
定义类的语法格式如下:
class 类名:
类体
要点如下:
①类名必须符合“标识符"的规则;一般规定,首字母大写,多个单词使用“驼峰原则”。
②类体中我们可以定义属性和方法
③属性用来描述数据,方法(即函数)用来描述这些数据相关的操作
class Student:
def __init__(self, name, score): # self参数是必须有的
self.name = name # 实例属性
self.score = score # 实例属性
def say_score(self):
print("{0}的分数是{1}".format(self.name, self.score))
s1 = Student("john", 61) # 自动调用__init__()方法
print(s1.name, s1.score)
s1.say_score()
"""
john 61
john的分数是61
"""
对象完整内存结构
类是抽象的,也称之为“对象的模板”。我们需要通过类这个模板,创建类的实例对象,然后才能使用类定义的功能。
我们前面说过一个Python对象包含三个部分: id
(identity识别码)、 type
(对象类型)、 value
(对象的值)。
现在,我们可以更进一步的说,一个Python对象包含如下部分:
__init__
构造方法和__new__
方法
初始化对象,我们需要定义构造函数 __init__()
方法。构造方法用于执行“实例对象的初始化工作”,即对象创建后,初始化当前对象的相关属性,无返回值。
构造方法是负责初始化(装修),不是建对象(房子)
__init__()
构造函数的要点如下:
-
名称固定,必须为:
__init__()
-
第一个参数固定,必须为:
self
。self
指的就是刚刚创建好的实例对象 -
构造函数通常用来初始化实例对象的实例属性,如下代码就是初始化实例属性:
name
和score
def __init__(self, name, score): # self参数是必须有的 self.name = name # 实例属性 self.score = score # 实例属性
-
通过“类名(参数列表)”来调用构造函数。调用后,将创建好的对象返回给相应的变量。比如:
s1=Student("张三",80)
-
__init__()
方法:初始化创建好的对象,初始化指的是:“给实例属性赋值” -
__new__()
方法:用于创建对象,但我们一般无需重定义该方法 -
如果我们不定义
__init__
方法,系统会提供一个默认的__init__
方法。如果我们定义了带参的__init__
方法,系统不创建默认的__init__
方法
Python中的
self
相当于C++中的self指针
,JAVA和C#中的this
关键字。Python中self
必须为构造函数的第一个参数,名字可以任意修改。但一般惯例,都叫做self
实例属性和实例方法
实例属性
实例属性是从属于实例对象的属性,也称为“实例变量”。他的使用有如下几个要点:
-
实例属性一般在
__init__()
方法中通过如下代码定义:self.实例属性名 = 初始值
-
在本类的其他实例方法中,也是通过
self
进行访问:self.实例属性名
-
创建实例对象后,通过实例对象访问:
obj01=类名()
#创建和初始化对象,调用__init__()
初始化属性obj01.实例属性名 = 值
#可以给已有属性赋值,也可以新加属性
class Student:
def __init__(self,name,score):
self.name = name # 增加name属性
self.score = score # 增加score属性
def say_score(self):
self.age = 18 # 增加age属性
print("{0}的分数是{1}".format(self.name,self.score))
s1 = Student("张三",80)
s1.say_score()
print(s1.age)
s1.salary = 3000 # s1对象增加salary属性
s2 = Student("李四",90)
s2.say_score()
print(s2.age)
实例方法
实例方法是从属于实例对象的方法。实例方法的定义格式如下:
def 方法名(self [, 形参列表]):
函数体
方法的调用格式如下:
对象.方法名([实参列表])
要点:
-
定义实例方法时,第一个参数必须为
self
。和前面一样,self
指当前的实例对象。 -
调用实例方法时,不需要也不能给
self
传参。self
由解释器自动传参
函数和方法的区别
- 都是用来完成一个功能的语句块,本质一样。
- 方法调用时,通过对象来调用。方法从属于特定实例对象,普通函数没有这个特点
- 直观上看,方法定义时需要传递
self
,函数不需要
实例对象的方法调用本质
其他操作
dir(obj)
可以获得对象的所有属性、方法obj.__dict__
对象的属性字典pass
空语句isinstance(对象,类型)
判断对象"是不是"指定类型”
类对象、类属性、类方法、静态方法
类对象
我们在前面讲的类定义格式中, class 类名:
。实际上,当解释器执行class
语句时,就会创建一个类对象
类属性
类属性是从属于“类对象"的属性,也称为“类变量”。由于,类属性从属于类对象,可以被所有实例对象共享。
类属性的定义方式:
class 类名:
类名变量 = 初始值
在类中或者类的外面,我们可以通过:类名.类变量名
来读写
内存分析实例对象和类对象创建过程(重要)
class Student:
school = "HNU" # 类属性
count = 0 # 类属性
def __init__(self, name, score):
self.name = name # 实例属性
self.score = score # 实例属性
Student.count = Student.count + 1
def say_score(self): # 实例方法
print("我的学校是:", Student.school)
print("{0}的分数是{1}".format(self.name, self.score))
s1 = Student("john", 61) # s1是实例对象,自动调用__init__()方法
s2 = Student("jack", 91)
s1.say_score()
print("一共创建了{0}个Student对象".format(Student.count))
"""
我的学校是: HNU
john的分数是61
一共创建了2个Student对象
"""
类方法
==类方法是从属于“类对象"的方法。==类方法通过装饰器@classmethod
来定义,格式如下:
@classmethod
def 类方法名(cls [, 形参列表]):
方法体
要点如下:
@classmethod
必须位于方法上面一行- 第一个
cls
必须有;cls
指的就是“类对象"本身 - 调用类方法格式:
类名.类方法名(参数列表)
。参数列表中,不需要也不能给cls
传值 - 类方法中访问实例属性和实例方法会导致错误
- 子类继承父类方法时,传入cls是子类对象,而非父类对象(讲完继承再说)
class Student:
school = "HNU" # 类属性
@classmethod
def printSchool(cls):
print(cls.school)
Student.printSchool()
静态方法
Python中允许定义与"类对象"无关的方法,称为“静态方法”。
“静态方法”和在模块中定义普通函数没有区别,只不过“静态方法”放到了“类的名字空间里面”,需要通过“类调用”。
静态方法通过装饰器@staticmethod
来定义,格式如下:
@staticmethod
def 静态方法名([形参列表]):
方法体
要点如下:
-
@staticmethod
必须位于方法上面一行 -
调用静态方法格式:
类名.静态方法名(参数列表)
-
静态方法中访问实例属性和实例方法会导致错误
class Student:
school = "HNU" # 类属性
@staticmethod
def add(a, b): # 静态方法
print("{0}+{1}={2}".format(a, b, a + b))
return a+b
Student.add(30, 40)
__del__
方法(析构函数)和垃圾回收机制
Python实现自动的垃圾回收
__del__()
称为“析构方法”,用于实现对象被销毁时所需的操作。比如:释放对象占用的资源,例如:打开的文件资源、网络连接等。
Python实现自动的垃圾回收,当对象没有被引用时(引用计数为0),由垃圾回收器调用__del__()
。
我们也可以通过del语句
删除对象,从而保证调用__del__()
。
系统会自动提供__del__
方法,一般不需要自定义析构方法。
# 析构函数
class Person:
def __del__(self):
print("销毁对象:{}".format(self))
p1 = Person()
p2 = Person()
del p2
print("程序结束")
"""
销毁对象:<__main__.Person object at 0x0000021E3FE14FA0>
程序结束
销毁对象:<__main__.Person object at 0x0000021E3FE14FD0>
"""
call方法和可调用对象
Python中,凡是可以将()
直接应用到自身并执行,都称为可调用对象。
可调用对象包括自定义的函数、Python内置函数、以及本节所讲的实例对象。
定义了__call__()
的对象,称为==“可调用对象”,即该对象可以像函数一样被调用==。
该方法使得实例对象可以像调用普通函数那样,以"对象名()"
的形式使用。
def f1():
print("f1")
f1() # 本质也是调用了__call__()方法
"""f1"""
class Car:
def __call__(self, age, money):
print("call方法")
print("车龄{0},金额{1}".format(age, money))
c = Car()
c(3, 20000)
"""
call方法
车龄3,金额20000
"""
方法没有重载
如果我们在类体中定义了多个重名的方法,只有最后一个方法有效。
建议:不要使用重名的方法!Python中方法没有重载。
在其他一些语言(比如:Java)中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含3个部分:方法名、参数数量、参数类型。
Python中,方法的的参数没有声明类型(调用时确定参数的类型),参数的数量也可以由可变参数控制。因此,Python中是没有方法的重载的。
方法的动态性
Python是动态语言,我们可以动态的为类添加新的方法,或者动态的修改类的已有的方法
#测试方法的动态性
class Person:
def work(self):
print("努力上班!")
def play_game(self):
print("玩游戏")
def work2(s):
print("好好工作,努力上班!")
Person.play = play_game
Person.work = work2
p = Person()
p.play()
p.work()
我们可以看到, Person 动态的新增了 play_game
方法,以及用 work2
替换了 work
方法
私有属性和私有方法(实现封装)
Pytho对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。关于私有属性和私有方法,有如下要点:
①通常我们约定,两个下划线开头的属性是私有的(private)。其他为公 共的(public)。
②类内部可以访问私有属性(方法)
③类外部不能直接访问私有属性(方法)
④类外部可以通过 _类名__私有属性(方法)名
访问私有属性(方法)
【注】==方法本质上也是属性!==只不过是可以通过()执行而已。
所以,此处讲的私有属性和公有属性,也同时讲解了私有方法和公有方法的用法。
如下测试中,同时也包含了私有方法和公有方法的例子。
class Employee:
__company = "Ailibab" # 私有属性,解释器运行时把__company转化成_Employee__company
def __init__(self, name, age):
self.name = name
self.__age = age # 私有属性
def say_company(self):
print("我的公司名字是:", Employee.__company)
print("我的年龄是:", self.__age)
def __work(self): # 私有方法
print("好好工作")
print(Employee._Employee__company)
a = Employee("haha", 20)
a.say_company()
print(a._Employee__age) # 调用私有属性
a._Employee__work() # 调用私有方法
"""
Ailibab
我的公司名字是: Ailibab
我的年龄是: 20
20
好好工作
"""
@property装饰器
@property
可以将一个方法的调用方式变成“属性调用”。
@property
主要用于帮助我们处理属性的读操作、写操作。对于某一个属性,我们可以直接通过:
emp1.salary= 30000
如上的操作读操作、写操作。但是,这种做法不安全。比如,我需要限制薪水必须为1-10000
的数字。这时候,我们就需要通过使用装饰器@property
来处理。
class Employee:
def __init__(self, name, salary):
self.name = name
self.__salary = salary
@property # 只能读 print(emp1.salary) 相当于属性的调用
def salary(self):
print("薪资是:", self.__salary)
return self.__salary
@salary.setter # 修改使用这个函数 emp1.salary = 50000
def salary(self, salary):
if 0 < salary < 100000:
self.__salary = salary
else:
print("薪资录入错误!只能在0-100000之间")
emp1 = Employee("john", 20000)
emp1.salary = 50000
print(emp1.salary)
emp1.salary = 100000000
print(emp1.salary)
"""
薪资是: 50000
50000
薪资录入错误!只能在0-100000之间
薪资是: 50000
50000
"""
属性和方法命名总结
_xxx
:保护成员,不能用from module import *
导入,只有类对象和子类对象能访问这些成员。
__xxx__
:系统定义的特殊成员
__xxx
:类中的私有成员,只有类对象自己能访问,子类对象也不能访问。(但,在类外部可以通过 对像名._类名__xxx
这种特殊方式访问。Python不存在严格意义的私有成员)
⚠️再次强调,方法和属性都遵循上面的规则。
类编码风格
- 类名首字母大写,多个单词之间采用驼峰原则。
- 实例名、模块名采用小写,多个单词之间采用下划线隔开
- 每个类,应紧跟“文档字符串”,说明这个类的作用
- 可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类
None对象的特殊性
None是什么?
-
与C和JAVA不同,Pythont中是没有
NULL
的,取而代之的是None
。 -
None
是一个特殊的常量,表示变量没有指向任何对象。 -
在Python中,
None
本身实际上也是对象,有自己的类型NoneType
。 -
你可以将
None
赋值给任何变量,但我们不能创建NoneType
类型的对象
None不是False,None不是0,None不是空字符串。None和任何其他的数据类型比较永远返回False。
None和其他类型的比较
None和其他任何类型比较都会返回False
空列表、空字符串、0之间的比较
- if语句判断时,
空列表[]
、空字典{}
、空元组()
、空字符串
、0
、None
等一系列代表空和无的对象会被转换成False
==
和is
判断时,空列表、空字符串不会自动转成False
面向对象的三大特征说明(封装、继承、多态)
Python是面向对象的语言,支持面向对象编程的三大特性:继承、封装(隐藏)、多态。
封装(隐藏)
隐藏对象的属性和实现细节,只对外提供必要的方法。相当于将“细节封装起来”,只对外暴露”相关调用方法”。
通过前面学习的“私有属性、私有方法"的方式,实现封装”。Pythoni追求简洁的语法,没有严格的语法级别的“访问控制符”,更多的是依靠程序员自觉实现。
继承
继承可以让子类具有父类的特性,提高了代码的重用性。
从设计上是一种增量进化,原有父类设计不变的情况下,可以增加新的功能,或者改进已有的算法。
多态
多态是指同一个方法调用由于对象不同会产生不同的行为。
生活中这样的例子比比皆是:同样是休息方法,人不同休息方法不同。张三休息是睡觉,李四休息是玩游戏,程序员休息是“敲几行代码”。
继承详解
子类扩展父类
继承是面向对象编程的三大特征之一。继承让我们更加容易实现类的扩展。实现代码的重用,不用再重新发明轮子(don’t reinvent wheels)。
如果一个新类继承自一个设计好的类,就直接具备了已有类的特征,就大大降低了工作难度。已有的类,我们称为“父类或者基类”。新的类,我们称为“子类或者派生类”。
语法格式
Python支持多重继承,一个子类可以继承多个父类。继承的语法格式如下:
class 子类类名(父类1[, 父类2, ...]):
类体
如果在类定义中没有指定父类,则默认父类是
object类
。也就是说,object
是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__()
关于构造函数:
-
子类不重写
__init__
,实例化子类时,会自动调用父类定义的__init__
。class Person: def __init__(self, name, age): print("创建Person") self.name = name self.age = age def say_age(self): print("{0}的年龄是{1}".format(self.name, self.age)) class Student(Person): pass s1 = Student("haha", 20) s1.say_age() print(dir(s1)) """ 创建Person haha的年龄是20 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age'] """
-
子类重写了
__init__
时,实例化子类,就不会调用父类已经定义的__init__
class Person: def __init__(self, name, age): print("创建Person") self.name = name self.age = age def say_age(self): print("{0}的年龄是{1}".format(self.name, self.age)) class Student(Person): def __init__(self, name, age, score): # 两种调用父类的构造方法 # 1. # Person.__init__(self, name, age) # 2. super(Student, self).__init__(name, age) print("创建Student") self.score = score def say_score(self): print("我的分数:", self.score) s1 = Student("haha", 20, 90) s1.say_score() print(dir(s1)) """ 创建Person 创建Student 我的分数: 90 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age', 'say_score', 'score'] """
-
如果重写了
__init__
时,要使用父类的构造方法,可以使用super
关键字,也可以使用如下格式调用:
父类名.__init__(self, 参数列表)
class Person: def __init__(self, name, age): print("创建Person") self.name = name self.age = age def say_age(self): print("{0}的年龄是{1}".format(self.name, self.age)) class Student(Person): def __init__(self, name, age, score): # 两种调用父类的构造方法 # 1. # Person.__init__(self, name, age) # 2. super(Student, self).__init__(name, age) print("创建Student") self.score = score s1 = Student("haha", 20, 90) print(dir(s1)) """ 创建Person 创建Student ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age', 'score'] """
类成员继承和重写
成员继承:子类继承了父类除构造方法(__init__()
)之外的所有成员。
⚠️私有属性、私有方法也被继承
class Person:
def __init__(self, name, age):
print("创建Person")
self.name = name
self.__age = age # 私有属性也会被继承
def say_age(self):
print("{0}的年龄是{1}".format(self.name, self.__age))
class Student(Person):
def __init__(self, name, age, score):
# 两种调用父类的构造方法
# 1.
# Person.__init__(self, name, age)
# 2.
super(Student, self).__init__(name, age)
print("创建Student")
self.score = score
def say_score(self):
print("我的分数:", self.score)
s1 = Student("haha", 20, 90)
s1.say_age()
s1.say_score()
print(dir(s1))
"""
创建Person
创建Student
haha的年龄是20
我的分数: 90
['_Person__age', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'name', 'say_age', 'say_score', 'score']
"""
方法重写:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为“重写”
class Person:
def __init__(self, name, age):
print("创建Person")
self.name = name
self.age = age
def say_age(self):
print("{0}的年龄是{1}".format(self.name, self.age))
def say_name(self):
print("我的名字是:", self.name)
class Student(Person):
def __init__(self, name, age, score):
Person.__init__(self, name, age)
print("创建Student")
self.score = score
def say_score(self):
print("我的分数:", self.score)
def say_name(self): # 重写父类的方法
print("hello ,my name is ", self.name)
s1 = Student("haha", 20, 90)
s1.say_age()
s1.say_score()
s1.say_name()
print(dir(s1))
"""
创建Person
创建Student
haha的年龄是20
我的分数: 90
hello ,my name is haha
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age', 'say_name', 'say_score', 'score']
"""
查看类的继承层次结构
通过类的方法mro()
或者类的属性__mro__
可以输出这个类的继承层次结构。
class A:
pass
class B(A):
pass
class C(B):
pass
print(C.mro())
"""
[<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]
"""
object根类
object
类是所有类的父类,因此所有的类都有object
类的属性和方法。我们显然有必要深入研究一下object
类的结构。对于我们继续深入学习Python很有好处。
dir()
查看对象属性
为了深入学习对象,先学习内置函数dir()
,他可以让我们方便的看到指定对象所有的属性
⚠️快捷键 Alt+ 7 打开模块结构
class Person:
def __init__(self, name, age):
print("创建Person")
self.name = name
self.age = age
def say_age(self):
print("{0}的年龄是{1}".format(self.name, self.age))
# 快捷键 Alt+ 7 打开模块结构
obj = object()
print(dir(obj))
s2 = Person("haha", 20)
print(dir(s2))
# 方法的本质也是是属性
print(s2.say_age)
print(type(s2.say_age))
"""
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
创建Person
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'age', 'name', 'say_age']
<bound method Person.say_age of <__main__.Person object at 0x00000222E819BFD0>>
<class 'method'>
"""
从上面我们可以发现这样几个要点:
- Person对象增加了六个属性:
__dict__
__module__
__weakref__
age
name
say_age
object
的所有属性,Person
类作为object
的子类,显然包含了所有的属性- 我们打印
age
、name
、say_age
,发现say_age
虽然是方法,实际上也是属性。只不过这个属性的类型是method而已。
age <class 'int'>
name <class 'str'>
say_age <class 'method'>
重写__str__()
方法
object
有一个__str()__
方法,用于返回一个对于"对象的描述”。内置函数str(对象)
,调用的就是__str()__
__str()__
经常用于print()
方法,帮助我们查看对象的信息。__str()__
可以重写
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
"""将对象转化成一个字符串描述,一般用于print方法"""
print("重写str方法")
return "名字是:{0},年龄是{1}".format(self.name, self.age)
p = Person("haha", 20)
print(p)
s = str(p)
"""
重写str方法
名字是:haha,年龄是20
重写str方法
"""
多重继承
Pytho支持多重继承,一个子类可以有多个"直接父类"。这样,就具备了"多个父类"的特点。但是由于,这样会被“类的整体层次"”搞的异常复杂,尽量避免使用。
class A:
def aa(self):
print("aa")
class B:
def bb(self):
print("bb")
class C(B,A):
def cc(self):
print("cc")
c = C()
c.cc()
c.bb()
c.aa()
类结构为:
MRO方法解析顺序
Pythor支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将**“从左向右”**按顺序搜索。
MRO(Method Resolution Order):方法解析顺序。我们可以通过mro()
方法获得"类的层次结构",方法解析顺序也是按照这个“类的层次结构”寻找的。
super()获得父类的定义
在子类中,如果想要获得父类的方法时,我们可以通过super()
来做。
super()
代表父类的定义,不是父类对象。
想调用父类的构造方法:
super(子类名称, self).__init__(参数列表)
class Person:
def __init__(self, name, age):
print("创建Person")
self.name = name
self.__age = age # 私有属性也会被继承
def say_age(self):
print("{0}的年龄是{1}".format(self.name, self.__age))
class Student(Person):
def __init__(self, name, age, score):
# Person.__init__(self)
super(Student, self).__init__(name, age) # 调用父类的构造方法
print("创建Student")
self.score = score
def say_age(self):
# Person.say_age(self)
super().say_age() # 通过super()调用父类的方法
多态详解
多态(polymorphism)是指同一个方法调用由于对象不同可能会产生不同的行为。
关于多态要注意以下2点:
- 多态是方法的多态,属性没有多态。
- 多态的存在有2个必要条件:继承、方法重写
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() # 会产生多态,传入对象不同,则调用方法不同
animalShout(Dog())
animalShout(Cat())
"""
小狗叫了一声
小猫叫了一声
"""
特殊方法和运算符重载
特殊属性
对象的浅拷贝和深拷贝
浅拷贝
Python拷贝一般都是浅拷贝。
浅拷贝:拷贝时,拷贝源对象,但对象包含的子对象内容不拷贝。
深拷贝
使用 copy
模块的 deepcopy
函数,递归拷贝对象中包含的子对象。
深拷贝:拷贝时,拷贝源对象,也递归拷贝对象中包含的子对象
组合
除了继承,“组合”也能实现代码的复用!“组合"核心是“将父类对象作为子类的属性”。
is-a
关系,我们可以使用"继承”。从而实现子类拥有的父类的方法和属性。is-a
关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。
has-a
关系,我们可以使用"组合”,也能实现一个类拥有另一个类的方法和属性。has-a
关系指的是这样的关系:手机拥有CPU。MobilePhone has a CPU
设计模式
设计模式是面向对象语言特有的内容,是我们在面临某一类问题时候固定的做法,设计模式有很多种,比较流行的是:GOF(Goup Of Four)23种设计模式。当然,我们没有必要全部学习,学习几个常用的即可。
对于初学者,我们学习两个最常用的模式:工厂模式和单例模式。
工厂模式实现
工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。
# 工厂模式实现
class Benz: pass
class BMW: pass
class BYD: pass
class CarFactory:
def createCar(self, brand):
if brand == "宝马":
return BMW()
elif brand == "奔驰":
return Benz()
elif brand == "比亚迪":
return BYD()
else:
return "未知品牌,无法创建"
factory = CarFactory()
c1 = factory.createCar("奔驰")
c2 = factory.createCar("宝马")
print(c1)
print(c2)
"""
<__main__.Benz object at 0x0000021C16D12FA0>
<__main__.BMW object at 0x0000021C16D12F70>
"""
单例模式实现
单例模式(Singleton Pattern)的核心作用是确保一个类只有一个实例,并且提供一个访问该实例的全局访问点。
单例模式只生成一个实例对象,减少了对系统资源的开销。当一个对象的产生需要比较多的资源,如读取配置文件、产生其他依赖对象时,可以产生一个“单例对象”,然后永久驻留内存中,从而极大的降低开销。
单例模式有多种实现的方式,我们这里推荐重写__new__()的方法。
# 单例模式实现
class MySingleton:
__obj = None
__init_flag = True
def __new__(cls, *args, **kwargs):
if cls.__obj is None:
cls.__obj = object.__new__(cls)
return cls.__obj
def __init__(self, name):
if MySingleton.__init_flag:
print("初始化第一个对象...")
self.name = name
MySingleton.__init_flag = False
a = MySingleton("aa")
print(a)
b = MySingleton("bb")
print(b)
"""
初始化第一个对象...
<__main__.MySingleton object at 0x000001E70B8E2FA0>
<__main__.MySingleton object at 0x000001E70B8E2FA0>
"""
工厂和单例模式结合起来
设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现:
# 工厂和单例模式结合
class CarFactory:
__obj = None
__init_flag = True
def __new__(cls, *args, **kwargs):
if cls.__obj is None:
cls.__obj = object.__new__(cls)
return cls.__obj
def __init__(self):
if CarFactory.__init_flag:
print("初始化第一个对象...")
CarFactory.__init_flag = False
def createCar(self, brand):
if brand == "宝马":
return BMW()
elif brand == "奔驰":
return Benz()
elif brand == "比亚迪":
return BYD()
else:
return "未知品牌,无法创建"
class Benz:
pass
class BMW:
pass
class BYD:
pass
factory = CarFactory()
c1 = factory.createCar("奔驰")
c2 = factory.createCar("宝马")
print(c1)
print(c2)
factory2 = CarFactory()
print(factory)
print(factory2)
"""
初始化第一个对象...
<__main__.Benz object at 0x0000018C683F3F70>
<__main__.BMW object at 0x0000018C683F3F10>
<__main__.CarFactory object at 0x0000018C683F3FA0>
<__main__.CarFactory object at 0x0000018C683F3FA0>
"""
Python开发环境搭建
环境配置参考这篇文章
Python环境配置保姆教程(Anaconda、Jupyter、GPU环境)!
内容包括:
- Anaconda的安装与常用命令小总
- Jupyter的安装与相关配置
- CUDA与Cudnn的安装(GPU支持必备)
- 建立tf虚拟环境并安装tf2.0GPU版本
- 建立pytorch虚拟环境并安装pytorchGPU版本