Python入门

编程基本概念

Python程序的构成

image-20240204132244728

  1. Python程序由模块组成。一个模块对应python源文件,一般后缀名是:.py
  2. 模块由语句组成。运行Python程序时,按照模块中语句的顺序依次执行
  3. 语句是Python程序的构造单元,用于创建对象、变量赋值、调用函数、控制语句等

代码的组织和缩进

很多编程语言通过字符(例如:花括号{})、关键字(例如:begain/end)来划分代码块。同时,在配合代码的缩进增加可读性。“龟叔”设计Python语言时,直接通过缩进来组织代码块。“缩进”成为了Python语法强制的规定。缩进时,几个空格都是允许的,但是数目必须统一。通常采用“四个空格”表示一个缩进

同时,也要避免将“tab制表符”或者tab与空格混合的缩进风格。目前,常用的编辑器一般设置成:tab制表符就是4个空格

使用\行连接符

一行程序长度是没有限制的,但是为了可读性更强,通常将一行比较长的程序分为多行。这是,我们可以使用\行连接符,把它放在行结束的地方。Python解释器仍然将它们解释为同一行。

1
2
3
4
5
6
7
8
9
a = [10,20,30,40,\
  50,60,70,\
  80,90,100]
b = 'abcdefg\
    hijklmn\
    opqrst\
    uvwxyz'
print(a)
print(b)

对象

  1. Python中,一切皆对象。
  2. 每个对象由:标识(identity)、类型(type)、value(值)组成

image-20231110201745676

  1. 标识用于唯一标识对象,通常对应于对象在计算机内存中的地址。使用内置函数id(obj)可返回对象obj的标识。
  2. 类型用于表示对象存储的“数据”的类型。类型可以限制对象的取值范围以及可执行的操作。可以使用type(obj)获得对象的所属类型。
  3. 值表示对象所存储的数据的信息。使用print(obj)可以直接打印出值。

对象的本质就是:

一个内存块,拥有特定的值,支持特定类型的相关操作

1
2
3
4
5
6
7
8
a=3
print(a)
print(id(a))
print(type(a))
b='我爱你'
print(b)
print(id(b))
print(type(b))

内存示意图

image-20240203204036127

引用

在Python中,变量也称为:对象的引用(reference)。变量存储的就是对象的地址。

变量通过地址引用了“对象”。

image-20240203204134853

变量位于:栈内存(压栈出栈等细节,后续再介绍)。

对象位于:堆内存。

如下源代码对应的内存图:

1
2
a=3
b="我爱你"

image-20240203204837202

==Python是动态类型语言==:变量不需要显式声明类型。根据变量引用的对象,Python解释器自动确定数据类型

标识符规则

image-20240203205008189

image-20231110203017623

基本用法

标识符规则:用于变量、函数、类、模块等的名称。标识符有如下特定的规则:

  1. 区分大小写。如: sxtSXT 是不同的

  2. 第一个字符必须是字母、下划线。其后的字符是:字母、数字、下划线

  3. 不能使用关键字。比如: iforwhile

  4. 双下划线开头和结尾的名称通常有特殊含义,尽量避免这种写法。比如: __init__ 是类的构造函数

    image-20231110203126470

Python标识符命名规则

image-20231110203446167

变量和简单赋值语句

变量的声明和赋值

变量的声明和赋值:用于将一个变量绑定到一个对象上,格式: 变量名 = 表达式

image-20240203205345804

最简单的表达式就是字面量。比如: a = 123 。运行过程中,解释器先运行右边的表达式,生成一个代表表达式运算结果的对象;然后,将这个对象地址赋值给左边的变量。

:warning:变量在使用前必须先被初始化(先被赋值)

删除变量和垃圾回收机制

  1. 可以通过del语句删除不再使用的变量。
  2. 如果对象没有变量引用,就会被垃圾回收器回收,清空内存空间。

【操作】删除变量示例

1
2
3
a=123
del a
print(a)

报错如下:

image-20240203205612487

常量

image-20240204130134606

Python不支持常量,即没有语法规则限制改变一个常量的值。我们只能约定常量的命名规则,以及在程序的逻辑上不对常量的值作出修改。

1
2
3
4
MAX_SPEED = 120
print(MAX_SPEED)  # 输出120
MAX_SPEED = 140   # 实际是可以改的。只能逻辑上不做修改。
print(MAX_SPEED)  # 输出140

链式赋值

image-20231110204725378

系列解包赋值

系列数据赋值给对应相同个数的变量(个数必须保持一致)

a,b,c=4,5,6 相当于: a=4;b=5;c=6

【操作】使用系列解包赋值实现变量值交换

1
2
3
a,b=1,2
a,b=b,a  # 变量值互换
print(a,b)

使用系列解包复制可以轻松实现变量值交换

最基本内置数据类型

image-20231110210320597

python中变量没有类型,但是对象都有类型,python中最基本的内置数据类型:

  1. 整型 int

    整数, 2345 , 10 , 50

  2. 浮点型 float

    小数, 3.14 或者科学计数法 314e-2

  3. 布尔型 bool

    表示真假,仅包含: True 、 False

  4. 字符串型 str

由字符组成的序列。 “abc” , ‘sxt’ , “尚学堂” , “百战程序员”

数字和基本运算符

image-20231110210508378

整数

image-20240204123731238

==Python的整数可以无限大,任意大==

三种进制

image-20231110210750024

使用int()实现类型转换
  1. 浮点数直接舍去小数部分。如: int(9.9) 结果是: 9
  2. 布尔值 True 转为 1False 转为 0 。 如: int(True) 结果是 1
  3. 字符串符合整数格式(浮点数格式不行)则直接转成对应整数,否则报错
自动转型

整数和浮点数混合运算时,表达式结果自动转型成浮点数。比如: 2+8.0 的结果是 10.0

整数可以任意大

Python2中, int 是32位,可以存储从 -21474836482147483647 的整数(约±21亿)。Long类型是64位,可以存储:-2^63—2^63-1之间的数值。

Python3中, int 可以存储任意大小的整数, long 被取消。

Python3中可以做超大数的计算,而不会造成“整数溢出”,这也是Python特别适合科学运算的特点

浮点数 float

  1. 浮点数用科学计数法表示。比如: 3.14 ,表示成: 314E-2 或者 314e-2
  2. 这些数字在内存中也是按照科学计数法存储。

类型转换和四舍五入

  1. 类似于 int() ,我们也可以使用 float() 将其他类型转化成浮点数。
  2. 整数和浮点数混合运算时,表达式结果自动转型成浮点数。比如: 2+8.0 的结果是 10.0
  3. round(value) 可以返回四舍五入的值。但不会改变原有值,而是产生新的值

==round(value))可以返回四舍五入的值==。但不会改变原有值,而是产生新的值

增强型赋值运算符

运算符 +-*///**% 和赋值符 = 结合可以构成“增强型赋值运算符”。

image-20240204124421151

:warning:注意:

  1. “+=”中间不能加空格!
  2. 结合的是右侧整个表达式:
1
y *= x+2  # 相当于:y = y*(x+2)  而不是:y = y*x+2

时间的表示

image-20231110212157494

python中可以通过time.time() 获得当前时刻,返回的值是以秒为单位,带微秒(1/1000毫秒)精度的浮点值。例如:1635063628.5632517

1
2
3
4
5
6
import time
b = int(time.time())
totalMinutes = b//60
totalHours = totalMinutes//60
totalDays = totalHours//24
totalYears = totalDays//365  #忽略闰年情况

布尔值

Python2中没有布尔值,直接用数字 0 表示 False , 用数字 1 表示True

Python3中,把 TrueFalse 定义成了关键字,但他们的本质还是 10 ,甚至可以和数字相加。

在Python语言底层,会将布尔值True看作1,将布尔值False看作0,尽管从表面上看,True1False0是完全不同的两个值,但实际上,它们是相同的。

在Python语言中有一些特殊的布尔类型值为False,例如False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。其他情况,均为True。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

运算符

逻辑运算符

image-20240204125036900

1
2
3
4
5
#测试逻辑运算符
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 表示假。这分别与特殊变量TrueFalse 等价。

以下假设变量 a为15 ,变量 b为30 :

image-20240204125302905

1
2
3
4
5
a = 4
print(a<=30)
# 关系运算符可以连用
if(3<a<10):
   print("a在3和10之间")

关系运算符可以连用 如: 2<a<10

位运算符

按位运算符是把数字看作二进制来进行计算的。Python中的按位运算法则如表所示。

image-20240204125414233

1
2
3
4
5
6
7
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

加法操作补充

  1. 数字相加 3+2 结果是 5
  2. 字符串拼接 "3"+"2" 结果是 "2"
  3. 列表、元组等合并 [10,20,30]+[5,10,100] 结果是 [10,20,30,5,10,100]

乘法操作补充

  1. 数字相乘 3*2 结果是 6
  2. 字符串复制 "sxt" * 3 结果是 "sxtsxtsxt"
  3. 列表、元组等复制 [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]

增强赋值运算符(补充)

复合赋值可以让程序更加精炼,提高效率。

image-20240204130748779

:warning:与C和JAVA不一样,Python不支持自增(++)和自减(—)

同一运算符

image-20240204130917419

同一运算符用于比较两个对象的存储单元,实际比较的是对象的地址。

image-20240204130945916

is== 区别:

  1. is 用于判断两个变量引用对象是否为同一个,既比较对象的地址。
  2. == 用于判断引用变量引用对象的值是否相等,默认调用对象的__eq__() 方法。

总结

  1. is 比较两个对象的 id 值是否相等,是否指向同一个内存地址
  2. == 比较的是两个对象的内容是否相等,值是否相等
  3. is 运算符比 == 效率高,在变量和 None 进行比较时,应该使用 is
