Python入门
Python入门
编程基本概念
Python程序的构成
- Python程序由模块组成。一个模块对应python源文件,一般后缀名是:.py
- 模块由语句组成。运行Python程序时,按照模块中语句的顺序依次执行
- 语句是Python程序的构造单元,用于创建对象、变量赋值、调用函数、控制语句等
代码的组织和缩进
很多编程语言通过字符(例如:花括号{})、关键字(例如:begain/end)来划分代码块。同时,在配合代码的缩进增加可读性。“龟叔”设计Python语言时,直接通过缩进来组织代码块。“缩进”成为了Python语法强制的规定。缩进时,几个空格都是允许的,但是数目必须统一。通常采用“四个空格”表示一个缩进。
同时,也要避免将“tab制表符”或者tab与空格混合的缩进风格。目前,常用的编辑器一般设置成:tab制表符就是4个空格
使用\
行连接符
一行程序长度是没有限制的,但是为了可读性更强,通常将一行比较长的程序分为多行。这是,我们可以使用\
行连接符,把它放在行结束的地方。Python解释器仍然将它们解释为同一行。
1 | a = [10,20,30,40,\ |
对象
- Python中,一切皆对象。
- 每个对象由:标识(identity)、类型(type)、value(值)组成
- 标识用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数
id(obj)
可返回对象obj
的标识。- 类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用
type(obj)
获得对象的所属类型。- 值表示对象所存储的数据的信息。使用
print(obj)
可以直接打印出值。对象的本质就是:
一个内存块,拥有特定的值,支持特定类型的相关操作
1 | a=3 |
内存示意图
引用
在Python中,变量也称为:对象的引用(reference)。变量存储的就是对象的地址。
变量通过地址引用了“对象”。
变量位于:栈内存(压栈出栈等细节,后续再介绍)。
对象位于:堆内存。
如下源代码对应的内存图:
1 | a=3 |
==Python是动态类型语言==:变量不需要显式声明类型。根据变量引用的对象,Python解释器自动确定数据类型
标识符规则
基本用法
标识符规则:用于变量、函数、类、模块等的名称。标识符有如下特定的规则:
区分大小写。如:
sxt
和SXT
是不同的第一个字符必须是字母、下划线。其后的字符是:字母、数字、下划线
不能使用关键字。比如:
if
、or
、while
等以双下划线开头和结尾的名称通常有特殊含义,尽量避免这种写法。比如:
__init__
是类的构造函数
Python标识符命名规则
变量和简单赋值语句
变量的声明和赋值
变量的声明和赋值:用于将一个变量绑定到一个对象上,格式: 变量名 = 表达式
最简单的表达式就是字面量。比如: a = 123
。运行过程中,解释器先运行右边的表达式,生成一个代表表达式运算结果的对象;然后,将这个对象地址赋值给左边的变量。
:warning:变量在使用前必须先被初始化(先被赋值)
删除变量和垃圾回收机制
- 可以通过del语句删除不再使用的变量。
- 如果对象没有变量引用,就会被垃圾回收器回收,清空内存空间。
【操作】删除变量示例
1 | a=123 |
报错如下:
常量
Python不支持常量,即没有语法规则限制改变一个常量的值。我们只能约定常量的命名规则,以及在程序的逻辑上不对常量的值作出修改。
1 | MAX_SPEED = 120 |
链式赋值
系列解包赋值
系列数据赋值给对应相同个数的变量(个数必须保持一致)
a,b,c=4,5,6
相当于: a=4;b=5;c=6
【操作】使用系列解包赋值实现变量值交换
1 | a,b=1,2 |
使用系列解包复制可以轻松实现变量值交换
最基本内置数据类型
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位,可以存储:-2^63—2^63-1之间的数值。
Python3中, int
可以存储任意大小的整数, long
被取消。
Python3中可以做超大数的计算,而不会造成“整数溢出”,这也是Python特别适合科学运算的特点
浮点数 float
- 浮点数用科学计数法表示。比如:
3.14
,表示成:314E-2
或者314e-2
- 这些数字在内存中也是按照科学计数法存储。
类型转换和四舍五入
- 类似于
int()
,我们也可以使用float()
将其他类型转化成浮点数。 - 整数和浮点数混合运算时,表达式结果自动转型成浮点数。比如:
2+8.0
的结果是10.0
round(value)
可以返回四舍五入的值。但不会改变原有值,而是产生新的值
==round(value))可以返回四舍五入的值==。但不会改变原有值,而是产生新的值
增强型赋值运算符
运算符 +
、 -
、 *
, /
、 //
、 **
和 %
和赋值符 =
结合可以构成“增强型赋值运算符”。
:warning:注意:
- “+=”中间不能加空格!
- 结合的是右侧整个表达式:
1 y *= x+2 # 相当于:y = y*(x+2) 而不是:y = y*x+2
时间的表示
python中可以通过time.time()
获得当前时刻,返回的值是以秒为单位,带微秒(1/1000毫秒)精度的浮点值。例如:1635063628.5632517
1 | import time |
布尔值
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。
1 | a = True |
运算符
逻辑运算符
1 | #测试逻辑运算符 |
比较运算符
所有比较运算符返回 1
表示真,返回 0
表示假。这分别与特殊变量True
和 False
等价。
以下假设变量 a为15 ,变量 b为30 :
1 | a = 4 |
关系运算符可以连用 如: 2<a<10
位运算符
按位运算符是把数字看作二进制来进行计算的。Python中的按位运算法则如表所示。
1 | a = 0b11001 |
加法操作补充
- 数字相加
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]
增强赋值运算符(补充)
复合赋值可以让程序更加精炼,提高效率。
:warning:与C和JAVA不一样,Python不支持自增(++)和自减(—)
同一运算符
同一运算符用于比较两个对象的存储单元,实际比较的是对象的地址。
is
与 ==
区别:
is
用于判断两个变量引用对象是否为同一个,既比较对象的地址。==
用于判断引用变量引用对象的值是否相等,默认调用对象的__eq__()
方法。
总结
is
比较两个对象的id
值是否相等,是否指向同一个内存地址==
比较的是两个对象的内容是否相等,值是否相等is
运算符比==
效率高,在变量和None
进行比较时,应该使用is
1 | a=20 |
【操作】同一运算符测试
1 | a = 1000 |
整数缓存问题
- 命令行模式下,Python仅仅对比较小的整数对象进行缓存(范围为
[-5, 256]
)缓存起来[C语言底层用数组实现,连续分配空间,便于查找 ]
,而并非是所有整数对象。 - 文件模式下,所有数字都会被缓存,范围是:
[-无穷大,+无穷大]
- 缓存实现:
[-5,256]
仍然底层用数组实现 ;不在[-5,256]
出现的数,缓存到链表中,不连续分配空间 。
成员运算符
成员运算符测试实例中包含了一系列的成员,包括字符串,列表或元组。
1 | a = "python" |
运算符优先级问题
如下优先级,从高到低。
1 | #测试运算符优先级 |
实际使用中,记住如下简单的规则即可,复杂的表达式一定要使用小括号组织。
- 乘除优先加减
- 位运算和算术运算>比较运算符>赋值运算符>逻辑运算符
基本运算符总结
序列
序列的本质和内存结构
序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放多个值的连续的内存空间。比如一个整数序列[10,20,30,40]
,示意表示:
由于Python3中一切皆对象,在内存中实际是按照如下方式存储的:
==序列中存储的是整数对象的地址,而不是整数对象的值==
字符串
字符串基本特点
- 字符串的本质是:字符序列。
- Python不支持单字符类型,单字符也是作为一个字符串使用的。
:warning:Python的==字符串是不可变的==,我们无法对原字符串做任何修改。但,可以将字符串的一部分复制到新创建的字符串,达到“看起来修改”的效果。
字符串的编码
Python3直接支持Unicode,可以表示世界上任何书面语言的字符。Python3的字符默认就是16位Unicode编码,ASCII码是Unicode编码的子集。
使用内置函数ord()可以把字符转换成对应的Unicode码;
使用内置函数chr()可以把十进制数字转换成对应的字符。
1 | ord('A') #65 |
引号创建字符串
我们可以通过单引号或双引号创建字符串。例如: a='abc'
b="sxt"
使用两种引号的好处是可以创建本身就包含引号的字符串,而不用使用转义字符。例如:
1 | a = "I'm a teacher!" |
连续三个单引号或三个双引号,可以帮助我们创建多行字符串。在长字符串中会保留原始的格式。例如:
1 | s=''' |
空字符串和len()函数
Python允许空字符串的存在,不包含任何字符且长度为0。例如:
1 | c = '' |
len()用于计算字符串含有多少字符。例如:
1 | d = 'abc尚学堂' |
转义字符
我们可以使用 +特殊字符 ,实现某些难以用字符表示的效果。比如:换行等。常见的转义字符有这些:
【操作】测试转义字符的使用
1 | a = 'I\nlove\nU' |
字符串拼接
- 可以使用
+
将多个字符串拼接起来。例如:'aa' +'bb'
结果是'aabb'
- 如果
+
两边都是字符串,则拼接。 - 如果
+
两边都是数字,则加法运算 - 如果
+
两边类型不同,则抛出异常
- 如果
- 可以将多个字面字符串直接放到一起实现拼接。例如:
'aa' 'bb'
结果是'aabb'
【操作】字符串拼接操作
1 | a = 'sxt'+'gaoqi' #结果是:'sxtgaoqi' |
字符串复制
使用*
可以实现字符串复制
1 | a = 'Sxt'*3 # 结果:'SxtSxtSxt' |
不换行打印
我们前面调用print时,会自动打印一个换行符。有时,我们不想换行,不想自动添加换行符。我们可以自己通过参数end = "任意字符串"
。实现末尾添加任何内容:
1 | print("sxt",end=' ') |
从控制台读取字符串
我们可以使用input()从控制台读取键盘输入的内容。
1 | myname = input("请输入名字:") |
replace()
实现字符串替换
==字符串是不可变的==,我们通过[]可以获取字符串指定位置的字符,但是我们不能改变字符串。我们尝试改变字符串中某个字符,
发现报错了:
1 | 'abcdefghijklmnopqrstuvwxyz' a = |
字符串不可改变。但是,我们确实有时候需要替换某些字符。这时,只能通过创建新的字符串来实现。
1 | 'abcdefghijklmnopqrstuvwxyz' a = |
整个过程中,实际上我们是创建了新的字符串对象,并指向了变量a,而不是修改了以前的字符串。 内存图如下:
str()
实现数字转型字符串
str()可以帮助我们将其他数据类型转换为字符串。例如:
1 | a = str(5.20) # 结果是:a = '5.20' |
当我们调用print()函数时,解释器自动调用了str()将非字符串的对象转成了字符串。
使用[]
提取字符
字符串的本质就是字符序列,我们可以通过在字符串后面添加[]
,在[]
里面指定偏移量,可以提取该位置的单个字符。
正向搜索:
最左侧第一个字符,偏移量是
0
,第二个偏移量是1
,以此类推。直到len(str)-1
为止。反向搜索:
最右侧第一个字符,偏移量是
-1
,倒数第二个偏移量是-2
,以此类推,直到-len(str)
为止。
【操作】使用[]
提取字符串中的字符
1 | 'abcdefghijklmnopqrstuvwxyz' a = |
字符串切片slice
操作
切片slice操作可以让我们快速的提取子字符串。标准格式为:
[起始偏移量start: 终止偏移量end: 步长step]
典型操作(三个量为正数的情况)如下:==包头不包尾==
其他操作(三个量为负数)的情况:
字符串逆序 [::-1]
切片操作时,起始偏移量和终止偏移量不在[0,字符串长度-1]
这个范围,也不会报错。起始偏移量小于0则会当做0,终止偏移量大于“长度-1”会被当成-1。例如:
1 | "abcdefg"[3:50] |
我们发现正常输出了结果,没有报错。
split()
分割和join()
合并
split()
可以基于指定分隔符将字符串分隔成多个子字符串(存储到列表中)。如果不指定分隔符,则默认使用空白字符(换行符/空格/制表符)。示例代码如下:
1 | "to be or not to be" a = |
join()
的作用和split()
作用刚好相反,用于将一系列子字符串连接起来。示例代码如下:
1 | 'sxt','sxt100','sxt200'] a = [ |
拼接字符串要点:
使用字符串拼接符
+
,会生成新的字符串对象,因此==不推荐使用+
来拼接字符串==。==推荐使用join
函数==,因为join
函数在拼接字符串之前会计算所有字符串的长度,然后逐一拷贝,仅新建一次对象。
==join拼接字符串效率高==
字符串驻留机制
字符串驻留:常量字符串只保留一次
1 | c = "dd#" |
字符串比较和同一性
我们可以直接使用 ==
!=
对字符串进行比较,是否含有相同的字符。
我们使用 is
not is
,判断两个对象是否同一个对象。比较的是对象的地址,即 id(obj1)
是否和 id(obj2)
相等。
== 和!=比较是否含有相同字符
is和not is判断是否同一对象
成员操作符判断子字符串
in
not in
关键字,判断某个字符(子字符串)是否存在于字符串中。
1 | "ab" in "abcdefg" #true |
字符串常用方法汇总
常用查找方法
去除首尾信息
我们可以通过strip()
去除字符串首尾指定信息。通过lstrip()
去除字符串左边指定信息,rstrip()
去除字符串右边指定信息。
【操作】去除字符串首尾信息
1 | "*s*x*t*".strip("*") |
大小写转换
编程中关于字符串大小写转换的情况,经常遇到。我们将相关方法汇总到这里。为了方便学习,先设定一个测试变量:
1 | a = "gaoqi love programming, love SXT" |
格式排版
center()
、 ljust()
、 rjust()
这三个函数用于对字符串实现排版。示例如下:
1 | "SXT" a= |
特征判断方法
- isalnum() 是否为字母或数字
- isalpha() 检测字符串是否只由字母组成(含汉字)
- isdigit() 检测字符串是否只由数字组成
- isspace()检测是否为空白符
- isupper() 是否为大写字母
- islower() 是否为小写字母
1 | "sxt100".isalnum() |
字符串的格式化
format()
基本用法
基本语法是通过 {}
和 : 来代替以前的 %
。
format()
函数可以接受不限个数的参数,位置可以不按顺序。
1 | "名字是:{0},年龄是:{1}" a = |
我们可以通过{索引}/{参数名},直接映射参数值,实现对字符串的格式化,非常方便。
填充与对齐
- 填充常跟对齐一起使用
^
、<
、>
分别是居中、左对齐、右对齐,后面带宽度:
号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充
1 | "{:*>8}".format("245") |
数字格式化
浮点数通过 f
,整数通过 d
进行需要的格式化。案例如下:
1 | "我是{0},我的存款有{1:.2f}" a = |
可变字符串
- Python中,字符串属于不可变对象,不支持原地修改,如果需要修改其中的值,只能创建新的字符串对象。
- 确实需要原地修改字符串,可以使用
io.StringIO
对象或array
模块
使用io.StringIO
可以将字符串变为可变字符串
1 | import io |
类型转换总结
与C++、Java等高级程序设计语言一样,Python语言同样也支持数据类型转换。
1 | #类型转换 |
列表
列表简介
列表:用于存储任意数目、任意类型的数据集合。
列表是内置可变序列,是包含多个元素的有序连续的内存空间。列表的标准语法格式:
a = [10,20,30,40]
其中,10,20,30,40这些称为:列表a的元素。
列表中的元素可以各不相同,可以是任意类型。比如:
a = [10,20,'abc',True]
Python的列表大小可变,根据需要随时增加或缩小。
列表对象的常用方法
字符串和列表都是序列类型,一个字符串是一个字符序列,一个列表是任何元素的序列。我们前面学习的很多字符串的方法,在列表中也有类似的用法,几乎一模一样。
创建列表的4种方式
基本语法 []
创建
1 | a = [10,20,'gaoqi','sxt'] |
list()
创建
使用list()
可以将任何可迭代的数据转化成列表。
1 | a = list() #创建一个空的列表对象 |
range()
创建整数列表
range()
可以帮助我们非常方便的创建整数列表,这在开发中及其有用。语法格式为:
range([start,] end [,step])
start参数:可选,表示起始数字。默认是0
end参数:必选,表示结尾数字。
step参数:可选,表示步长,默认为1
:warning:python3中
range()
返回的是一个range对象,而不是列表。我们需要通过list()
方法将其转换成列表对象。
1 | a = list(range(3,15,2)) # 结果:[3, 5, 7, 9, 11, 13] |
推导式生成列表
(简介一下,重点在for循环后讲)
使用列表推导式可以非常方便的创建列表,在开发中经常使用。
1 | # 循环创建多个元素 [0, 2, 4, 6, 8] |
增加列表元素的5种方式
当列表增加和删除元素时,列表会自动进行内存管理,大大减少了程序员的负担。但这个特点涉及列表元素的大量移动,效率较低。
:warning:除非必要,我们一般只在列表的尾部添加元素或删除元素,这会大大提高列表的操作效率。
append()
方法
原地修改列表对象,是真正的列表尾部添加新的元素,速度最快,推荐使用。
1 | a = [20,40] |
+
运算符操作
并不是真正的尾部添加元素,而是创建新的列表对象;将原列表的元素和新列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用。
1 | a = [20,40] |
通过如上测试,我们发现变量a的地址发生了变化。也就是创建了新的列表对象。
extend()
方法
将目标列表的所有元素添加到本列表的尾部,属于原地操作,不创建新的列表对象。
1 | a = [20,40] |
insert()
插入元素
使用 insert()
方法可以将指定的元素插入到列表对象的任意制定位置。这样会让插入位置后面所有的元素进行移动,会影响处理速度。涉及大量元素时,尽量避免使用。类似发生这种移动的函数还有:remove()
、 pop()
、 del()
,它们在删除非尾部元素时也会发生操作位置后面元素的移动。
1 | a = [10,20,30] |
乘法扩展
使用乘法扩展列表,生成一个新列表,新列表元素是原列表元素的多次重复。
1 | a = ['sxt',100] |
适用于乘法操作的,还有:字符串、元组。例如:
1 | c = 'sxt' |
列表元素的删除
del 删除
删除列表指定位置的元素。
del()传的是索引
1 | a = [100,200,888,300,400] |
pop()
方法
pop()删除并返回指定位置元素,如果未指定位置则默认操作列表最后一个元素。
pop()传的是空或索引
1 | a = [10,20,30,40,50] |
remove()
方法
删除首次出现的指定元素,若不存在该元素抛出异常。
remove() 传的是元素
1 | a = [10,20,30,40,50,20,30,20,30] |
列表元素访问和计数
通过索引直接访问元素
我们可以通过索引直接访问元素。索引的区间在 [0, 列表长度-1]
这个范围。超过这个范围则会抛出异常。
1 | a = [10,20,30,40,50,20,30,20,30] |
index()
获得指定元素在列表中首次出现的索引
index()
可以获取指定元素首次出现的索引位置。语法是: index(value,[start,[end]])
。其中, start
和 end
指定了搜索的范围。
1 | 10,20,30,40,50,20,30,20,30] a = [ |
count()
获得指定元素在列表中出现的次数
count()
可以返回指定元素在列表中出现的次数。
1 | 10,20,30,40,50,20,30,20,30] a = [ |
len()
返回列表长度
len()
返回列表长度,即列表中包含元素的个数。
1 | 10,20,30] a = [ |
成员资格判断
判断列表中是否存在指定的元素,我们可以使用 count()
方法,返回0则表示不存在,返回大于0则表示存在。但是,一般我们会使用更加简洁的 in
关键字来判断,直接返回 True
或 False
1 | 10,20,30,40,50,20,30,20,30] a = [ |
切片操作
类似字符串的切片操作,对于列表的切片操作和字符串类似。
切片是Python序列及其重要的操作,适用于列表、元组、字符串等等。
切片slice
操作可以让我们快速提取子列表或修改。标准格式为:
[起始偏移量start:终止偏移量end[:步长step]]
==包头不包尾==
典型操作(三个量为正数的情况)如下:
其他操作(三个量为负数)的情况:
切片操作时,起始偏移量和终止偏移量不在
[0,字符串长度-1]
这个范围,也不会报错。起始偏移量 小于0 则会当做 0 ,终止偏移量大于”长度-1” 会被当成 “长度-1” 。例如:[10,20,30,40][1:30]
结果: [20, 30, 40]
我们发现正常输出了结果,没有报错。
列表的遍历
1 | a = [10,20,30,40] |
复制列表所有的元素到新列表对象
如下代码实现列表元素的复制了吗?
1
2 list1 = [30,40,50]
list2 = list1只是将list2也指向了列表对象,也就是说list2和list2持有地址值是相同的,列表对象本身的元素并没有复制。
我们可以通过如下简单方式,实现列表元素内容的复制:
1 | list1 = [30,40,50] |
注:我们后面也会学习copy模块,使用浅复制或深复制实现我们的复制操作
列表排序
修改原列表,不建新列表的排序
使用sort()
1 | 20,10,30,40] a = [ |
建新列表的排序
我们也可以通过内置函数sorted()
进行排序,这个方法返回新列表,不对原列表做修改。
1 | 20,10,30,40] a = [ |
通过上面操作,我们可以看出,生成的列表对象b和c都是完全新的列表对象。
reversed()
返回迭代器
内置函数reversed()
也支持进行逆序排列,与列表对象reverse()
方法不同的是,内置函数reversed()
不对原列表做任何修改,只是返回一个逆序排列的迭代器对象。
1 | 20,10,30,40] a = [ |
我们打印输出c发现提示是:list_reverseiterator
。也就是一个迭代对象。同时,我们使用list(c)
进行输出,发现只能使用一次。第一次输出了元素,第二次为空。那是因为迭代对象在第一次时已经遍历结束了,第二次不能再使用。
列表相关的其他内置函数汇总
max和min
用于返回列表中最大和最小值。
1 | 3,10,20,15,9] a = [ |
sum
对数值型列表的所有元素进行求和操作,对非数值型列表运算则会报错。
1 | 3,10,20,15,9] a = [ |
多维列表
二维列表
一维列表可以帮助我们存储一维、线性的数据。
二维列表可以帮助我们存储二维、表格的数据。例如下表的数据:
嵌套循环打印二维列表所有的数据:
1 | a = [ |
元组tuple
==元组不可变序列,不能修改元组中的元素==
- 列表属于可变序列,可以任意修改列表中的元素。
- 元组属于不可变序列,不能修改元组中的元素。
因此,元组没有增加元素、修改元素、删除元素相关的方法。
因此,我们只需学元组的创建和删除,元素的访问和计数即可。元组支持如下操作:
- 索引访问
- 切片操作
- 连接操作
- 成员关系操作
- 比较运算操作
- 计数:元组长度
len()
、最大值max()
、最小值min()
、求和sum()
等
元组的创建
通过()创建元组
小括号可以省略。
a = (10,20,30) 或者 a = 10,20,30
==如果元组只有一个元素,则必须后面加逗号==。这是因为解释器会把(1)
解释为整数1
,(1,)
解释为元组。
a = 10,
1 | 1) a = ( |
通过tuple()创建元组
tuple(可迭代的对象)
1 | a = tuple() # 创建一个空元组对象 |
总结:
tuple()
可以接收列表、字符串、其他序列类型、迭代器等生成元组。list()
可以接收元组、字符串、其他序列类型、迭代器等生成列表。
元组的元素访问和计数
元组的元素不能修改
1 | 20,10,30,9,8) a = ( |
元组的元素访问、index()、count()、切片等操作,和列表一样。
1 | 20,10,30,9,8) a = ( |
列表关于排序的方法list.sort()
是修改原列表对象,元组没有该方法。如果要对元组排序,只能使用内置函数sorted(tupleObj)
,并生成新元组的对象。(与列表的sorted(listObj)
一样)
1 | a = (20,10,30,9,8) |
zip
zip(列表1,列表2,...)
将多个列表对应位置的元素组合成为元组,并返回这个zip对象。
:warning:如果各个迭代器的元素个数不一致,则返回列表长度==与最短的对象相同==
1 | a = [10,20,30] |
生成器推导式创建元组
- 从形式上看,生成器推导式与列表推导式类似,只是生成器推导式使用小括号。
- 列表推导式直接生成列表对象,生成器推导式生成的不是列表也不是元组,而是一个生成器对象。
- 我们可以通过生成器对象,转化成列表或者元组。也可以使用生成器对象的
__next__()
方法进行遍历,或者直接作为迭代器对象来使用。不管什么方式使用,元素访问结束后,如果需要重新访问其中的元素,必须重新创建该生成器对象。
【操作】生成器的使用测试
1 | # 列表推导式: [0, 2, 4, 6, 8] |
元组总结
- 元组的核心特点是:==不可变序列==。
- 元组的访问和处理速度比列表快。
- 与整数和字符串一样,元组可以作为字典的键,==列表则永远不能作为字典的键使用==。
字典
字典是“键值对”的==无序可变序列==,字典中的每个元素都是一个“键值对”,包含:“键对象”和“值对象”。可以通过“键对象”实现快速获取、删除、更新对应的“值对象”。
一个典型的字典的定义方式:
a = {'name':'gaoqi', 'age':18, 'job':'programmer'}
列表中我们通过“下标数字”找到对应的对象。字典中通过“键对象”找到对应的“值对象”。
- “键”是任意的不可变数据,比如:整数、浮点数、字符串、元组。
- 但是:列表、字典、集合这些可变对象,不能作为“键”。
- 并且“键”不可重复。
- “值”可以是任意的数据,并且可重复。
字典的创建
我们可以通过
{}
、dict()
来创建字典对象。1
2
3
4
5a = {'name':'gaoqi','age':18,'job':'programmer'}
b = dict(name='gaoqi',age=18,job='programmer')
a = dict([("name","gaoqi"),("age",18)])
c = {} # 空的字典对象
d = dict() # 空的字典对象通过
zip()
创建字典对象1
2
3
4k = ['name','age','job']
v = ['gaoqi',18,'teacher']
d = dict(zip(k,v))
print(d) # {'name': 'gaoqi', 'age': 18, 'job': 'techer'}通过
fromkeys
创建值为空的字典1
2f = dict.fromkeys(['name','age','job'])
print(f) #结果:{'name': None, 'age': None, 'job': None}
字典元素的访问
通过
[键]
获得“值”。若键不存在,则抛出异常。1
2
3a = {'name':'gaoqi','age':18,'job':'programmer'}
b = a['name']
print(b)通过
get()
方法获得“值”。❤️推荐使用。优点是:指定键不存在,返回None;也可以设定指定键不存在时默认返回的对象。推荐使用
get()
获取“值对象”1
2
3
4
5a = {'name':'gaoqi','age':18,'job':'programmer'}
b = a.get('name')
c = a.get('gender','不存在')
print(b)
print(c)列出所有的键值对
1
2
3a = {'name':'gaoqi','age':18,'job':'programmer'}
b = a.items()
print(b) # dict_items([('name', 'gaoqi'),('age', 18), ('job', 'programmer')])列出所有的键,列出所有的值
1
2
3
4
5a = {'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()
键值对的个数1
2
3a = {'name':'gaoqi','age':18,'job':'programmer'}
num = len(a)
print(num) #3检测一个“键”是否在字典中
1
2a = {'name':'gaoqi','age':18,'job':'programmer'}
print("name" in a) #True
字典元素的添加、修改、删除
给字典新增“键值对”。如果“键”已经存在,则覆盖旧的键值对;如果“键”不存在,则新增“键值对”
1
2
3
4
5a = {'name':'gaoqi','age':18,'job':'programmer'}
a['address']='西三旗1号院'
a['age']=16
print(a)
#{'name': 'gaoqi', 'age': 16, 'job':'programmer', 'address': '西三旗1号院'}使用
update()
将新字典中所有键值对全部添加到旧字典对象上。如果key
有重复,则直接覆盖1
2
3
4
5a = {'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()
删除指定键值对,并返回对应的“值对象”1
2
3
4
5a = {'name':'gaoqi','age':18,'job':'programmer'}
del(a['name'])
print(a) # {'age': 18, 'job':'programmer'}
age = a.pop('age')
print(age) # 18popitem()
:随机删除和返回该键值对。字典是“无序可变序列”,因此没有第一个元素、最后一个元素的概念;popitem
弹出随机的项,因为字典并没有”最后的元素”或者其他有关顺序的概念。若想一个接一个地移除并处理项,这个方法就非常有效(因为不用首先获取键的列表)1
2
3
4
5a = {'name':'gaoqi','age':18,'job':'programmer'}
r1 = a.popitem()
r2 = a.popitem()
r3 = a.popitem()
print(a) #{}
序列解包
序列解包可以用于元组、列表、字典。序列解包可以让我们方便的对多个变量赋值。
1 | x,y,z=(20,30,10) |
序列解包用于字典时,默认是对“键”进行操作; 如果需要对键值对操作,则需要使用items()
;如果需要对“值”进行操作,则需要使用values()
;
items()对键值进行操作返回的是元组,可以通过索引获得键和值
1 | s = {'name':'gaoqi','age':18,'job':'teacher'} |
表格数据使用字典和列表存储和访问
1 | r1 = {"name":"高小一","age":18,"salary":30000,"city":"北京"} |
字典核心底层原理(重要)
字典对象的核心是散列表。散列表是一个稀疏数组(总是有空白元素的数组),数组的每个单元叫做 bucket
。每个 bucket
有两部分:一个是键对象的引用,一个是值对象的引用。
由于,所有 bucket
结构和大小一致,我们可以通过偏移量来读取指定bucket
。
将一个键值对放进字典的底层过程
1 | a = {} |
假设字典a对象创建完后,数组长度为8:
我们要把"name"="gaoqi"
这个键值对放到字典对象a中,首先第一步需要计算键"name"
的散列值。Python中可以通过hash()
来计算。
1 | bin(hash("name")) |
由于数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即"101"
,十进制是数字5。我们查看偏移量5,对应的bucket
是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边3位作为偏移量,即"100"
,十进制是数字4。再查看偏移量为4的bucket
是否为空。直到找到为空的bucket
将键值对放进去。流程图如下:
扩容
- python会根据散列表的拥挤程度扩容。“扩容”指的是:创造更大的数组,将原有内容拷贝到新数组中。
- 接近2/3时,数组就会扩容。
根据键查找“键值对”的底层过程
明白了,一个键值对是如何存储到数组中的,根据键对象取到值对象,理解起来就简单了。
1 | "name") a.get( |
当调用a.get("name")
,就是根据键"name"
查找到"键值对"
,从而找到值对象"gaoqi"
。
我们仍然要首先计算"name"
对象的散列值:
1 | bin(hash("name")) |
和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。 假设数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即 101
,十进制是数字5。我们查看偏移量5,对应的 bucket
是否为空。如果为空,则返回 None
。如果不为空,则将这个 bucket
的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回None
。流程图如下:
用法总结:
字典在内存中开销巨大,典型的空间换时间。
键查询速度很快
往字典里面添加新键值对可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字典的同时进行字典的修改
键必须可散列
数字、字符串、元组,都是可散列的
自定义对象需要支持下面三点:(面向对象章节中再展开说)
支持
hash()
函数支持通过
__eq__()
方法检测相等性若
a==b
为真,则hash(a)==hash(b)
也为真
集合
集合是无序可变,元素不能重复。实际上,集合底层是字典实现,集合的所有元素都是字典中的“键对象”,因此是不能重复的且唯一的。
集合创建和删除
使用
{}
创建集合对象,并使用add()
方法添加元素1
2a = {3,5,7}
a.add(9) # {9, 3, 5, 7}使用
set()
,将列表、元组等可迭代对象转成集合。如果原来数据存在重复数据,则只保留一个1
2a = ['a','b','c','b']
b = set(a) # {'b', 'a', 'c'}remove()
删除指定元素;clear()
清空整个集合1
2a = {10,20,30,40,50}
a.remove(20) # {10, 50, 40,30}
集合相关操作
像数学中概念一样,Python对集合也提供了并集、交集、差集等运算。我们给出示例:
1 | 1,3,'sxt'} a = { |
控制语句
控制语句和逻辑思维
控制语句:把语句组合成能完成一定功能的小逻辑模块。
分为三类:顺序、选择和循环。
学会控制语句,是真正跨入编程界的“门槛”,是成为“程序猿”的“门票”。
- “顺序结构”代表
“先执行a,再执行b”
的逻辑。比如,先找个女朋友,再给女朋友打电话;先订婚,再结婚; - “条件判断结构”代表
“如果…,则…”
的逻辑。比如,如果女朋友来电,则迅速接电话;如果看到红灯,则停车; - “循环结构”代表
“如果…,则重复执行…”
的逻辑。比如,如果没打通女朋友电话,则再继续打一次; 如果没找到喜欢的人,则再继续找
很神奇的是,三种流程控制语句就能表示所有的事情!
选择结构(条件判断结构)
选择结构通过判断条件是否成立,来决定执行哪个分支。选择结构有多种形式,分为:单分支、双分支、多分支。
单分支选择结构
if语句单分支结构的语法形式如下:
1 | if 条件表达式: |
- 条件表达式:可以是逻辑表达式、关系表达式、算术表达式等等。
- 语句/语句块:可以是一条语句,也可以是多条语句。多条语句,缩进必须对齐一致
条件表达式详解
在选择和循环结构中,条件表达式的值为 False
的情况如下:
False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。
其他情况,均为 True
。这么看来,Python所有的合法表达式都可以看做条件表达式,甚至包括函数调用的表达式。
【操作】测试各种条件表达式
1 | if 3: # 整数作为条件表达式 |
:warning:条件表达式中,不能有赋值操作符
=
在Python中,条件表达式不能出现赋值操作符
=
,避免了其他语言中经常误将关系运算符==
写作赋值运算符=
带来的困扰。如下代码将会报语法错误:
1
2 if 3 < c and (c=20): # 直接报语法错误!
print("赋值符不能出现在条件表达式中")
双分支选择结构
双分支结构的语法格式如下:
1 | if 条件表达式: |
【操作】输入一个数字,小于10,则打印该数字;大于10,则打印“数字太大”
1 | num = input("输入一个数字:") |
三元条件运算符
1 | 条件为真时的值 if (条件表达式) else 条件为假时的值 |
上一个案例代码,可以用三元条件运算符实现:
1 | num = input("输入一个数字:") |
可以看到,这种写法更加简洁,易读。
多分支选择结构
多分支选择结构的语法格式如下:
1 | if 条件表达式1 : |
:warning:多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序
【操作】输入一个学生的成绩,将其转化成简单描述:不及格(小于60)、及格(60-79)、良好(80-89)、优秀(90-100)
方法1(使用完整的条件表达)
1 | score = int(input("请输入分数")) |
上面的每个分支都使用了独立的、完整的判断,顺序可以随意挪动,而不影响程序运行。
方法2(利用多分支结构)
1 | ''' |
:warning:多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序
选择结构的嵌套
选择结构可以嵌套,使用时一定要注意控制好不同级别代码块的缩进量,因为缩进量决定了代码的从属关系。
【操作】输入一个分数。分数在0-100之间。90以上是A,80以上是B,70以上是C,60以上是D。60以下是E
1 | score = int(input("输入一个0-100之间的数字:")) |
1 | # 更少的代码方法 |
循环结构
循环结构用来重复执行一条或多条语句。表达这样的逻辑:如果符合条件,则反复执行循环体里的语句。在每次执行完后都会判断一次条件是否为True
,如果为True
则重复执行循环体里的语句。图示如下:
循环体里面的语句至少应该包含改变条件表达式的语句,以使循环趋于结束;否则,就会变成一个死循环。
while循环
while循环的语法格式如下:
1 | while 条件表达式: |
【操作】利用while循环,计算1-100之间数字的累加和;计算1-100之间偶数的累加和,计算1-100之间奇数的累加和
1 | num = 0 |
for循环和可迭代对象遍历
for循环通常用于可迭代对象的遍历。for循环的语法格式如下:
1 | for 变量 in 可迭代对象: |
【操作】遍历一个元组或列表
1 | for x in (20, 30, 50): |
可迭代对象
Python包含以下几种可迭代对象:
- 序列。包含:字符串、列表、元组、字典、集合
- 迭代器对象(iterator)
- 生成器函数(generator)
- 文件对象
我们已经在前面学习了序列、字典等知识,迭代器对象和生成器函数将在后面进行详解。接下来,我们通过循环来遍历这几种类型的数据:
【操作】遍历字符串中的字符
1 | for temp in "weqwewe": |
【操作】遍历字典
1 | d = {"name": "haha", "age": 12, "sex": "male"} |
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之间奇数的累加和。
1 | for x in range(3, 10, 2): # start=3 end=10 step=2 包头不包尾 |
嵌套循环
1 | for x in range(5): |
嵌套循环练习
【操作】利用嵌套循环打印九九乘法表
1 | # 九九乘法表 |
【操作】用列表和字典存储下表信息,并打印出表中工资高于15000的数据
1 | # 用列表和字典存储下表信息,并打印出表中工资高于15000的数据 |
break语句
break语句可用于while和for循环,用来结束整个循环。当有嵌套循环时,break语句只能跳出最近一层的循环。
【操作】使用break语句结束循环
1 | while True: |
continue语句
continue语句用于结束本次循环,继续下一次。多个循环嵌套时,continue也是应用于最近的一层循环。
【操作】要求输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员工的数量和薪资明细,以及平均薪资
1 | # 要求输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员王的数量和薪资明细,以及平均薪资 |
else语句
while
、for
循环可以附带一个else
语句(可选)。如果for
、while
语句没有被break
语句结束,则会执行else
子句,否则不执行。语法格式如下:
1 | while 条件表达式: |
【操作】员工一共4人。录入这4位员工的薪资。全部录入后,打印提示“您已经全部录入4名员工的薪资”。最后,打印输出录入的薪资和平均薪资
1 | salarySum = 0 |
循环代码优化技巧
虽然计算机越来越快,空间也越来越大,我们仍然要在性能问题上“斤斤计较”。编写循环时,遵守下面三个原则可以大大提高运行效率,避免不必要的低效计算:
- 尽量减少循环内部不必要的计算
- 嵌套循环中,尽量减少内层循环的计算,尽可能向外提
- 局部变量查询较快,尽量使用局部变量
其他优化手段
- ==连接多个字符串或列表,使用join()或append()而不使用+== +会创建新的字符串,join不会
- ==列表进行元素插入和删除,尽量在列表尾部操作==
zip()并行送代多个序列
我们可以通过zip()函数对多个序列进行并行迭代,zip()函数在最短序列“用完”时就会停止。
1 | names = ("haohao", "hehe", "huaiyue") |
推导式创建序列
推导式是从一个或者多个迭代器快速创建序列的一种方法。它可以将循环和条件判断结合,从而避免冗长的代码。
❤️推导式是典型的Python风格,会使用它,代表你已经超过Python初学者的水平。
列表推导式
列表推导式生成列表对象,语法如下:
1 | [表达式 for item in 可迭代对象 ] |
1 | [x for x in range(1,5)] #[1, 2, 3, 4] |
1 | a = [x for x in range(1, 10) if x % 2 == 0] |
字典推导式
字典的推导式生成字典对象,格式如下:
1 | {key_expression: value_expression for 表达式 in 可迭代对象} |
类似于列表推导式,字典推导也可以增加if条件判断、多个for循环。
1 | values = ["北京","上海","深圳","广州"] |
生成字典对象:
1 | {100: '北京', 200: '上海', 300: '深圳', 400:'广州'} |
【操作】统计文本中字符出现的次数:
1 | # 统计字数 |
集合推导式
集合推导式生成集合,和列表推导式的语法格式类似:
1 | {表达式 for item in 可迭代对象 } |
1 | for x in range(1,100) if x%9==0} {x |
生成器推导式(不直接生成元组)
很多同学可能会问:“都有推导式,元组有没有?”,能不能用小括号呢?
1 | for x in range(1,100) if x%9==0) (x |
我们发现提示的是“一个生成器对象”。显然,元组是没有推导式的。
一个生成器只能运行一次。第一次迭代可以得到数据,第二次迭代发现数据已经没有了。
1 | gnt = (x for x in range(1,100) if x%9==0) |
综合练习
绘制不同颜色的同心圆
1 | # 绘制不同颜色的同心圆 |
函数和内存底层分析
函数是可重用的程序代码块。
函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。
在编写函数时,函数体中的代码写法和我们前面讲述的基本一致,只是对代码实现了封装,并增加了函数调用、传递参数、返回计算结果等内容。
:warning:为了让大家更容易理解,掌握的更深刻。我们也要深入内存底层进行分析。绝大多数语言内存底层都是高度相似的,这样大家掌握了这些内容也便于以后学习其他语言。
函数简介
函数(function)的基本概念
- 一个程序由一个一个的任务组成;函数就是代表一个任务或者一个功能(function)。
- 函数是代码复用的通用机制
Python函数的分类
Python函数分为如下几类:
内置函数
我们前面使用的
str()
、list()
、len()
等这些都是内置函数,我们可以拿来直接使用。标准库函数
我们可以通过 import 语句导入库,然后使用其中定义的函数
第三方库函数
Python社区也提供了很多高质量的库。下载安装这些库后,也是通过 import 语句导入,然后可以使用这些第三方库的函数
用户自定义函数
用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。今天我们学习的就是如何自定义函数。
函数的定义和调用
核心要点
Python中,定义函数的语法如下:
1 | def 函数名 ([参数列表]) : |
简单定义一个函数:
1 | def add(a,b,c): |
要点:
- 我们使用
def
来定义函数,然后就是一个空格和函数名称;- Python执行
def
时,会创建一个函数对象,并绑定到函数名变量上。
- Python执行
- 参数列表
- 圆括号内是形式参数列表,有多个参数则使用逗号隔开
- 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
- 调用时的实际参数必须与形参列表一一对应
return
返回值- 如果函数体中包含
return
语句,则结束函数执行并返回值; - 如果函数体中不包含
return
语句,则返回None
值。
- 如果函数体中包含
- 调用函数之前,必须要先定义函数,即先调用 def 创建函数对象
- 内置函数对象会自动创建
- 标准库和第三方库函数,通过
import
导入模块时,会执行模块中的def
语句
形参和实参
形参和实参的要点:
- 圆括号内是形式参数列表,有多个参数则使用逗号隔开
- 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
- 调用时的实际参数必须与形参列表一一对应
【操作】定义一个函数,实现两个数的比较,并返回较大的值
1 | def printMax(a,b): |
上面的 printMax
函数中,在定义时写的 printMax(a,b)
。 a 和 b 称为“形式参数”,简称“形参”。也就是说,形式参数是在定义函数时使用的。 形式参数的命名要符合“标识符”命名规则。在调用函数时,传递的参数称为“实际参数”,简称“实参”。上面代码中, printMax(10,20)
, 10 和 20 就是实际参数。
文档字符串(函数的注释)
程序的可读性最重要,一般建议在函数体开始的部分附上函数定义说明,这就是“文档字符串”,也有人成为“函数的注释”。我们通过三
个单引号或者三个双引号来实现,中间可以加入多行文字进行说明。
【操作】测试文档字符串的使用
我们调用 help(函数名)
可打印输出函数的文档字符串。
我们也可以通过 函数名.__doc__
直接获取到函数的文档字符串,自己进行打印。
1 | def printMax(a, b): |
返回值详解
return
返回值要点:
- 如果函数体中包含
return
语句,则结束函数执行并返回值 - 如果函数体中不包含
return
语句,则返回None
值 - 要返回多个值,使用列表、元组、字典、集合将多个值“存起来”即可
1 | def printShape(n): |
函数也是对象,内存底层分析
==Python中,”一切都是对象”==。实际上,执行def定义函数后,系统就创建了相应的==函数对象==。
1 | def print_star(n): |
- 显然,我们可以看出变量
c
和print_star
都是指向了同一个函数对象。因此,执行c(3)
和执行print_star(3)
的效果是完全一致的。 - Python中,圆括号意味着调用函数。在没有圆括号的情况下,Python会把函数当做普通对象。
与此核心原理类似,我们也可以做如下操作:
1 | zhengshu = int |
显然,我们将内置函数对象 int() 赋值给了变量 zhengshu ,这样zhengshu 和 int 都是指向了同一个内置函数对象。
:warning:当然,此处仅限于原理性讲解,实际开发中没必要这么做。
变量的作用域(全局变量和局部变量)
变量起作用的范围称为变量的作用域,不同作用域内同名变量之间互不影响。变量分为:全局变量、局部变量。
全局变量
- 在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
- 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
- 要在函数内改变全局变量的值,使用
global
声明一下
局部变量
- 在函数体中(包含形式参数)声明的变量。
- 局部变量的引用比全局变量快,优先考虑使用
- 如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量
【操作】 输出局部变量和全局变量
1 | a = 100 |
局部变量和全局变量效率测试
==局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是在循环的时候。==
==在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量提高运行速度。==
参数的传递
函数的参数传递本质上就是:从实参到形参的赋值操作。Python中“一切皆对象”,所有的赋值操作都是“引用的赋值”。所以,Python中参数的传递都是“引用传递”,不是“值传递”。
具体操作时分为两类:
- 对“可变对象”进行“写操作”,直接作用于原对象本身。
- 对“不可变对象”进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。
:warning:
可变对象有:
==字典、列表、集合==、自定义的对象等
不可变对象有:
数字、==字符串、元组、function==等
传递可变对象的引用
传递参数是可变对象(例如:列表
、字典
、集合
、自定义的其他可变对象等),实际传递的还是对象的引用。在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。
【操作】参数传递:传递可变对象的引用
1 | b = [10,20] |
传递不可变对象的引用
传递参数是不可变对象(例如: int
、 float
、字符串
、元组
、布尔值
),实际传递的还是对象的引用。在”赋值操作”时,由于不可变对象无法修改,系统会新创建一个对象。
【操作】参数传递:传递不可变对象的引用
1 | a = 100 |
显然,通过 id
值我们可以看到 n
和 a
一开始是同一个对象。给n
赋值后,n
是新的对象。
浅拷贝和深拷贝
- 浅拷贝:==拷贝对象,但不拷贝子对象的内容,只是拷贝子对象的引用。==
- 深拷贝:==拷贝对象,并且会连子对象的内存也全部(递归)拷贝一份,对子对象的修改不会影响源对象==
传递不可变对象包含的子对象是可变的情况
传递不可变对象时,不可变对象里面包含的子对象是可变的。则方法内修改了这个可变对象,源对象也发生了变化。
1 | a = (10,20,[5,6]) |
参数的几种类型
位置参数
函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。按位置传递的参数,称为:“位置参数”。
【操作】测试位置参数
1 | def f1(a,b,c): |
默认值参数
我们可以为某些参数设置默认值,这样这些参数在传递时就是可选的。称为“默认值参数”。默认值参数放到位置参数后面。
【操作】测试默认值参数
1 | def f1(a,b,c=10,d=20): # 默认值参数必须位于普通位置参数后面 |
命名参数(关键字参数)
我们也可以按照形参的名称传递参数,称为“命名参数”,也称“关键字参数”。
1 | def f1(a,b,c): |
可变参数
可变参数指的是“可变数量的参数”。分两种情况:
*param
(一个星号),将多个参数收集到一个“元组”对象中。**param
(两个星号),将多个参数收集到一个“字典”对象中。
【操作】测试可变参数处理(元组、字典两种方式)
1 | def f1(a,b,*c): |
强制命名参数
在带星号的“可变参数”后面增加新的参数,必须在调用的时候“强制命名参数”。
1 | def f1(*a,b,c): |
lambda表达式和匿名函数
lambda
表达式可以用来声明匿名函数。 lambda
函数是一种简单的、在同一行中定义函数的方法。 lambda
函数实际生成了一个函数对象。
lambda
表达式只允许包含一个表达式,不能包含复杂语句,该表达式的计算结果就是函数的返回值。
lambda
表达式的基本语法如下:
1 | lambda arg1,arg2,arg3... : <表达式> |
arg1
arg2
arg3
为函数的参数。<表达式>
相当于函数体。运算结果是:表达式的运算结果。
【操作】lambda表达式使用
1 | f = lambda a, b, c: a + b + c |
eval()函数
功能:将字符串 str
当成有效的表达式来求值并返回计算结果。
语法: eval(source[, globals[, locals]]) -> value
参数:
source
:一个Python表达式或函数compile()
返回的代码对象globals
:可选。必须是dictionary
locals
:可选。任意映射对象
:warning:
eval函数
会将字符串当做语句来执行,因此会被注入安全隐患。比如:字符串中含有删除文件的语句。那就麻烦大了。因此,使用时候,要慎重!!!
1 | s = "print('abcd')" |
递归函数
- 递归(recursion)是一种常见的算法思路,在很多算法中都会用到。比如:深度优先搜索(DFS:Depth First Search)等。
- 递归的基本思想就是“自己调用自己”
递归函数指的是:自己调用自己的函数,在函数体内部直接或间接的自己调用自己。每个递归函数必须包含两个部分:
终止条件
表示递归什么时候结束。一般用于返回值,不再调用自己。
递归步骤
把第n步的值和第n-1步相关联。
:warning:递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时,谨慎使用。
1 | def my_recursion(n): |
【操作】 使用递归函数计算阶乘(factorial)
1 | def factorial(n): |
嵌套函数(内部函数)
嵌套函数:在函数内部定义的函数!
一般在什么情况下使用嵌套函数?
封装-数据隐藏
外部无法访问“嵌套函数”。
贯彻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中,”一切皆对象”。类也称为“==类对像==”,类的实例也称为“实例对象”。
定义类的语法格式如下:
1 | class 类名: |
要点如下:
①类名必须符合“标识符”的规则;一般规定,==首字母大写,多个单词使用“驼峰原则”==。
②类体中我们可以定义属性和方法
③属性用来描述数据,方法(即函数)用来描述这些数据相关的操作
1 | class Student: |
对象完整内存结构
类是抽象的,也称之为“对象的模板”。我们需要通过类这个模板,创建类的实例对象,然后才能使用类定义的功能。
我们前面说过一个Python对象包含三个部分: id
(identity识别码)、 type
(对象类型)、 value
(对象的值)。
现在,我们可以更进一步的说,一个Python对象包含如下部分:
__init__
构造方法和__new__
方法
初始化对象,我们需要定义构造函数 __init__()
方法。构造方法用于执行“实例对象的初始化工作”,即对象创建后,初始化当前对象的相关属性,无返回值。
构造方法是负责初始化(装修),不是建对象(房子)
__init__()
构造函数的要点如下:
名称固定,必须为:
__init__()
第一个参数固定,必须为:
self
。self
指的就是刚刚创建好的实例对象构造函数通常用来初始化实例对象的实例属性,如下代码就是初始化实例属性:
name
和score
1
2
3def __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.实例属性名 = 值
#可以给已有属性赋值,也可以新加属性
1 | class Student: |
实例方法
实例方法是从属于实例对象的方法。实例方法的定义格式如下:
1 | def 方法名(self [, 形参列表]): |
方法的调用格式如下:
对象.方法名([实参列表])
要点:
定义实例方法时,第一个参数必须为
self
。和前面一样,self
指当前的实例对象。调用实例方法时,不需要也不能给
self
传参。self
由解释器自动传参
函数和方法的区别
- 都是用来完成一个功能的语句块,本质一样。
- 方法调用时,通过对象来调用。方法从属于特定实例对象,普通函数没有这个特点
- 直观上看,方法定义时需要传递
self
,函数不需要
实例对象的方法调用本质
其他操作
dir(obj)
可以获得对象的所有属性、方法obj.__dict__
对象的属性字典pass
空语句isinstance(对象,类型)
判断对象”是不是”指定类型”
类对象、类属性、类方法、静态方法
类对象
我们在前面讲的类定义格式中, class 类名:
。实际上,当解释器执行class
语句时,就会创建一个类对象
类属性
类属性是从属于“类对象”的属性,也称为“类变量”。由于,类属性从属于类对象,可以被所有实例对象共享。
类属性的定义方式:
1 | class 类名: |
在==类中或者类的外面==,我们可以通过:类名.类变量名
来读写
内存分析实例对象和类对象创建过程(重要)
1 | class Student: |
类方法
==类方法是从属于“类对象”的方法。==类方法通过装饰器@classmethod
来定义,格式如下:
1 |
|
要点如下:
@classmethod
必须位于方法上面一行- 第一个
cls
必须有;==cls
指的就是“类对象”本身== - 调用类方法格式:
类名.类方法名(参数列表)
。参数列表中,不需要也不能给cls
传值 - 类方法中访问实例属性和实例方法会导致错误
- 子类继承父类方法时,传入cls是子类对象,而非父类对象(讲完继承再说)
1 | class Student: |
静态方法
Python中允许定义与”类对象”无关的方法,称为“静态方法”。
“静态方法”和在模块中定义普通函数没有区别,只不过“静态方法”放到了“类的名字空间里面”,需要通过“类调用”。
静态方法通过装饰器@staticmethod
来定义,格式如下:
1 |
|
要点如下:
@staticmethod
必须位于方法上面一行调用静态方法格式:
类名.静态方法名(参数列表)
静态方法中访问实例属性和实例方法会导致错误
1 | class Student: |
__del__
方法(析构函数)和垃圾回收机制
==Python实现自动的垃圾回收==
__del__()
称为“析构方法”,用于实现对象被销毁时所需的操作。比如:释放对象占用的资源,例如:打开的文件资源、网络连接等。
Python实现自动的垃圾回收,当对象没有被引用时(引用计数为0),由垃圾回收器调用__del__()
。
我们也可以通过del语句
删除对象,从而保证调用__del__()
。
系统会自动提供__del__
方法,一般不需要自定义析构方法。
1 | # 析构函数 |
call方法和可调用对象
Python中,凡是可以将()
直接应用到自身并执行,都称为可调用对象。
可调用对象包括自定义的函数、Python内置函数、以及本节所讲的实例对象。
定义了__call__()
的对象,称为==“可调用对象”,即该对象可以像函数一样被调用==。
该方法使得实例对象可以像调用普通函数那样,以"对象名()"
的形式使用。
1 | def f1(): |
方法没有重载
如果我们在类体中定义了多个重名的方法,只有最后一个方法有效。
建议:==不要使用重名的方法!Python中方法没有重载。==
在其他一些语言(比如:Java)中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含3个部分:方法名、参数数量、参数类型。
Python中,方法的的参数没有声明类型(调用时确定参数的类型),参数的数量也可以由可变参数控制。因此,Python中是没有方法的重载的。
方法的动态性
Python是动态语言,我们可以动态的为类添加新的方法,或者动态的修改类的已有的方法
1 | #测试方法的动态性 |
我们可以看到, Person 动态的新增了 play_game
方法,以及用 work2
替换了 work
方法
私有属性和私有方法(实现封装)
Pytho对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。关于私有属性和私有方法,有如下要点:
①通常我们约定,两个下划线开头的属性是私有的(private)。其他为公 共的(public)。
②类内部可以访问私有属性(方法)
③类外部不能直接访问私有属性(方法)
④类外部可以通过 _类名__私有属性(方法)名
访问私有属性(方法)
【注】==方法本质上也是属性!==只不过是可以通过()执行而已。
所以,此处讲的私有属性和公有属性,也同时讲解了私有方法和公有方法的用法。
如下测试中,同时也包含了私有方法和公有方法的例子。
1 | class Employee: |
@property装饰器
@property
可以将一个方法的调用方式变成“属性调用”。
@property
主要用于帮助我们处理属性的读操作、写操作。对于某一个属性,我们可以直接通过:
emp1.salary= 30000
如上的操作读操作、写操作。但是,这种做法不安全。比如,我需要限制薪水必须为1-10000
的数字。这时候,我们就需要通过使用装饰器@property
来处理。
1 | class Employee: |
属性和方法命名总结
_xxx
:保护成员,不能用from module import *
导入,只有类对象和子类对象能访问这些成员。
__xxx__
:系统定义的特殊成员
__xxx
:类中的私有成员,只有类对象自己能访问,子类对象也不能访问。(但,在类外部可以通过 对像名._类名__xxx
这种特殊方式访问。Python不存在严格意义的私有成员)
:warning:再次强调,方法和属性都遵循上面的规则。
类编码风格
- 类名首字母大写,多个单词之间采用驼峰原则。
- 实例名、模块名采用小写,多个单词之间采用下划线隔开
- 每个类,应紧跟“文档字符串”,说明这个类的作用
- 可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类
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支持==多重继承,一个子类可以继承多个父类==。继承的语法格式如下:
1 | class 子类类名(父类1[, 父类2, ...]): |
如果在类定义中没有指定父类,则默认父类是
object类
。也就是说,object
是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__()
关于构造函数:
子类不重写
__init__
,实例化子类时,会自动调用父类定义的__init__
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class 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__
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36class 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, 参数列表)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31class 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__()
)之外的所有成员。==
:warning:私有属性、私有方法也被继承
1 | class Person: |
方法重写:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为“重写”
1 | class Person: |
查看类的继承层次结构
通过类的方法mro()
或者类的属性__mro__
可以输出这个类的继承层次结构。
1 | class A: |
object根类
==
object
类是所有类的父类==,因此所有的类都有object
类的属性和方法。我们显然有必要深入研究一下object
类的结构。对于我们继续深入学习Python很有好处。
dir()
查看对象属性
为了深入学习对象,先学习内置函数dir()
,他可以让我们方便的看到指定对象所有的属性
:warning:快捷键 Alt+ 7 打开模块结构
1 | class Person: |
从上面我们可以发现这样几个要点:
- Person对象增加了六个属性:
__dict__
__module__
__weakref__
age
name
say_age
object
的所有属性,Person
类作为object
的子类,显然包含了所有的属性- 我们打印
age
、name
、say_age
,发现say_age
虽然是==方法,实际上也是属性==。只不过这个属性的类型是method而已。
1 | age <class 'int'> |
重写__str__()
方法
object
有一个__str()__
方法,用于返回一个对于”对象的描述”。内置函数str(对象)
,调用的就是__str()__
__str()__
经常用于print()
方法,帮助我们查看对象的信息。__str()__
可以重写
1 | class Person: |
多重继承
Pytho支持多重继承,一个子类可以有多个”直接父类”。这样,就具备了”多个父类”的特点。但是由于,这样会被“类的整体层次””搞的异常复杂,==尽量避免使用==。
1 | class A: |
类结构为:
MRO方法解析顺序
Pythor支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将“从左向右”按顺序搜索。
MRO(Method Resolution Order):方法解析顺序。我们可以通过mro()
方法获得”类的层次结构”,方法解析顺序也是按照这个“类的层次结构”寻找的。
super()获得父类的定义
在子类中,如果想要获得父类的方法时,我们可以通过super()
来做。
super()
代表父类的定义,不是父类对象。
想调用父类的构造方法:
super(子类名称, self).__init__(参数列表)
1 | class Person: |
多态详解
多态(polymorphism)是指同一个方法调用由于对象不同可能会产生不同的行为。
关于多态要注意以下2点:
- 多态是方法的多态,属性没有多态。
- 多态的存在有==2个必要条件:继承、方法重写==
1 | class Animal: |
特殊方法和运算符重载
特殊属性
对象的浅拷贝和深拷贝
浅拷贝
Python拷贝一般都是浅拷贝。
浅拷贝:拷贝时,拷贝源对象,但对象包含的子对象内容不拷贝。
深拷贝
使用 copy
模块的 deepcopy
函数,递归拷贝对象中包含的子对象。
深拷贝:拷贝时,拷贝源对象,也递归拷贝对象中包含的子对象
组合
除了继承,“组合”也能实现代码的复用!“组合”核心是“将父类对象作为子类的属性”。
is-a
关系,我们可以使用”==继承==”。从而实现子类拥有的父类的方法和属性。is-a
关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。
has-a
关系,我们可以使用”==组合==”,也能实现一个类拥有另一个类的方法和属性。has-a
关系指的是这样的关系:手机拥有CPU。MobilePhone has a CPU
设计模式
设计模式是面向对象语言特有的内容,是我们在面临某一类问题时候固定的做法,设计模式有很多种,比较流行的是:GOF(Goup Of Four)23种设计模式。当然,我们没有必要全部学习,学习几个常用的即可。
对于初学者,我们学习两个最常用的模式:工厂模式和单例模式。
工厂模式实现
工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。
1 | # 工厂模式实现 |
单例模式实现
单例模式(Singleton Pattern)的核心作用是确保一个类只有一个实例,并且提供一个访问该实例的全局访问点。
单例模式只生成一个实例对象,减少了对系统资源的开销。当一个对象的产生需要比较多的资源,如读取配置文件、产生其他依赖对象时,可以产生一个“单例对象”,然后永久驻留内存中,从而极大的降低开销。
单例模式有多种实现的方式,我们这里推荐重写__new__()的方法。
1 | # 单例模式实现 |
工厂和单例模式结合起来
设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现:
1 | # 工厂和单例模式结合 |
Python开发环境搭建
环境配置参考这篇文章
Python环境配置保姆教程(Anaconda、Jupyter、GPU环境)!
内容包括:
- Anaconda的安装与常用命令小总
- Jupyter的安装与相关配置
- CUDA与Cudnn的安装(GPU支持必备)
- 建立tf虚拟环境并安装tf2.0GPU版本
- 建立pytorch虚拟环境并安装pytorchGPU版本