目录
D.对称差集s.symmetric_difference或^
B.intersection_update(*others)
D.symmetric_difference_update(others)
tips:知识点搜索可以在页面ctrl+F出现搜索框后进行查找
C.__iter__(self)和__next__(self)
16.__call__(self [ , args...])
一、变量与字符串
1.转义字符
上述只表示引号没有其他特殊的语法含义 e.x.
>>> print('\"let\'s learn Python.\"')
"let's learn Python"
若想表示原始路径(字符串信息),为避免与反斜杠冲突,可在前加“r”,此时,转义字符将不再有效【自然,若路径过短,此处也可用双反斜杠消除反斜杠带来的冲突】
>>> print(r"D:\three\two\one\now")
D:\three\two\one\now
2.长字符串换行
'''前后呼应'''or"""成双成对"""。可直接通过回车实现换行
>>> poetry="""
春暖花开
面朝大海
从明天起做个幸福的人
"""
>>> print(poetry)
春暖花开
面朝大海
从明天起做个幸福的人
3.字符串乘法操作
>>> print('加油'*300)
4.比较运算符
二、数字类型+Python模块使用
使用idle的“help”指令寻找
要使用一个Python模块,第一步需要将其导入。以“random”为例,具体代码如下
1.生成随机数--random
>>> import random
#import+获取模块
random用法
random.randint(a,b),其中参数a,b表示希望获取的随机整数范围
random生成的随机数重现,要实现对伪随机数的攻击,就需要拿到它的种子默认情况下,random使用系统时间来作为随机数种子,
random.getstate() 可用于获取随机数种子加工之后,随机数生成器的内部状态
2.精确计算浮点数--decimal
decimal(十进制)具体应用
>>> import decimal
>>> a = decimal.Decimal('0.1')
#实例化一个对象
>>> b = decimal.Decimal('0.2')
>>> print(a + b)
0.3
3.E记法,科学计数法
>>> 0.0005
5e-05
4.复数
>>> x = 1 + 2j
>>> x.real
1.0
>>> x.imag
2.0
5.数字之间的运算
注:
1)地板除:确保两数相除的结果为一整数,若不是整数,则往下取整,即取比目标结果小的最大整数。
2)被除数 X == (x // y) * y + (x % y)
3)divmod函数可以直接调用,因为是内置函数
>>> divmod(3,2)
4)abs函数对于复数,取值为模
5)pow函数若引入第三个参数可以表示取余运算
>>> pow(2,3,5) 3 #实际运算为 2 ** 3 % 5
三、布尔类型
bool()内置函数 可以直接给出True或者False的结果
注:
1)无论内容是什么,只要出现在引号里面,他们都是字符串,输出结果都为True.。对于字符串,只有空字符串的结果是Flase,其他都是True,哪怕只有一个空格
>>> bool("假") True >>> bool("False") True >>> bool("") False >>> bool(" ") True
2)对于数值,只有等于0结果为False,其他都为True
>>> bool(250) True >>> bool(0) False >>> bool(0.0) False >>> bool(0j) False
总结--False的情况
Fraction(0,1)表示分子为0,分母为1的有理数
四、逻辑运算符
1.逻辑运算
逻辑运算也称为布尔运算,运算对象是布尔类型的对象
注:
1)Python中任何对象都能直接进行真值测试(测试该对象的布尔类型值为True或者False),用于if或者while语句的条件判断,也可以作为布尔逻辑运算符的操作符。
2)or和and都遵循短路逻辑
短路逻辑的核心思想:从左到右,只有当第一个操作数的值无法确定逻辑运算的结果时,才对第二个操作数进行求值
>>> 3 and 4 4 >>> 3 or 4 3 >>> (not 1) or (0 and 1) or (3 and 4) or (5 and 6) or (7 and 8 and 9) 4 #先处理括号内,变为 False or 0 or 4 or 6 or 9,在处理or,到4时可直接输出
2.运算符优先级
注:从低到高,最上面是较低优先级
五、分支与循环
1.判断多个条件
>>> if condition1:
statement(s)
elif condition2:
statement(s)
elif condition3:
statement(s)
...
除此之外,在上述情况下添加一个else,表示上面所有的条件均不成立的情况下,执行某条语句或某个代码块。
>>> if condition1:
statement(s)
elif condition2:
statement(s)
elif condition3:
statement(s)
...
else:
statement(s)
2.条件表达式
格式:条件成立时执行的语句 if condition else 条件不成立时执行的语句
>>> score = 6
>>> if 0 <= score < 60:
level = "D"
if 60 <= score < 80:
level = "C"
if 80 <= score < 90:
level = "B"
if 90 <= score < 100:
level = "A"
ese:
level = "请输入正确成绩"
>>> print(level)
改用条件表达式后
>>> score = 66
>>> level = ('D' if 0 <= score < 60 else
'C' if 60 <= score < 80 else
'B' if 80 <= score < 90 else
'A' if 90 <= score < 100 else
"请输入正确成绩")
>>> print(level)
3.分支的嵌套
else语句--当循环语句不在为真的时候,else的语句才会被执行。
当循环语句不在为真的时候,else的语句才会被执行。
当用break语句跳出循环时,else语句不会被执行。
>>> i = 1
>>> while i < 5:
print("循环内,i值是",i)
if i == 2:
break
i += 1
else:
print("循环外,i值是",i)
输出结果
循环内,i值是 1
循环内,i值是 2
4.循环的嵌套
无论是break语句还是continue语句,它们只能作用于一层循环体!
5.for循环
for 变量 in 可迭代对象:
statement(s)
"FishC"
" F " " i " " s " " h " " C"
>>> for each in "FishC":
print(each)
输出结果
F
i
s
h
C
上述输出结果同样可以用while循环表示
>>> i = 0
>>> while i < len("FishC"):
print("FishC"[i])
i += 1
代码解析:
1)length函数用于获取一个对象的长度 此处FishC总共有5个字符,即长度为5。
2)在循环体内,通过下标索引的方式,依次去访问字符串里面的每一个字符,并把它打印出来
下面介绍用for循环表示i从1加到10000,代码之前,需要先介绍一个语法--range,range可用于生成一个数字序列
range() 用法
1)range(stop),表示生成从0开始到stop的整数数列
>>> for i in range(10): print(i) #输出 0-9间所有整数
2)range(start,stop)
>>> for i in range(5,10): print(i) #输出结果为 5-9
3)range(start,stop,step)
>>> for i in range(5,10,2): print(i) #输出结果为 5,7,9
也可以表示为负数
>>> for i in range(10,5,-2): print(i) #输出结果为 10,8,6
特别的,无论使用上述哪一种用法,其参数均为整型。
对此,上述代码可表示为
>>> sum = 0
>>> for i in range(10001):
sum += i
>>> print(sum)
下面介绍for循环的嵌套,以及与break和continue的搭配语句
#下面找寻0-10以内的素数
>>> for n in range(2,10):
for x in range(2,n):
if n % x == 0:
print(n, "=", x, "*", n // x)
break
else:
print(n,"是一个素数")
输出结果
2 是一个素数
3 是一个素数
4 = 2 * 2
5 是一个素数
6 = 2 * 3
7 是一个素数
8 = 2 * 4
9 = 3 * 3
六、列表
1.创建列表
创建列表:列表名+[列表元素]
#创建列表,展示,访问列表(序列)中每一个元素
>>> rhyme = [1, 2, 3, 4, 5, "上山打老虎"]
>>> print(rhyme)
#输出结果 [1, 2, 3, 4, 5, '上山打老虎']
>>> for each in rhyme:
print(each)
#输出结果
#1
#2
#3
#4
#5
#上山打老虎
2.访问列表
若想访问列表中的某一个元素,可采用下标索引的方法
元素 | 1 | 2 | 3 | 4 | 5 | '上山打老虎' |
下标 | 0 | 1 | 2 | 3 | 4 | 5 |
>>> rhyme[0]
#输出结果 1
>>> rhyme[5]
#输出结果 '上山打老虎'
若对于访问过长列表中的某一元素,可以有其他方法(以访问列表最后一个元素为例)
法一:用列表长度表示
>>> length = len(rhyme) >>> rhyme[length - 1]
法二:用负数表示
此时,可以说明列表的下标索引值也可为负数>>> rhyme[-1]
元素 1 2 3 4 5 '上山打老虎' 下标 -6 -5 -4 -3 -2 -1 以上输出结果均为'上山打老虎'
3.列表切片
>>> rhyme[0:3]
#输出结果 [1, 2, 3]
同样也可表示为
>>> rhyme[:3]
#输出结果为 [1, 2, 3]
>>> rhyme[3:]
#输出结果为 [4, 5, '上山打老虎']
>>> rhyme[:]
#输出结果为 [1, 2, 3, 4, 5, '上山打老虎']
也可想range一样设置步长,跳序列出
>>> rhyme[0:6:2]
#输出结果为 [1, 3, 5]
对应上述可简写为
>>> rhyme[::2]
#输出结果为 [1, 3, 5]
>>> rhyme[::-2]
#输出结果为 ['上山打老虎', 4, 2]
>>> rhyme[::-1]
#输出结果为 ['上山打老虎', 5, 4, 3, 2, 1]
4.列表的增删改查
(1)增
append(),表示往列表的末尾来添加一个指定的元素
>>> heros = ["钢铁侠", "绿巨人"]
>>> heros.append("黑寡妇")
>>> heros
#输出结果为 ['钢铁侠', '绿巨人', '黑寡妇']
extend(),允许直接添加一个可迭代对象
Attention:extend()方法的参数必须是一个可迭代对象,新的内容是追加到原列表最后一个元素的后面
>>> heros.extend(["鹰眼", "灭霸"])
>>> heros
#输出结果为 ['钢铁侠', '绿巨人', '黑寡妇', '鹰眼', '灭霸']
列表切片表示
>>> s = [1, 2, 3, 4, 5]
>>> s[len(s):] = [6]
>>> s
#输出结果为 [1, 2, 3, 4, 5, 6]
#作用相似于s.append(6)
>>> s[len(s):] = [7, 8, 9]
>>> s
#输出结果为 [1, 2, 3, 4, 5, 6, 7, 8, 9]
#作用相似于s.extend([7, 8, 9])
insert(),可用于在列表的任意位置增加数据
insert(a,b),其中a表示指定待插入的位置,b表示指定待插入的元素
>>> s = [1, 3, 4, 5]
>>> s.insert(1,2)
>>> s
#输出结果为 [1, 2, 3, 4, 5]
(2)删
remove()
Attention:
1)如果列表中存在多个匹配的元素,它只会删第一个
2)如果指定元素不存在,那么程序就会报错
>>> heros = ['钢铁侠', '绿巨人', '黑寡妇', '鹰眼', '灭霸']
>>> heros.remove("灭霸")
>>> heros
#输出结果为 ['钢铁侠', '绿巨人', '黑寡妇', '鹰眼']
pop(),删除某个位置的元素。其参数即元素的下标索引值
>>> heros.pop(2)
#输出结果为 '黑寡妇'
>>> heros
#输出结果为 ['钢铁侠', '绿巨人', '鹰眼']
#同样有负数表示
>>> heros.pop(-1)
#输出结果为 '鹰眼'
clear(),表示清空列表内所有元素,即变成空列表
(3)改
单一元素替换
>>> heros = ['蜘蛛侠', '绿巨人', '黑寡妇', '鹰眼', '灭霸']
>>> heros[4] = '钢铁侠'
>>> heros
#输出结果为 ['蜘蛛侠', '绿巨人', '黑寡妇', '鹰眼', '钢铁侠']
多个元素替换
>>> heros[3:] = ['武松', '林冲', '李逵']
>>> heros
#输出结果为 ['蜘蛛侠', '绿巨人', '黑寡妇', '武松', '林冲', '李逵']
列表排序。
sort(),直接将数字从小到大进行排序。若设定其中参数reverse为True,则作用效果与单独使用reverse相同
reverse(),直接将数字从大到小进行排序。此处若对于字符串,则是倒着重现所有字符串
>>> nums = [3, 1, 9, 6, 8, 3, 5, 3]
>>> nums.sort()
>>> nums
#输出结果为 [1, 3, 3, 3, 5, 6, 8, 9]
>>> nums = [3, 1, 9, 6, 8, 3, 5, 3]
>>> nums.sort(reverse = True)
>>> nums
#输出结果为 [9, 8, 6, 5, 3, 3, 3, 1]
>>> nums.reverse()
>>> nums
#输出结果为 [9, 8, 6, 5, 3, 3, 3, 1]
(4)查
count(),表示查找某个元素出现的次数
index(),表示某个元素的索引值。
此处可应用于不知道索引值的情况下更换元素
Attention:
1)遇到多个相同元素时,index会返回第一个找到的元素下标值
2)index(x,start,end),其中start和end参数表示指定查找的开始和结束位置
>>> heros = ['蜘蛛侠', '绿巨人', '黑寡妇', '武松', '林冲', '李逵']
>>> heros[heros.index('绿巨人')] = '神奇女侠'
>>> heros
#输出结果为 ['蜘蛛侠', '神奇女侠', '黑寡妇', '武松', '林冲', '李逵']
5.嵌套列表
列表的加乘
#列表的乘法
>>> s = [1, 2, 3]
>>> s * 3
#输出结果为 [1, 2, 3, 1, 2, 3, 1, 2, 3]
(1)创建嵌套列表
#创建嵌套列表(二维列表)
>>> matrix = [[1, 2, 3], [4, 5, 6]]
#上述也可写为
>>> matrix = [[1, 2, 3],
[4, 5, 6]]
(2)访问嵌套列表
#通过嵌套循环范围列表
>>> for i in matrix:
for each in i:
print(each, end = ' ')
print()
#输出结果为
#1 2 3
#4 5 6
#通过下标索引访问列表
>>> matrix[0]
#输出结果为 [1, 2, 3]
#通过下标索引访问列表对应元素,第一个表示行,第二个表示列
>>> matrix[0][0]
#输出结果为 1
(3)循环在列表中的运用
通过循环语句创建和初始化二维列表
>>> A = [0] * 3
>>> for i in range(3):
A[i] = [0] * 3
>>> A
#输出结果为 [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
特别的,上述情况虽可与切片表示同样输出结果,但会出现高级错误
>>> B = [[0] * 3] * 3 >>> B #输出结果为 [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
错误表示例证为
>>> A[1][1] = 1 >>> A #输出结果为 [[0, 0, 0], [0, 1, 0], [0, 0, 0]] >>> B[1][1] = 1 >>> B #输出结果为 [[0, 1, 0], [0, 1, 0], [0, 1, 0]]
此处引入is同义运算符,解释错误缘由
>>> A[0] is A[1] False >>> A[1] is A[2] False >>> B[0] is B[1] True >>> B[1] is B[2] True
对此,可以看出A,B的布局为
(4)引用
将数据与变量进行挂钩。将一个变量赋值给另一个变量,其实就是将一个变量的引用传递给另一个变量
>>> x = [1, 2, 3]
>>> y = x
>>> x[1] = 1
>>> x
[1, 1, 3]
>>> y
[1, 1, 3]
(5)列表的拷贝
a.浅拷贝
copy(),此时若改变x的值,y的值不会变。因为copy拷贝的是整个列表对象,而不仅仅是整个列表的引用。
>>> x = [1, 2, 3]
>>> y = x.copy()
>>> x[1] = 1
>>> x
[1, 1, 3]
>>> y
[1, 2, 3]
#同理切片也不会影响
>>> x = [1, 2, 3]
>>> y = x[:]
>>> x[1] = 1
>>> x
[1, 1, 3]
>>> y
[1, 2, 3]
b.深拷贝
若用最初copy处理二维列表,则y会在x受到改变时同样受到影响。因为浅拷贝只是拷贝了外层的对象,如果包含嵌套对象,那么拷贝的只是其引用。
为解决上述问题,需要用到深拷贝。要实现深拷贝,需要借助copy模块。模块包括两个函数,第一个时copy函数,对比最开始浅拷贝的copy,浅拷贝时指的是列表的copy方法,而用浅拷贝copy函数(注意此处依然时浅拷贝)指的是copy模块的一个copy函数。后者拷贝的有列表、字符串、元组等等,但是输出结果y依然会受到x的影响。
另一个是deepcopy(),深拷贝
>>> import copy
>>> x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
>>> y = copy.deepcopy(x)
>>> x[1][1] = 0
>>> x
[[1, 2, 3], [4, 0, 6], [7, 8, 9]]
>>> y
[[1, 2, 3], [4, 5, 6], [7, 8, 9]]
(6)列表推导式
问题引入:将已知列表中的每一个元素的值都变成原来的2倍
传统循环解答--效率较低
>>> oho = [1, 2, 3]
>>> for i in range(len(oho)):
oho[i] = oho[i] * 2
>>> oho
[2, 4, 6]
若用列表推导式
>>> oho = [1, 2, 3]
>>> oho = [i * 2 for i in oho]
>>> oho
[2, 4, 6]
列表推导式
基本语法:[expression for target in iterable]
列表推导式的结果是使用一组数据来填充这个列表,故需要一个for语句来配合。
后左侧用expression,相当于循环体,经过运算最终才决定存放在列表中的数据。
>>> x = [i for i in range(10)] >>> x [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] >>> x = [i + 1 for i in range(10)] >>> x [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a.处理字符串
列表推导式同样可以处理字符串
>>> y = [c * 2 for c in "FishC"] >>> y ['FF', 'ii', 'ss', 'hh', 'CC']
如果想将每一个字符串都转换成对应的Unicode编码并保存为列表。
#调用一个ord内置函数,其作用是将单个字符串转换成对应的编码 >>> code = [ord(c) for c in "FishC"] >>> code [70, 105, 115, 104, 67]
b.提取矩阵元素
用列表推导式提取矩阵元素
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] #提取矩阵第二列的元素 >>> col2 = [row[1] for row in matrix] >>> col2 [2, 5, 8] #提取矩阵斜对角的元素 >>> diag = [matrix[i][i] for i in range(len(matrix))] >>> diag [1, 5, 9]
c.创建嵌套列表
>>> S = [[0] * 3 for i in range(3)] >>> S [[0, 0, 0], [0, 0, 0], [0, 0, 0]] #修改列表元素 >>> S[1][1] = 1 >>> S [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
将列表推导式加入if筛选分句
基本语法:[expression for target in iterable if condition]
>>> words = ["Great", "FishC", "Brilliant", "Excellent", "Fantastic"] >>> fwords = [w for w in words if w[0] == 'F'] >>> fwords ['FishC', 'Fantastic']
d.列表推导式的嵌套
将二维列表降级为一维列表
>>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] >>> flatten = [col for row in matrix for col in row] >>> flatten [1, 2, 3, 4, 5, 6, 7, 8, 9] #用循环解释 >>> flatten = [] >>> for row in matrix: for col in row: flatten.append(col) >>> flatten [1, 2, 3, 4, 5, 6, 7, 8, 9]
再举一例
>>> [x + y for x in "fishc" for y in "FISHC"] ['fF', 'fI', 'fS', 'fH', 'fC', 'iF', 'iI', 'iS', 'iH', 'iC', 'sF', 'sI', 'sS', 'sH', 'sC', 'hF', 'hI', 'hS', 'hH', 'hC', 'cF', 'cI', 'cS', 'cH', 'cC']
e.终极语法
[expression for target1 in iterable1 if condition1
target2 in iterable2 if condition2
......
targetN in iterableN if conditionN]
>>> [[x, y] for x in range(10) if x % 2 == 0 for y in range(10) if y % 3 == 0] [[0, 0], [0, 3], [0, 6], [0, 9], [2, 0], [2, 3], [2, 6], [2, 9], [4, 0], [4, 3], [4, 6], [4, 9], [6, 0], [6, 3], [6, 6], [6, 9], [8, 0], [8, 3], [8, 6], [8, 9]]
七、元组
引用--有关序列,目前已学字符串和列表,而接下来介绍的元组也是一个序列。
从语法上来看
列表-[元素1,元素2,...]
元组-(元素1,元素2,...) ,有时也可以不带括号。
元组特点
(1)同其他序列一样,元组也可用下标获取元素
(2)元组不可变,不可修改其中元素
(3)元组支持切片操作,切片方法同列表,只是对象为元组
(4)元组仅支持查操作--count(),index()
>>> nums = (3, 1, 5, 9, 4, 5, 3) >>> nums.count(3) 2 >>> heros = ("蜘蛛侠", "黑寡妇", "绿巨人") >>> heros.index("黑寡妇") 1
(5)元组支持加和乘--拼接和重复
(6)元组支持嵌套
>>> s = (1, 2, 3) >>> t = (4, 5, 6) >>> w = s, t #用逗号表示,逗号是构成元组的基本条件 >>> w ((1, 2, 3), (4, 5, 6))
(7)元组支持迭代,对于嵌套的元组同样使用嵌套的循环
(8)元组支持列表推导式,但是本身不存在元组推导式,把方括号变成圆括号生成的是”生成器“
(9)如何生成只有一个元素的元组
>>> x = (520,) #没有,则为int类型 >>> type(x) #可用type()来判断变量的类型 <class 'tuple'>
(10)打包和解包
生成一个元组有时也成为一个元组的打包
>>> t = (123, "FishC", 3.14) >>> t (123, 'FishC', 3.14) #把数字,字符串,浮点数打包到了一起 >>> x, y, z = t >>> x 123 >>> y 'FishC' >>> z 3.14 #元组的解包
上述打包不仅适用于元组,同样适用于任何的序列类型。比如说列表,字符串,以后者为例
>>> a, b, c, d, e = "FishC" >>> a 'F' >>> b 'i' >>> c 's' >>> d 'h' >>> e 'C'
同时对于哪一种序列的解包,都需要注意的是赋值号左边的变量名数量必须跟右侧序列的元素数量一致,否则会报错。
#一个小技巧 >>> a, b, *c = "FishC" >>> a 'F' >>> b 'i' >>> c ['s', 'h', 'C']
(11)元组可修改的情况
元组中的元素虽然是不可修改的,但若元组中的元素是指向一个可变的列表,那么依然是可以修改列表里面的内
八、字符串
引例--判断一个数字是否是回文数
#因为imput()引入变量都是字符串,对此可以直接设定
>>> x = "12321"
>>> "是回文数" if x == x[::-1] else "不是回文数"
1.大小写字母变换
capitalize(),将字母首字母变成大写,其余字母变成小写
>>> x = "I love FishC" >>> x.capitalize() 'I love fishc' >>> x 'I love FishC' #返回的不是原字符串,因为字符串是不可变的对象,现在只是按照规则生成一个新的字符串
casefold(),返回一个所有字母都是小写的新字符串,可以处理除英语语言的其他语言字符
>>> x.casefold() 'i love fishc'
title(),将字符串的每个单词的首字母都变成大写,该单词的所有字母都变成小写
>>> x.title() 'I Love Fishc'
swapcase(),将字符串中的所有字母大小写翻转
>>> x.swapcase() 'i LOVE fISHc'
upper(),将所有字母变成大写
>>> x.upper() 'I LOVE FISHC'
lower(),将所有字母变成小写,只能处理英文字符
>>> x.lower() 'i love fishc'
2.左中右对齐
下述四个方法都有一个width参数,用来指定整个字符串的宽度。若指定宽度小于或者等于源字符串,就直接源字符串输出。
前三有个fillchar参数,默认为空格,可自定义填充字符
center(width,fillchar=' ')
ljust(width,fillchar=' '),左对齐
rjust(width,fillchar=' '),右对齐
zfill(width),用0填充左侧,一般多用于数据报表
>>> "520".zfill(5) '00520' >>> "-520".zfill(5) '-0520'
3.查找
count(sub[,start[,end]]),用于查找sub参数指定子字符串在字符串中出现的次数
>>> x = "上海自来水来自海上" >>> x.count("海") 2 >>> x.count("海", 0, 5) 1
find(sub[,start[,end]]),用于从左往右查找sub参数指定字符串在字符串中的索引下标值
rfind(sub[,start[,end]]),用于从右往左查找sub参数指定字符串在字符串中的索引下标值
>>> x.find("海") 1 >>> x.rfind("海") 7 >>> x.find("gui") -1 >>> x.rfind("gui") -1
index(sub[,start[,end]]),作用相似于find,区别在于若定位不到子字符串后处理方式不一
index显示报错
rindex(sub[,start[,end]])
4.替换
expandtabs([tabsize=8]),使用空格来替换制表符,并返回一个新字符串
>>> code = """ print("I love FishC") print("i love my wife")""" >>> new_code = code.expandtabs(4) >>> print(new_code) print("I love FishC") print("i love my wife")
replace(old,new,count=-1),count参数表示替换次数,默认为-1--替换全部
>>> "在吗!我到了".replace("在吗", "想你") '想你!我到了'
translate(table),返回一个根据table参数转换后的新字符串
此处需介绍一个str.maketrans(x[,y[,z]])方法来获取一个表格。
>>> table = str.maketrans("ABCDEFG", "1234567") >>> "I love FishC".translate(table) 'I love 6ish3' >>> "I love FishC".translate(str.maketrans("ABCDEFG", "1234567")) 'I love 6ish3'
此处此方法还存在一个参数,将指定的字符给忽略
>>> "I love FishC".translate(str.maketrans("ABCDEFG", "1234567", "love")) 'I 6ish3'
5.判断检测
返回为布尔类型的值--True/False
startswith(prefix[,start[,end]]),用于判断参数指定子字符串是否出现在字符串的起始位置
endswith(prefix[,start[,end]]),用于判断参数指定子字符串是否出现在字符串的结束位置
特别的,这两个函数他们的参数支持以元组的形式传入多个待匹配的字符串
>>> x = "她爱Python" >>> if x.startswith(("你", "我", "她")): print("总有人喜爱Python") 总有人喜爱Python
istitle(),判断字符串中所有单词首字母是否都以大写字母开头,其余都为小写
isupper(),是否所有字母都是大写字母
islower(),是否所有字母都是小写字母
isalpha(),判断一个字符串中只有字母构成。注意,空格不是字母
isspace(),判断一个字符串是否为空格字符串。注意,tab键也判断为True,转义字符同理
>>> " \n".isspace() True
isprintable(),判断一个字符串中是否所有字符都是可打印的。转义字符不可打印
isdecimal(),isdigit(),isnumeric(),都是用来判断数字,区别在于判断范围不同
>>> x = "12345" >>> x.isdecimal() True >>> x.isdigit() True >>> x.isnumeric() True >>> x = "2²" >>> x.isdecimal() False >>> x.isdigit() True >>> x.isnumeric() True >>> x = "ⅠⅡⅢⅣⅤ" >>> x.isdecimal() False >>> x.isdigit() False >>> x.isnumeric() True >>> x = "一二三四五" >>> x.isdecimal() False >>> x.isdigit() False >>> x.isnumeric() True
isalnum(),集大成者,只要isalpha(),isdecimal(),isdigit(),isnumeric()只要有一返回True则为True
isidentifier(),用于判断一个字符串是否一个合法的Python标识符。有空格则非合法标识符,如果把空格替换为下划线则问题不大;标识符不能以数字开头
补充:
判断一个字符串是否为Python的保留标识符,e.x. if、while......可用keyword模块的iskeyword函数来进行实现
>>> import keyword >>> keyword.iskeyword("if") True
6.截取字符串
lstrip(chars=None),截掉字符串左侧空白
rstrip(chars=None),截掉字符串右侧空白
strip(chars=None),戒掉字符串左右两侧空白
默认参数为None,若设定去除字符串可截掉不要字符串。虽然传入为一串字符,但是按照单个字符为单位进行匹配去剔除的
>>> "www.ilovefishc.com".lstrip("wcom.") 'ilovefishc.com' >>> "www.ilovefishc.com".rstrip("wcom.") 'www.ilovefish' >>> "www.ilovefishc.com".strip("wcom.") 'ilovefish'
removeprefix(prefix),若想剔除一个具体的子字符串,指定剔除的前缀
removesuffix(prefix),指定剔除的后缀
>>> "www.ilovefishc.com".removeprefix("www.") 'ilovefishc.com' >>> "www.ilovefishc.com".removesuffix(".com") 'www.ilovefishc'
7.拆分和拼接
partition(sep),将字符串以参数指定的分割符为依据进行切割,并将切割后的结果返回一个三元组(三个元素的元组)
rpartition(sep),同样上述操作,只不过方向变为从右往左
>>> "www.ilovefishc.com".partition(".") ('www', '.', 'ilovefishc.com') >>> "www.ilovefishc.com".rpartition(".") ('www.ilovefishc', '.', 'com') >>>
split(sep=None,maxsplit=-1),根据分割符将字符串切成一小块一小块。默认情况切分空格
rsplit(),同样操作,方向从右往左
>>> "苟日新 日日新 又日新".split() ['苟日新', '日日新', '又日新']
对于第二个参数,可以决定切分数
>>> "苟日新,日日新,又日新".split(',',1) ['苟日新', '日日新,又日新'] >>> "苟日新,日日新,又日新".rsplit(',',1) ['苟日新,日日新', '又日新']
splitlines(keepends=False),将字符串按行进行分割,并将结果以列表的形式返回。此处可以解决对于不同操作系统(Linux换行为\n,Mac为\r,Windows为\n\r)的换行困扰
>>> "苟日新\n日日新\r又日新".splitlines() ['苟日新', '日日新', '又日新'] >>> "苟日新\n日日新\r又日新\n\r".splitlines() ['苟日新', '日日新', '又日新', '']
splitlines内包含参数keepends,其参数是指定结果是否要包含这个换行符,True表示包含,默认False不包含
>>> "苟日新\n日日新\r又日新".splitlines(True) ['苟日新\n', '日日新\r', '又日新']
join(iterable),待拼接子字符串可用列表或元组包含
>>> ".".join(["www", "ilovefishc", "com"]) 'www.ilovefishc.com' >>> ".".join(("www", "ilovefishc", "com")) 'www.ilovefishc.com'
一般也用于拼接字符串(传统加号连接会使人误认为新手上路)
>>> "".join(("FishC", "FishC")) 'FishCFishC'
8.格式化字符串
format(),用{}在原字符串占据位置,插入参数放在format方法的参数里
>>> "1+2={}, 2的平方是{}, 3的立方是{}".format(1+2, 2*2, 3*3*3) '1+2=3, 2的平方是4, 3的立方是27'
可在花括号里写数字(索引值)表示想插入的参数。同一个索引值可以被多次引用
>>> "{1}喜欢{0}".format("wo", "ni") 'ni喜欢wo' #参数中的字符串将被当作元组的元素来对待
也可用关键词进行调用
>>> "我叫{name}, 我爱{fav}".format(name="xiaojiayu", fav="Python") '我叫xiaojiayu, 我爱Python'
位置索引和关键字索引可以一起使用
若想直接输出{},可以通过参数设定为{},或者使用{}来注释{}
>>> "{}, {}, {}".format(1, "{}", 2) '1, {}, 2' >>> "{}, {{}}, {}".format(1, 2) '1, {}, 2'
字符串格式化语法参考--
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
下先讲 [align]选项,对齐
>>> "{:^}".format(250) '250' #使用[width]选项指定更大的宽度 >>> "{:^10}".format(250) #可以得到十个字符的宽度 ' 250 '
“:”必须的,其左边是位置或者关键词的索引,右边是格式化选项
>>> "{1:^10}{0:^10}".format(520, 250) ' 250 520 ' >>> "{1:>10}{0:<10}".format(520, 250) ' 250520 '
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
0在指定宽度前面添加,表示为数字类型启用感知正负号的0填充效果。且仅对字符串有效
>>> "{:010}".format(520) '0000000520' >>> "{:010}".format(-520) '-000000520'
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
在对齐选项前面通过填充选项来指定填充的字符
>>> "{1:%>10}{1:%>10}".format(520, 250) '%%%%%%%250%%%%%%%250'
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
符号选项,仅对数字类型有效
>>> "{:+} {:-}".format(520, -250) '+520 -250'
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
千分位的分割符,有两个值可以选择,一个是逗号,一个是下横线。如果位数不足,千位分割符是不显示的
>>> "{:,}".format(1234) '1,234' >>> "{:_}".format(1234) '1_234' >>> "{:_}".format(123) '123' >>> "{:,}".format(123456789) '123,456,789'
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
精度选项,只需填入一个十进制的整数,对于不同类型的参数其效果不一
A.对于[type]设置为‘f‘或'F'的浮点数来说,是限定小数点后显示多少个数位
B.对于[type]设置为‘g‘或'G'的浮点数来说,是限定小数点前后显示多少个数位
C.对于非数字类型来说,限定的是最大字段的大小
D.对于整数类型来说,则不允许使用此选项
>>> "{:.2f}".format(3.1415) '3.14' >>> "{:.2g}".format(3.1415) '3.1' >>> "{:.6}".format("I love FishC") 'I love' >>> "{:.2}".format(520) #报错 Traceback (most recent call last): File "<pyshell#35>", line 1, in <module> "{:.2}".format(520) ValueError: Precision not allowed in integer format specifier
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
此选项决定数据应应当如何来呈现
A.当为整数时
>>> "{:b}".format(80) '1010000' >>> "{:c}".format(80) 'P' >>> "{:o}".format(80) '120'
[[fill]align][sign][#][0][width][grouping_option][.precision][type]
#选项即参数以二进制、八进制或者十六进制在字符串输出的时候,会自动追加一个前缀
>>> "{:#b}".format(80) '0b1010000'
B.当为浮点数和复数时
>>> "{:e}".format(3.1415) '3.141500e+00' >>> "{:E}".format(3.1415) '3.141500E+00' >>> "{:f}".format(3.1415) '3.141500' >>> "{:g}".format(123456789) '1.23457e+08' >>> "{:g}".format(1234.56789) '1234.57' >>> "{:%}".format(0.98) '98.000000%' #设定精确到小数点后两位 >>> "{:.2%}".format(0.98) '98.00%'
Python支持通过设置关键字参数来设置此选项的值
>>> "{:.{prec}f}".format(3.1415, prec=2) '3.14' #多关键字参数 >>> "{:{fill}{align}{width}.{prec}{ty}}".format(3.1415, fill='+', align='^', width=10, prec=3, ty='g') '+++3.14+++'
f-字符串
执行效率高于format,但部署于3.6后产物
>>> "{:.2%}".format(0.98) '98.00%' #将上述用字符串表示 >>> f"{0.98:.2%}" '98.00%' >>> "{:{fill}{align}{width}.{prec}{ty}}".format(3.1415, fill='+', align='^', width=10, prec=3, ty='g') '+++3.14+++' #将上述用字符串表示 >>> fill = '+' >>> align = '^' >>> width = 10 >>> prec = 3 >>> ty = 'g' >>> f"{3.1415:{fill}{align}{width}.{prec}{ty}}" '+++3.14+++'
九、序列
引入,上述学过的列表、元组和字符串有以下共同点
(1)都可以通过索引获取每一个元素
(2)第一个元素的索引值都是0
(3)都可以通过切片的方法获取一个范围内元素的集合
(4)都有很多共同的运算符
对此,Python把上述统称为序列。根据是否可修改分为可变序列和不可变序列,列表为可变序列,另外两个为不可变序列。
下讲述能够作用于序列的一些运算符和函数
1.+和*
+ -- 序列的拼接;*--序列的重复(拷贝)
增量赋值--在赋值的过程中同时进行计算
id()
在Python中,每一个对象都有三个基本属性--唯一标志、类型和值。唯一标志是随着对象创建的时候就存在的,不可被修改,不会有重复的值。id(),函数的作用是返回一个代表指定对象的唯一标识的整数值。
>>> s = [1, 2, 3] >>> id(s) 1767519359744 >>> s *= 2 >>> s [1, 2, 3, 1, 2, 3] >>> id(s) 1767519359744
如果对象是不可变序列则会发生变化
>>> t =(1, 2, 3) >>> id(t) 1767550583808 >>> t *= 2 >>> t (1, 2, 3, 1, 2, 3) >>> id(t) 1767550036480
2.is和is not
用于检测对象的id值是否相等,从而判断是否是同一个对象。因此也被称为同一性运算符
3.in和not in
判断某个元素是否包含在序列中
4.del语句
用于删除一个或多个指定的对象
(1)直接删除对象
(2)删除可变序列中某个指定元素
>>> x = [1, 2, 3, 4, 5] >>> del x[1:4] >>> x [1, 5] >>> y = [1, 2, 3, 4, 5] >>> y[1:4] = [] >>> y [1, 5]
对于此同样可用切片语法实现
补充一道切片语法题
(2’)del语句也可完成切片语句无法完成任务。
>>> x = [1, 2, 3, 4, 5] >>> del x[::2] >>> x [2, 4] >>> y = [1, 2, 3, 4, 5] >>> y[::2] = [] #报错 Traceback (most recent call last): File "<pyshell#20>", line 1, in <module> y[::2] = [] ValueError: attempt to assign sequence of size 0 to extended slice of size 3
(3)用del语句实现列表clear()内置方法
>>> x = [1, 2, 3, 4, 5] >>> x.clear() >>> x [] >>> y = [1, 2, 3, 4, 5] >>> del y[:] >>> y [] #自己想的补充 >>> y = [1, 2, 3, 4, 5] >>> del y[::1] >>> y []
5.列表、元组和字符串相互转换
list(),转换为列表
tuple(),转换为元组
str(),转换为字符串--只是在外围加了引号,原先括号依然存在
6.min()和max()
对比传入的参数,返回最小值或最大值
>>> s = [1, 1, 2, 3] >>> min(s) 1 >>> t = "FishC" >>> max(t) 's' #如果传入为字符串,那么min和max函数比较的是字符串中每个字符的编码值 #编码值大致为26字母顺序,大写字母编码值在小写字母前
如果传入的是空的可迭代对象则会报错,但可以通过设置default参数解决问题
>>> s = [] >>> min(s,default="啥都没有") '啥都没有'
min和max函数同样支持传入多个参数直接输出结果
7.len()和sum()
len函数参数有最大限制范围,对于64位平台是2**63-1,同理32位,超过范围会报错
sum(),函数里有start参数,用于指定求和计算的起始值
>>> s = [1, 2] >>> sum(s, start=100) 103
8.sorted()和reversed()
A.sorted()
引:列表里也有个sort()方法,对列表的元素进行原地排序
此处sorted()功能类似,区别在于sorted是返回一个全新的列表,而原来的列表并不会受到影响。而如果调用列表sort方法则列表会发生改变
>>> s = [4, 2, 3] >>> sorted(s) [2, 3, 4] >>> s [4, 2, 3] >>> s.sort() >>> s [2, 3, 4]
sorted()函数也支持key和reverse两个参数,用法与列表的sort()方法一致
>>> sorted(s, reverse=True) [4, 3, 2]
key参数使用
>>> t = ["FishC", "Apple", "Book", "Banana", "Pen"] >>> sorted(t) ['Apple', 'Banana', 'Book', 'FishC', 'Pen'] #单独传入t时对比列表中每一个元素,此处对比每一个字符串的编码值 #当字符串第一个字母一样时去找第二个字母 >>> sorted(t, key=len) ['Pen', 'Book', 'FishC', 'Apple', 'Banana'] #key参数指定的是一个干预排序算法的函数。此处指定len函数(只写函数名就可) #对此先将列表中每一个元素自动调用一次len函数,后比较其返回结果
列表sort方法同理
>>> t.sort(key=len) >>> t ['Pen', 'Book', 'FishC', 'Apple', 'Banana']
Attention:列表的sort方法只能够处理列表,而sorted()函数可以接受任何形式的可迭代对象作为参数
B.reversed()
reversed()函数返回的是一个参数的反向迭代器。迭代器也属于可迭代对象
>>> s = [1, 2, 5, 4] >>> reversed(s) <list_reverseiterator object at 0x000001E9427571F0> >>> list(reversed(s)) [4, 5, 2, 1] >>> list(reversed(range(0, 10))) [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
9.all()和any()
all()判断可迭代对象中是否所有元素的值都为真
any()判断是否存在某个元素的值为真
10.enumerate()
用于返回一个枚举对象,其功能是将可迭代对象中的每个元素及从0开始的序号共同构成一个二元组的列表
>>> ss = ["A", "B", "C", "D"] >>> enumerate(ss)#使用enumerate函数创建枚举对象 <enumerate object at 0x000001E9427A1580> >>> list(enumerate(ss))#用列表转化 [(0, 'A'), (1, 'B'), (2, 'C'), (3, 'D')] #将ss列表中每一个元素抽取后跟从0开始的索引去构成一个元组 >>> list(enumerate(ss, 10))#start参数自定义 [(10, 'A'), (11, 'B'), (12, 'C'), (13, 'D')]
其中start参数可以自定义序号开始的值
11.zip()
用于创建一个聚合多个可迭代对象的迭代器。能将作为参数传入的每个可迭代对象的每个元素依次组合成元组,即第i个元组包含来自每个参数的第i个元素
>>> x = [1, 2, 3] >>> y = [4, 5, 6] >>> zipped = zip(x, y) >>> list(zipped) [(1, 4), (2, 5), (3, 6)] #加入第三个 >>> z = [7, 8, 9] >>> zipped = zip(x, y, z) >>> list(zipped) [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
若长度不一致会以最短的为准,多余的丢掉
扩展:
若需要丢掉部分,需要使用一个itertools模块里的zip_longest()的一个函数来代替zip()
>>> z = [7, 8, 9, 10] >>> import itertools >>> zipped = itertools.zip_longest(x, y, z) >>> list(zipped) [(1, 4, 7), (2, 5, 8), (3, 6, 9), (None, None, 10)]
12.map()
根据提供的函数对指定的可迭代对象的每个元素进行运算,并将返回运算结果的迭代器
>>> mapped = map(ord, "FishC") >>> list(mapped) [70, 105, 115, 104, 67]
第一个参数提供的是计算方法--ord求出传入字符对应的Unicode编码
第二个参数是传入第一个参数的这个对象
ord函数只需一个参数,即第二个参数”FishC",若需要多个参数,即增加提供的可迭代对象的数量
>>> mapped = map(pow, [2, 3, 10], [5, 2, 3]) >>> list(mapped) [32, 9, 1000] #等价于下面写法 >>> [pow(2, 5), pow(3, 2), pow(10, 3)] [32, 9, 1000]
若可迭代对象不一致,则在短的可迭代对象处终止操作
13.filter()
与map函数类似。根据提供的函数对指定的可迭代对象的每个元素进行计算,并将运算结果为真的元素,以迭代器的形式返回
>>> list(filter(str.islower, "FishC")) ['i', 's', 'h'] #将只有小写(True)的结果输出
补充:
迭代器vs可迭代对象
一个迭代器肯定是一个可迭代对象。可迭代对象可重复使用,迭代器是一次性的
>>> mapped = map(ord, "FishC") >>> for each in mapped: print(each) 70 105 115 104 67 >>> list(mapped) []
14.iter()
将可迭代对象变成一次性迭代器
>>> x = [1, 2] >>> y = iter(x) >>> type(x) <class 'list'> >>> type(y) <class 'list_iterator'> #y变为列表的迭代器
15.next()
逐个将迭代器中的元素提取出来
>>> next(y) 1 >>> next(y) 2 >>> next(y) Traceback (most recent call last): File "<pyshell#66>", line 1, in <module> next(y) StopIteration #“报错”(异常)
如果不想出现异常,可以给next函数传入第二个参数
>>> z = iter(x) >>> next(z, "over") 1 >>> next(z, "over") 2 >>> next(z, "over") 'over'
十、字典
Python中唯一实行映射关系的内置类型
引例:将摩斯密码一一对应
法一:创建两个列表进行一一对应
# 摩斯密文表 c_table = [".-", "-...", "-.-."] # 摩斯明文表 d_table = ["A", "B", "C"] code = input("请输入摩斯密码:") split_code = code.split(" ") result = [d_table[c_table.index(each)] for each in split_code] print(result) """上述可解析为 result = [] for each in aplit_code: _ = c_table.index(each) result.append(d_table[_])"""
法二:创建一个列表
# 摩斯密码比对表 c_table = [".-", "A", "-...", "B", "-.-.", "C"] code = input("请输入摩斯密码:") split_code = code.split(" ") result = [c_table[c_table.index(each) + 1] for each in split_code] print(result)
1.字典的创建
(1)大括号和冒号组合
>>> y = {"吕布":"口口布", "关羽":"关习习"} >>> type(y) <class 'dict'>
冒号左边称为字典的“键”,右边称为字典的“值”。
序列是通过位置的偏移来存取数据,而字典是通过键来实现写入和读取。在字典中,只要提供键便可以获取其对应的值。
>>> y["吕布"] '口口布'
提供指定一个不存在于字典中的“键”来创建一个新的键值对。
>>> y["刘备"] = "刘baby" >>> y {'吕布': '口口布', '关羽': '关习习', '刘备': '刘baby'}
(2)dict()函数
类似于list、tuple、str。其每一参数就是一个对应键值对,键与值用等号进行挂钩
>>> b = dict(吕布="口口布", 关羽="关习习", 刘备="刘baby")
(3)使用列表作为参数
列表中每个元素是使用元组包裹起来的键值对。
>>> c = dict([("吕布", "口口布"), ("关羽", "关习习"), ("刘备", "刘baby")])
(4)第一种方法的参数传递
将第一种方法作为参数传递给dict函数。
>>> d = dict({"吕布":"口口布", "关羽":"关习习", "刘备":"刘baby"})
(5)混合
>>> e = dict({"吕布":"口口布", "关羽":"关习习"}, 刘备="刘baby")
(6)zip()
运用zip()函数创建一个聚合多个可迭代对象的迭代器,后作为参数传递给dict()函数
>>> f = dict(zip(["吕布", "关羽", "刘备"], ["口口布", "关习习", "刘baby"]))
2.字典的增删改查
(1)增
formkey(iterable[, values])
可用iterable参数指定的可迭代对象来创建一个新的字典,并将所有的值初始化为values参数指定的值
>>> d = dict.fromkeys("Fish", 250) >>> d {'F': 250, 'i': 250, 's': 250, 'h': 250} #从无到有创建一个初始化值相同的字典 >>> d['F'] = 70 >>> d {'F': 70, 'i': 250, 's': 250, 'h': 250} #修改某一个键的值 >>> d['C'] = 67 >>> d {'F': 70, 'i': 250, 's': 250, 'h': 250, 'C': 67} #增加一个键值对
序列中元素可重复,而字典中的项(键值对),一键对应一值,键不会重复
(2)删
A.pop(key[, default])
返回的是指定的键对应的值
>>> d.pop('s') 250 >>> d {'F': 70, 'i': 250, 'h': 250, 'C': 67}
若没有此键则会出现异常。可以通过default参数显示替代文字
>>> d.pop('gou', '没有') '没有'
B.popitem()
在Python3.7前表示随机删除一个键值对。3.7后删除最后一个加入字典的键值
>>> d.popitem() ('C', 67)
C.del关键字
用于删除一个指定的字典元素。del也可直接加上字典的名字,删除整个字典
>>> del d['i'] >>> del d
D. clear()
只想清空字典中的内容
>>> d = dict.fromkeys('FishC', 250) >>> d {'F': 250, 'i': 250, 's': 250, 'h': 250, 'C': 250} >>> d.clear() >>> d {}
(3)改
修改单个值
>>> d = dict.fromkeys('FishC') >>> d['s'] = 115 >>> d {'F': None, 'i': None, 's': 115, 'h': None, 'C': None}
update([other])
支持同时传入多个键值对,也可直接传入另外一个字典或者一个包含键值对的可迭代对象
>>> d.update({'i':105, 'h':104}) >>> d {'F': None, 'i': 105, 's': 115, 'h': 104, 'C': None} >>> d.update(F='70', C='67') >>> d {'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67'}
(4)查
传统方法--直接查,但若遇到不在字典中的值则会报错
A.get(key[, default])
default可以设定当找不到键时返回一个值
>>> d.get('c', 'meiyou') 'meiyou'
B.setdefault(key[, default])
当需要查找某键是否存在于字典中,若在则返回对应值不在则给它指定一个新的值
>>> d.setdefault('C', 'code') '67' #不存在 >>> d.setdefault('c', 'code') 'code' >>> d {'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67', 'c': 'code'}
C.items()、keys()、values()
三个方法分别用于获取字典的键值对、键和值的视图对象
视图对象:即字典的动态视图,这就意味着当字典的内容发生改变的时候,视图对象的内容也会相应地跟着改变
>>> keys = d.keys() >>> values = d.values() >>> items = d.items() >>> items dict_items([('F', '70'), ('i', 105), ('s', 115), ('h', 104), ('C', '67'), ('c', 'code')]) >>> values dict_values(['70', 105, 115, 104, '67', 'code']) >>> keys dict_keys(['F', 'i', 's', 'h', 'C', 'c']) #剔除c >>> d.pop('c') 'code' >>> d {'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67'} >>> items dict_items([('F', '70'), ('i', 105), ('s', 115), ('h', 104), ('C', '67')]) >>> keys dict_keys(['F', 'i', 's', 'h', 'C']) >>> values dict_values(['70', 105, 115, 104, '67'])
D.copy方法实现浅拷贝
>>> e = d.copy() >>> e {'F': '70', 'i': 105, 's': 115, 'h': 104, 'C': '67'}
3.len()函数
可用于获取字典中键值对的数量
>>> len(d) 5
4.in和not in
可用于判断某个键是否存在于字典中
>>> 'c' in d False >>> 'c' not in d True
5.转换为列表
list(d)--得到的是字典中所有的键构成的列表,相当于list(d.keys())
>>> list(d) ['F', 'i', 's', 'h', 'C'] >>> list(d.values()) ['70', 105, 115, 104, '67']
6.iter()函数
将字典的键构成一个迭代器
>>> e = iter(d) >>> next(e) 'F' >>> next(e) 'i' >>> next(e) 's' >>> next(e) 'h' >>> next(e) 'C'
7.reversed()函数
Python3.8后可用此对字典内部的键值对进行逆向操作
>>> list(reversed(d.values())) ['67', 104, 115, 105, '70']
8.字典的嵌套
>>> d = {"吕布": {"语文":60, "数学":70, "英语":80}, "关羽": {"语文":30, "数学":90, "英语":90}} >>> d["吕布"]["数学"] 70
嵌套也可以嵌套一个序列
>>> d = {"吕布": [60, 70, 80], "关羽": [30, 90, 90]} >>> d["吕布"][1] 70
9.字典推导式
>>> d = {'F':70, 'i':105, 's':115} >>> c = {v:k for k,v in d.items() if v > 100} >>> c {105: 'i', 115: 's'}
可用得到每个字符的编码值
>>> d = {x:ord(x) for x in "FishC"} >>> d {'F': 70, 'i': 105, 's': 115, 'h': 104, 'C': 67}
一道易错题
>>> d = {x:y for x in [1, 3, 5] for y in [2, 4, 6]} >>> d {1: 6, 3: 6, 5: 6}
解析
十一、集合
集合中所有元素独一无二、且无序的
1.集合的创建
A.使用花括号创建
>>> {'1', 'Python'}
B.使用集合推导式
>>> {s for s in "FishC"}
{'C', 'h', 'F', 's', 'i'}
#可以看出集合无序性
C.使用类型构造器set
>>> set("FishC")
{'C', 'h', 'F', 's', 'i'}
2.集合的访问
由于集合是无序的故无法通过下标索引进行访问。但可以用in和not in判断元素是否在集合中。
可以通过迭代对集合进行访问
>>> for each in s:
print(each)
C
h
F
s
i
3.集合的唯一性
可以通过此特性对集合进行去重操作。
>>> set([1, 1, 2, 3,5])
{1, 2, 3, 5}
也可用于检测列表中是否有相同元素
>>> s = [1, 1,2]
>>> len(s) == len(set(s))
False
4.集合浅拷贝拷贝
>>> t = s.copy()
>>> t
[1, 1, 2]
5.isdisjoint
用于检测两个集合间是否毫无相干
>>> s = set("FishC")
>>> s
{'C', 'h', 'F', 's', 'i'}
>>> s.isdisjoint(set("Python"))
False
#有共同元素h
>>> s.isdisjoint(set("Java"))
True
#只有传入一个可迭代对象都可以
>>> s.isdisjoint("Java")
True
6.issubest或<
检测该集合是否是另一个集合的子集
>>> s.issubset("FishC.com.cn")
True
python也有专门运算符进行计算。真子集则为"<"。
>>> s <= set("FishC")
True
7.issuperset或>
检测集合是否是另一个集合的超集。超集定义于子集相反
>>> s.issuperset("Fish")
True
python也有专门运算符进行计算。真超集则为">"。
8.集合的计算
前三支持多参数
A.并集s.union或|
>>> s.union({1, 2, 3}, "Python")
{1, 2, 3, 'y', 'i', 'n', 'C', 'h', 'F', 'P', 's', 't', 'o'}
python也有专门运算符进行计算
>>> s | {1, 2, 3} | set("Python")
{1, 'C', 2, 'h', 'F', 3, 'P', 'y', 's', 't', 'i', 'o', 'n'}
B.交集s.intersection或&
>>> s.intersection("Php", "Python")
{'h'}
python也有专门运算符进行计算
>>> s & set("Php") & set("Python")
{'h'}
C.差集s.difference或-
>>> s.difference("Php", "Python")
{'C', 's', 'F', 'i'}
python也有专门运算符进行计算
>>> s - set("Php") - set("Python")
{'i', 'C', 's', 'F'}
D.对称差集s.symmetric_difference或^
排除s和other容器中共有元素后剩余的所有元素。此只有唯一参数。
>>> s.symmetric_difference("Python")
{'C', 'F', 'P', 'y', 't', 's', 'i', 'o', 'n'}
python也有专门运算符进行计算
>>> s ^ set("Python")
{'y', 'i', 'n', 'C', 'F', 'P', 't', 's', 'o'}
Attention:使用运算符号时,两边都必须是集合类型的数据
9.frozenset()
集合细分为可变和不可变对象。前者是set();后者是frozenset(),对象不可变
>>> t = frozenset("FishC")
>>> t
frozenset({'h', 'F', 'C', 's', 'i'})
上述1-8不存在对集合元素的改动,同样适用于forzenset。
10.集合内容的改动
A.update(*others)
使用others参数指定的值来更新集合。others表示支持多个参数(若写other则表示仅支持一个参数)。
>>> s = set("FishC")
>>> s.update([1, 1], "23")
>>> s
{1, 'h', 'F', 'C', '2', 's', 'i', '3'}
#由于集合是不可重复的,故虽然插入了一个包含两个重复元素的列表,当只有一个1被放进去了;第二个参数是“23”,其作为单独字符串也被插入进去了
也为union_update的简写
B.intersection_update(*others)
使用交集的方式来更新集合
>>> s.intersection_update("FishC")
>>> s
{'h', 'F', 'C', 's', 'i'}
C.difference_update(*others)
使用差集的方式来更新集合
>>> s.difference_update("Php", "Python")
>>> s
{'F', 'C', 's', 'i'}
D.symmetric_difference_update(others)
使用对称差集的方式来更新集合
>>> s.symmetric_difference_update("Python")
>>> s
{'h', 'F', 't', 'C', 'n', 'y', 'P', 's', 'i', 'o'}
E.添加某个数据add(elem)
此方法区分与A.方法中,此时为作为整个元素插入字符串中,而前者是分别插入。
>>> s.add("45")
>>> s
{'h', 'F', 't', 'C', 'n', 'y', 'P', '45', 's', 'i', 'o'}
F.删除某个元素
a.remove(elem)
b.discard(elem)
若指定元素不存在,a会抛出异常,而b默默处理(不显示)
c.pop()
随机从集合中弹出一个元素(弹出顺序为储存顺序,此时的随机是因为存储的随机)
d.clear()
直接将集合清空
11.可哈希
如果一个对象是可哈希的,那么要求他的哈希值必须在其整个程序的生命周期中保持不变
hash(object)函数可以获取一个对象的哈希值
>>> hash(1)
1
>>> hash(1.0)
1
>>> hash(1.001)
2305843009213441
(1)对一个整数求哈希值,其值永远等于它本身
(2)如果两个对象的值是相等的,尽管不同对象,哈希值依然相等
Python中,大多数不可变的对象都是可哈希的,而可变的对象都是不可哈希的。e.x.字符串是个典型不可变对象;而列表可变;元组不可变,也有哈希值。
只有可哈希的对象才有资格作为字典的键以及集合的元素
问:如何实现一个嵌套的集合?
>>> x = {1, 2}
>>> x = frozenset(x)
>>> y = {x, 4, 5}
>>> y
{4, 5, frozenset({1, 2})}
#对本身集合可变,若用frozenset则可变为不可变集合
tips:级别变成集合后,效率可以提高倍数级。因为集合的背后是有散列表的支持,而列表没有。
十二、函数
1.创建和调用函数
>>> def myfunc():
pass
用“def”定义函数,‘myfunc’即函数名。‘:’下面为函数体,函数体是一个代码块,表示每一次调用函数时将被执行的内容
2.函数的参数
通过函数的参数来实现功能的定制。
>>> def myfunc(name):
for i in range(3):
print(f"I love {name}.")
>>> myfunc("Python")
I love Python.
I love Python.
I love Python.
#在Python中,当字符串中包含变量时,你可以使用 f-string(格式化字符串字面值)来轻松地将变量的值注入到字符串中。通过在字符串前加上字母"f",你可以在字符串中直接引用变量。这简化了字符串和变量的拼接过程,使代码更易读和易维护。
#多个参数
>>> def myfunc(name, times):
for i in range(times):
print(f"I love {name}.")
>>> myfunc("Python", 5)
I love Python.
I love Python.
I love Python.
I love Python.
I love Python.
从调用的角度,参数实际叫法可细分为形式参数和实际参数。形式参数(形参)是函数定义的时候写的参数名字,e.x.name和times;而实际参数(实参)实在调用函数时传递进去的值,e.x.Python和5.
A.位置参数
>>> def myfunc(s, vt, o):
return " ".join((o, vt, s))
>>> myfunc("小甲鱼", "打了", "我")
'我 打了 小甲鱼'
将传入的三个参数顺序颠倒,然后组成字符串并返回
B.关键字参数
若参数过多顺序容易弄混时可以采用关键字参数
>>> myfunc(o="小甲鱼", vt="打了", s="我")
'小甲鱼打了我'
Attention:位置参数必须在关键字参数之前
C.默认参数
Python允许函数的参数在定义时就指定默认值,此时在调用函数时,若没有传入实参,那么就将采用默认的参数值来代替。
>>> def myfunc(s, vt, o="小甲鱼"):
return "".join((o, vt, s))
>>> myfunc("香蕉", "吃")
'小甲鱼吃香蕉'
>>>
>>> myfunc("香蕉", "吃", "不二如是")
'不二如是吃香蕉'
要使用默认参数时需要把其放在最后。
>>> def myfunc(s="苹果", vt, o="小甲鱼"):
return "".join((o, vt, s))
SyntaxError: non-default argument follows default argument
tips:
在使用help函数来查看函数文档时,经常在函数的原型中看到 ’ / ‘。表示为斜杠左侧的参数必须传递位置参数,而不能是关键字参数;斜杠右侧则都可以。在自定义函数时也是遵从此语法
同样也有只能用关键字参数而不能用位置参数的表示 ’ * ‘。其左侧即可以是位置参数,也可以是关键字参数;当右侧只可以是关键字参数。
>>> def abd(a, *, b, c):
print(a, b, c)
D.收集参数
参数数量不限定的情况。定义收集参数的方法,只需在形参的名字前面加上一个星号表示即可。
>>> def myfunc(*args):
print("有{}个参数。".format(len(args)))
print("第2个参数是:{}".format(args[1]))
#format格式化字符串,len获取收集参数的长度
>>> myfunc("小甲鱼", "不二如是")
有2个参数。
第2个参数是:不二如是
>>> myfunc(1, 2, 3, 4, 5)
有5个参数。
第二个参数是:2
背后实现逻辑是元组
>>> def myfunc(*args):
print(args)
>>> myfunc(1, 2, 3, 4, 5)
(1, 2, 3, 4, 5)
元组具有打包和解包的能力,在函数这里同样可以显现。
Q:函数返回多个值。Python实际用元组进行打包
>>> def myfunc():
return 1, 2,3
>>> myfunc()
(1, 2, 3)
Q:用元组解包
>>> x, y, z = myfunc()
>>> x
1
>>> y
2
>>> z
3
函数的参数同样道理。通过’ * ‘其实就是实现了打包的操作,将多个参数打包到一个元组里头。
>>> def myfunc(*args):
print(type(args))
>>> myfunc(1, 2, 3, 4, 5)
<class 'tuple'>
Attention:如果在收集参数的后面还需要指定其他参数,那么在调用函数时就应该使用关键字参数来指定后面的参数。否则python就会把实参纳入到收集参数中。
>>> def myfunc(*args, a, b):
print(args, a, b)
>>> myfunc(1, 2, 3, a=4, b=5)
(1, 2, 3) 4 5
除了将多个参数打包为元组,收集参数还可以将参数打包为字典。通过两个连续的**
>>> def myfunc(**kwargs):
print(kwargs)
>>> myfunc(a=1, b=2, c=3)
{'a': 1, 'b': 2, 'c': 3}
#此时传递参数时就必须使用关键字参数。因为字典的元素都是键值对
混合表示:
>>> def myfunc(a, *b, **c):
print(a, b, c)
>>> myfunc(1, 2, 3, 4, x=5, y=6)
1 (2, 3, 4) {'x': 5, 'y': 6}
字符串的format方法,拥有同时两种收集参数的方法
E.解包参数
*和**不仅可以在函数定义时使用,也可在函数调用的时候。在形参上使用称为参数的打包,而在实参上的使用称为解包参数。
>>> args = (1, 2, 3, 4)
>>> def myfunc(a, b, c, d):
print(a, b, c, d)
>>> myfunc(*args)
1 2 3 4
在传递的过程中,args不再是一个元组,而是解包成1234,然后分别传递到abcd四个形参中
而两个**解包为关键字参数
>>> kwargs = {'a':1, 'b':2, 'c':3, 'd':4}
>>> myfunc(**kwargs)
1 2 3 4
3.函数的返回值
使用return语句便可实现。只要有return语句,函数就会立刻马上直接返回而不理会后面是否还有其他语句
>>> def div(x, y):
z = x / y
return z
'''也可写为
def div(x, y):
return x / y'''
>>> div(4, 2)
2.0
如果函数没有return语句,他也会自己在执行完函数体中的所有语句之后悄悄返回一个none值。
>>> def myfunc():
pass
>>> print(myfunc())
None
4.作用域
指一个变量可以被访问的范围。通常一个变量的作用域总是由它在代码中被赋值的位置来决定。
A.局部作用域
如果一个变量定义的位置是在一个函数的里面,那么它的作用域就仅限于该函数中,也将它称之为局部变量。
B.全局作用域
如果实在任何函数的外部去定义一个变量,那么它的作用域就是全局的,也将其称为全局变量。具有全局作用域的变量,在函数内部完全可以访问到。
若在函数变量内部定义一个跟全局变量一样的局部变量,在函数中,局部变量就会覆盖同名的全局变量。可以id来显示区别。全局变量可以在局部变量被访问到,但无法在函数中去修改它的值
5.global语句
可用于在函数内部修改全局变量。但不提倡使用
>>> def myfunc():
global x
x = 520
print(x)
>>> myfunc()
520
>>> print(x)
520
6.嵌套函数
>>> def funA():
x = 520
def funB():
x = 880
print("In funB, x =", x)
print("In funA, x =", x)
>>> funA()
In funA, x = 520
无法在外部调用funB
>>> def funA():
x = 520
def funB():
x = 880
print("In funB, x =", x)
funB()
print("In funA, x =", x)
>>> funA()
In funB, x = 880
In funA, x = 520
#也可以funA()()或者
#funny = funA()
#funny()
#调用
7.nonlocal语句
在内部函数去修改外部函数的变量。作用方法类似于global
8.LEGB规则
L--Local,局部作用域
E--Enclosed,嵌套函数的外层函数作用域
G--Global,全局作用域
B--Build-In,内置作用域。
当局不作用域于全局作用域发生冲突时,Python会使用局部作用域的变量,除非使用Global语句进行特别声明。当函数嵌套发生时,局部作用域又会覆盖外层函数的作用域,除非是有nonlocal语句进行声明。最后一个,比如说BIPF,Build-In Funtion,只要起一个变量名跟它一样,就足以毁掉内置函数。
>>> str = "字符串str没了"
>>> str(520)
Traceback (most recent call last):
File "<pyshell#112>", line 1, in <module>
str(520)
TypeError: 'str' object is not callable
前面把它变成一个变量名赋值了一个字符串,此时str变成了一个全局变量,而不是Build-In(因为根据法则,G比B要高)
9.闭包
也称为工厂函数。
>>> def power(exp):
def exp_of(base):
return base ** exp
return exp_of
>>> square = power(2)
>>> cube = power(3)
>>> square(2)
4
>>> square(5)
25
>>> cube(2)
8
>>> cube(5)
125
运用nonlocal语句
>>> def outer():
x = 0
y = 0
def inner(x1, y1):
nonlocal x, y
x += x1
y += y1
print(f"现在,x = {x}, y = {y}")
return inner
#x+=x1表示‘x=x+y’,第一个x为结果,第一个x为外部函数变量值
>>> move = outer()
>>> move(1, 2)
现在,x = 1, y = 2
>>> move(-2, 2)
现在,x = -1, y = 4
move在这里实现了一个带记忆功能的函数,在实际的开发中,闭包可以用于在游戏开发中,需要将游戏中的角色移动位置给保护起来,不希望被其他函数轻易的就能够去修改。
10.装饰器
把函数作为参数传递给另一个函数
>>> def myfunc():
print("正在调用myfunc...")
>>> def report(func):
print("开始调用函数")
func()
print("调用完毕")
#func在这里是一个参数,也可以用其他名称代替
>>> report(myfunc)
开始调用函数
正在调用myfunc...
调用完毕
设计一个函数,用于记录传入函数的运行时间。涉及到时间,需要用到一个time模块
>>> import time
>>> def time_master(func):
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束程序运行...")
print(f"一共耗费了 {(stop-start):.2f} 秒。 ")
#通过time模块的time()函数来获取当前的一个时间戳,放到变量start里
>>> def myfunc():
time.sleep(2)
print("Hello FishC.")
>>> time_master(myfunc)
开始运行程序...
Hello FishC.
结束程序运行...
一共耗费了 2.02 秒。
但由于每次想知道运行时间都需要去调用time_master()函数,更好的方案是在调用myfunc()函数时能够自觉的去执行time_master()函数
import time
def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束运行程序...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc()
@+装饰器名字,就是语法糖(类似于f-string和format的关系,f字符串就是一个语法糖),装饰器原来的样子
import time
def time_master(func):
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束运行程序...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc() = time_master(myfunc)
myfunc()
运用语法糖,则后续调用myfunc的时候并不是直接去调用myfunc,而是把myfunc函数作为一个参数塞到装饰器里,然后去调用这个装饰器。
Q:多个装饰器能否同时用在同一个函数上呢
def add(func):
def inner():
x = func()
return x + 1
return inner
def cube(func):
def inner():
x = func()
return x * x * x
return inner
def square(func):
def inner():
x = func()
return x * x
return inner
@add
@cube
@square
def test():
return 2
print(test())
#先调用square,后调用cube,再去调用add
Q:如何给装饰器传递参数
import time
def logger(msg):
def time_master(func):
def call_func():
start = time.time()
func()
stop = time.time()
print(f"[{msg}]一共耗费了 {(stop-start):.2f}")
return call_func
return time_master
@logger(msg="A")
def funA():
time.sleep(1)
print("正在调用funA...")
@logger(msg="B")
def funB():
time.sleep(1)
print("正在调用funB...")
funA()
funB()
把语法糖改写格式,运行效果不变
import time
def logger(msg):
def time_master(func):
def call_func():
start = time.time()
func()
stop = time.time()
print(f"[{msg}]一共耗费了 {(stop-start):.2f}")
return call_func
return time_master
def funA():
time.sleep(1)
print("正在调用funA...")
def funB():
time.sleep(1)
print("正在调用funB...")
funA = logger(msg="A")(funA)
funB = logger(msg="B")(funB)
funA()
funB()
11.lambda表达式
语法格式:
lambda arg1, arg2, arg3, ... argN : expression
lambda是关键字,冒号左边是传入函数的参数,冒号右边是函数实现表达式以及返回值
传统函数表达式:
def <lambda>(arg1, arg2, arg3, ... argN) :
return expression
#传统创建函数
>>> def squareX(x):
return x * x
>>> squareX(3)
9
#lambda表达式
>>> squareY = lambda y : y * y
>>> squareY(3)
9
对此,由于lambda是表示式,可以放到常规函数不能放的位置,如列表。但一般不这么使用
>>> y = [lambda x : x * x, 2, 3]
>>> y[0](y[1])
4
先前在序列中map()和fliter()函数的第一个参数便可以用lambda表达式代替
>>> mapped = map(lambda x : ord(x) +10, "FishC")
>>> list(mapped)
[80, 115, 125, 114, 77]
#传统函数写法
>>> def boring(x):
return ord(x) + 10
>>> list(map(boring, "FishC"))
[80, 115, 125, 114, 77]
#filter的lambda表达式参数
>>> list(filter(lambda x : x % 2, range(10)))
[1, 3, 5, 7, 9]
总结:lambda式表达式而非语句,能出现在Python语法中不允许def语句出现的地方,但由于语句受限,lambda通常也只能实现那些较为简单的需求。lambda是匿名函数,可以处理简单的函数,复杂的函数则交予def处理
12.生成器
闭包可以让函数在退出之后还能保留状态,同理还有全局变量。此外还有生成器,没有全局变量会污染空间的劣势渝闭包复杂的定义
A.生成器的定义
用yield代替return语句
>>> def counter():
i = 0
while i <= 5:
yield i
i += 1
B.生成器的使用
直接放到for语句中
>>> for i in counter():
print(i)
0
1
2
3
4
5
生成器每调用一次提供一个数据,并且会记录当时的状态。而列表、元组这些可迭代对象则是容器,里面存放的是早已准备好的所有数据。
生成器是一种特殊的迭代器,其一是不走回头路,其二是支持next()函数
>>> c = counter()
>>> next(c)
0
>>> next(c)
1
>>> next(c)
2
>>> next(c)
3
>>> next(c)
4
>>> next(c)
5
>>> next(c)
Traceback (most recent call last):
File "<pyshell#63>", line 1, in <module>
next(c)
StopIteration
由于生成器每调用一次获取一个结果的特性,导致生成器对象是无法使用下标索引的这种随机访问的方式
Q:用生成器求解斐波那契数列
>>> def fib():
back1, back2 = 0, 1
while True:
yield back1
back1, back2 = back2, back1 + back2
>>> f = fib()
>>> next(f)
0
>>> next(f)
1
>>> next(f)
1
>>> next(f)
2
若用for语句则会没有停止的一直运行(ctrl+c停止运行)
>>> for i in f:
print(i)
C.生成器表达式
前面讲过列表推导式,而元组则没有。
利用推导的形式获得生成器的方法称之为生成器表达式
>>> (i ** 2 for i in range(10))
<generator object <genexpr> at 0x0000020AF8735F90>
>>> t = (i ** 2 for i in range(10))
>>> next(t)
0
>>> next(t)
1
>>> next(t)
4
>>> for i in t:
print(i)
9
16
25
36
49
64
81
生成器表达式和列表推导式最大的不同就是列表推导式会直接生产出所有数据并放到一个列表中,而生成器表达式一次只出一个数据。
13.递归
递归就是函数调用自身的过程。直接内部调用函数会出现失控情况,对此要使用递归必须要有一个结束条件。并且每次调用都会向着这个结束条件去推进
Q:求一个数的阶乘
#迭代
>>> def factIter(n):
result = n
for i in range(1, n):
result *= i
return result
>>> factIter(5)
120
#递归
>>> def factRecur(n):
if n == 1:
return 1
else:
return n * factRecur(n-1)
>>> factRecur(5)
120
Q:斐波那契数列
#迭代
>>> def fibIter(n):
a = 1
b = 1
c = 1
while n > 2:
c = a + b
a = b
b = c
n -= 1
return c
>>> fibIter(12)
144
#递归
>>> def fibRecur(n):
if n == 1 or n == 2:
return 1
else:
return fibRecur(n-1) + fibRecur(n-2)
>>> fibRecur(12)
144
使用递归会使得效率变低
Q:汉诺塔
def hanoi(n, x, y, z):
if n == 1:
print(x, '-->', z)#如果只有一层,直接将金片从x移到z
else:
hanoi(n-1, x, z, y)#将x上的n-1个金片移到y
print(x, '-->', z)#将最底下的金片从x移到z
hanoi(n-1, y, x, z)#将y上的n-1个金片移到z
n = int(input('汉诺塔层数:'))
hanoi(n, 'A', 'B', 'C')
14.函数文档
写法:写完后可以用help查看
>>> def exchange(dollar, rate=6.32):
"""
功能:汇率转换,美元 -> 人民币
参数:
- dollar 美元数量
- rate 汇率,默认值是 6.32(2022-03-08)
返回值:
- 人民币的数量
"""
return dollar * rate
15.类型注释
作者告诉我们传入参数s为字符串,n为整数类型,返回为字符串类型。但实际Python并不会阻止其他类型参数的传入
>>> def times(s:str, n:int) -> str:
return s * n
>>> times("FishC", 5)
'FishCFishCFishCFishCFishC'
>>> times(5, 5)
25
>>> from typing import List
#3.8版本不兼容,为表示整数类型的列表,需要先导入List类型,其位于typing模块中
>>> def times(s: List[int], n: int = 3) -> List[int]:
return s * n
希望字典的键是字符串,值是整数
from typing import Dict, List
def times(s: Dict[str, int], n: int = 3) -> List[str]:
return list(s.keys()) * n
16.内省
指在程序运行的时候能够进行自我检测的一种机制。
#查看函数的名字
>>> times.__name__
'times'
#查看函数类型注释
>>> times.__annotations__
{'s': typing.Dict[str, int], 'n': <class 'int'>, 'return': typing.List[str]}
#查看函数文档
>>> exchange.__doc__
'\n\t功能:汇率转换,美元 -> 人民币\n\t参数:\n\t- dollar 美元数量\n\t- rate 汇率,默认值是 6.32(2022-03-08)\n\t返回值:\n\t- 人民币的数量\n\t'
>>> print(exchange.__doc__)
功能:汇率转换,美元 -> 人民币
参数:
- dollar 美元数量
- rate 汇率,默认值是 6.32(2022-03-08)
返回值:
- 人民币的数量
#用print可以把转义字符解析出来
17.高阶函数
当一个函数接受另一个函数作为参数的时候,此时这种函数就称之为高阶函数。例如前面学过的装饰器,map、filter、min、max,sorted他们有key参数,接收的正是一个函数对象
functools
A.reduce
reduce第一个参数指定函数,第二个参数是一个可迭代对象。其作用为键可迭代对象中的元素一次传递到第一个参数指定的函数中,最终返回累积的结果。
>>> def add(x, y):
return x + y
>>> import functools
>>> functools.reduce(add, [1, 2, 3, 4, 5])
15
#相当于
>>> add(add(add(add(1, 2), 3), 4), 5)
15
#lambda表示
>>> functools.reduce(lambda x,y:x*y, range(1, 11))
3628800
B.偏函数
偏函数是指对指定的函数进行二次包装,通常是将现有的函数部分参数预先给绑定从而得到一个新的函数,后该函数称为偏函数。简而言之,偏函数的作用是将一个函数的多个参数拆分多次进行传递。
>>> square = functools.partial(pow, exp=2)
>>> square(2)
4
C.@wraps
先前讲解的装饰器(装饰器代码3)
>>> myfunc.__name__
'call_func'
对此修改代码,消除副作用
import time
import functools
def time_master(func):
@functools.wraps(func)
def call_func():
print("开始运行程序...")
start = time.time()
func()
stop = time.time()
print("结束运行程序...")
print(f"一共耗费了 {(stop-start):.2f} 秒。")
return call_func
@time_master
def myfunc():
time.sleep(2)
print("I love FishC.")
myfunc()
>>> myfunc.__name__
'myfunc'
tips:知识点搜索可以在页面ctrl+F出现搜索框后进行查找
十三、存储
1.打开文件
open()函数
>>> f = open("FishC.txt", "w")
>>> f.write("I love Python.")
14
>>> f.writelines(["I love FishC.\n", "I love my wife."])
>>> f.close()
若要重新对文件进行操作就要重新打开
>>> f = open("FishC.txt", "r+")
>>> #r+模式打开,即更新文件的方式打开,既可以写入也可以读取
>>> f.readable()
True
>>> f.writable()
True
2.文件读取
Python文件支持迭代,将文件放到for语句里面去实现读取
>>> for each in f:
print(each)
I love Python.I love FishC.
I love my wife.
根据文件对象,也有读取文件内容的方法read()和readline()两种方法
>>> f.read()
''
由于文件对象的内部事实上有一个“文件指针”,负责指向文件的当前位置。当在文件中读取一个字符的时候,文件指针就会指向下一个字符,直到文件的末尾EOF(end of file)。而由于先前用来for语句使得现在指针指向文件的末尾。
可以使用tell方法来追踪文件指针的位置
>>> f.tell()
44
若想修改文件指针可以用seek方法
>>> f.seek(0)
0
#此时文件指针在开头位置
此时再读取
>>> f.readline()
'I love Python.I love FishC.\n'
#readline读取一行,会读取到换行符
>>> f.read()
'I love my wife.'
#read读取文件末尾
3.文件写入
在不关闭文件对象的前提下(没有close),将内容保存到硬盘的文件中可用flush方法
>>> f.write("I love my WIFI.")
15
>>> f.flush()
4.文件截断
A.truncate
truncate(pos=None,/)
将文件对象截取到指定的位置。如果没有pos参数,则截取到文件指针当前指定的位置,截取就是后面的内容全部删除。
B.单独写入模式打开
>>> f = open("FishC.txt", "w")
>>> f.close()
结果文件无内容
5.路径处理
首先需要导入模块
>>> from pathlib import Path
#从pathlib模块中单独导入Path,使用这种导入方式在后续调用时就不需要加入模块名了
>>> Path.cwd()
WindowsPath('C:/Users/Administrator/Desktop')
#cwd()获取当前目录的路径
直接指定一个路径传递到Path中,此时会生成一个路径对象
>>> p = Path('C:/Users/Administrator/Desktop')
>>> p
WindowsPath('C:/Users/Administrator/Desktop')
将文本文件添加到路径下
>>> q = p / "FishC.txt"
>>> q
WindowsPath('C:/Users/Administrator/Desktop/FishC.txt')
A.is_dir()
判断一个路径是否为一个文件夹
>>> p.is_dir()
True
>>> q.is_dir()
False
B.is_file()
判断一个路径是否为一个文件
C.exists()
检测一个路径是否存在
>>> Path("C:/404").exists()
False
D.name
name属性获取路径的最后一个部分
>>> p.name
'Desktop'
>>> q.name
'FishC.txt'
E.stem
获取文件名
F.suffix
获取文件后缀
G.parent
获取父级目录
parents--获取逻辑祖先路径构成的序列
>>> p.parents
<WindowsPath.parents>
>>> ps = p.parents
>>> for each in ps:
print(each)
C:\Users\Administrator
C:\Users
C:\
>>> #此处还支持索引
>>> ps[0]
WindowsPath('C:/Users/Administrator')
>>> ps[1]
WindowsPath('C:/Users')
H.parts
将路径的各个组件拆分成元组
>>> p.parts
('C:\\', 'Users', 'Administrator', 'Desktop')
I.stat()
查询文件或文件夹的状态信息
>>> p.stat()
os.stat_result(st_mode=16749, st_ino=2814749767109627, st_dev=2925560252, st_nlink=1, st_uid=0, st_gid=0, st_size=49152, st_atime=1706445721, st_mtime=1706445721, st_ctime=1660532113)
>>> p.stat().st_size
49152
指定的时此文件或者文件夹的尺寸,其单位是字节
6.相对路径和绝对路径
绝对路径:文件真正存在的路径。如果一个文件从根目录开始一级一级的指向最终的文件或文件夹,那么此路径就是绝对路径,比如上述的p和q
相对路径:以当前目录最为基准,进行一级一级的目录推导的路径。
>>> Path("./doc")
WindowsPath('doc')
#表示当前路径下的doc文件夹
通常使用点来表示当前所在的目录,使用两个紧挨这的点来表示上一级目录
A.resolve()
将相对路径转换为绝对路径
>>> Path("../FishC")
WindowsPath('../FishC')
>>> Path("../FishC").resolve()
WindowsPath('E:/Download/FishC')
B.iterdir()
获取当前路径下所有子文件和子文件夹
>>> p.iterdir()
<generator object Path.iterdir at 0x00000264F9C58580>
#得到一个generator object生成器
>>> for each in p.iterdir():
print(each)
E:\Download\python\DLLs
E:\Download\python\Doc
E:\Download\python\include
E:\Download\python\Lib
E:\Download\python\libs
E:\Download\python\LICENSE.txt
E:\Download\python\NEWS.txt
E:\Download\python\python.exe
E:\Download\python\python3.dll
E:\Download\python\python38.dll
E:\Download\python\pythonw.exe
E:\Download\python\Scripts
E:\Download\python\tcl
E:\Download\python\Tools
E:\Download\python\vcruntime140.dll
E:\Download\python\vcruntime140_1.dll
若想将当前路径下所有的文件整理成一个列表(不包含文件夹),则可以增加一个条件过滤
>>> [x for x in p.iterdir() if x.is_file()]
[WindowsPath('E:/Download/python/LICENSE.txt'), WindowsPath('E:/Download/python/NEWS.txt'), WindowsPath('E:/Download/python/python.exe'), WindowsPath('E:/Download/python/python3.dll'), WindowsPath('E:/Download/python/python38.dll'), WindowsPath('E:/Download/python/pythonw.exe'), WindowsPath('E:/Download/python/vcruntime140.dll'), WindowsPath('E:/Download/python/vcruntime140_1.dll')]
C.mkdir()
创建文件夹。若创建文件夹已存在,则会报错
>>> n = p / "FishC"
>>> n.mkdir()
为避开报错信息,可以用exist_ok参数设置为True即可
>>> n.mkdir(exist_ok=True)
若路径中存在多个不存在的父级目录也会发生出错
>>> n = p / "FIshC/A/B/C"
>>> n.mkdir(exist_ok=True)
#因为路径C之前的A,B不存在
对于此种情况,可将parents参数设置为True即可。且此时FishC里面有多个文件夹--A,B,C
>>> n.mkdir(parents=True, exist_ok=True)
D.open()
Path内部同时打包了open()方法,除了不用传入第一个参数路径之外其他参数跟open()函数一模一样
>>> n = n / "FishC.txt"
>>> f = n.open("w")
>>> f.write("I love FishC.")
#写入字符
>>> f.close()
#内容才会从文件对象的缓冲区写到文件里面去
7.rename()
修改文件或文件夹名字
>>> n.rename("NewFIshC.txt")
WindowsPath('NewFishC.txt')
需要注意给新名字参数的时候不包含路径,直接给到“NewFishC.txt",故将修改名字放到Python大本营(在idle里修改的话)
8.replace()
替换指定的文件或文件夹
9.rmdir()和unlink()
前者用于删除文件夹;后者用于删除文件
>>> n.unlink()
>>> n.parent.rmdir()
#需要先删除文件,再删除文件夹,否则会报错
10.glob()
功能强大的查找功能
>>> p = Path('.')
#用相对目录,"."表示Python大本营
>>> p.glob("*.txt")
#查找当前文件夹下面的所有以.txt为后缀的文件
#>>> 生成一个generator object
>>> list(p.glob("*.txt"))
#>>> 列表形式展现
若要查找当前目录下下一级目录的所有.py后缀的文件
>>> list(p.glob("*/*.py"))
若想进行向下的递归搜索,即查找当前目录以及该目录下面的所有子目录
>>> list(p.glob("**/*.py"))
11.with语句和上下文管理器
传统方法--打开文件、操作文件、关闭文件
>>> f = open("FishC.txt", "w")
>>> f.write("I love FishC.")
13
>>> f.close()
用with语句。效果和上面等效
>>> with open("FishC.txt", "w") as f:
f.write("I love FishC.")
#若有其他操作则继续往下写即可
>>> 13
若用传统方法,在中间有出错语句时,程序并没有机会执行到文件关闭的操作,使得写入文件的内容依然在缓冲区中,并没有写入硬盘中。若有with形式,即便出错,with上下文管理器依然能确保文件可以正常的关闭
12.pickle
pickle模块解决的时永久存储Python对象的问题。即允许将字符串、列表、字典这些Python对象给保存为文件的形式。平时.py打包的为源代码,时可以被执行的独立单元。
当下是将Python对象序列化,所谓序列化就是将Python对象转换为二进制字节流的过程。简而言之,就是将代码转换为0101001的二进制组合。具体实现函数为dump和load
import pickle
x, y, z = 1, 2, 3
s = "FishC"
l = ["小甲鱼", 520, 3.14]
d = {"one":1, "two":2}
with open("data.pkl", "wb")as f:
pickle.dump(x, f)
pickle.dump(y, f)
pickle.dump(z, f)
pickle.dump(s, f)
pickle.dump(l, f)
pickle.dump(d, f)
#要保存为pickle文件,后缀应为pkl
#以二进制可写入形式打开,故用“wb”
简便写法。用元组的形式进行打包
import pickle
x, y, z = 1, 2, 3
s = "FishC"
l = ["小甲鱼", 520, 3.14]
d = {"one":1, "two":2}
with open("data.pkl", "wb")as f:
pickle.dump((x, y, z, s, l, d), f)
此时会生成一个.pkl文件,若使用文本文件打开会生成鸟文
此时,用read.py文件去读取鸟文文件
import pickle
with open("data.pkl", "rb")as f:
x = pickle.load(f)
y = pickle.load(f)
z = pickle.load(f)
s = pickle.load(f)
l = pickle.load(f)
d = pickle.load(f)
print(x, y, z, s, l, d, sep="\n")
#sep分割符换行,rb读取一个二进制文件
同样简便写法,效果等效。进行一个解包操作
import pickle
with open("data.pkl", "rb")as f:
x, y, z, s, l, d = pickle.load(f)
print(x, y, z, s, l, d, sep="\n")
输出结果
1
2
3
FishC
['小甲鱼', 520, 3.14]
{'one': 1, 'two': 2}
十四、异常
1.try-except语句
利用try-except语句来捕获并处理异常
语法:
try:
检测范围
except [expression [as identifier] ]:
异常处理代码
在异常情况下
>>> try:
1 / 0
except:
print("出错了")
出错了
若在正常情况下
>>> try:
1 / 1
except:
print("出错了")
1.0
同时可以在except后面指定一个具体的异常。只有当捕获到的异常是指定的具体的异常的时候才会执行语句中的内容,否则依然报错
>>> try:
1 / 1
except ZeroDivisionError:
print("出错了")
1.0
>>> try:
"FishC" + 520
except ZeroDivisionError:
print("出错了")
Traceback (most recent call last):
File "<pyshell#18>", line 2, in <module>
"FishC" + 520
TypeError: can only concatenate str (not "int") to str
在指定异常后面还可以加上一个可选的as,将异常的原因提取出来
>>> try:
1 / 0
except ZeroDivisionError as e:
print(e)
division by zero
在不知道会出现哪种具体异常时,可以将多个可能出现的异常使用元组的形式给包裹起来
>>> try:
1 / 0
520 + "FishC"
except (ZeroDivisionError, ValueError, TypeError):
pass
#但凡try语句中检测到包含这三个异常中的任意一个,都会执行pass语句直接忽略跳过
也可以单独处理不同的异常,用多个except语句即可
>>> try:
1 / 0
520 + "FishC"
except ZeroDivisionError:
print("除数不能为0")
except ValueError:
print("值不正确")
except TypeError:
print("类型不正确")
除数不能为0
#当捕获到异常则直接跳到了指定的异常处理的代码后继续往下运行
2.try-except-else语句
当try语句中没有检测到任何异常的情况,就会执行else语句的内容
>>> try:
1 / 1
except:
print("逮到了")
else:
print("没逮到")
1.0
没逮到
3.try-except-finally
无论异常是否发生都必须会执行的内容
>>> try:
1 / 0
except:
print("逮到了")
else:
print("没逮到")
finally:
print("逮没逮到都会出现")
逮到了
逮没逮到都会出现
>>> try:
1 / 1
except:
print("逮到了")
else:
print("没逮到")
finally:
print("1")
1.0
没逮到
1
通常finally用于执行收尾工作,比如关闭文件。
>>> try:
f = open("FishC.txt", "w")
f.write("I love FishC")
except:
print("出错了")
finally:
f.close()
12
#无论try语句中是否存在异常,文件都能够正确地被关闭。和先前讲解的with上下文管理器一样
还有一种搭配的方式是try和finally,去掉except
>>> try:
while True:
pass
finally:
print("晚安")
晚安
Traceback (most recent call last):
File "<pyshell#30>", line 3, in <module>
pass
KeyboardInterrupt
#发生异常与否都会去执行finally里面的内容
综上述,异常处理的代码综合如下:
方括号代表内容是可选的,*表示内容可以是零个或者是多个
try:
检测范围
except [expression [as identifier]]:
异常处理代码
[except [expression [as identifier]]:
异常处理代码]*
[else:
没有出发异常是执行的代码]
[finally:
收尾工作执行的代码]
或者
try:
检测范围
finally:
收尾工作执行的代码
4.异常的嵌套
>>> try:
try:
520 + "FishC"
except:
print("内部异常")
1 / 0
except:
print("外部异常")
finally:
print("收尾工作")
内部异常
外部异常
收尾工作
>>> try:
1 / 0
try:
520 + "FishC"
except:
print("内部异常")
1 / 0
except:
print("外部异常")
finally:
print("收尾工作")
外部异常
收尾工作
#内部异常被跳过了
5.raise语句
先前讲述的异常处理都是要有触发异常的语句或者表达式,异常才会发生,接下来讲述的可以直接自爆
>>> raise ValueError("值不正确")
Traceback (most recent call last):
File "<pyshell#44>", line 1, in <module>
raise ValueError("值不正确")
ValueError: 值不正确
Attention:不能用raise生成一个不存在的异常
A.替换异常名字
>>> try:
1 / 0
except:
raise ValueError("这样不行")
Traceback (most recent call last):
File "<pyshell#49>", line 2, in <module>
1 / 0
ZeroDivisionError: division by zero
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<pyshell#49>", line 4, in <module>
raise ValueError("这样不行")
ValueError: 这样不行
B.异常链
>>> raise ValueError("这样可不行") from ZeroDivisionError
ZeroDivisionError
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<pyshell#50>", line 1, in <module>
raise ValueError("这样可不行") from ZeroDivisionError
ValueError: 这样可不行
此时是一个链接,ValueError是来自于ZeroDivisionError形式的
6.assert语句
和raise语句类型,都是主动引发异常,不过assert语句只能引发一个叫做AssertionError的异常。此语句通常用于代码调试
>>> s = "FishC"
>>> assert s == "FishC"
>>> assert s != "FishC"
Traceback (most recent call last):
File "<pyshell#53>", line 1, in <module>
assert s != "FishC"
AssertionError
类似于一个if语句的判断,若条件成立正常运行;若条件不成立就会抛出一个AssertionError的异常
7.利用异常实现goto
>>> try:
while True:
while True:
for i in range(10):
if i > 3:
raise
print(i)
print("跳过了")
print("跳过了")
print("跳过了")
except:
print("到这里了")
0
1
2
3
到这里了
十五、类和对象
1.创建对象
对象=属性(静态特征)+方法(对象所能做的事情)
在创造一个对象前,需要先创建一个类(class),后再通过创造类来创造实际的对象
class Turtle:
head = 1
eyes = 2
legs = 4
shell = True
def crawl(self):
print("1好")
def run(self):
print("2好")
def bite(self):
print("3好")
def eat(self):
print("4好")
def sleep(self):
print("Zzzz")
#类就是写在属性里面的变量,所谓方法就是写在类里面的函数
运行程序后,在idle里就拥有了turtle这个类。后像调用函数一样去操作
>>> t1 = Turtle()
此时t1现在就是一个Turtle类的对象,也叫实例对象。它拥有了这个类所定义的属性和方法
>>> t1.head
1
>>> t1.bite()
3好
理论上一个类可以创造出无数个对象。而当一个对象被创建出来时就可以去修改其对应属性值了,但其他对象数据不会被改变
>>> t2 = Turtle()
>>> t2.legs = 3
>>> t2.legs
3
>>> t1.legs
4
也可以动态的去创建一个属性,跟在字典中添加一个新的键值对一样
>>> t1.mouth = 1
>>> t1.mouth
1
#使用dir()函数去看t1和t2里面对了的属性
>>> dir(t1)
['__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__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'mouth', 'run', 'shell', 'sleep']
>>> dir(t2)
['__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__', 'bite', 'crawl', 'eat', 'eyes', 'head', 'legs', 'run', 'shell', 'sleep']
Q:封装
封装是面向对象编程的三个基本特征之一,另外两个是继承和多态。前面讲述即将甲鱼的特征属性和行为能力给封装到了一起
在封装上,不要把不相关的属性和方法都放到一起
Q:self
若定义类的时候没有self参数,调用时会引发TypeError异常
>>> class C:
def hello():
print("你好")
>>> c = C()
>>> c.hello()
Traceback (most recent call last):
File "<pyshell#16>", line 1, in <module>
c.hello()
TypeError: hello() takes 0 positional arguments but 1 was given
加上self
>>> class C:
def get_self(self):
print(self)
>>> c = C()
>>> c.get_self()
<__main__.C object at 0x00000137ED6C5580>
>>> c
<__main__.C object at 0x00000137ED6C5580>
可以看出传递给方法的就是实例对象本身。
同一个类可以生成无数个对象,当我们在调用类里面的一个方法的时候,Python通过self参数传递的信息来确定是哪个对象在调用。故类中的每一个方法默认的第一个参数都是self
2.继承
A.基本
Python类支持继承,可以使用现有类的所有功能并在无需重新编写代码的情况下对这些功能进行扩展
通过继承创建的新类称之为子类,而被继承的类称之为父类、基类或者超类
>>> class A:
x = 520
def hello(self):
print("我是A")
#括号内表示父类
>>> class B(A):
pass
>>> b = B()
>>> b.x
520
>>> b.hello()
我是A
若B类存在与A一样的属性和方法名,会实现覆盖效果
>>> class B(A):
x = 880
def hello(self):
print("我是B")
>>> b = B()
>>> b.x
880
>>> b.hello()
我是B
B.isinstance()函数
判断一个对象是否属于某个类,为BIF函数
>>> isinstance(b, B)
True
>>> isinstance(b, A)
True
#类B是由类A继承下来的,自然拥有其父类的所有属性和方法
C.issubclass()函数
检测一个类是否为某个类的子类
>>> issubclass(A, B)
False
>>> issubclass(B, A)
True
D.多重继承
一个子类可以同时继承多个父类
访问顺序从左到右。只有在当前类中找不到了,在左侧A中也搜索未果,才会去父类B中寻找
>>> class B:
x = 880
y = 250
def hello(self):
print("我是B")
>>> class C(A, B):
pass
>>> c = C()
>>> c.x
520
>>> c.hello()
我是A
3.组合
>>> class Turtle:
def say(self):
print("小甲鱼")
>>> class Cat:
def say(self):
print("小猫")
>>> class Garden:
t = Turtle()
c = Cat()
def say(self):
self.t.say()
self.c.say()
>>> g = Garden()
>>> g.say()
小甲鱼
小猫
将相关的实例放到花园的类别里
4.绑定
先前讲到的self的作用为“绑定”,即实例对象跟类的方法进行绑定。因为类的实例对象可以有很多,当这些实例对象却是共享类里面的方法,故我们在调用实例c.get_self()时(1-Qself代码),其实际含义是调用类C的get_self()方法,并将实例对象作为参数给传递进去进而实现绑定。相当于
>>> C.get_self(c)
<__main__.C object at 0x00000137ED6C5190>
在实例中,除了类的方法是共享之外,实例的属性却可以是自己的
>>> d = C()
>>> d.x = 250
>>> d.x
250
>>> c.x
Traceback (most recent call last):
File "<pyshell#90>", line 1, in <module>
c.x
AttributeError: 'C' object has no attribute 'x'
#c.x与d.x无关
>>> c.x = 520
>>> c.x
520
A.查看对象属性
若想知道对象当前拥有那些属性。可以通过__dict__进行内省。通过字典的形式来保存其属性以及对应的值
>>> c.__dict__
{'x': 520}
>>> d.__dict__
{'x': 250}
B.设置对象属性
如果希望通过类里面的方法来设置对象自己的属性,可以通过self来建立绑定
>>> class C:
def set_x(self, v):
self.x = v
>>> c = C()
>>> c.__dict__
{}
>>> c.set_x(250)
>>> c.__dict__
{'x': 250}
>>> c.x
250
再举一例
>>> class C:
x = 100
def set_x(self, v):
x = v
>>> c = C()
>>> c.set_x(250)
>>> c.x
100
#若要修改对象的属性,就需通过self进行绑定才可
>>> C.x
100
#先前的操作只是在set_x()函数的内部创建了一个局部变量x,然后其值赋为250。它跟类和对象的x属性都没有关系,故都无法进行修改
>>> C.x = 250
>>> c.x
250
#通过类对其属性x进行修改,实例对象x属性也会发生变化
#因为此时对象c是没有属性x,这里能够访问到是因为它是由类C实例过来的,所以它也能够访问到类C的属性
>>> c.__dict__
{}
#对象中没有不代表生成它的类没有;它的类没有不代表它的父类没有(Python会层层寻找)
对于这种直接通过类来修改类属性的操作是不建议的,因为所以从这个类生成的实例对象都共享着这个属性,若直接去修改很容易牵动其他
tip:定义最小的类
最小的类就是什么都没有的类。可以当作字典来使用
>>> class C:
pass
>>> c.x = 250
>>> c.y = "小甲鱼"
>>> c.z = [1, 2, 3]
>>> print(c.x)
250
>>> print(c.y)
小甲鱼
>>> print(c.z)
[1, 2, 3]
#类和对象的属性就是通过字典进行存放
>>> d = {}
>>> d['x'] = 250
>>> d['y'] = "小甲鱼"
>>> d['z'] = [1, 2, 3]
>>> print(d['x'])
250
>>> print(d['y'])
小甲鱼
>>> print(d['z'])
[1, 2, 3]
使用类来表示映射会方便点。但常用做法为通过空类生成的实例来模拟字典
>>> class C:
pass
>>> c = C()
>>> c.x = 250
>>> c.y = "小甲鱼"
>>> c.z = [1, 2, 3]
5.构造函数
函数时可以通过参数来进行个性化定制的,而类在实例化时也支持个性化定制对象。
在定义类的时候同时去定义一个构造函数就可以实现自我发挥了
构造函数有个特殊的名称:__init__()。只要在类中定义__init__()方法,那么就可以在实例化对象的同时实现个性化定制
>>> class C:
def __init__(self, x, y):
self.x = x
#等号左边的self.x是绑定到实例化对象里的x属性,而等号右边的x是传进来的x参数
self.y = y
def add(self):
return self.x + self.y
def mul(self):
return self.x * self.y
>>> c = C(2, 3)
#在实例化的同时传入对象的两个变量
>>> c.add()
5
>>> c.mul()
6
>>> c.__dict__
{'x': 2, 'y': 3}
>>> d = C(4, 5)
>>> d.add()
9
>>> d.mul()
20
>>> d.__dict__
{'x': 4, 'y': 5}
6.重写
如果对父类的某个属性或某个方法,可以重新写一个同名的属性或方法对其进行覆盖,此种行为即为子类对父类的重写
>>> class D(C):
def __init__(self, x, y, z):
C.__init__(self, x, y)
#这样子就不用去写self.x=x,self.y=y,self.z=z。直接用C类的构造函数
self.z = z
def add(self):
return C.add(self) + self.z
def mul(self):
return C.mul(self) * self.z
>>> d = D(2, 3,4)
>>> d.add()
9
>>> d.mul()
24
A.钻石继承
上述直接通过类名访问类里面的方法的做法称之为调用未绑定的父类方法,这种方法比较直接,有时候可能会造成钻石继承的问题
>>> class A:
def __init__(self):
print("我是A")
>>> class B1(A):
def __init__(self):
A.__init__(self)
print("我是B1")
>>> class B2(A):
def __init__(self):
A.__init__(self)
print("我是B2")
>>> class C(B1, B2):
def __init__(self):
B1.__init__(self)
B2.__init__(self)
print("我是C")
>>> c = C()
我是A
我是B1
我是A
我是B2
我是C
类C是同时继承类B1和类B2的,类B1和类B2又是继承类A的。故当类C去调用类B1和类B2的构造函数时,类A的构造函数也会被跟着调用两次
B.super()函数
为解决上述问题,需要采用super()函数。super函数能够在父类中搜索指定过的方法并自动绑定好self参数
>>> class B1(A):
def __init__(self):
super().__init__()
print("我是B1")
>>> class B2(A):
def __init__(self):
super().__init__()
print("我是B2")
>>> class C(B1, B2):
def __init__(self):
super().__init__()
print("我是C")
>>> c = C()
我是A
我是B2
我是B1
我是C
只要使用super函数去查找父类的方法,他就会自动按照MRO顺序去搜索父类的相关方法并且自动避免重复调用的问题
C.MRO
Method-Resolution-Order。先前讲解多继承时,若出现同名的属性或方法,Python会有一个明确的一个查找覆盖顺序,即MRO--方法解析顺序。
要查找一个类的MRO顺序有两种方法,一种是通过MRO方法
>>> C.mro()
[<class '__main__.C'>, <class '__main__.B1'>, <class '__main__.B2'>, <class '__main__.A'>, <class 'object'>]
上述出现object是所有类的基类,就算不写也会被隐式的继承
第二种方法是通过MRO属性
>>> B2.__mro__
(<class '__main__.B2'>, <class '__main__.A'>, <class 'object'>)
7.Mixin
即Mix-in,翻译过来为混入或乱入。其实是一种设计模式,利用编程语言已有的特性针对面向对象开发过程中反复出现的问题而设计出来的解决方案
class Displayer:
def display(self, message):
print(message)
class LoggerMixin:
def log(self, message, filename="logfile.txt"):
with open(filename, "a") as f:
f.write(message)
def display(self, message):
super().display(message)
self.log(message)
class MySubClass(LoggerMixin, Displayer):
def log(self, message):
super().log(message, filename="subclasslog.txt")
subclass = MySubClass()
subclass.display("This is a test.")
分析:subclass实例化对象,参数是“This is...”,调用display()方法(从父类查找方法,(LoggrMixin,Displayer)Python会先左后右去查找这个方法)。
选择LoggerMixin里display()方法,其带有一个message参数,会把“This...”传递过去。
方法第一句为super,super()回去父类里查找对应的方法,此处没有写父类,默认为object(任何类都是object的子类),但object作为一个总的基类,没有display方法。super()函数是严重依赖MRO顺序的
>>> MySubClass.mro() [<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]
所以在类LoggerMixin里调用super函数,它会先去类Displayer查找而不是object。
紧接log方法是谁的,取决于self,即subclass对象本身(绑定)。而对象来自MySubClass的,而类MySubClass里正好有log方法的实现。发现里面正好有super函数,根据MRO顺序(下图)
Python去LoggerMixin寻找,其功能就是去创建一个文件,并将message追加保存到文件中,文件名若有转递则有传递,无则用logfile.txt,而在MySubClass中指定文件名为subclasslog.txt
8.多态
指同一个运算符、函数或对象在不同的场景下具有不同的作用效果的一个技能
>>> 3 + 5
8
>>> "Fish" + "C"
'FishC'
#运算符多态
>>> len("FishC")
5
>>> len(["FishC", "Python", "Me"])
3
#函数的多态
重写就是类继承的多态
>>> class shape:
def __init__(self, name):
self.name = name
def area(self):
pass
>>> class Square(shape):
def __init__(self, length):
super().__init__("正方形")
self.length = length
def area(self):
return self.length * self.length
>>> class Circle(shape):
def __init__(self, radius):
super().__init__("圆形")
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
>>> class Triangle(shape):
def __init__(self, base, height):
super().__init__("三角形")
self.base = base
self.height = height
def area(self):
return self.base * self.height / 2
>>> s = Square(5)
>>> c = Circle(6)
>>> t = Triangle(3, 4)
>>> s.area()
25
>>> c.area()
113.03999999999999
>>> t.area()
6.0
自定义函数实现多态接口
>>> class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def intro(self):
print(f"gou,我是{self.name},{self.age}岁")
def say(self):
print("wang!")
>>> class Pig:
def __init__(self, name, age):
self.name = name
self.age = age
def intro(self):
print(f"zhu,我是{self.name},{self.age}岁")
def say(self):
print("Ohi!")
>>> c =Dog("小狗", 4)
>>> d = Dog("小狗", 4)
>>> p = Pig("小猪", 5)
>>> def animal(x):
x.intro()
x.say()
>>> animal(c)
gou,我是小狗,4岁
wang!
9.“私有变量”
通过某种手段,使得对象中的属性或方法无法被外部所访问。在Python中,仅限从一个对象内部才能访问的“私有变量”并不存在,Python中存在name mangling机制,翻译为名字改编,名称改写或者名称修饰。名称前加__表示私有变量(但没办法做到真正的私有化)
>>> class C:
def __init__(self, x):
self.__x = x
def set_x(self, x):
self.__x = x
def get_x(self):
print(self.__x)
>>> c = C(250)
>>> c.__x
Traceback (most recent call last):
File "<pyshell#82>", line 1, in <module>
c.__x
AttributeError: 'C' object has no attribute '__x'
想要访问变量就需要指定的接口,比如说代码里的set_x()和get_x()两种方法
>>> c.get_x()
250
>>> c.set_x(520)
>>> c.get_x()
520
当然,也有直接访问的方法。也为常说的“名字改编术”--下横线加上类名再加上变量的名字
>>> c.__dict__
{'_C__x': 520}
>>> c._C__x
520
方法名同理
>>> class D:
def __func(self):
print("hello")
>>> d = D()
>>> d.__func()
Traceback (most recent call last):
File "<pyshell#95>", line 1, in <module>
d.__func()
AttributeError: 'D' object has no attribute '__func'
>>> d._D__func()
hello
但不建议这样做,有意使用双下划线开头就表示此属性或方法是希望不被外界所访问的,故最好遵循这个约定俗成的规则
在对象诞生之后,可以通过动态添加属性的方法来添加一个“私有变量”。后期添加的不会存在被名字改编
>>> c.__y = 250
>>> c.__dict__
{'_C__x': 520, '__y': 250}
故可以得到结论,名字改编是发生再实例化对象时的事情。
_单个下横线开头的变量代表仅供内部使用的变量
单个下横线结尾的变量_,比如要用class,但由于代表类了,所以要用的时候需要结尾加_
10.舍/得
Python有时会为了对象的灵活性会牺牲大量的存储空间。比如动态添加属性,背后实现原理是字典,即__dict__属性,对象属性通常是放到__dict__里
>>> c.y = 520
>>> c.__dict__
{'_C__x': 520, '__y': 250, 'y': 520}
我们可以直接通过给字典添加键值对的方式来创建对象的属性
>>> c.__dict__['z'] = 666
>>> c.__dict__
{'_C__x': 520, '__y': 250, 'y': 520, 'z': 666}
>>> c.z
666
字典效率之高的背后是空间的消耗。舍空间得时间
__slots__
若明确知道一个类得对象设计出来就只需要固定得几个属性,且将来也不会有动态添加属性得这种功能的需求,就不在需要利用字典来存放属性这种空间的牺牲。针对这种情况,Python设计了一个__slots__的类属性,避免了利用字典来存放造成空间上的浪费
>>> class C:
__slots__ = ["x", "y"]
def __init__(self, x):
self.x = x
>>> c = C(250)
>>> c.x
250
>>> c.y = 520
>>> c.y
520
若想动态添加属性会出现报错。同时在类的内部想要创建一个__slots__不包含的属性也是不被允许的
>>> class D:
__slots__ = ["x", "y"]
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
>>> d = D(2, 3, 4)
Traceback (most recent call last):
File "<pyshell#128>", line 1, in <module>
d = D(2, 3, 4)
File "<pyshell#127>", line 6, in __init__
self.z = z
AttributeError: 'D' object has no attribute 'z'
由于使用了__slots__属性,对象就会划分一个固定大小的空间来存放指定的属性,此时__dict__属性也就不需要了,节约了空间
Attention:继承自父类的__slots__属性是不会在子类中生效的,Python只会关注各个具体的类中定义的__slots__属性
>>> class E(C):
pass
>>> e = E(250)
>>> e.z = 666
>>> e.__slots__
['x', 'y']
>>> e.__dict__
{'z': 666}
虽然E有__slots__属性,当时其是类C的且E同时拥有一个__dict__属性
11.魔法方法
A.__init__()方法
__init__(self[, ...]),在类实例化对象的时候自动进行调用
B.__new__方法
__new__(cls[, ...]),在__init__之前被调用。事实上对象是由__new__()方法来创建的,故第一个参数是cls--类,而不是self。对象创建过程是先调用__new__()方法来创建一个类的实例,后将其传递给__init__()方法,在进行个性化定制的流程。
需要重写__new__()方法的情况很少,其一为在元类中去定制类;其二是在继承不可变数据类型时可以通过重写__new__()方法进行拦截
>>> class CapStr(str):
#继承自str字符串
def __new__(cls, string):
string = string.upper()
#把参数变成大写字母组成的形式
return super().__new__(cls, string)
>>> cs = CapStr("FishC")
>>> cs
'FISHC'
之所以能够对不可变对象进行修改,是因为在实例化对象被创建之前进行了拦截。后才调用super().__new__()去创建真正的实例
由于CapStr继承自str,故还可
>>> cs.capitalize()
'Fishc'
C.__del__(self)
在对象被销毁的时
>>> class C:
def __init__(self):
print("我来了")
def __del__(self):
print("我走了")
>>> c = C()
我来了
>>> del c
我走了
不一定调用del c就一定会触发__del__()方法,因为触发条件时对象被销毁时才触发。Python引用了垃圾回收机制garbage collection,即当检测到一个对象没有任何引用的时候才会将其销毁。所以在del c之前将c给送出去,del c理论上就不会调用到__del__()方法,因为此时对象还存在引用
>>> d = c
>>> del c
#没有用
>>> del d
我走了
#它的最后一个引用被销毁,那么这个对象才会被销毁
del方法可以通过创建一个该实例的新引用来推迟其销毁,这也被称之为对象的重生(但不建议)
>>> class D:
def __init__(self, name):
self.name = name
def __del__(self):
global x
#注解Ⅰ
x = self
>>> d = D("小甲鱼")
>>> d
<__main__.D object at 0x00000277CC48CD30>
>>> d.name
'小甲鱼'
>>> del d
>>> d
Traceback (most recent call last):
File "<pyshell#176>", line 1, in <module>
d
NameError: name 'd' is not defined
>>> x
<__main__.D object at 0x00000277CC48CD30>
>>> x.name
'小甲鱼'
#注解Ⅱ
*注解Ⅰ--由于无法通过返回值的形式,因为去销毁一个对象是del加上一个对象名,而del是一个语句,没办法执行语句的同时又把它的返回值赋值给另一个变量去存储,故通过返回值把self送出去不可行。但可以利用全局变量,因为方法也是一个函数,只是对了一个对象绑定的操作和参数,故global语句理应可使用
#注解Ⅱ--事实上就是对象d,因为在临了时把self又给送出来了
先前说过非迫不得已不要使用全局变量,因为会污染命名空间。对此可以考虑函数调用的形式,让对象在销毁之前去调用一个函数,然后通过参数传递的形式将self给传出去
>>> class E:
def __init__(self, name, func):
#通过实例化的时候先传入参数func
self.name = name
self.func = func
def __del__(self):
self.func(self)
self是通过参数传递进来,拿到后是局部变量,而局部变量在函数调用后就会消失,对此可以使用闭包。Python闭包会通过某种方式将外层函数的局部变量给保存下来,
>>> def outter():
x = 0
def inner(y=None):
nonlocal x
#解析Ⅰ
if y:
x = y
else:
return x
return inner
>>> f = outter()
>>> e = E("小甲鱼", f)
>>> e
<__main__.E object at 0x00000277CC48CF40>
>>> e.name
'小甲鱼'
>>> del e
>>> e
Traceback (most recent call last):
File "<pyshell#203>", line 1, in <module>
e
NameError: name 'e' is not defined
>>> g = f()
>>> g
<__main__.E object at 0x00000277CC48CF40>
>>> g.name
'小甲鱼'
#解析Ⅰ--`nonlocal` 是 Python 中用来声明一个变量不是局部变量,也不是全局变量,而是外层嵌套函数的变量。通常在嵌套函数中,可以修改或引用外层函数作用域的变量值。
这在闭包(closure)中特别有用,因为它允许内部函数修改外部函数的变量。`nonlocal` 关键字可以将一个变量标记为在上层函数作用域中定义,这样在内部函数中对这些变量进行修改时,就不会被当作新的局部变量。
整个过程的分析:此处定义为闭包是为了让self保存在外部函数的x变量中,而内部函数的作用是用于窃取self对象,在__del__()方法中调用时是带有参数的,它就把参数给存储起来;如果在外部嗲用这个函数时不带参数(就像此处),就使用默认值None,那么它就会返回刚刚拿到的self对象
12.运算相关
A.算术运算
让两个字符串相加的结果不是拼接,而是统计两者字符个数之和
>>> class S(str):
def __add__(self, other):
#self参数用于绑定对象,other
return len(self) + len(other)
>>> s1 = S("F")
>>> s2 = S("is")
>>> s1 + s2
3
加法操作重写__add__方法时属于前面对象的方法。s1+s2相当于s1.__add__(s2)
>>> s1 + "PY"
3
>>> "py" + s2
'pyis'
B.反算数运算
相似于算术运算,区别在于:(加法举例)
__radd__()方法调用前提--当两个对象相加的时候,如果左侧数对象和右侧的对象不同类型,并且左侧的对象没有定义__add__()方法,或者其__add__()返回Notlmplemented,那么Python就会去右侧的对象中查找是否有__add__()方法的定义
>>> class S1(str):
def __add__(self, other):
return NotImplemented
#返回表示此方法未实现
>>> class S2(str):
def __radd__(self, other)
SyntaxError: invalid syntax
>>> class S2(str):
def __radd__(self, other):
return len(self) + len(other)
>>> s1 + s2
3
这里还能调用S2的__radd__()方法首先是因为S2实现了__radd__()方法,其次S1和S2是基于不同类的一个对象,再者,S1里必须不能实现__add__()方法,否则还是会优先执行左侧对象的__add__()方法。此处add方法为NotImplemented未实现,S1不写此方法也可以实现
C.增强赋值运算
此方法执行的是运算兼赋值的操作,即S1+=S2就相当于S1=S1.__iadd__(S2)
>>> class S1(str):
def __iadd__(self, other):
return len(self) + len(other)
>>> s1 = S1("Apple")
>>> s1 += s2
>>> s1
7
>>> type(s1)
<class 'int'>
若增强赋值运算符的左侧对象没有实现相应的方法,比如+=的左侧对象没有实现__iadd__()方法,那么Python就是采取使用相应的__add__()方法和__radd__()方法来替代
>>> class S2(str):
def __radd__(self, other):
return len(self) + len(other)
>>> s2 = S2("ri")
>>> s2 += s2
>>> s2
'riri'
>>> type(s2)
<class 'str'>
Attention:此处得到结果不是字符个数的原因--要成功调用__radd__()方法需要两者基于不同的类(此处S2和S2是相同类),故此处是去str父类中查找__add__()方法得到的就是字符串的拼接
D.其他运算
class ZH_INT:
def __init__(self, num):
self.num = num
def __int__(self):
try:
return int(self.num)
except ValueError:
zh = {"零":0, "一":1, "二":2, "三":3, "四":4, "五":5, "六":6, "七":7, "八":8, "九":9, "壹":1, "贰":2, "叁":3, "肆":4, "伍":5, "陆":6, "柒":7, "捌":8, "玖":9}
result = 0
for each in self.num:
if each in zh:
result += zh[each]
else:
result += int(each)
result *= 10
return result // 10
运行结果:
>>> n = ZH_INT("五二零1314")
>>> int(n)
5201314
__index__
>>> class C:
def __index__(self):
print("被拦截了")
return 3
>>> c = C()
>>> c[2]
Traceback (most recent call last):
File "<pyshell#35>", line 1, in <module>
c[2]
TypeError: 'C' object is not subscriptable
#当对象作为索引值或参数时才会触发index方法
>>> s = "FishC"
>>> s[c]
被拦截了
'h'
>>> bin(c)
被拦截了
'0b11'
#相当于把3作为参数传给bin(),得到的就是3对应的二进制0b11
E.位运算
常见的位运算有按位与、或、非、异或。前三者和and or not的执行逻辑其实是相似的,不过所应用的场景不一。前者为对两个整数进行位运算
a.按位与&
>>> 3 & 4
0
>>> 3 & 2
2
#bin()可以用于获得一个整数的二进制形式
>>> bin(2)
'0b10'
#0b表示一个二进制的字符串
>>> bin(3)
'0b11'
>>> bin(4)
'0b100'
此处&是按位进行与运算,只有当相同位的值均为1的情况其结果对应的位才是1。
b.按位或|
只要其中某个位的值是1,那么对应结果的二进制位也是1
>>> 3 | 2
3
>>> 3 | 4
7
c.按位非~
就是将每个二进制位进行取反,符号为“~”。涉及到补码的概念
>>> ~2
-3
>>> ~3
-4
>>> ~4
-5
d.按位异或^
当两个相同的二进制位的值不一样的时候,那么结果对应二进制的值为1
>>> 3 ^ 2
1
>>> 3 ^ 4
7
e.左、右移运算符<<、>>
运算符的左侧是运算对象,右侧是指定要移动的位数。可以记为右移n位就是除以n的2次方
>>> bin(8)
'0b1000'
>>> 8 >> 2
2
>>> 8 >> 3
1
>>> 8 << 2
32
>>> 8 // pow(2, 2)
2
#右移两位
>>> 8 * pow(2, 2)
32
#左移两位
#由于移位结果为整数,故此处为地板除。因为移位是会丢失数据的
>>> bin(9)
'0b1001'
>>> 9 >> 2
2
Attention:左移右移运算符右边表示移动多少位的这个数不能是负数,否则会出现ValueError异常
注意优先级
F.数学运算
math模块里有ulp()函数,用于表示对应浮点数的最低有效位
>>> 0.1 + 0.2 ==0.3
False
>>> import math
>>> 0.1 + 0.2 == 0.3 + math.ulp(0.3)
True
#math.ulp在3.9版本以上才有
13.属性访问
对象可以通过“.”进行属性访问,不仅可以访问已有的属性还可以新创建属性。
在Python中,有几个BIF()函数是专门为对象的属性访问服务的,分别是hasattr()、getattr()、setattr()、delattr()
A.属性访问函数
a.hasattr()
检测对象是否有name属性
>>> class C:
def __init__(self, name, age):
self.name = name
self.__age = age
>>> c = C("小甲鱼", 18)
>>> hasattr(c, "name")
True
b.getattr()
获取对象中的某个属性值
>>> getattr(c, "name")
'小甲鱼'
若要获取一个私有变量的值,需要了解name mangling的一个规则--名字改编技术
>>> getattr(c, "_C__age")
18
c.setattr()
用于设置对象中指定属性的值
>>> setattr(c, "_C__age", 19)
>>> getattr(c, "_C__age")
19
d.delattr()
相当于del语句
>>> delattr(c, "_C__age")
>>> hasattr(c, "_C__age")
False
B.魔法方法
b.__getattribute__()
与A-b对应
>>> class C:
def __init__(self, name, age):
self.name = name
self.__age = age
def __getattribute__(self, attrname):
print("拿来吧你")
return super().__getattribute__(attrname)
>>> c = C("小甲鱼", 18)
>>> getattr(c, "name")
拿来吧你
'小甲鱼'
>>> 拿来吧你
拿来吧你
c._C__age
拿来吧你
18
>>> c.FishC
拿来吧你
Traceback (most recent call last):
File "<pyshell#66>", line 1, in <module>
c.FishC
File "<pyshell#62>", line 7, in __getattribute__
return super().__getattribute__(attrname)
AttributeError: 'C' object has no attribute 'FishC'
b-2.__getattr__()
当用户试图去获取一个不存在的属性的时候才会被触发
>>> class C:
def __init__(self, name, age):
self.name = name
self.__age = age
def __getattribute__(self, attrname):
print("拿来吧你")
return super().__getattribute__(attrname)
def __getattr__(self, attrname):
if attrname == "FishC":
print("I love FishC")
else:
raise AttributeError(attrname)
此处设置了一个机关,尽管对象不存在叫FishC的属性名,但如果去访问也会有回复。其他情况抛出AttributeError异常
>>> c = C("小甲鱼", 18)
>>> c.FishC
拿来吧你
I love FishC
>>> c.x
拿来吧你
Traceback (most recent call last):
File "<pyshell#76>", line 1, in <module>
c.x
File "<pyshell#73>", line 12, in __getattr__
raise AttributeError(attrname)
AttributeError: x
同时当我们去访问一个不存在的属性,__getattribute__()方法会先响应,然后才会给到__getattr__()方法
c.__setattr__()
>>> class D:
def __setattr__(self, name, value):
self.__dict__[name] = value
#若用self.name = value,则会出现死循环,无限递归操作
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.name
'小甲鱼'
d.__delattr__()
>>> class D:
def __setattr__(self, name, value):
self.__dict__[name] = value
def __delattr__(self, name):
del self.__dict__[name]
>>> d = D()
>>> d.name = "小甲鱼"
>>> d.__dict__
{'name': '小甲鱼'}
>>> del d.name
>>> d.__dict__
{}
14.索引切片
A.__getitem__(self, index)
当对象被索引时调用。其既能响应单个下标的索引操作,又能支持代表范围的切片索引方式
>>> class C:
def __getitem__(self, index):
print(index)
>>> c = C()
>>> c[2]
2
>>> c[2:8]
slice(2, 8, None)
打印结果为slice()函数,其为BIF内置函数,切片操作相当于它的一个语法糖
>>> s = "I love FishC"
>>> s[2:6]
'love'
>>> s[slice(2, 6)]
'love'
>>> s[7:]
'FishC'
>>> s[slice(7, None)]
'FishC'
>>> s[::4]
'Ivi'
>>> s[slice(None, None, 4)]
'Ivi'
B.__setitem__()
为索引或切片赋值的操作时,就会被__setitem__()方法拦截
>>> class D:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
def __setitem__(self, index, value):
self.data[index] = value
>>> d = D([1, 2, 3, 4, 5])
>>> d[1]
2
>>> d[1] = 1
>>> d[1]
1
#切片操作
>>> d[2:4] = [2, 3]
>>> d[:]
[1, 1, 2, 3, 5]
__getitem__()不仅仅拦截了索引或切片操作,其实与“获取”相关的操作也会被拦截。比如在for循环中每次循环都会从可迭代对象中取出东西,这样操作可以触发__getitem__()方法
>>> class D:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index] * 2
>>> d = D([1, 2, 3, 4, 5])
>>> for i in d:
print(i, end=' ')
2 4 6 8 10
C.__iter__(self)和__next__(self)
针对可迭代对象。它们对应的BIF函数时iter()函数和next()函数,
根据Python迭代协议,若一个对象定义了__iter__()方法,那么它就是一个可迭代对象;若一个可迭代对象定义了__next__()方法,那么它就是一个迭代器。比如列表,是一个可迭代对象,不是迭代器,没有__next__()方法。
>>> x = [1,2]
#dir() 函数可以返回一个对象的所有属性和方法
>>> dir(x)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
当可迭代对象定义了__iter__()方法,只要调用此方法,就会得到一个相应的迭代器对象。故当我们使用迭代工具比如for循环语句对一个可迭代对象进行操作时
>>> for i in x:
print(i, end=' ')
1 2
#for语句的第一步操作是将对象传入内置函数iter()中并由此拿到一个迭代器
#第二步利用__next__()方法进行迭代操作
#模拟for实现
>>> _ = iter(x)
>>> while True:
try:
i = _.__next__()
except StopIteration:
#当迭代器到尽头时会抛出一个StopIteration异常
break
print(i, end=' ')
1 2
自主创造一个迭代器对象
>>> class Double:
def __init__(self, start, stop):
self.value = start - 1
self.stop = stop
def __iter__(self):
#返回一个迭代器。此处由于自己就是一个迭代器,所以直接返回就行
return self
def __next__(self):
if self.value == self.stop:
raise StopIteration
self.value += 1
return self.value * 2
>>> d = Double(1, 5)
>>> for i in d:
print(i, end=' ')
2 4 6 8 10
15.代偿
A.__contains__(self, item)
用于实现成员关系的检测。对应的运算符是in和not in
>>> class C:
def __init__(self, data):
self.data = data
def __contains__(self, item):
print("hi")
return item in self.data
>>> c = C([1, 2, 3, 4, 5])
>>> 3 in c
hi
True
>>> #3对应__contains__()里item参数,后它(item)是in self.data([1,...5])的话就会返回True;反之即为False
>>> 6 in c
hi
False
上节讲过的迭代,假若没有定义__iter__()和__next__()方法,那么当把对象放到迭代工具中--for循环,Python在找不到__iter__()和__next__()方法的情况下就会尝试去查找__getitem__()方法。此就称之为代偿
__contains__()也有相似的代偿方法。若没有实现此方法,当又使用了in和not in进行成员关系判断,那么Python就会尝试查找__iter__()和__next__()方法
>>> class C:
def __init__(self, data):
self.data = data
def __iter__(self):
print("Iter", end=' -> ')
self.i = 0
return self
def __next__(self):
print("Next", end=' -> ')
if self.i == len(self.data):
raise StopIteration
item = self.data[self.i]
self.i += 1
return item
>>> c = C([1, 2, 3, 4, 5])
>>> 3 in c
Iter -> Next -> Next -> Next -> True
>>> 6 in c
Iter -> Next -> Next -> Next -> Next -> Next -> Next -> False
若连iter和next都没有,Python就会去__getitem__()方法
>>> class C:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
print("Getitem", end=' -> ')
return self.data[index]
>>> c = C([1, 2, 3, 4, 5])
>>> 3 in c
Getitem -> Getitem -> Getitem -> True
>>> 6 in c
Getitem -> Getitem -> Getitem -> Getitem -> Getitem -> Getitem -> False
B.布尔测试
若遇到bool函数,Python会先寻找__bool__()方法
>>> class D:
def __bool__(self):
print("Bool")
return True
>>> d = D()
>>> bool(d)
Bool
True
若没有定义布尔方法,Python就回去找__len__()方法的定义。若有则此方法返回的值是非零True;否则是False
>>> class D:
def __init__(self, data):
self.data = data
def __len__(self):
print("Len")
return len(self.data)
>>> d = D("Fish")
>>> bool(d)
Len
True
>>> d = D(" ")
>>> bool(d)
Len
True
C.比较运算相关
a. < __It__(self, other) 拦截小于号
b. <= __le__(self, other)
c. > __gt__(self, other)
d. >= __ge__(self, other)
e. == __eq__(self, other)
f. != __ne__(self, other)
>>> class S(str):
def __lt__(self, other):
return len(self) < len(other)
def __gt__(self, other):
return len(self) > len(other)
def __eq__(self, other):
return len(self) == len(other)
>>> s1 = S("FishC")
>>> s2 = S("fishc")
>>> s1 < s2
False
>>> s1 > s2
False
>>> s1 == s2
True
>>> s1 != s2
True
通过__eq__()方法实现了对等值判断的拦截,当并不意味着等值判断就会自动取等值判断的相反结果。
若执行s1!=s2的判断,由于在类中没有定义,走的还是其父类,也就是字符串的一个传统比较路线--比较编码值。同理小于等于,比较的还是编码值
>>> s1 <= s2
True
>>> s2 >= s2
True
若不想让某个方法生效,可以直接将其赋值为None
>>> class S(str):
def __lt__(self, other):
return len(self) < len(other)
def __gt__(self, other):
return len(self) > len(other)
def __eq__(self, other):
return len(self) == len(other)
__le__ = None
__ge__ = None
__ne__ = None
>>> s1 = S("FishC")
>>> s2 = S("fishc")
>>> s1 != s2
Traceback (most recent call last):
File "<pyshell#172>", line 1, in <module>
s1 != s2
TypeError: 'NoneType' object is not callable
>>> class C:
def __init__(self, data):
self.data = data
def __iter__(self):
print("Iter", end=' -> ')
self.i = 0
return self
def __next__(self):
print("Next", end=' -> ')
if self.i == len(self.data):
raise StopIteration
item = self.data[self.i]
self.i += 1
return item
__contains__ = None
>>> c = C([1, 2, 3, 4, 5])
>>> 3 in c
Traceback (most recent call last):
File "<pyshell#178>", line 1, in <module>
3 in c
TypeError: 'C' object is not a container
#明确__contains__()方法赋值为None,即明确表示不希望关系判断出现。同样会影响到not in
16.__call__(self [ , args...])
Python可以像调用函数一样去调用函数,要求就是需要此对象的类去定义一个叫做__call__()方法
>>> class C:
def __call__(self):
print("hi")
>>> c = C()
>>> c()
hi
此方法支持位置参数和关键字参数
>>> class C:
def __call__(self, *args, **kwargs):
#一个*代表位置参数,两个*代表关键字参数
print(f"位置参数 -> {args}\n关键字参数 -> {kwargs}")
>>> c = C()
>>> c(1, 2, 3, x = 250, y = 520)
位置参数 -> (1, 2, 3)
关键字参数 -> {'x': 250, 'y': 520}
可以实现闭包函数。生成一个工厂函数,计算平方、立方...
>>> class Power:
def __init__(self, exp):
self.exp = exp
def __call__(self, base):
return base ** self.exp
>>> square = Power(2)
>>> square(2)
4
>>> cube = Power(3)
>>> cube(2)
8
17.字符串相关
__str__(self)和__repr__(self)
str响应的是str内置函数的方法;repr对应内置函数repr()。str()函数是将参数转换为字符串对象,给人看;repr()函数则是将对象转换为程序可执行的字符串,给程序看
>>> str(123)
'123'
>>> repr(123)
'123'
>>> str("FishC")
'FishC'
>>> repr("FishC")
"'FishC'"
谈及两者的区别,先讲解eval()函数,作用为将参数去引号后执行。
>>> eval("1+2")
3
故当我们把str()函数得到结果作为eval()函数的一个参数的话会报错,因为参数去引号的结果是裸FishC,Python会将其作为变量名来引用,结果就是找不到FishC的定义;若使用的是repr()函数则正常
>>> eval(str("FishC"))
Traceback (most recent call last):
File "<pyshell#210>", line 1, in <module>
eval(str("FishC"))
File "<string>", line 1, in <module>
NameError: name 'FishC' is not defined
>>> eval(repr("FishC"))
'FishC'
对此,也有称eval()函数是repr()函数的反函数,因为repr()函数返回的字符串作为参数传递给eval()函数的话,所得到的结果必定是repr()函数的参数
>>> eval(repr(12))
12
回到本节讲解的方法,首先这两个方法必须是返回字符串的类型,其次__repr__()方法可以对__str__()方法进行代偿,即若只定义了repr方法,调用str方法也可以被响应;反过来则不行
>>> class C:
def __repr__(self):
return "I love FishC"
>>> c = C()
>>> repr(c)
'I love FishC'
>>> str(c)
'I love FishC'
>>> class C:
def __str__(self):
return "I love FishC"
>>> c = C()
>>> str(c)
'I love FishC'
>>> repr(c)
'<__main__.C object at 0x000002046BEF3BB0>'
Attention:__str__()方法定义的只能应用于对象出现在打印操作的顶层,即若我们把多个对象放到一个列表中,然后去把这个列表打印出来的话,我们就无法去访问到这个对应的字符串了
>>> cs = [C(), C(),C()]
>>> for each in cs:
print(each)
I love FishC
I love FishC
I love FishC
>>> print(cs)
[<__main__.C object at 0x000002046BEF38B0>, <__main__.C object at 0x000002046BEF3F70>, <__main__.C object at 0x000002046BEF3CD0>]
#用repr方法
>>> class C:
def __repr__(self):
return "I love FishC"
>>> cs = [C(), C(),C()]
>>> for each in cs:
print(each)
I love FishC
I love FishC
I love FishC
>>> print(cs)
[I love FishC, I love FishC, I love FishC]
综上述可以看出repr有多个使用场景。
通过同时定义两个方法的实现,可以让对象在不同场景下支持不同的显示效果
>>> class C:
def __init__(self, data):
self.data = data
def __str__(self):
return f"data = {self.data}"
def __repr__(self):
return f"C({self.data})"
def __add__(self, other):
self.data += other
>>> c = C(250)
>>> print(c)
data = 250
>>> c
C(250)
>>> c + 250
>>> print(c)
data = 500
>>> c
C(500)
18.property()
class property(fget=None, fest= None, fdel=None, doc=None)
虽然我们习惯叫它“函数”,但其实跟int()、str()、float()函数一样,是一个“Built-in Class”
property()函数用于返回一个property属性对象
>>> class C:
def __init__(self):
self._x = 250
#变量_表示有意将变量隐藏使用
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = property(getx, setx, delx)
>>> c = C()
>>> #没有下横线的x在此处全权代理了_x。通过对x的访问和修改,都会影响到_x的值
>>> c.x
250
>>> c.x = 520
>>> c.__dict__
{'_x': 520}
>>> del c.x
>>> c.__dict__
{}
使用__getattr__、__setattr__、__delattr__也可以实现相同效果,只是会代码麻烦。此是property函数的第一优点
>>> class D:
def __init__(self):
self._x = 250
def __getattr__(self, name):
if name == 'x':
return self._x
else:
super().getattr__(name)
def __setattr__(self, name, value):
if name == 'x':
super().__setattr__('_x', value)
else:
super().__setattr__(name, value)
def __delattr__(self, name):
if name == 'x':
super().__delattr__('_x')
else:
super().__delattr__(name)
>>> d = D()
>>> d.x
250
>>> d.x = 520
>>> d.__dict__
{'_x': 520}
>>> del d.x
>>> d.__dict__
{}
property()函数的前三个参数都是函数,即是传入一个函数作为它的参数。装饰器实现的原理也是通过传入函数参数来实现,这也是property()函数最经典的应用了 --若我们将property()函数的参数作为装饰器来使用回让创建只读属性的工作变得极为简单
>>> class E:
def __init__(self):
self._x = 250
@property
def x(self):
return self._x
>>> e = E()
>>> e.x
250
>>> e.x = 520
Traceback (most recent call last):
File "<pyshell#320>", line 1, in <module>
e.x = 520
AttributeError: can’t set attribute
#装饰器其实是一个语法糖,简化为下
>>> class E:
def __init__(self):
self._x = 250
def x(self):
return self._x
x = property(x)
因为此处只赋值了property()的第一个参数--用于实现获取的fget参数,另外两个值都采用默认值None,表示不支持写入和删除,故会报错,同时删除也会出现报错
property属性对象提供了getter、setter和deleter三个方法,这三个方法对应的就是property()函数的三个参数接口
>>> class E:
def __init__(self):
self._x = 250
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
>>> e = E()
>>> e.x
250
>>> e.x = 520
>>> e.__dict__
{'_x': 520}
>>> del e.x
>>> e.__dict__
{}
实现效果和本节第一个例子一样
19.类方法
在类里面的方法称之为方法,但是通常定义在类里的方法类自己是无法去直接调用的,需要通过实例对象去操作,究其根本是因为方法需要对象来绑定
接下来讲解的是类方法,即专门用于绑定类的方法
@classmethod装饰器
>>> class C:
def funA(self):
print(self)
@classmethod
def funB(cls):
print(cls)
>>> c = C()
>>> c.funA()
<__main__.C object at 0x000002046BE931C0>
>>> #绑定的是一个对象
>>> c.funB()
<class '__main__.C'>
#类方法
#此处无论是self参数还是cls参数都是约定俗成的方法,是可以使用其他参数名来代替的
类方法的特点是绑定的是类,而非实例对象。编程有时候对象多起来,可能就需要程序能够统计对象的数量或者说创建一个列表来维护一个类对应的所有对象,对于这种类型的需要就可以使用类方法来实现
>>> class C:
count = 0
def __init__(self):
C.count += 1
@classmethod
def get_count(cls):
print(f"一共实例化了 {cls.count} 个对象")
>>> c1 = C()
>>> c2 = C()
>>> c3 = C()
>>> c3.get_count()
一共实例化了 3 个对象
>>> c2.get_count()
一共实例化了 3 个对象
若在对象中去创建一个跟类属性同名的实例属性,那么后者就会覆盖类属性。不过get_count()是类方法,故就算实例属性覆盖了类属性问题也不大
>>> c3.count = 1
>>> c3.get_count()
一共实例化了 3 个对象
此处直接使用类名来访问类属性也是可以的,但若涉及到继承问题,那么使用类方法无疑会有更大的优势
20.静态方法
指的是放在类里的函数,跟普通函数的区别在于可以放到类里。通常说类里的函数叫方法,是因为方法需要跟对象进行绑定,但函数没有绑定的操作,而静态方法就是可以做到在类里去定义一个不需要绑定的函数
需要使用装饰器@staticmethod
>>> class C:
@staticmethod
def funC():
print("I love FishC")
>>> c = C()
>>> c.funC()
I love FishC
#使用对象c访问静态方法
>>> C.funC()
I love FishC
#直接通过类名访问静态方法
使用静态方法效法类方法来做一个统计实例对象数量的demo
>>> class C:
count = 0
def __init__(self):
C.count += 1
@staticmethod
def get_count():
print(f"一共实例化了 {C.count} 个对象")
>>> c1 = C()
>>> c2 = C()
>>> c3 = C()
>>> c3.get_count()
一共实例化了 3 个对象
#使用静态方法不必担心对象覆盖属性的问题,因为是点名C.count去获取类的属性
但操作不涉及类属性或者实例属性引用时,使用静态方法更合适
但若想统计实例对象数量的任务交给类方法会更好
>>> class C:
count = 0
@classmethod
def add(cls):
cls.count += 1
#相当于对应的类属性的count+=1
def __init__(self):
self.add()
@classmethod
def get_count(cls):
print(f"一共实例化了 {cls.count} 个对象")
>>> class D(C):
count = 0
>>> class E(C):
count = 0
>>> c1 = C()
>>> d1, d2 = D(), D()
>>> e1, e2, e3 = E(), E(), E()
>>> c1.get_count()
一共实例化了 1 个对象
>>> d1.get_count()
一共实例化了 2 个对象
>>> e1.get_count()
一共实例化了 3 个对象
当实例化对象发生时,就会去调用对象的add()方法,由于add()方法是一个类方法,会自动将对应的类传递进去
21.描述符
前面讲述的property()函数、类方法、静态方法背后的实现技术都是依赖于描述符的
描述符协议:
只要实现__get__(self, instance, owner=None)、__set(self, instance, value)__、__delete__(self, instance)三个中任何一个或多个方法的类,那么这个类就叫做描述符。这三个功能分别是用于拦截对象属性的读取、写入和删除的操作,但是管的是其他属性
>>> class D:
def __get__(self, instance, owner):
print(f"get~\nself -> {self}\ninstance -> {instance}\nowner -> {owner}")
def __set__(self, instance, owner):
print(f"get~\nself -> {self}\ninstance -> {instance}\nowner -> {owner}")
def __delete__(self, instance):
print(f"get~\nself -> {self}\ninstance -> {instance}")
>>> class C:
x = D()
>>> c = C()
>>> c.x = 250
get~
self -> <__main__.D object at 0x000002046BEF9D90>
instance -> <__main__.C object at 0x000002046BEF93A0>
owner -> 250
>>> c.x
get~
self -> <__main__.D object at 0x000002046BEF9D90>
instance -> <__main__.C object at 0x000002046BEF93A0>
owner -> <class '__main__.C'>
>>> del c.x
get~
self -> <__main__.D object at 0x000002046BEF9D90>
instance -> <__main__.C object at 0x000002046BEF93A0>
self参数对应的即是描述符这个类的实例对象,相当于x属性的值
instance参数对应的是被描述符拦截的属性所在的类的实例对象
owner参数对应的是被描述符拦截的属性所在的类
改编property()节讲解的第一个代码,用描述符展示
>>> class D:
def __get__(self, instance, owner):
return instance._x
def __set__(self, instance, value):
instance._x = value
def __delete__(self, instance):
del instance._x
>>> class C:
def __init__(self, x=250):
self._x = x
x = D()
>>> c = C()
>>> c.x
250
>>> c.x = 520
>>> c.__dict__
{'_x': 520}
>>> del c.x
>>> c.__dict__
{}
但对比property()函数的代码,不建议此种方法实现,此代码为在类D中访问类C的内部私有属性,不是一个合理的办法
先有描述符再有property属性。下讲述如何利用描述符去造一个property()函数
>>> class MyProperty():
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, instance, owner):
return self.fget(instance)
def __set__(self, instance, value):
self.fset(instance, value)
def __delete__(self, instance):
self.fdel(instance)
>>> class C:
def __init__(self):
self._x = 250
def getx(self):
return self._x
def setx(self, value):
self._x = value
def delx(self):
del self._x
x = MyProperty(getx, setx, delx)
#描述符。可以通过x来管理私有的_x
>>> c = C()
>>> c.x
250
>>> c.x = 520
>>> c.__dict__
{'_x': 520}
>>> del c.x
>>> c.__dict__
{}
>>>
property()函数还可以当装饰器使用,getter()、setter()、deleter()三个方法。现用自己定义的来实现
>>> class MyProperty():
def __init__(self, fget=None, fset=None, fdel=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
def __get__(self, instance, owner):
return self.fget(instance)
def __set__(self, instance, value):
self.fset(instance, value)
def __delete__(self, instance):
self.fdel(instance)
def getter(self, func):
self.fget = func
return self
def setter(self, func):
self.fset = func
return self
def deleter(self, func):
self.fdel = func
return self
>>> class D:
def __init__(self):
self._x = 250
@MyProperty
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
>>> d = D()
>>> d.x
250
>>> d.x = 520
>>> d.__dict__
{'_x': 520}
>>> del d.x
>>> d.__dict__
{}
代码也可以更改为
>>> class E:
def __init__(self):
self._x = 250
x = MyProperty()
@x.getter
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
@x.deleter
def x(self):
del self._x
>>> e = E()
>>> e.x
250
>>> e.x = 520
>>> e.__dict__
{'_x': 520}
>>> del e.x
>>> e.__dict__
{}
对象方法绑定、静态方法、类方法、类属性__slots__也是基于描述符协议来实现的
描述符被编写为独立的类,后将描述符的实例对象赋值给类属性,从而实现对该属性读取、写入和删除的全方位拦截
>>> class D:
def __get__(sekf, instance, owner):
print("get~")
#打印ge就说明拦截成功
>>> class C:
def __init__(self):
self.x = D()
>>> c = C()
>>> c.x
<__main__.D object at 0x000001236E1ED280>
描述符只能用于类属性,但这里用在了对象属性了故无法拦截而是打印对象。要想描述符发挥作用,代码更改为
>>> class C:
x = D()
>>> c = C()
>>> c.x
get~
B.数据描述符与非数据描述符
主要根据所实现的方法不同而划分。若实现__set__()和__delete__()就是数据描述符;若单单实现了__get__()就是非数据描述符
当发生属性访问时,优先级不同。优先级从高到低分别时:
数据描述符-->实例对象属性-->非数据描述符-->类属性
实例对象属性--放在对象的__dict__字典里的属性
类还需考虑到继承,MRO顺序去查找对应的属性
对此上述的代码为非数据描述符,若通过给实例对象的同名属性赋值那么它就会覆盖掉描述符的拦截
>>> c.x = "FishC"
>>> c.x
'FishC'
#描述符拦截失效,取而代之的是”FishC"
若仍然使用类名访问的话描述符还是可以被拦截。只是对象c被覆盖
>>> C.x
get~
若用数据描述符
>>> class D:
def __get__(sekf, instance, owner):
print("get~")
def __set__(self,insance, value):
print("set~")
>>> class C:
x = D()
>>> c = C()
>>> c.x
get~
>>> c.x = "FishC"
set~
>>> c.x
get~
>>> c.__dict__
{}
#因为被数据描述符给拦截了,没有成功写入
直接赋值行不通,间接赋值也无法实现
>>> c.__dict__['x'] = "FishC"
>>> c.__dict__
{'x': 'FishC'}
>>> c.x
get~
优先级是定义在__getattribute__()方法的实现,其管理的是属性的获取。所以优先级其实就是魔法方法的默认实现逻辑
>>> class C:
x = D()
def __getattribute__(self, name):
print("aha")
>>> c = C()
>>> c.x
aha
至此介绍了描述符的三个方法--__get__()、__set__()、__delete__(),下介绍第四个方法
__set_name__(self, name, owner)
先前讲解的描述符都是对属性的拦截。在实际开发中,还需要去执行相应的访问、赋值或删除操作。如通过描述符拦截了对象的x属性进行了合法性检验,若符合要求仍然需把它写入实例对象的属性中,即需再描述符里去操作实例对象的__dict__字典,这个过程需要instance参数,instance参数代表的即是描述符所拦截的属性所在的实例对象,因此代码可写为
>>> class D:
def __init__(self, name):
self.name = name
#name作用为去记住这个描述符拦截的属性名,通过参数传递进去
def __get__(self, instance, owner):
#instance参数为描述符所管理的属性所在的实例的对象
print("get")
#打印get表示被get方法拦截
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
print("set")
instance.__dict__[self.name] = value
>>> class C:
x = D("x")
>>> c = C()
>>> c.x
get
>>> c.__dict__
{}
>>> c.x = 520
set
>>> c.__dict__
{'x': 520}
>>> c.x
get
520
#通过描述符间接把数据写到对象的__dict__()字典里
#成功访问了对象的__dict__字典的键值对
但是x=D("x")写的不合理,对此诞生描述符第四种方法。代码改写
>>> class D:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
print("get")
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
print("set")
instance.__dict__[self.name] = value
>>> class C:
x = D()
>>> c = C()
>>> c.x
get
>>> c.__dict__
{}
>>> c.x = 520
set
>>> c.__dict__
{'x': 520}
>>> c.x
get
520
C.函数、方法、静态方法、类方法的底层逻辑
函数和方法其实是一个东西。实现了对象绑定的函数叫做方法,没有和对象绑定的就是普通的函数
>>> class D:
def __get__(self, instance, owner):
if instance is None:
print("函数")
else:
print("方法")
>>> class C:
x = D()
>>> c = C()
>>> c.x
方法
>>> C.x
函数
静态方法和普通方法的区别就是可以放到类里,不需要绑定类和对象,是一个自由的放在类里的函数
补:type()若传入一个对象的话,是返回该对象所属的类
>>> class C:
pass
>>> c = C()
>>> type(c) is C
True
类方法--加了hasattr()条件分支,其原因是为了让类方法和其他装饰器可以串联起来一起使用。
>>> class C:
@classmethod
def __doc__(cls):
return f"I love FishC {cls.__name__}"
>>> c = C()
>>> c.__doc__ ()
'I love FishC C'
>>> C.__doc__()
'I love FishC C'
若想添加属性访问的方式,即希望不使用访问函数的形式,通过属性的形式得到相同的结果,这也是property的一个功能之一
>>> class D:
@classmethod
@property
def __doc__(cls):
return f"I love FishC {cls.__name__}"
>>> d = D()
>>> d.__doc__
<bound method ? of <class '__main__.D'>>
>>> D.__doc__
<bound method ? of <class '__main__.D'>>
接下讲述为什么需要hasattr分支,是什么原理使得MethodType可以对@property装饰器进行修改和干扰
>>> class MethodType:
def __init__(self, func, obj):
self.__func__ = func
self.__self__ = obj
def __call__(self, *args, **kwargs):
func = self.__func__
obj = self.__self__
print("小白")
return func(obj, *args, **kwargs)
>>> class ClassMethod:
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
if cls is None:
print("旺财")
if hasattr(type(self.f), '__get__'):
print(f"来福,type(self.f) -> {type(self.f)}")
return self.f.__get__(cls, cls)
return MethodType(self.f, cls)
>>> class D:
@ClassMethod
@property
def __doc__(cls):
return f"I love FishC {cls.__name__}"
>>> d = D()
>>> d.__doc__
来福,type(self.f) -> <class 'property'>
'I love FishC D'
如果是多个装饰器串联的话,那么type(self.f)也就是传入这个参数的类,就是property;当多个装饰器装饰同一个对象时,它们的执行顺序是自下往上的
故@property先对__doc__()进行了改造,相当于“__doc__=property(___doc__),把它变成了一个property对象,__doc__其实就是一个property实例对象
后到@ClassMethod装饰器,由于存在hasattr分支,type(self.f)的结果就是property类,property类有__get__方法(property就是靠此拦截属性访问),此处对__get__方法进行改造。self.f现在是一个property对象,此处self.f.__get__(cls,cls)调用的是property类里的__get__()方法,且再本是instance参数的位置(也就是指向对象的参数值)被替换成了指向类cls---hasattr实现原理
若更改代码,依然是"来福”。Python万物皆对象,函数也是对象,此处type(self.f)拿到的是一个<class 'function' >类
>>> class D:
@ClassMethod
def __doc__(cls):
return f"I love FishC {cls.__name__}"
>>> d = D()
>>> d.__doc__()
来福,type(self.f) -> <class 'function'>
'I love FishC D'
D.类装饰器
前面说过装饰器可以拦截函数的调用。其实装饰器可以作用到类上
>>> def report(cls):
def oncall(*args, **kwargs):
print("开始实例化对象")
_ = cls(*args, **kwargs)
print("实例化完成")
return _
return oncall
>>> @report
class C:
pass
>>> c = C()
开始实例化对象
实例化完成
类装饰器和之前谈到的装饰在函数上的装饰器用法相似
类里添加构造函数
>>> @report
class C:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
print("构造函数被调用了")
>>> c = C(1, 2, 3)
开始实例化对象
构造函数被调用了
实例化完成
可以看出先被装饰器调用了,后实例化对象再返回
分析:@report此处是一个语法糖,相当于“C=report(C)";c=C(1,2,3,)相当于”oncall(1,2,3)",去调用oncall函数,传入参数(1,2,3)。
在oncall函数,由于形参是收集参数,所以(1,2,3)被打包成元组的形式,存放到了args参数里,后打印“开始实例化",接着调用闭包函数外层传递进来的cls参数,此时才是真正实例化对象的时刻,而刚刚的args是收集参数,所以拿到的是一个打包好的元组【args->(1,2,3)】,将多个参数打包成一个元组【收集】。
不能直接将元组作为参数给进行实例化对象,因为class C:的构造函数现需分别给到x,y,z三个形参,所以需要*对元组进行解包【_=cls(*args...】。实例化对象调用类的构造函数打印”被调用了",此时还在oncall()函数体内,需要把类C的实例对象传递出去,给回到c,故这里动用了临时变量“_”,最后通过return语句将临时变量返回,c成功拿到了类C的实例化对象
类装饰器的作用就是在类被实例化对象之前对其进行拦截和干预。上述展现的是类装饰器在类上的实现效果,现讲解类做装饰器,用其来装饰函数
>>> class Counter:
def __init__(self):
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
#当对象被当作函数,加个小括号去调用时就回去访问__call__()方法
print(f"已经被调用了{self.count}次")
>>> c = Counter()
>>> c()
已经被调用了1次
>>> #当对象被像函数一样去调用时就回去统计调用次数
>>> c()
已经被调用了2次
>>> c()
已经被调用了3次
若想把类当作装饰器去使用,计划是用其装饰指定的函数,统计函数被调用的次数
>>> class Counter:
def __init__(self, func):
#要修改一个函数,需在装饰器生效(实例化对象时,因为现在装饰器是一个类)时获取函数
self.count = 0
self.func = func
def __call__(self, *args, **kwargs):
self.count += 1
print(f"已经被调用了{self.count}次")
return self.func(*args, **kwargs)
>>> @Counter
def say_hi():
print("hi")
分析:@counter相当于“say_hi=Counter(say_hi),装饰器就相当于实例化对象,并将对象的名字叫做say_hi,同时将say_hi()函数传递给Counter的构造函数,所以返回的say_hi()函数已经被掉包成立Counter的实例化对象
>>> say_hi <__main__.Counter object at 0x000001BE124D1880>
因此当在调用say_hi时,其实是将Counter对象当作函数来调用,从而触发了__call__()方法
>>> say_hi() 已经被调用了1次 hi >>> say_hi() 已经被调用了2次 hi
小试一下
>>> class Check:
def __init__(self, cls):
self.cls = cls
def __call__(self, *args, **kwargs):
self.obj = self.cls(*args, **kwargs)
return self
def __getattr__(self, name):
print(f"正在访问{name}...")
return getattr(self.obj, name)
>>> @Check
class C:
def say_hi(self):
print("hi")
def say_hey(self):
print("hei")
>>> c = C()
>>> c.say_hi()
正在访问say_hi...
hi
>>> c.say_hey()
正在访问say_hey...
hei
若把class C改写如下
>>> @Check
class C:
def __init__(self, name):
self.name = name
def say_hi(self):
print(f"hi{self.name}")
def say_hey(self):
print(f"hei{self.name}")
>>> c1 = C("c1")
>>> c2 = C("c2")
>>> c1.name
正在访问name...
'c2'
>>> c2.name
正在访问name...
'c2'
>>> c1.say_hi()
正在访问say_hi...
hic2
>>> c2.say_hey()
正在访问say_hey...
heic2
发现c1的name属性被c2的给覆盖了。下面详细进行分析
类C被@Check类装饰器给处理了,于是c1和c2看着虽然像是类C的实例对象,但其实是Check类的对象
>>> c1
<__main__.Check object at 0x000001BE124D1F70>
>>> c2
<__main__.Check object at 0x000001BE124D1F70>
所以c1和c2两个字符串参数其实传递给了Check类的__call__()方法。看着像是实例化对象,但其实是调用早已实例好的对象,因为对象是在@Check时就诞生了。
class C:反而是在__call__()方法的第一个语句完成了实例化,并将实例化对象传递给了self.obj属性,但是并不是返回self.obj,返回的是self,即Check类的对象自身,而非类C的对象。因此在访问c1.name和c2.name时实际上访问的是Check类实例对象的name属性,但Check没有name属性,它就会去试图查找__getattr__(self,name)方法【当去访问一个对象并没有存在的属性时才会被触发】。恰好这个方法有定义--打印字符串+调用getattr(self.obj,name)函数,获取self.obj的name属性。self.obj保存的是类C的实例化对象。
Check只实例化一个对象,是在做装饰器时,仅仅被实例化了一次。而c1和c2只是把对象当函数调用了两次,从而访问了两次相应的__call__()方法。由于调用两次__call__()方法,所以self.obj第一次保存的时name为c1的类C实例化对象,而第二次调用则被name为c2的类实例化对象所覆盖。
解决这种bug,在装饰器的外面再套上一层
>>> def report(cls):
class Check:
def __init__(self, *args, **kwargs):
self.obj = cls(*args, **kwargs)
#实例化对象
def __getattr__(self, name):
print(f"正在访问{name}...")
return getattr(self.obj, name)
return Check
>>> @report
class C:
def __init__(self, name):
self.name = name
def say_hi(self):
print(f"hi{self.name}")
def say_hey(self):
print(f"hei{self.name}")
>>> c1 = C("c1")
>>> c2 = C("c2")
>>> c2.name
正在访问name...
'c2'
>>> c1.name
正在访问name...
'c1'
>>> c1.say_hi()
正在访问say_hi...
hic1
被@report装饰过class C:其实时被替换为class Check:。因为report()它返回的时Check类。当在执行”c1=C("c1")..."两句话时相当于实例化Check类,实例化check类就会调用它的构造函数。
Check类构造函数就是去做实例化@report装饰器装饰过的这个类,即classC:,然后把实例化对象保存在self.obj属性中。
此处Check类总共被实例化了两次,不会出现覆盖的bug
22.type()
A.第一种
判断数据类型。但不推荐试验type()函数来检测对象类型,而是isinstance()更为合适,因为isinstance()函数会考虑到子类的情况
>>> isinstance(3.14, float)
True
>>> isinstance(3.14, (float, str, int))
True
type函数返回的是对象对应的类,由此便有
>>> type(250)("520")
520
>>> int("520")
520
>>> type({}).fromkeys("Python")
{'P': None, 'y': None, 't': None, 'h': None, 'o': None, 'n': None}
甚至可以返回自定义的类
>>> class C:
def __init__(self, x):
self.x = x
>>> c = C(520)
>>> c
<__main__.C object at 0x000002691CBA3520>
>>> d = type(c)(520)
>>> d
<__main__.C object at 0x000002691CBB8D90>
看一个对象的类型也可以使用class属性,返回结果会相对简洁点
>>> c.__class__
<class '__main__.C'>
生成对象的类自身也是对象,是由type()生成而来的对象,所以把classC传入type()结果为type。type也是它自己的对象
>>> type(C)
<class 'type'>
>>> type(type)
<class 'type'>
type()函数会根据参数不同有两种不同的用法。其一是根据object参数类型返回值为type对象,通常与object.__class__所返回的对象相同;其二是根据传入的三个参数,返回一个新的type对象
B.第二种
第二种用法 class type(name,bases,dict,**kwds)
参数 | 含义 |
name | 指定将要创造的类的名字 |
bases | 指定将要创造的类的父类 |
dict | 指定将要创造的类的属性和方法 |
kwds | (可选)收集参数,当且仅当需要时,该收集参数将被传递给适当的元类机制(通常为__init_subclass__()) |
>>> class C:
pass
>>> C = type('C', (), {})
#跟上面的C等价
>>> c = C()
>>> c.__class__
<class '__main__.C'>
>>> C.__bases__
(<class 'object'>,)
#查看父类是什么
#指定父类
>>> D = type('D', (C,), {})
#第二个参数需要元组类型,加“,”
>>> D.__bases__
(<class '__main__.C'>,)
指定类属性和方法,要求类型为字典的形式
#属性
>>> E = type('E', (), dict(x=250, y=520))
>>> E.x
250
>>> E.y
520
#函数
>>> def funC(self, name="FishC"):
print(f"Hello {name}")
>>> F = type('F', (), dict(say_hi=funC))
>>> f = F()
>>> f.say_hi()
Hello FishC
>>> f.say_hi("小甲鱼")
Hello 小甲鱼
__init_subclass__()
Python3.6后添加的类方法,作用为加强父类对子类的管理,
>>> class C:
def __init_subclass__(cls):
print("父爱如山")
cls.x = 520
>>> class D(C):
x = 250
父爱如山
>>> D.x
520
#实现原理相当于前面讲解的类装饰器,在类定义完之后,它被拦截了,子类的属性才会被覆盖
__init_subclass__方法除第一个参数需要传递类之外,还可以传入其他参数
>>> class C:
def __init_subclass__(cls, value):
print("父爱如山")
cls.x = value
>>> class D(C, value=520):
x = 250
父爱如山
>>> D.x
520
当使用type()函数来构造像classD这种继承了定义过__init_subclass__()父类时,若需要给__init_subclass__()传递参数(比如此处需要传递value参数),就可以通过第四个参数进行接力
>>> D = type("D", (C,), dict(x=250), value=520)
父爱如山
>>> D.x
520
此处参数为收集参数,可以支持多个
>>> class C:
def __init_subclass__(cls, value1, value2):
print("父爱如山")
cls.x = value1
cls.y = value2
>>> D = type("D", (C,), dict(x=250), value1=520, value2=555)
父爱如山
>>> D.x
520
>>> D.y
555
23.元类
通俗来说,元类就是创造类的模板。type本身就是一个元类。元类也存在继承关系,所有的元类都继承自type
A.创建元类
>>> class MetaC(type):
pass
#若想通过元类创建类,要使用关键字metaclass引入刚刚创建好的元类
>>> class C(metaclass=MetaC):
pass
>>> c = C()
>>> type(c)
<class '__main__.C'>
>>> type(C)
<class '__main__.MetaC'>
>>> type(MetaC)
<class 'type'>
对象被创建是会调用__init__()方法,故__init__()又叫做构造函数。但__init__()并非实例化对象调用的第一个方法,第一个应该是__new__()方法
需要父类来实现
>>> class MetaC(type):
def __new__(mcls, name, bases, attrs):
print("__new__() in MetaC~")
return type.__new__(mcls, name, bases, attrs)
def __init__(cls, name, bases, attrs):
print("__init__() in MetaC~")
type.__init__(cls, name, bases, attrs)
>>> class C(metaclass=MetaC):
def __new__(cls):
print("__new__() in C~")
return super().__new__(cls)
def __init__(self):
print("__init__() in C~")
__new__() in MetaC~
__init__() in MetaC~
>>> c = C()
__new__() in C~
__init__() in C~
元类MetaC里的__new__()方法是类C定义完成的那一刻被触发的;而类C的__new__()方法是在类实例化对象才被触发。触发时间点、触发参数不一
类C的super.__new__()调用的是object的new。当一个类没有指定父类时,就回去找object,因为object默认是所有类的父类。尽管如此,父类跟子类仍然是属于同一级别的物种,而元类则是比类更高一个级别
接着讨论元类MetaC的__new__()和__init__()方法拿到的参数
>>> class MetaC(type):
def __new__(mcls, name, bases, attrs):
print("__new__() in MetaC~")
print(f"mcls={mcls}, name={name}, bases={bases}, attrs={attrs}")
return type.__new__(mcls, name, bases, attrs)
def __init__(cls, name, bases, attrs):
print("__init__() in MetaC~")
print(f"cls={cls}, name={name}, bases={bases}, attrs={attrs}")
type.__init__(cls, name, bases, attrs)
>>> class C(metaclass=MetaC):
def __new__(cls):
print("__new__() in C~")
return super().__new__(cls)
def __init__(self):
print("__init__() in C~")
__new__() in MetaC~
mcls=<class '__main__.MetaC'>, name=C, bases=(), attrs={'__module__': '__main__', '__qualname__': 'C', '__new__': <function C.__new__ at 0x000002691CC85A60>, '__init__': <function C.__init__ at 0x000002691CC85AF0>, '__classcell__': <cell at 0x000002691CC75730: empty>}
__init__() in MetaC~
cls=<class '__main__.C'>, name=C, bases=(), attrs={'__module__': '__main__', '__qualname__': 'C', '__new__': <function C.__new__ at 0x000002691CC85A60>, '__init__': <function C.__init__ at 0x000002691CC85AF0>, '__classcell__': <cell at 0x000002691CC75730: MetaC object at 0x000002691C956980>}
在new里的mcls拿到的是MetaC,而在__init__里拿到的是__main__.C
接着讲解__call__()方法。在类里__call__()方法就是拦截对象被当作函数调用时候的操作,若把__call__()方法定义到元类中,拦截类实例化对象的操作
>>> class MetaC(type):
def __call__(cls, *args, **kwargs):
print("__call__() in MetaC~")
>>> class C(metaclass=MetaC):
pass
>>> c = C()
__call__() in MetaC~
B.元类应用
a.给所有类添加一个属性
>>> class MetaC(type):
def __new__(mcls, name, bases, attrs):
attrs["author"] = "FishC"
return type.__new__(mcls, name, bases, attrs)
>>> class C(metaclass=MetaC):
pass
>>> class D(metaclass=MetaC):
pass
>>> c = C()
>>> d = D()
>>> c.author
'FishC'
>>> d.author
'FishC'
>>> class MetaC(type):
def __init__(cls, name, bases, attrs):
cls.author = "FishC"
return type.__init__(cls, name, bases, attrs)
>>> class C(metaclass=MetaC):
pass
>>> class D(metaclass=MetaC):
pass
>>> c = C()
>>> d = D()
>>> c.author
'FishC'
>>> d.author
'FishC'
b.对类名的定义规范做限制
比如支持类名只支持大写字母开头
>>> class MetaC(type):
def __init__(cls, name, bases, attrs):
if not name.istitle():
raise TypeError("类名必须大写字母开头")
type.__init__(cls, name, bases, attrs)
>>> class mycls(metaclass=MetaC):
pass
Traceback (most recent call last):
File "<pyshell#131>", line 1, in <module>
class mycls(metaclass=MetaC):
File "<pyshell#128>", line 4, in __init__
raise TypeError("类名必须大写字母开头")
TypeError: 类名必须大写字母开头
c.修改对象的属性值
如把对象的所有字符串属性值都变成大写
>>> class MetaC(type):
def __call__(cls, *args, **kwargs):
new_args = [each.upper() for each in args if isinstance(each, str)]
return type.__call__(cls, *new_args, **kwargs)
>>> class C(metaclass=MetaC):
def __init__(self, name):
self.name = name
>>> c = C("FishC")
>>> c.name
'FISHC'
d.限制类实例化时的传参方式
如要求类在实例化对象时只能够通过关键字参数进行传参,此时若是位置参数会报错
>>> class MetaC(type):
def __call__(cls, *args, **kwargs):
if args:
raise TypeError("仅支持关键字参数~")
return type.__call__(cls, *args, **kwargs)
>>> class C(metaclass=MetaC):
def __init__(self, name):
self.name = name
>>> c = C("FishC")
Traceback (most recent call last):
File "<pyshell#155>", line 1, in <module>
c = C("FishC")
File "<pyshell#150>", line 4, in __call__
raise TypeError("仅支持关键字参数~")
TypeError: 仅支持关键字参数~
e.禁止一个类被直接实例化
>>> class NoInstance(type):
def __call__(cls, *args, **kwargs):
raise TypeError("该类不允许直接实例化对象")
>>> class C(metaclass=NoInstance):
pass
>>> c = C()
Traceback (most recent call last):
File "<pyshell#163>", line 1, in <module>
c = C()
File "<pyshell#159>", line 3, in __call__
raise TypeError("该类不允许直接实例化对象")
TypeError: 该类不允许直接实例化对象
可以通过静态方法的形式访问
>>> class C(metaclass=NoInstance):
@staticmethod
def static_ok():
print("静态方法直接访问")
>>> C.static_ok()
静态方法直接访问
>>> c = C()
Traceback (most recent call last):
File "<pyshell#169>", line 1, in <module>
c = C()
File "<pyshell#159>", line 3, in __call__
raise TypeError("该类不允许直接实例化对象")
TypeError: 该类不允许直接实例化对象
类方法也可以做到
>>> class C(metaclass=NoInstance):
@classmethod
def class_ok(cls):
print("类方法直接访问")
>>> C.class_ok()
类方法直接访问
f.只允许实例化一个对象
>>> class SimpleInstance(type):
def __init__(cls, *args, **kwargs):
cls.__instance = None
type.__init__(cls, *args, **kwargs)
def __call__(cls, *args, **kwargs):
if cls.__instance is None:
cls.__instance = type.__call__(cls, *args, **kwargs)
return cls.__instance
else:
return cls.__instance
>>> class C(metaclass=SimpleInstance):
pass
>>> c1 = C()
>>> c2 = C()
>>> c1 is c2
True
>>> dir(C)
['_SimpleInstance__instance', '__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__']
对象保存在_SimpleInstance__instance这么一个被改变了名字的属性。其实是__instance,又由于名字改编技术,成了_SimpleInstance__instance
24.抽象基类
先前讲到的Mixin类,作用为为一些不相关的子类提供一些额外功能的实现,相当于插件或者外挂的存在。所以对于Mixin类是不建议对它直接实例化的,要用它里面提供的方法作为父类将它插入即可。
下介绍一种不能被实例化的类--抽象基类
Python的抽象基类又两个特点:不能被直接实例化,只能够被继承使用;子类必须实现抽象基类中定义的抽象方法,否则无法被实例化
在Python中若要使用抽象基类,最稳妥的方法就是使用abc模块abc--AbstractBaseClasses,里面最常见的代表就是ABCMeta和abstractmethod
>>> from abc import ABCMeta, abstractmethod
>>> class Fruit(metaclass=ABCMeta):
#若想将一个类定义为抽象基类,只需指定它的元类为ABCMeta
def __init__(self, name):
self.name = name
#使用第二个导入的abstractmethod(是一个装饰器),用此装饰器来指定抽象方法
#若定义了抽象方法,即被abstractmethod装饰器所装饰的方法,那么继承这个抽象基类的子类就需要实现它才行
@abstractmethod
def good_for_health(self):
pass
#实例化会报错
>>> fruit = Fruit("水果")
Traceback (most recent call last):
File "<pyshell#210>", line 1, in <module>
fruit = Fruit("水果")
TypeError: Can’t instantiate abstract class Fruit with abstract methods good_for_health
子类继承抽象基类时,需要实现抽象基类里的抽象方法,否则也会报错。正确做法是在子类中重写抽象方法
>>> class Banana(Fruit):
def good_for_health(self):
print("香蕉")
>>> banana = Banana("香蕉儿")
>>> banana.good_for_health()
香蕉
十六、模块和包
引入:提供模块打包调用
写hello.py源文件
def say_hello():
print("Hello FishC")
def say_hi():
print("Hi FishC.")
写call_hello.py源文件
import hello
hello.say_hi()
hello.say_hello()
此时可以成功调用hello.py代码
Hi FishC.
Hello FishC
1.模块导入
Python中导入模块有三种方案
A.import 模块名称
使用<模块名称>.<对象名称>进行访问
B.from 模块名称 import 对象名称
from hello import say_hi, say_hello
say_hi()
say_hello()
对于此处情况,若对象很多有种不建议的写法,用*
from hello import *
say_hi()
say_hello()
此方法需要注意命名冲突的问题。同时若导入两个模块的方式都为此,且连模块内容对象相同,会出现后导入的模块覆盖较早导入模块的方法的情况
C.import 模块名称 as 关联名称
多用于模块名字较长时
import hello as h
h.say_hi()
h.say_hello()
2. if __name__ =="__main__"
在模块中存在自身测试的语句,会在导入运行后一并执行。只需在代码执行前判断 __name__ 是否等于__main__ 即可,因为当一个模块被作为脚本独立执行时,它的__name__属性就会被赋值为"__main__"。
但一个模块作为模块的形式导入到其他程序中,他此时的__name__是模块的名称
3.包
但源代码过多时,Python支持通过"."将源文件组织成多个分级的形式,即支持我们将模块分门别类然后存储到不同的文件夹中。后在导入模块时使用"."将包和模块分割
设置负责初始化文件__init__.py,可以在里定义属于包的全局变量,此变量可实现跨文件调用;可在模块中访问;但不能够执行脚本来访问包的全局变量,因为在模块是看不到包的,需要将其作为模块来使用才可,在调用出也可以访问全局变量和修改变量值
若多个包出现,可通过其一imort...as...语法给模块起别名,在访问是直接使用别名即可;其二是利用__init__.py这个包的构造文件,让包在导入的时候自动导入模块
4.遏制from...ipmort*的附加伤害
把模块内全部导入易造成命名空间的污染,Python提供在模块中使用__all__属性解决此种问题
__all__ = ["say_hello", "x"]
x = 250
s = "FishC"
def say_hello():
print("hello")
def say_hi():
print("hi")
此时只有say_hello和x能被访问,若用from...import*则会报错,但其他导入方式(即使全部也不会报错)
import hello as h
print(h.x)
h.say_hello()
print(h.s)
h.say_hi()
__all__属性还可以作用于包的构造文件中,因为使用from...import*的语法导入一个包是无法直接访问其包含的模块的
__all__ = ["fc1"]
#可以看到模块fc1,即可以去访问了
总结:
对于模块来说,若没有定义__all__属性,那么from...import*的语法将导入模块中的所有东西
对于包来说,若没有定义__all__,那么from...import*的语法则不导入包里面的任何模块