1
2
3
4
5
6
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

【操作】同一运算符测试

1
2
3
4
5
6
7
a = 1000
b = 1000
a == b   # True
a is b   # 命令行下是False。 文件下执行是True
c = 10
d = 10
c is d   # True

整数缓存问题

  1. 命令行模式下,Python仅仅对比较小的整数对象进行缓存(范围为[-5, 256])缓存起来 [C语言底层用数组实现,连续分配空间,便于查找 ],而并非是所有整数对象。
  2. 文件模式下,所有数字都会被缓存,范围是:[-无穷大,+无穷大]
  3. 缓存实现:[-5,256]仍然底层用数组实现 ;不在[-5,256]出现的数,缓存到链表中,不连续分配空间 。

成员运算符

成员运算符测试实例中包含了一系列的成员,包括字符串,列表或元组。

image-20240204131629913

1
2
3
4
5
a = "python"
b = "py"
print(b in a)        #True
c = [10,20,30]
print(10 not in c)   #False

运算符优先级问题

如下优先级,从高到低。

image-20240204131751402

1
2
3
4
5
6
7
8
#测试运算符优先级
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)

实际使用中,记住如下简单的规则即可,复杂的表达式一定要使用小括号组织。

  1. 乘除优先加减
  2. 位运算和算术运算>比较运算符>赋值运算符>逻辑运算符

基本运算符总结

image-20240204131948004

序列

image-20231112203832984

序列的本质和内存结构

序列是一种数据存储方式,用来存储一系列的数据。在内存中,序列就是一块用来存放多个值的连续的内存空间。比如一个整数序列[10,20,30,40],示意表示:

image-20240204132820099

由于Python3中一切皆对象,在内存中实际是按照如下方式存储的:

image-20240204132839057

==序列中存储的是整数对象的地址,而不是整数对象的值==

字符串

字符串基本特点

  1. 字符串的本质是:字符序列。
  2. Python不支持单字符类型,单字符也是作为一个字符串使用的。

:warning:Python的==字符串是不可变的==,我们无法对原字符串做任何修改。但,可以将字符串的一部分复制到新创建的字符串,达到“看起来修改”的效果。

字符串的编码

Python3直接支持Unicode,可以表示世界上任何书面语言的字符。Python3的字符默认就是16位Unicode编码,ASCII码是Unicode编码的子集。

image-20240204133145422

使用内置函数ord()可以把字符转换成对应的Unicode码;

使用内置函数chr()可以把十进制数字转换成对应的字符。

1
2
3
4
>>> ord('A')  #65
>>> ord('高') #39640
>>> chr(66) #'B'
>>> ord('淇') #28103

引号创建字符串

我们可以通过单引号双引号创建字符串。例如: a='abc' b="sxt"

使用两种引号的好处是可以创建本身就包含引号的字符串,而不用使用转义字符。例如:

1
2
3
4
a = "I'm a teacher!" 
print(a) # I'm a teacher!
b = 'my_name is "TOM"'  
print(b) # my_name is "TOM"

连续三个单引号或三个双引号,可以帮助我们创建多行字符串。在长字符串中会保留原始的格式。例如:

1
2
3
4
5
6
s='''
I
  Love
      Python
'''
print(s)

空字符串和len()函数

Python允许空字符串的存在,不包含任何字符且长度为0。例如:

1
2
c = ''
print(len(c)) # 结果:0

len()用于计算字符串含有多少字符。例如:

1
2
d = 'abc尚学堂'
len(d)   # 结果:6

转义字符

我们可以使用 +特殊字符 ,实现某些难以用字符表示的效果。比如:换行等。常见的转义字符有这些:

image-20240204133642825

【操作】测试转义字符的使用

1
2
3
4
5
6
7
8
9
10
a = 'I\nlove\nU'
print(a)
print('aabb\\cc')
"""
输出:
I
love
U
aabb\cc
"""

字符串拼接

  1. 可以使用 + 将多个字符串拼接起来。例如: 'aa' +'bb' 结果是 'aabb'
    • 如果 + 两边都是字符串,则拼接。
    • 如果 + 两边都是数字,则加法运算
    • 如果 + 两边类型不同,则抛出异常
  2. 可以将多个字面字符串直接放到一起实现拼接。例如: 'aa' 'bb' 结果是 'aabb'

【操作】字符串拼接操作

1
2
a = 'sxt'+'gaoqi'    #结果是:'sxtgaoqi'
b = 'sxt''gaoqi'     #结果是:'sxtgaoqi'

字符串复制

使用*可以实现字符串复制

1
a = 'Sxt'*3    # 结果:'SxtSxtSxt'

不换行打印

我们前面调用print时,会自动打印一个换行符。有时,我们不想换行,不想自动添加换行符。我们可以自己通过参数end = "任意字符串"。实现末尾添加任何内容:

1
2
3
4
5
6
7
print("sxt",end=' ')
print("sxt",end='##')
print("sxt")
"""
sxt sxt##sxt

"""

从控制台读取字符串

我们可以使用input()从控制台读取键盘输入的内容。

1
2
myname = input("请输入名字:")
print("您的名字是:"+myname)

replace()实现字符串替换

image-20240204135112227

==字符串是不可变的==,我们通过[]可以获取字符串指定位置的字符,但是我们不能改变字符串。我们尝试改变字符串中某个字符,

发现报错了:

1
2
3
4
5
6
7
8
9
>>> 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

字符串不可改变。但是,我们确实有时候需要替换某些字符。这时,只能通过创建新的字符串来实现。

1
2
3
4
5
>>> a = 'abcdefghijklmnopqrstuvwxyz'
>>> a
'abcdefghijklmnopqrstuvwxyz'
>>> a = a.replace('c','高')
'ab高defghijklmnopqrstuvwxyz'

整个过程中,实际上我们是创建了新的字符串对象,并指向了变量a,而不是修改了以前的字符串。 内存图如下:

image-20240204135112227

str()实现数字转型字符串

str()可以帮助我们将其他数据类型转换为字符串。例如:

1
2
3
a = str(5.20)     # 结果是:a = '5.20' 
b = str(3.14e2)   # 结果是:b = '314.0'
c = str(True)     # 结果是:c = 'True'

当我们调用print()函数时,解释器自动调用了str()将非字符串的对象转成了字符串。

使用[]提取字符

字符串的本质就是字符序列,我们可以通过在字符串后面添加[],在[]里面指定偏移量,可以提取该位置的单个字符。

  1. 正向搜索:

    最左侧第一个字符,偏移量是0,第二个偏移量是1,以此类推。直到len(str)-1为止。

  2. 反向搜索:

    最右侧第一个字符,偏移量是-1,倒数第二个偏移量是-2,以此类推,直到-len(str)为止。

【操作】使用[]提取字符串中的字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> 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操作

image-20240204135915541

切片slice操作可以让我们快速的提取子字符串。标准格式为:

[起始偏移量start: 终止偏移量end: 步长step]

典型操作(三个量为正数的情况)如下:==包头不包尾==

image-20240204140041462

其他操作(三个量为负数)的情况:

image-20240204140151411

字符串逆序 [::-1]

切片操作时,起始偏移量和终止偏移量不在[0,字符串长度-1]这个范围,也不会报错。起始偏移量小于0则会当做0终止偏移量大于“长度-1”会被当成-1。例如:

1
2
>>> "abcdefg"[3:50]
'defg'

我们发现正常输出了结果,没有报错。

split()分割和join()合并

split()可以基于指定分隔符将字符串分隔成多个子字符串(存储到列表中)。如果不指定分隔符,则默认使用空白字符(换行符/空格/制表符)。示例代码如下:

1
2
3
4
5
>>> a = "to be or not to be"
>>> a.split()
['to', 'be', 'or', 'not', 'to', 'be']
>>> a.split('be')
['to ', ' or not to ', '']

join()的作用和split()作用刚好相反,用于将一系列子字符串连接起来。示例代码如下:

1
2
3
>>> a = ['sxt','sxt100','sxt200']
>>> '*'.join(a)
'sxt*sxt100*sxt200'

拼接字符串要点:

使用字符串拼接符 + ,会生成新的字符串对象,因此==不推荐使用 + 来拼接字符串==。==推荐使用 join 函数==,因为 join 函数在拼接字符串之前会计算所有字符串的长度,然后逐一拷贝,仅新建一次对象。

==join拼接字符串效率高==

字符串驻留机制

字符串驻留:常量字符串只保留一次

1
2
3
c = "dd#"
d = "dd#"
print(c is d)   #True

字符串比较和同一性

我们可以直接使用 == != 对字符串进行比较,是否含有相同的字符

我们使用 is not is ,判断两个对象是否同一个对象。比较的是对象的地址,即 id(obj1) 是否和 id(obj2) 相等。

== 和!=比较是否含有相同字符

is和not is判断是否同一对象

成员操作符判断子字符串

in not in 关键字,判断某个字符(子字符串)是否存在于字符串中。

1
"ab" in "abcdefg"     #true

字符串常用方法汇总

image-20240204141005422

常用查找方法

image-20240204141055893

去除首尾信息

我们可以通过strip()去除字符串首尾指定信息。通过lstrip()去除字符串左边指定信息,rstrip()去除字符串右边指定信息。

【操作】去除字符串首尾信息

1
2
3
4
5
6
7
8
>>> "*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'
大小写转换

编程中关于字符串大小写转换的情况,经常遇到。我们将相关方法汇总到这里。为了方便学习,先设定一个测试变量:

1
a = "gaoqi love programming, love SXT"

image-20240204141327484

格式排版

center()ljust()rjust() 这三个函数用于对字符串实现排版。示例如下:

1
2
3
4
5
6
7
>>> a="SXT"
>>> a.center(10,"*")
'***SXT****'
>>> a.center(10)
'   SXT   '
>>> a.ljust(10,"*")
'SXT*******'
特征判断方法
  1. isalnum() 是否为字母或数字
  2. isalpha() 检测字符串是否只由字母组成(含汉字)
  3. isdigit() 检测字符串是否只由数字组成
  4. isspace()检测是否为空白符
  5. isupper() 是否为大写字母
  6. islower() 是否为小写字母
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> "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() 函数可以接受不限个数的参数,位置可以不按顺序。

1
2
3
4
5
6
7
8
9
10
11
>>> 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'

我们可以通过{索引}/{参数名},直接映射参数值,实现对字符串的格式化,非常方便。

填充与对齐
  1. 填充常跟对齐一起使用
  2. ^<> 分别是居中、左对齐、右对齐,后面带宽度
  3. : 号后面带填充的字符,只能是一个字符,不指定的话默认是用空格填充
1
2
3
4
>>> "{:*>8}".format("245")
'*****245'
>>> "我是{0},我喜欢数字{1:*^8}".format("杰克","666")
'我是杰克,我喜欢数字**666***'
数字格式化

浮点数通过 f ,整数通过 d 进行需要的格式化。案例如下:

1
2
3
>>> a = "我是{0},我的存款有{1:.2f}"
>>> a.format("高淇",3888.234342)
'我是高淇,我的存款有3888.23'

image-20240204142036151

可变字符串

image-20240204142108726

  1. Python中,字符串属于不可变对象,不支持原地修改,如果需要修改其中的值,只能创建新的字符串对象。
  2. 确实需要原地修改字符串,可以使用io.StringIO对象或array模块

使用io.StringIO可以将字符串变为可变字符串

1
2
3
4
5
6
7
8
9
10
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语言同样也支持数据类型转换。

image-20240204142410256

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
36
37
38
39
40
#类型转换
#转换为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))

列表

列表简介

  1. 列表:用于存储任意数目、任意类型的数据集合。

  2. 列表是内置可变序列,是包含多个元素的有序连续的内存空间。列表的标准语法格式:a = [10,20,30,40]

    其中,10,20,30,40这些称为:列表a的元素

  3. 列表中的元素可以各不相同,可以是任意类型。比如:a = [10,20,'abc',True]

  4. Python的列表大小可变,根据需要随时增加或缩小。

列表对象的常用方法

image-20240204142930427

字符串和列表都是序列类型,一个字符串是一个字符序列,一个列表是任何元素的序列。我们前面学习的很多字符串的方法,在列表中也有类似的用法,几乎一模一样。

创建列表的4种方式

基本语法 []创建
1
2
3
a = [10,20,'gaoqi','sxt']
b = []   # 创建一个空的列表对象
print(a)
list()创建

使用list()可以将任何可迭代的数据转化成列表。

1
2
3
4
5
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

:warning:python3中range()返回的是一个range对象,而不是列表。我们需要通过list()方法将其转换成列表对象。

1
2
3
4
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循环后讲)

使用列表推导式可以非常方便的创建列表,在开发中经常使用。

1
2
3
4
5
# 循环创建多个元素 [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种方式

image-20240204143856202

当列表增加和删除元素时,列表会自动进行内存管理,大大减少了程序员的负担。但这个特点涉及列表元素的大量移动,效率较低。

:warning:除非必要,我们一般只在列表的尾部添加元素或删除元素,这会大大提高列表的操作效率。

append()方法

原地修改列表对象,是真正的列表尾部添加新的元素,速度最快,推荐使用

1
2
3
a = [20,40]
a.append(80)
print(a)       # 结果:[20, 40, 80]
+运算符操作

并不是真正的尾部添加元素,而是创建新的列表对象;将原列表的元素和新列表的元素依次复制到新的列表对象中。这样,会涉及大量的复制操作,对于操作大量元素不建议使用。

1
2
3
4
a = [20,40]
print(id(a))
a = a+[50]
print(id(a)) # 两次地址不一样,创建了新的对象

通过如上测试,我们发现变量a的地址发生了变化。也就是创建了新的列表对象。

extend()方法

将目标列表的所有元素添加到本列表的尾部,属于原地操作,不创建新的列表对象。

1
2
3
4
5
6
7
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() ,它们在删除非尾部元素时也会发生操作位置后面元素的移动。

1
2
3
a = [10,20,30]
a.insert(2,100)
print(a)   # 结果:[10, 20, 100, 30]
乘法扩展

使用乘法扩展列表,生成一个新列表,新列表元素是原列表元素的多次重复。

1
2
3
4
a = ['sxt',100]
b = a*3
print(a) # 结果:['sxt', 100]
print(b) # 结果:['sxt', 100, 'sxt', 100, 'sxt', 100]

适用于乘法操作的,还有:字符串、元组。例如:

1
2
c = 'sxt'
d = c*3     # 结果:'sxtsxtsxt'

列表元素的删除

image-20240204173752878

del 删除

删除列表指定位置的元素。

del()传的是索引

1
2
3
a = [100,200,888,300,400]
del a[2]
print(a)   # 结果:[100,200,300,400]

image-20240204173905654

pop()方法

pop()删除并返回指定位置元素,如果未指定位置则默认操作列表最后一个元素。

pop()传的是空或索引

1
2
3
4
5
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() 传的是元素

1
2
3
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

列表元素访问和计数

image-20240204174302699

通过索引直接访问元素

我们可以通过索引直接访问元素。索引的区间在 [0, 列表长度-1] 这个范围。超过这个范围则会抛出异常。

1
2
3
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]]) 。其中, startend 指定了搜索的范围。

1
2
3
4
>>> 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()可以返回指定元素在列表中出现的次数。

1
2
3
>>> a = [10,20,30,40,50,20,30,20,30]
>>> a.count(20)
3
len()返回列表长度

len()返回列表长度,即列表中包含元素的个数。

1
2
3
>>> a = [10,20,30]
>>> len(a)
3
成员资格判断

判断列表中是否存在指定的元素,我们可以使用 count() 方法,返回0则表示不存在,返回大于0则表示存在。但是,一般我们会使用更加简洁的 in 关键字来判断,直接返回 TrueFalse

1
2
3
4
5
6
7
>>> a = [10,20,30,40,50,20,30,20,30]
>>> 20 in a
True
>>> 100 not in a
True
>>> 30 not in a
False

切片操作

image-20240204174901657

类似字符串的切片操作,对于列表的切片操作和字符串类似。

切片是Python序列及其重要的操作,适用于列表、元组、字符串等等。

切片slice操作可以让我们快速提取子列表或修改。标准格式为:

[起始偏移量start:终止偏移量end[:步长step]]

==包头不包尾==

典型操作(三个量为正数的情况)如下:

image-20240204175030240

其他操作(三个量为负数)的情况:

image-20240204175047464

切片操作时,起始偏移量和终止偏移量不在 [0,字符串长度-1] 这个范围,也不会报错。起始偏移量 小于0 则会当做 0 ,终止偏移量大于”长度-1” 会被当成 “长度-1” 。例如:[10,20,30,40][1:30]

结果: [20, 30, 40]

我们发现正常输出了结果,没有报错。

列表的遍历

1
2
3
a = [10,20,30,40]
for obj in a: # obj是临时变量名称,随意起
   print(obj)
复制列表所有的元素到新列表对象

如下代码实现列表元素的复制了吗?

1
2
list1 = [30,40,50]
list2 = list1

只是将list2也指向了列表对象,也就是说list2和list2持有地址值是相同的,列表对象本身的元素并没有复制。

我们可以通过如下简单方式,实现列表元素内容的复制:

1
2
list1 = [30,40,50]
list2 = [] + list1 #生成了新列表对象

注:我们后面也会学习copy模块,使用浅复制或深复制实现我们的复制操作

列表排序

修改原列表,不建新列表的排序

使用sort()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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()进行排序,这个方法返回新列表,不对原列表做修改

1
2
3
4
5
6
7
8
9
10
11
>>> 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()不对原列表做任何修改,只是返回一个逆序排列的迭代器对象。

1
2
3
4
5
6
7
8
>>> 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

用于返回列表中最大和最小值。

1
2
3
4
5
>>> a = [3,10,20,15,9]
>>> max(a)
20
>>> min(a)
3
sum

对数值型列表的所有元素进行求和操作,对非数值型列表运算则会报错。

1
2
3
>>> a = [3,10,20,15,9]
>>> sum(a)
57

多维列表

image-20240204180112359

二维列表

一维列表可以帮助我们存储一维、线性的数据。

二维列表可以帮助我们存储二维、表格的数据。例如下表的数据:

image-20231113221834012

image-20231113221906067

嵌套循环打印二维列表所有的数据:

1
2
3
4
5
6
7
8
9
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

==元组不可变序列,不能修改元组中的元素==

image-20240204180418518

  • 列表属于可变序列,可以任意修改列表中的元素。
  • 元组属于不可变序列,不能修改元组中的元素。

因此,元组没有增加元素、修改元素、删除元素相关的方法。

因此,我们只需学元组的创建和删除,元素的访问和计数即可。元组支持如下操作:

  1. 索引访问
  2. 切片操作
  3. 连接操作
  4. 成员关系操作
  5. 比较运算操作
  6. 计数:元组长度len()、最大值max()、最小值min()、求和sum()

元组的创建

通过()创建元组

小括号可以省略。

a = (10,20,30) 或者 a = 10,20,30

==如果元组只有一个元素,则必须后面加逗号==。这是因为解释器会把(1)解释为整数1(1,)解释为元组。

a = 10,

1
2
3
4
5
6
>>> a = (1)
>>> type(a)
<class 'int'>
>>> a = (1,)     # 或者 a = 1,
>>> type(a)
<class 'tuple'>
通过tuple()创建元组

tuple(可迭代的对象)

1
2
3
4
a = tuple()  # 创建一个空元组对象
b = tuple("abc")
c = tuple(range(3))
d = tuple([2,3,4])

总结:

  1. tuple()可以接收列表、字符串、其他序列类型、迭代器等生成元组。
  2. list()可以接收元组、字符串、其他序列类型、迭代器等生成列表。

元组的元素访问和计数

元组的元素不能修改

1
2
3
4
5
6
7
>>> 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()、切片等操作,和列表一样。

1
2
3
4
5
6
7
>>> 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)一样)

1
2
a = (20,10,30,9,8)
b = sorted(a)   # b是新对象,内容是:[8, 9, 10, 20, 30]

zip

zip(列表1,列表2,...)将多个列表对应位置的元素组合成为元组,并返回这个zip对象。

:warning:如果各个迭代器的元素个数不一致,则返回列表长度==与最短的对象相同==

1
2
3
4
5
6
7
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)

生成器推导式创建元组

  1. 从形式上看,生成器推导式与列表推导式类似,只是生成器推导式使用小括号。
  2. 列表推导式直接生成列表对象,生成器推导式生成的不是列表也不是元组,而是一个生成器对象。
  3. 我们可以通过生成器对象,转化成列表或者元组。也可以使用生成器对象的 __next__() 方法进行遍历,或者直接作为迭代器对象来使用。不管什么方式使用,元素访问结束后,如果需要重新访问其中的元素,必须重新创建该生成器对象。

【操作】生成器的使用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 列表推导式: [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

元组总结

  1. 元组的核心特点是:==不可变序列==。
  2. 元组的访问和处理速度比列表快。
  3. 整数和字符串一样,元组可以作为字典的键,==列表则永远不能作为字典的键使用==。

字典

字典是“键值对”的==无序可变序列==,字典中的每个元素都是一个“键值对”,包含:“键对象”和“值对象”。可以通过“键对象”实现快速获取、删除、更新对应的“值对象”。

image-20240204184816365

一个典型的字典的定义方式:

a = {'name':'gaoqi', 'age':18, 'job':'programmer'}

列表中我们通过“下标数字”找到对应的对象。字典中通过“键对象”找到对应的“值对象”。

  1. “键”是任意的不可变数据,比如:整数、浮点数、字符串、元组。
  2. 但是:列表、字典、集合这些可变对象,不能作为“键”。
  3. 并且“键”不可重复。
  4. “值”可以是任意的数据,并且可重复。

字典的创建

  1. 我们可以通过{}dict()来创建字典对象。

    1
    2
    3
    4
    5
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    b = dict(name='gaoqi',age=18,job='programmer')
    a = dict([("name","gaoqi"),("age",18)])
    c = {}   # 空的字典对象
    d = dict()  # 空的字典对象
  2. 通过zip()创建字典对象

    1
    2
    3
    4
    k = ['name','age','job']
    v = ['gaoqi',18,'teacher']
    d = dict(zip(k,v))
    print(d) # {'name': 'gaoqi', 'age': 18, 'job': 'techer'}
  3. 通过fromkeys创建值为空的字典

    1
    2
    f = dict.fromkeys(['name','age','job'])
    print(f)  #结果:{'name': None, 'age': None, 'job': None}

image-20231114145951487

字典元素的访问

  1. 通过 [键] 获得“值”。若键不存在,则抛出异常。

    1
    2
    3
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    b = a['name']
    print(b)
  2. 通过get()方法获得“值”。❤️推荐使用。优点是:指定键不存在,返回None;也可以设定指定键不存在时默认返回的对象。推荐

    使用get()获取“值对象”

    1
    2
    3
    4
    5
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    b = a.get('name')
    c = a.get('gender','不存在')
    print(b)
    print(c)
  3. 列出所有的键值对

    1
    2
    3
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    b = a.items()
    print(b)   # dict_items([('name', 'gaoqi'),('age', 18), ('job', 'programmer')])
  4. 列出所有的键,列出所有的值

    1
    2
    3
    4
    5
    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'])
  5. len() 键值对的个数

    1
    2
    3
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    num = len(a)
    print(num)   #3
  6. 检测一个“键”是否在字典中

    1
    2
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    print("name" in a)  #True

字典元素的添加、修改、删除

  1. 给字典新增“键值对”。如果“键”已经存在,则覆盖旧的键值对;如果“键”不存在,则新增“键值对”

    1
    2
    3
    4
    5
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    a['address']='西三旗1号院'
    a['age']=16
    print(a)
    #{'name': 'gaoqi', 'age': 16, 'job':'programmer', 'address': '西三旗1号院'}
  2. 使用 update() 将新字典中所有键值对全部添加到旧字典对象上。如果 key 有重复,则直接覆盖

    1
    2
    3
    4
    5
    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': '男的'}
  3. 字典中元素的删除,可以使用 del() 方法;或者 clear() 删除所有键值对; pop() 删除指定键值对,并返回对应的“值对象”

    1
    2
    3
    4
    5
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    del(a['name'])
    print(a)       # {'age': 18, 'job':'programmer'}
    age = a.pop('age')
    print(age)     # 18
  4. popitem() :随机删除和返回该键值对。字典是“无序可变序列”,因此没有第一个元素、最后一个元素的概念; popitem 弹出随机的项,因为字典并没有”最后的元素”或者其他有关顺序的概念。若想一个接一个地移除并处理项,这个方法就非常有效(因为不用首先获取键的列表)

    1
    2
    3
    4
    5
    a = {'name':'gaoqi','age':18,'job':'programmer'}
    r1 = a.popitem()
    r2 = a.popitem()
    r3 = a.popitem()
    print(a)    #{}

序列解包

序列解包可以用于元组、列表、字典。序列解包可以让我们方便的对多个变量赋值。

1
2
3
x,y,z=(20,30,10)
(a,b,c)=(9,8,10)
[m,n,p]=[10,20,30]

序列解包用于字典时,默认是对“键”进行操作; 如果需要对键值对操作,则需要使用items();如果需要对“值”进行操作,则需要使用values()

items()对键值进行操作返回的是元组,可以通过索引获得键和值

1
2
3
4
5
6
7
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

表格数据使用字典和列表存储和访问

image-20231114154449214

1
2
3
4
5
6
7
8
9
10
11
12
13
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

image-20240204191242334

将一个键值对放进字典的底层过程
1
2
a = {}
a["name"]="gaoqi"

假设字典a对象创建完后,数组长度为8:

image-20240204191323540

我们要把"name"="gaoqi"这个键值对放到字典对象a中,首先第一步需要计算键"name"的散列值。Python中可以通过hash()来计算。

1
2
>>> bin(hash("name"))
'-0b1010111101001110110101100100101'

由于数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即"101",十进制是数字5。我们查看偏移量5,对应的bucket是否为空。如果为空,则将键值对放进去。如果不为空,则依次取右边3位作为偏移量,即"100",十进制是数字4。再查看偏移量为4的bucket是否为空。直到找到为空的bucket将键值对放进去。流程图如下:

image-20240204191545530

image-20240204191702494

扩容

  1. python会根据散列表的拥挤程度扩容。“扩容”指的是:创造更大的数组,将原有内容拷贝到新数组中。
  2. 接近2/3时,数组就会扩容。
根据键查找“键值对”的底层过程

明白了,一个键值对是如何存储到数组中的,根据键对象取到值对象,理解起来就简单了。

1
2
>>> a.get("name")
'gaoqi'

当调用a.get("name"),就是根据键"name"查找到"键值对",从而找到值对象"gaoqi"

我们仍然要首先计算"name"对象的散列值:

1
2
>>> bin(hash("name"))
'-0b1010111101001110110101100100101'

和存储的底层流程算法一致,也是依次取散列值的不同位置的数字。 假设数组长度为8,我们可以拿计算出的散列值的最右边3位数字作为偏移量,即 101 ,十进制是数字5。我们查看偏移量5,对应的 bucket 是否为空。如果为空,则返回 None 。如果不为空,则将这个 bucket 的键对象计算对应散列值,和我们的散列值进行比较,如果相等。则将对应“值对象”返回。如果不相等,则再依次取其他几位数字,重新计算偏移量。依次取完后,仍然没有找到。则返回None 。流程图如下:

image-20231114160311802

用法总结

  1. 字典在内存中开销巨大,典型的空间换时间。

  2. 键查询速度很快

  3. 往字典里面添加新键值对可能导致扩容,导致散列表中键的次序变化。因此,不要在遍历字典的同时进行字典的修改

  4. 键必须可散列

    • 数字、字符串、元组,都是可散列的

    • 自定义对象需要支持下面三点:(面向对象章节中再展开说)

      支持 hash() 函数

      支持通过 __eq__() 方法检测相等性

      a==b 为真,则 hash(a)==hash(b) 也为真

集合

集合是无序可变元素不能重复。实际上,集合底层是字典实现,集合的所有元素都是字典中的“键对象”,因此是不能重复的且唯一的。

集合创建和删除

  1. 使用{}创建集合对象,并使用add()方法添加元素

    1
    2
    a = {3,5,7}
    a.add(9)    # {9, 3, 5, 7}
  2. 使用set(),将列表、元组等可迭代对象转成集合。如果原来数据存在重复数据,则只保留一个

    1
    2
    a = ['a','b','c','b']
    b = set(a)   # {'b', 'a', 'c'}
  3. remove()删除指定元素;clear()清空整个集合

    1
    2
    a = {10,20,30,40,50}
    a.remove(20)   # {10, 50, 40,30}

集合相关操作

像数学中概念一样,Python对集合也提供了并集、交集、差集等运算。我们给出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> 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}

控制语句

控制语句和逻辑思维

控制语句:把语句组合成能完成一定功能的小逻辑模块。

分为三类:顺序、选择和循环。

学会控制语句,是真正跨入编程界的“门槛”,是成为“程序猿”的“门票”。

  1. “顺序结构”代表 “先执行a,再执行b” 的逻辑。比如,先找个女朋友,再给女朋友打电话;先订婚,再结婚;
  2. “条件判断结构”代表 “如果…,则…” 的逻辑。比如,如果女朋友来电,则迅速接电话;如果看到红灯,则停车;
  3. “循环结构”代表 “如果…,则重复执行…” 的逻辑。比如,如果没打通女朋友电话,则再继续打一次; 如果没找到喜欢的人,则再继续找

很神奇的是,三种流程控制语句就能表示所有的事情!

选择结构(条件判断结构)

选择结构通过判断条件是否成立,来决定执行哪个分支。选择结构有多种形式,分为:单分支、双分支、多分支。

单分支选择结构

image-20240204193246251

if语句单分支结构的语法形式如下:

1
2
if 条件表达式:
语句/语句块
  1. 条件表达式:可以是逻辑表达式、关系表达式、算术表达式等等。
  2. 语句/语句块:可以是一条语句,也可以是多条语句。多条语句,缩进必须对齐一致

条件表达式详解

在选择和循环结构中,条件表达式的值为 False 的情况如下:

False、0、0.0、空值None、空序列对象(空列表、空元祖、空集合、空字典、空字符串)、空range对象、空迭代对象。

其他情况,均为 True 。这么看来,Python所有的合法表达式都可以看做条件表达式,甚至包括函数调用的表达式。

【操作】测试各种条件表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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")

:warning:条件表达式中,不能有赋值操作符 =

在Python中,条件表达式不能出现赋值操作符 = ,避免了其他语言中经常误将关系运算符 == 写作赋值运算符 = 带来的困扰。

如下代码将会报语法错误:

1
2
if 3 < c and (c=20):   # 直接报语法错误!
print("赋值符不能出现在条件表达式中")

双分支选择结构

image-20240204194426609

双分支结构的语法格式如下:

1
2
3
4
if 条件表达式:
语句1/语句块1
else:
语句2/语句块2

【操作】输入一个数字,小于10,则打印该数字;大于10,则打印“数字太大”

1
2
3
4
5
num = input("输入一个数字:")
if int(num)<10:
   print(num)
else:
   print("数字太大")

三元条件运算符

image-20240204194548177

1
条件为真时的值 if (条件表达式) else 条件为假时的值

上一个案例代码,可以用三元条件运算符实现:

1
2
num = input("输入一个数字:")
print(num + "<10" if(int(num) < 10) else str(num) + ">10")

可以看到,这种写法更加简洁,易读。

多分支选择结构

image-20231114164837210

多分支选择结构的语法格式如下:

1
2
3
4
5
6
7
8
9
10
if 条件表达式1 :
语句1/语句块1
elif 条件表达式2:
语句2/语句块2
...
elif 条件表达式n :
语句n/语句块n
[else:
语句n+1/语句块n+1
]

:warning:多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序

【操作】输入一个学生的成绩,将其转化成简单描述:不及格(小于60)、及格(60-79)、良好(80-89)、优秀(90-100)

方法1(使用完整的条件表达)

1
2
3
4
5
6
7
8
9
10
11
12
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(利用多分支结构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'''
### 多分支选择结构
'''
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))

:warning:多分支结构,几个分支之间是有逻辑关系的,不能随意颠倒顺序

选择结构的嵌套

选择结构可以嵌套,使用时一定要注意控制好不同级别代码块的缩进量,因为缩进量决定了代码的从属关系。

【操作】输入一个分数。分数在0-100之间。90以上是A,80以上是B,70以上是C,60以上是D。60以下是E

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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))
1
2
3
4
5
6
7
8
9
10
# 更少的代码方法
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则重复执行循环体里的语句。图示如下:

image-20240204195836334

循环体里面的语句至少应该包含改变条件表达式的语句,以使循环趋于结束;否则,就会变成一个死循环。

while循环

while循环的语法格式如下:

1
2
while 条件表达式:
循环体语句

【操作】利用while循环,计算1-100之间数字的累加和;计算1-100之间偶数的累加和,计算1-100之间奇数的累加和

1
2
3
4
5
6
num = 0
sum_all = 0         # 1-100所有数的累加和
while num<=100:
  sum_all += num
    num += 1         # 迭代,改变条件表达式,使循环趋于结束
print("1-100所有数的累加和",sum_all)

for循环和可迭代对象遍历

for循环通常用于可迭代对象的遍历。for循环的语法格式如下:

1
2
for  变量   in  可迭代对象:
循环体语句

【操作】遍历一个元组或列表

1
2
for x in (20, 30, 50):
print(x*3)

可迭代对象

Python包含以下几种可迭代对象:

  1. 序列。包含:字符串、列表、元组、字典、集合
  2. 迭代器对象(iterator)
  3. 生成器函数(generator)
  4. 文件对象

我们已经在前面学习了序列、字典等知识,迭代器对象和生成器函数将在后面进行详解。接下来,我们通过循环来遍历这几种类型的数据:

【操作】遍历字符串中的字符

1
2
for temp in "weqwewe":
print(x)

【操作】遍历字典

1
2
3
4
5
6
7
8
9
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之间奇数的累加和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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))

嵌套循环

image-20231116160125551

image-20231116160157632

1
2
3
4
for x in range(5):
for y in range(5):
print(x, end="\t")
print()
嵌套循环练习

image-20231116160523325

【操作】利用嵌套循环打印九九乘法表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 九九乘法表
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的数据

1
2
3
4
5
6
7
8
# 用列表和字典存储下表信息,并打印出表中工资高于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语句只能跳出最近一层的循环。

image-20240204200808384

【操作】使用break语句结束循环

1
2
3
4
5
6
7
while True:
  a = input("请输入一个字符(输入Q或q结束)")
  if a.upper()=='Q':
        print("循环结束,退出")
        break    
    else:
        print(a)

continue语句

continue语句用于结束本次循环,继续下一次。多个循环嵌套时,continue也是应用于最近的一层循环。

image-20240204200942740

【操作】要求输入员工的薪资,若薪资小于0则重新输入。最后打印出录入员工的数量和薪资明细,以及平均薪资

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 要求输入员工的薪资,若薪资小于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语句

whilefor循环可以附带一个else语句(可选)。如果forwhile语句没有被break语句结束,则会执行else子句,否则不执行。语法格式如下:

1
2
3
4
5
6
7
8
9
10
while  条件表达式:
循环体
else:
语句块

或者:
for  变量  in  可迭代对象:
循环体
else:
语句块

【操作】员工一共4人。录入这4位员工的薪资。全部录入后,打印提示“您已经全部录入4名员工的薪资”。最后,打印输出录入的薪资和平均薪资

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
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
"""

image-20231116164105716

循环代码优化技巧

虽然计算机越来越快,空间也越来越大,我们仍然要在性能问题上“斤斤计较”。编写循环时,遵守下面三个原则可以大大提高运行效率,避免不必要的低效计算:

  1. 尽量减少循环内部不必要的计算
  2. 嵌套循环中,尽量减少内层循环的计算,尽可能向外提
  3. 局部变量查询较快,尽量使用局部变量

其他优化手段

  1. ==连接多个字符串或列表,使用join()或append()而不使用+== +会创建新的字符串,join不会
  2. ==列表进行元素插入和删除,尽量在列表尾部操作==

zip()并行送代多个序列

我们可以通过zip()函数对多个序列进行并行迭代,zip()函数在最短序列“用完”时就会停止。

image-20231116165017217

1
2
3
4
5
6
7
8
9
10
11
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初学者的水平。

列表推导式

列表推导式生成列表对象,语法如下:

1
2
3
[表达式  for  item  in 可迭代对象 ]
或者:
{表达式  for  item  in 可迭代对象  if  条件判断}
1
2
3
4
5
6
7
[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)
1
2
3
4
5
6
7
8
9
10
11
12
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)]
'''

字典推导式

字典的推导式生成字典对象,格式如下:

1
{key_expression: value_expression  for  表达式 in  可迭代对象}

类似于列表推导式,字典推导也可以增加if条件判断、多个for循环。

1
2
3
values = ["北京","上海","深圳","广州"]
cities = {id*100:city for id,city in zip(range(1,5),values)}
print(cities)

生成字典对象:

1
{100: '北京', 200: '上海', 300: '深圳', 400:'广州'}

【操作】统计文本中字符出现的次数:

1
2
3
4
5
6
7
8
# 统计字数
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}
'''

集合推导式

集合推导式生成集合,和列表推导式的语法格式类似:

1
2
3
{表达式  for  item  in 可迭代对象 }
或者:
{表达式  for  item  in 可迭代对象  if  条件判断}
1
2
>>> {x for x in range(1,100) if x%9==0}
{99, 36, 72, 9, 45, 81, 18, 54, 90, 27, 63}

生成器推导式(不直接生成元组)

很多同学可能会问:“都有推导式,元组有没有?”,能不能用小括号呢?

1
2
>>> (x for x in range(1,100) if x%9==0)
<generator object <genexpr> at 0x0000000002BD3048>

我们发现提示的是“一个生成器对象”。显然,元组是没有推导式的

一个生成器只能运行一次。第一次迭代可以得到数据,第二次迭代发现数据已经没有了。

1
2
3
4
5
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=' ')

综合练习

绘制不同颜色的同心圆

image-20231116173335354

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 绘制不同颜色的同心圆
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() # 程序执行完毕,窗口还在

函数和内存底层分析

函数是可重用的程序代码块。

函数的作用,不仅可以实现代码的复用,更能实现代码的一致性。一致性指的是,只要修改函数的代码,则所有调用该函数的地方都能得到体现。

image-20240205163918788

在编写函数时,函数体中的代码写法和我们前面讲述的基本一致,只是对代码实现了封装,并增加了函数调用、传递参数、返回计算结果等内容。

:warning:为了让大家更容易理解,掌握的更深刻。我们也要深入内存底层进行分析。绝大多数语言内存底层都是高度相似的,这样大家掌握了这些内容也便于以后学习其他语言。

函数简介

函数(function)的基本概念

  1. 一个程序由一个一个的任务组成;函数就是代表一个任务或者一个功能(function)。
  2. 函数是代码复用的通用机制

Python函数的分类

image-20240205164155796

Python函数分为如下几类:

  1. 内置函数

    我们前面使用的 str()list()len() 等这些都是内置函数,我们可以拿来直接使用。

  2. 标准库函数

    我们可以通过 import 语句导入库,然后使用其中定义的函数

  3. 第三方库函数

    Python社区也提供了很多高质量的库。下载安装这些库后,也是通过 import 语句导入,然后可以使用这些第三方库的函数

  4. 用户自定义函数

    用户自己定义的函数,显然也是开发中适应用户自身需求定义的函数。今天我们学习的就是如何自定义函数。

函数的定义和调用

核心要点

Python中,定义函数的语法如下:

1
2
3
def  函数名 ([参数列表]) :
'''文档字符串'''
函数体/若干语句

简单定义一个函数:

1
2
3
4
5
6
7
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)

image-20231117164127321

要点:

  1. 我们使用 def 来定义函数,然后就是一个空格和函数名称;
    • Python执行 def 时,会创建一个函数对象,并绑定到函数名变量上。
  2. 参数列表
    • 圆括号内是形式参数列表,有多个参数则使用逗号隔开
    • 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
    • 调用时的实际参数必须与形参列表一一对应
  3. return 返回值
    • 如果函数体中包含 return 语句,则结束函数执行并返回值;
    • 如果函数体中不包含 return 语句,则返回 None 值。
  4. 调用函数之前,必须要先定义函数,即先调用 def 创建函数对象
    • 内置函数对象会自动创建
    • 标准库和第三方库函数,通过 import 导入模块时,会执行模块中的def语句

形参和实参

image-20240205165030263

形参和实参的要点:

  1. 圆括号内是形式参数列表,有多个参数则使用逗号隔开
  2. 定义时的形式参数不需要声明类型,也不需要指定函数返回值类型
  3. 调用时的实际参数必须与形参列表一一对应

【操作】定义一个函数,实现两个数的比较,并返回较大的值

1
2
3
4
5
6
7
8
9
10
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__ 直接获取到函数的文档字符串,自己进行打印。

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
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 返回值要点:

  1. 如果函数体中包含 return 语句,则结束函数执行并返回值
  2. 如果函数体中不包含 return 语句,则返回 None
  3. 要返回多个值,使用列表、元组、字典、集合将多个值“存起来”即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def printShape(n):
"""
返回一个列表
:param n:
:return:
"""
s1 = "#" * n
s2 = "$" * n
return [s1, s2]


s = printShape(5)
print(s)

"""
['#####', '$$$$$']
"""

函数也是对象,内存底层分析

==Python中,”一切都是对象”==。实际上,执行def定义函数后,系统就创建了相应的==函数对象==。

1
2
3
4
5
6
7
8
9
10
11
12
def print_star(n):                
   print("*"*n)

print(print_star)
print(id(print_star))
c = print_star
c(3)

"""
<function print_star at 0x0000000002BB8620>
45844000
"""

image-20231117170506090

image-20231117170556762

  1. 显然,我们可以看出变量 cprint_star 都是指向了同一个函数对象。因此,执行 c(3) 和执行print_star(3) 的效果是完全一致的。
  2. Python中,圆括号意味着调用函数。在没有圆括号的情况下,Python会把函数当做普通对象。

与此核心原理类似,我们也可以做如下操作:

1
2
zhengshu = int
zhengshu("234")

显然,我们将内置函数对象 int() 赋值给了变量 zhengshu ,这样zhengshu 和 int 都是指向了同一个内置函数对象。

:warning:当然,此处仅限于原理性讲解,实际开发中没必要这么做。

变量的作用域(全局变量和局部变量)

变量起作用的范围称为变量的作用域,不同作用域内同名变量之间互不影响。变量分为:全局变量、局部变量。

全局变量

  1. 在函数和类定义之外声明的变量。作用域为定义的模块,从定义位置开始直到模块结束。
  2. 全局变量降低了函数的通用性和可读性。应尽量避免全局变量的使用。
  3. 要在函数内改变全局变量的值,使用 global 声明一下

局部变量

  1. 在函数体中(包含形式参数)声明的变量。
  2. 局部变量的引用比全局变量快,优先考虑使用
  3. 如果局部变量和全局变量同名,则在函数内隐藏全局变量,只使用同名的局部变量

【操作】 输出局部变量和全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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>}

"""

局部变量和全局变量效率测试

==局部变量的查询和访问速度比全局变量快,优先考虑使用,尤其是在循环的时候。==

==在特别强调效率的地方或者循环次数较多的地方,可以通过将全局变量转为局部变量提高运行速度。==

参数的传递

image-20240205170408020

函数的参数传递本质上就是:从实参到形参的赋值操作。Python中“一切皆对象”,所有的赋值操作都是“引用的赋值”。所以,Python中参数的传递都是“引用传递”,不是“值传递”。

具体操作时分为两类:

  1. 对“可变对象”进行“写操作”,直接作用于原对象本身。
  2. 对“不可变对象”进行“写操作”,会产生一个新的“对象空间”,并用新的值填充这块空间。

:warning:

可变对象有:

==字典、列表、集合==、自定义的对象等

不可变对象有:

数字、==字符串、元组、function==等

传递可变对象的引用

传递参数是可变对象(例如:列表字典集合、自定义的其他可变对象等),实际传递的还是对象的引用。在函数体中不创建新的对象拷贝,而是可以直接修改所传递的对象。

【操作】参数传递:传递可变对象的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]
"""

传递不可变对象的引用

传递参数是不可变对象(例如: intfloat字符串元组布尔值),实际传递的还是对象的引用。在”赋值操作”时,由于不可变对象无法修改,系统会新创建一个对象。

【操作】参数传递:传递不可变对象的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 值我们可以看到 na 一开始是同一个对象。给n赋值后,n是新的对象。

浅拷贝和深拷贝

image-20240205171232805

  1. 浅拷贝:==拷贝对象,但不拷贝子对象的内容,只是拷贝子对象的引用。==
  2. 深拷贝:==拷贝对象,并且会连子对象的内存也全部(递归)拷贝一份,对子对象的修改不会影响源对象==

传递不可变对象包含的子对象是可变的情况

传递不可变对象时,不可变对象里面包含的子对象是可变的。则方法内修改了这个可变对象,源对象也发生了变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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])
"""

参数的几种类型

image-20240205171609469

位置参数

函数调用时,实参默认按位置顺序传递,需要个数和形参匹配。按位置传递的参数,称为:“位置参数”。

【操作】测试位置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
"""

默认值参数

我们可以为某些参数设置默认值,这样这些参数在传递时就是可选的。称为“默认值参数”。默认值参数放到位置参数后面。

【操作】测试默认值参数

1
2
3
4
5
6
7
8
9
10
11
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
"""

命名参数(关键字参数)

我们也可以按照形参的名称传递参数,称为“命名参数”,也称“关键字参数”。

1
2
3
4
5
6
7
8
9
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
"""

可变参数

可变参数指的是“可变数量的参数”。分两种情况:

  1. *param (一个星号),将多个参数收集到一个“元组”对象中。
  2. **param (两个星号),将多个参数收集到一个“字典”对象中。

【操作】测试可变参数处理(元组、字典两种方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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}
"""

强制命名参数

在带星号的“可变参数”后面增加新的参数,必须在调用的时候“强制命名参数”。

1
2
3
4
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 表达式的基本语法如下:

1
lambda  arg1,arg2,arg3... :  <表达式>

arg1 arg2 arg3 为函数的参数。<表达式>相当于函数体。运算结果是:表达式的运算结果。

【操作】lambda表达式使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

参数:

  1. source :一个Python表达式或函数 compile() 返回的代码对象
  2. globals :可选。必须是 dictionary
  3. locals :可选。任意映射对象

:warning:eval函数会将字符串当做语句来执行,因此会被注入安全隐患。比如:字符串中含有删除文件的语句。那就麻烦大了。因此,使用时候,要慎重!!!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"""

递归函数

  1. 递归(recursion)是一种常见的算法思路,在很多算法中都会用到。比如:深度优先搜索(DFS:Depth First Search)等。
  2. 递归的基本思想就是“自己调用自己”

递归函数指的是:自己调用自己的函数,在函数体内部直接或间接的自己调用自己。每个递归函数必须包含两个部分:

  1. 终止条件

    表示递归什么时候结束。一般用于返回值,不再调用自己。

  2. 递归步骤

    把第n步的值和第n-1步相关联。

:warning:递归函数由于会创建大量的函数对象、过量的消耗内存和运算能力。在处理大量数据时,谨慎使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
"""

image-20231117185027038

【操作】 使用递归函数计算阶乘(factorial)

1
2
3
4
5
6
7
8
def factorial(n):
if n == 1:
return 1
else:
return n * factorial(n - 1)


print(factorial(5))

image-20231117185357907

嵌套函数(内部函数)

嵌套函数:在函数内部定义的函数!

一般在什么情况下使用嵌套函数?

  1. 封装-数据隐藏

    外部无法访问“嵌套函数”。

  2. 贯彻DRY(Don’t Repeat Yourself)原则

  3. 嵌套函数,可以让我们在函数内部避免重复代码。

  4. 闭包(后面会讲解)

nonlocal和global关键字

image-20240205174327181

nonlocal 用来在内部函数中,声明外层的局部变量。

global 函数内声明全局变量,然后才使用全局变量

LEGB规则

Python在查找“名称”时,是按照LEGB规则查找的:

image-20240205174442308

Local 指的就是函数或者类的方法内部

Enclosed 指的是嵌套函数(一个函数包裹另一个函数,闭包)

Global 指的是模块中的全局变量

Built in 指的是Python为自己保留的特殊名称

如果某个 name 映射在局部 local 命名空间中没有找到,接下来就会在闭包作用域 enclosed 进行搜索,如果闭包作用域也没有找到,Python就会到全局 global 命名空间中进行查找,最后会在内建built-in 命名空间搜索 (如果一个名称在所有命名空间中都没有找到,就会产生一个 NameError

面向对象

面向对象简介

Python完全采用了面向对象的思想,是真正面向对象的编程语言,完全支持面向对象的基本功能,例如:继承、多态、封装等。

==Python中,一切皆对象==。我们在前面学习的数据类型、函数等,都是对象。

  • 面向对象(Object oriented Programming,OOP)编程的思想主要是针对大型软件设计而来的。
  • 面向对象编程使程序的扩展性更强、可读性更好,使编程可以像搭积木一样简单。
  • 面向对象编程将数据和操作数据相关的方法封装到对象中,组织代码和数据的方式更加接近人的思维,从而大大提高了编程的效率。

❤️Python支持面向过程、面向对象、函数式编程等多种编程范式。

面向过程和面向对象思想

面向过程和面向对象的区别

面向过程和面向对象都是对软件分析、设计和开发的一种思想,它指导着人们以不同的方式去分析、设计和开发软件。

==C语言是一种典型的面向过程语言,Java是一种典型的面向对象语言。==

面向过程是什么?

面向过程适合简单、不需要协作的事务,重点关注如何执行。面向过程时,我们首先思考“怎么按步骤实现?”。

比如,如何开车?我们很容易就列出实现步骤:

image-20240205174928790

面向对象是什么?

面向对象(Oriented-Object)思想更契合人的思维模式。我们首先思考的是”怎么设计这个事物?”。比如思考造车,我们就会先思考“车怎么设计?”,而不是“怎么按步骤造车的问题”。这就是思维方式的转变。天然的,我们就会从“车由什么组成”开始思考:

image-20240205175117899

为了协作,我们找轮胎厂完成制造轮胎的步骤,发动机厂完成制造发动机的步骤;这样,发现大家可以同时进行车的制造,最终进行组装,大大提高了效率。具体到轮胎厂的一个流水线操作,仍然是有步骤的,还是离不开执行者、离不开面向过程!

面向对象可以帮助我们从宏观上把握、从整体上分析整个系统。但是,具体到实现部分的微观操作(就是一个个方法),仍然需要面向过程的思路去处理。

我们干万不要把面向过程和面向对象对立起来。他们是相辅相成的。==面向对象离不开面向过程!==

面向对象和面向过程总结

①都是解决问题的思维方式,都是代码组织的方式。

②==面向过程是一种“执行者思维”==,解决简单问题可以使用面向过程

③==面向对象是一种“设计者思维”==,解决复杂、需要协作的问题可以使用面向对象

面向对象离不开面向过程:

  • 宏观上:通过面向对象进行整体设计
  • 微观上:执行和处理数据,仍然是面向过程

对象进化的小故事

image-20231117195725002

类的定义

类可以看做是一个模版,或者图纸,系统根据类的定义来造出对象。我们要造一个汽车,怎么样造?类就是这个图纸,规定了汽车的详细信息,然后根据图纸将汽车造出来。

类:我们叫做class。对象:我们叫做object,instance(实例)。以后我们说某个类的对象,某个类的实例。是一样的意思。

我们把对象比作一个“饼干”,类就是制造这个饼干的“模具”。

属性和方法

我们通过类定义数据类型的属性(数据)和方法(行为),也就是说,“类将行为和状态打包在一起”。

image-20240205175450838

image-20240205175508133

从一个类创建对象时,每个对象会共享这个类的行为(类中定义的方法),但会有自己的属性值(不共享状态)。更具体一点:“方法代码是共享的,属性数据不共享”。

image-20240205175550650

Python中,”一切皆对象”。类也称为“==类对像==”,类的实例也称为“实例对象”。

定义类的语法格式如下:

1
2
class  类名
类体

要点如下:

①类名必须符合“标识符”的规则;一般规定,==首字母大写,多个单词使用“驼峰原则”==。

②类体中我们可以定义属性和方法

③属性用来描述数据,方法(即函数)用来描述这些数据相关的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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对象包含如下部分:

image-20240205175738918

__init__构造方法和__new__方法

初始化对象,我们需要定义构造函数 __init__() 方法。构造方法用于执行“实例对象的初始化工作”,即对象创建后,初始化当前对象的相关属性,无返回值。

image-20240205175933233

构造方法是负责初始化(装修),不是建对象(房子)

__init__() 构造函数的要点如下:

  1. 名称固定,必须为:__init__()

  2. 第一个参数固定,必须为:selfself指的就是刚刚创建好的实例对象

  3. 构造函数通常用来初始化实例对象的实例属性,如下代码就是初始化实例属性:namescore

    1
    2
    3
    def __init__(self, name, score):  # self参数是必须有的
    self.name = name # 实例属性
    self.score = score # 实例属性
  4. 通过“类名(参数列表)”来调用构造函数。调用后,将创建好的对象返回给相应的变量。比如:s1=Student("张三",80)

  5. __init__()方法:==初始化创建好的对象==,初始化指的是:“给实例属性赋值”

  6. __new__()方法:==用于创建对象==,但我们一般无需重定义该方法

  7. 如果我们不定义__init__方法,系统会提供一个默认的__init__方法。如果我们定义了带参的__init__方法,系统不创建默认的__init__方法

Python中的self相当于C++中的self指针,JAVA和C#中的this关键字。Python中self必须为构造函数的第一个参数,名字可以任意修改。但一般惯例,都叫做self

image-20231117204441814

实例属性和实例方法

实例属性

实例属性是从属于实例对象的属性,也称为“实例变量”。他的使用有如下几个要点:

  1. 实例属性一般在__init__()方法中通过如下代码定义:

    self.实例属性名 = 初始值

  2. 在本类的其他实例方法中,也是通过self进行访问:

    self.实例属性名

  3. 创建实例对象后,通过实例对象访问:

    obj01=类名() #创建和初始化对象,调用__init__()初始化属性

    obj01.实例属性名 = 值 #可以给已有属性赋值,也可以新加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)

实例方法

实例方法是从属于实例对象的方法。实例方法的定义格式如下:

1
2
def 方法名(self [, 形参列表]):
函数体

方法的调用格式如下:

对象.方法名([实参列表])

要点:

  1. 定义实例方法时,第一个参数必须为self。和前面一样,self指当前的实例对象。

  2. 调用实例方法时,不需要也不能给self传参。self由解释器自动传参

函数和方法的区别

  1. 都是用来完成一个功能的语句块,本质一样。
  2. 方法调用时,通过对象来调用。方法从属于特定实例对象,普通函数没有这个特点
  3. 直观上看,方法定义时需要传递self,函数不需要

实例对象的方法调用本质

image-20231117210158863

其他操作

  1. dir(obj)可以获得对象的所有属性、方法
  2. obj.__dict__对象的属性字典
  3. pass空语句
  4. isinstance(对象,类型)判断对象”是不是”指定类型”

类对象、类属性、类方法、静态方法

类对象

我们在前面讲的类定义格式中, class 类名:。实际上,当解释器执行class语句时,就会创建一个类对象

类属性

类属性是从属于“类对象”的属性,也称为“类变量”。由于,类属性从属于类对象,可以被所有实例对象共享。

类属性的定义方式:

1
2
class 类名:
类名变量 = 初始值

在==类中或者类的外面==,我们可以通过:类名.类变量名 来读写

内存分析实例对象和类对象创建过程(重要)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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对象
"""

image-20231117212505195

image-20231117212336356

类方法

==类方法是从属于“类对象”的方法。==类方法通过装饰器@classmethod来定义,格式如下:

1
2
3
@classmethod
def 类方法名(cls [, 形参列表]):
方法体

要点如下:

  1. @classmethod必须位于方法上面一行
  2. 第一个cls必须有;==cls指的就是“类对象”本身==
  3. 调用类方法格式:类名.类方法名(参数列表)。参数列表中,不需要也不能给cls传值
  4. 类方法中访问实例属性和实例方法会导致错误
  5. 子类继承父类方法时,传入cls是子类对象,而非父类对象(讲完继承再说)
1
2
3
4
5
6
7
8
9
class Student:
school = "HNU" # 类属性

@classmethod
def printSchool(cls):
print(cls.school)


Student.printSchool()

静态方法

Python中允许定义与”类对象”无关的方法,称为“静态方法”。

“静态方法”和在模块中定义普通函数没有区别,只不过“静态方法”放到了“类的名字空间里面”,需要通过“类调用”。

静态方法通过装饰器@staticmethod来定义,格式如下:

1
2
3
@staticmethod
def 静态方法名([形参列表]):
方法体

要点如下:

  1. @staticmethod必须位于方法上面一行

  2. 调用静态方法格式:类名.静态方法名(参数列表)

  3. 静态方法中访问实例属性和实例方法会导致错误

1
2
3
4
5
6
7
8
9
10
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__方法,一般不需要自定义析构方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 析构函数
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__()的对象,称为==“可调用对象”,即该对象可以像函数一样被调用==。

该方法使得实例对象可以像调用普通函数那样,以"对象名()"的形式使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
"""

方法没有重载

image-20231117221944194

如果我们在类体中定义了多个重名的方法,只有最后一个方法有效。

建议:==不要使用重名的方法!Python中方法没有重载。==

在其他一些语言(比如:Java)中,可以定义多个重名的方法,只要保证方法签名唯一即可。方法签名包含3个部分:方法名、参数数量、参数类型。
Python中,方法的的参数没有声明类型(调用时确定参数的类型),参数的数量也可以由可变参数控制。因此,Python中是没有方法的重载的。

方法的动态性

Python是动态语言,我们可以动态的为类添加新的方法,或者动态的修改类的已有的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#测试方法的动态性
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 方法

私有属性和私有方法(实现封装)

image-20231118141259542

Pytho对于类的成员没有严格的访问控制限制,这与其他面向对象语言有区别。关于私有属性和私有方法,有如下要点:

①通常我们约定,两个下划线开头的属性是私有的(private)。其他为公 共的(public)。

②类内部可以访问私有属性(方法)

③类外部不能直接访问私有属性(方法)

④类外部可以通过 _类名__私有属性(方法)名 访问私有属性(方法)

【注】==方法本质上也是属性!==只不过是可以通过()执行而已。

所以,此处讲的私有属性和公有属性,也同时讲解了私有方法和公有方法的用法。

如下测试中,同时也包含了私有方法和公有方法的例子。

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
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来处理。

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
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不存在严格意义的私有成员)

:warning:再次强调,方法和属性都遵循上面的规则。

类编码风格

  1. 类名首字母大写,多个单词之间采用驼峰原则。
  2. 实例名、模块名采用小写,多个单词之间采用下划线隔开
  3. 每个类,应紧跟“文档字符串”,说明这个类的作用
  4. 可以用空行组织代码,但不能滥用。在类中,使用一个空行隔开方法;模块中,使用两个空行隔开多个类

None对象的特殊性

None是什么?

  1. 与C和JAVA不同,Pythont中是没有NULL的,取而代之的是None

  2. None是一个特殊的常量,表示变量没有指向任何对象。

  3. 在Python中,None本身实际上也是对象,有自己的类型NoneType

  4. 你可以将None赋值给任何变量,但我们不能创建NoneType类型的对象

None不是False,None不是0,None不是空字符串。None和任何其他的数据类型比较永远返回False。

None和其他类型的比较

None和其他任何类型比较都会返回False

空列表、空字符串、0之间的比较

  1. if语句判断时,空列表[]空字典{}空元组()空字符串0None等一系列代表空和无的对象会被转换成False
  2. ==is判断时,空列表、空字符串不会自动转成False

面向对象的三大特征说明(封装、继承、多态)

image-20231118165047726

==Python是面向对象的语言,支持面向对象编程的三大特性:继承、封装(隐藏)、多态。==

封装(隐藏)

隐藏对象的属性和实现细节,只对外提供必要的方法。相当于将“细节封装起来”,只对外暴露”相关调用方法”。

通过前面学习的“==私有属性、私有方法”的方式,实现封装==”。Pythoni追求简洁的语法,没有严格的语法级别的“访问控制符”,更多的是依靠程序员自觉实现。

继承

继承可以让子类具有父类的特性,提高了代码的重用性。

从设计上是一种==增量进化==,原有父类设计不变的情况下,可以增加新的功能,或者改进已有的算法。

多态

多态是指==同一个方法调用由于对象不同会产生不同的行为==。

生活中这样的例子比比皆是:同样是休息方法,人不同休息方法不同。张三休息是睡觉,李四休息是玩游戏,程序员休息是“敲几行代码”。

继承详解

子类扩展父类

image-20240205182542943

继承是面向对象编程的三大特征之一。继承让我们更加容易实现类的扩展。实现代码的重用,不用再重新发明轮子(don’t reinvent wheels)。

如果一个新类继承自一个设计好的类,就直接具备了已有类的特征,就大大降低了工作难度。已有的类,我们称为“父类或者基类”。新的类,我们称为“子类或者派生类”。

image-20231118165736909

语法格式

Python支持==多重继承,一个子类可以继承多个父类==。继承的语法格式如下:

1
2
class 子类类名(父类1[, 父类2, ...]):
类体

如果在类定义中没有指定父类,则默认父类是object类。也就是说,object是所有类的父类,里面定义了一些所有类共有的默认实现,比如:__new__()

关于构造函数:

  1. 子类不重写__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
    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']

    """
  1. 子类重写了__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
    36
    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']
    """
  1. 如果重写了__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
    31
    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__())之外的所有成员。==

:warning:私有属性、私有方法也被继承

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
36
37
38
39
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']

"""

方法重写:子类可以重新定义父类中的方法,这样就会覆盖父类的方法,也称为“重写”

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
36
37
38
39
40
41
42
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__可以输出这个类的继承层次结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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'>]
"""

image-20231118173835027

object根类

==object类是所有类的父类==,因此所有的类都有object类的属性和方法。我们显然有必要深入研究一下object类的结构。对于我们继续深入学习Python很有好处。

dir()查看对象属性

为了深入学习对象,先学习内置函数dir(),他可以让我们方便的看到指定对象所有的属性

:warning:快捷键 Alt+ 7 打开模块结构

image-20231118175130469

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
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'>
"""

从上面我们可以发现这样几个要点:

  1. Person对象增加了六个属性:__dict__ __module__ __weakref__ age name say_age
  2. object的所有属性,Person类作为object的子类,显然包含了所有的属性
  3. 我们打印agenamesay_age,发现say_age虽然是==方法,实际上也是属性==。只不过这个属性的类型是method而已。
1
2
3
age <class 'int'>
name <class 'str'>
say_age <class 'method'>

重写__str__()方法

  1. object有一个__str()__方法,用于返回一个对于”对象的描述”。内置函数str(对象),调用的就是__str()__
  2. __str()__经常用于print()方法,帮助我们查看对象的信息。__str()__可以重写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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方法
"""

多重继承

image-20231118181906059

Pytho支持多重继承,一个子类可以有多个”直接父类”。这样,就具备了”多个父类”的特点。但是由于,这样会被“类的整体层次””搞的异常复杂,==尽量避免使用==。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()

类结构为:

image-20240205183106268

MRO方法解析顺序

Pythor支持多继承,如果父类中有相同名字的方法,在子类没有指定父类名时,解释器将“从左向右”按顺序搜索。

MRO(Method Resolution Order):方法解析顺序。我们可以通过mro()方法获得”类的层次结构”,方法解析顺序也是按照这个“类的层次结构”寻找的。

super()获得父类的定义

在子类中,如果想要获得父类的方法时,我们可以通过super()来做。

super()代表父类的定义,不是父类对象

想调用父类的构造方法:

super(子类名称, self).__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
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()调用父类的方法


多态详解

image-20231119160951739

多态(polymorphism)是指同一个方法调用由于对象不同可能会产生不同的行为。

关于多态要注意以下2点:

  1. 多态是方法的多态,属性没有多态
  2. 多态的存在有==2个必要条件:继承、方法重写==
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
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())

"""
小狗叫了一声
小猫叫了一声
"""

特殊方法和运算符重载

image-20231119162419296

image-20231119162554609

image-20231119162639978

特殊属性

image-20231119162849422

对象的浅拷贝和深拷贝

image-20231119163101534

浅拷贝

Python拷贝一般都是浅拷贝。

浅拷贝:拷贝时,拷贝源对象,但对象包含的子对象内容不拷贝。

深拷贝

使用 copy 模块的 deepcopy 函数,递归拷贝对象中包含的子对象。

深拷贝:拷贝时,拷贝源对象,也递归拷贝对象中包含的子对象

组合

image-20231119164344071

除了继承,“组合”也能实现代码的复用!“组合”核心是“将父类对象作为子类的属性”

is-a关系,我们可以使用”==继承==”。从而实现子类拥有的父类的方法和属性。is-a关系指的是类似这样的关系:狗是动物,dog is animal。狗类就应该继承动物类。

has-a关系,我们可以使用”==组合==”,也能实现一个类拥有另一个类的方法和属性。has-a关系指的是这样的关系:手机拥有CPU。MobilePhone has a CPU

设计模式

设计模式是面向对象语言特有的内容,是我们在面临某一类问题时候固定的做法,设计模式有很多种,比较流行的是:GOF(Goup Of Four)23种设计模式。当然,我们没有必要全部学习,学习几个常用的即可。

对于初学者,我们学习两个最常用的模式:工厂模式和单例模式。

工厂模式实现

工厂模式实现了创建者和调用者的分离,使用专门的工厂类将选择实现类、创建对象进行统一的管理和控制。

image-20231119165631900

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
# 工厂模式实现
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__()的方法。

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
# 单例模式实现
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>
"""

工厂和单例模式结合起来

设计模式称之为“模式”,就是一些固定的套路。我们很容易用到其他场景上,比如前面讲的工厂模式,我们需要将工厂类定义成“单例”,只需要简单的套用即可实现:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# 工厂和单例模式结合
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版本