文章目录
系列目录
Python|Git remote|hosts|PyCharm常用快捷键|变量转换|命名|类型|运算符|分支|调整tab|循环|语言基础50课:学习记录(1)-项目简介及变量、条件及循环
Python|list|切片|列表的运算符、比较及遍历|生成式|元素位置和次数|元素排序和反转|sort() 方法|嵌套的列表|语言基础50课:学习记录(2)-常用数据结构之列表
Python|元组|字符串|语言基础50课:学习记录(3)-常用数据结构之元组及字符串相关
Python|集合|运算|哈希码|语言基础50课:学习记录(4)-常用数据结构之集合
Python|字典|函数和模块|应用及进阶|分数符号(Latex)|String库|operator库|处理数据三步骤|语言基础50课:学习记录(5)-常用数据结构之字典、函数和模块应用及进阶
Python|装饰器|执行时间|递归|动态属性|静态方法和类|继承和多态|isinstance类型判断|溢出|“魔法”方法|语言基础50课:学习记录(6)-函数的高级应用、面向对象编程、进阶及应用
Python|base64|collections|hashlib|heapq|itertools|random|os.path|uuid|文件|异常|JSON|API|CSV|语言基础50课:学习7
Python|xlwt|xlrd|调整单元格样式(背景,字体,对齐、虚线边框、列宽行高、添加公式)|xlutils|openpyxl|只读与只写|图表|语言基础50课:学习(8)
Python|python-docx|python-pptx|Pillow|smtplib|螺丝帽短信网关|正则表达式的应用|语言基础50课:学习(9)
Python|http|Chrome Developer Tools|Postman|HTTPie|builtwith库|python-whois库|爬虫及解析|语言基础50课:学习(10)
Python|线程和进程|阻塞|非阻塞|同步|异步|生成器和协程|资源竞争|进程间通信|aiohttp库|daemon属性值详解|语言基础50课:学习(11)
Python|并发编程|爬虫|单线程|多线程|异步I/O|360图片|Selenium及JavaScript|Scrapy框架|BOM 和 DOM 操作简介|语言基础50课:学习(12)
Python|MySQL概述|Windows-Linux-macOS安装|MySQL 基本命令|获取帮助|SQL注释|语言基础50课:学习(13)
Python|SQL详解之DDL|DML|DQL|DCL|索引|视图、函数和过程|JSON类型|窗口函数|接入MySQL|清屏|正则表达式|executemany|语言基础50课:学习(14)
原项目地址:
Python-Core-50-Courses(https://hub.fastgit.org/jackfrued/Python-Core-50-Courses.git)
第12课:常用数据结构之字典
Python程序中的字典以键值对(键和值的组合)的方式把数据组织到一起,我们可以通过键找到与之对应的值并进行操作。
创建和使用字典
在Python中创建字典可以使用{}
字面量语法,字典的{}
中的元素是以键值对的形式存在的,每个元素由:
分隔的两个值构成,:
前面是键,:
后面是值,代码如下所示。
xinhua = {
'麓': '山脚下',
'路': '道,往来通行的地方;方面,地区:南~货,外~货;种类:他俩是一~人',
'蕗': '甘草的别名',
'潞': '潞水,水名,即今山西省的浊漳河;潞江,水名,即云南省的怒江'
}
print(xinhua)
person = {
'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号',
'home': '中同仁路8号', 'tel': '13122334455', 'econtact': '13800998877'
}
print(person)
也可以使用内置函数dict
或者是字典的生成式语法来创建字典,代码如下所示。
# dict函数(构造器)中的每一组参数就是字典中的一组键值对
person = dict(name='王大锤', age=55, weight=60, home='中同仁路8号')
print(person) # {'name': '王大锤', 'age': 55, 'weight': 60, 'home': '中同仁路8号'}
# 可以通过Python内置函数zip压缩两个序列并创建字典
items1 = dict(zip('ABCDE', '12345'))
print(items1) # {'A': '1', 'B': '2', 'C': '3', 'D': '4', 'E': '5'}
items2 = dict(zip('ABCDE', range(1, 10)))
print(items2) # {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5}
# 用字典生成式语法创建字典
items3 = {x: x ** 3 for x in range(1, 6)}
print(items3) # {1: 1, 2: 8, 3: 27, 4: 64, 5: 125}
想知道字典中一共有多少组键值对,仍然是使用len
函数;如果想对字典进行遍历,可以用for
循环,但是需要注意,for
循环只是对字典的键进行了遍历,不过没关系,在讲完字典的运算后,我们可以通过字典的键获取到和这个键对应的值。
person = {'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号'}
print(len(person)) # 4
for key in person:
print(key)
字典的运算
对于字典类型来说,成员运算和索引运算肯定是最为重要的,前者可以判定指定的键在不在字典中,后者可以通过键获取对应的值或者向字典中加入新的键值对。值得注意的是,字典的索引不同于列表的索引,列表中的元素因为有属于自己有序号,所以列表的索引是一个整数;字典中因为保存的是键值对,所以字典的索引是键值对中的键,通过索引操作可以修改原来的值或者向字典中存入新的键值对。需要特别提醒大家注意的是,字典中的键必须是不可变类型,例如整数(int
)、浮点数(float
)、字符串(str
)、元组(tuple
)等类型的值;显然,列表(list
)和集合(set
)是不能作为字典中的键的,当然字典类型本身也不能再作为字典中的键,因为字典也是可变类型,但是字典可以作为字典中的值
person = {'name': '王大锤', 'age': 55, 'weight': 60, 'office': '科华北路62号'}
# 检查name和tel两个键在不在person字典中
print('name' in person, 'tel' in person) # True False
# 通过age修将person字典中对应的值修改为25
if 'age' in person:
person['age'] = 25
# 通过索引操作向person字典中存入新的键值对
person['tel'] = '13122334455'
person['signature'] = '你的男朋友是一个盖世垃圾,他会踏着五彩祥云去迎娶你的闺蜜'
print('name' in person, 'tel' in person) # True True
# 检查person字典中键值对的数量
print(len(person)) # 6
# 对字典的键进行循环并通索引运算获取键对应的值
for key in person:
print(f'{key}: {person[key]}')
需要注意,在通过索引运算获取字典中的值时,如指定的键没有在字典中,将会引发KeyError
异常。
字典的方法
# 字典中的值又是一个字典(嵌套的字典)
students = {
1001: {'name': '狄仁杰', 'sex': True, 'age': 22, 'place': '山西大同'},
1002: {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'},
1003: {'name': '武则天', 'sex': False, 'age': 20, 'place': '四川广元'}
}
# 使用get方法通过键获取对应的值,如果取不到不会引发KeyError异常而是返回None或设定的默认值
print(students.get(1002)) # {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'}
print(students.get(1005)) # None
print(students.get(1005, {'name': '无名氏'})) # {'name': '无名氏'}
#1002存在,则按更新后的命令输出
print(students.get(1002, {'names': '无名氏'}))
{'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'}
print(students.get(1002))
{'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'}
# 获取字典中所有的键
print(students.keys()) # dict_keys([1001, 1002, 1003])
# 获取字典中所有的值
print(students.values()) # dict_values([{...}, {...}, {...}])
# 获取字典中所有的键值对
print(students.items()) # dict_items([(1001, {...}), (1002, {....}), (1003, {...})])
# 对字典中所有的键值对进行循环遍历
for key, value in students.items():
print(key, '--->', value)
# 使用pop方法通过键删除对应的键值对并将该值赋给stu1
stu1 = students.pop(1002)
print(stu1) # {'name': '白元芳', 'sex': True, 'age': 23, 'place': '河北保定'}
print(len(students)) # 2
# stu2 = students.pop(1005) # KeyError: 1005
stu2 = students.pop(1005, {})
print(stu2) # {}
# 使用popitem方法删除字典中最后一组键值对并返回对应的二元组
# 如果字典中没有元素,调用该方法将引发KeyError异常
key, value = students.popitem() #popitem方法删除字典中最后一组键值对并返回对应的二元组
print(key, value) # 1003 {'name': '武则天', 'sex': False, 'age': 20, 'place': '四川广元'}
# 如果这个键在字典中存在,setdefault返回原来与这个键对应的值
# 如果这个键在字典中不存在,向字典中添加键值对,返回第二个参数的值,默认为None
result = students.setdefault(1005, {'name': '方启鹤', 'sex': True})
print(result) # {'name': '方启鹤', 'sex': True}
print(students) # {1001: {...}, 1005: {...}}
# 使用update更新字典元素,相同的键会用新值覆盖掉旧值,不同的键会添加到字典中
others = {
1005: {'name': '乔峰', 'sex': True, 'age': 32, 'place': '北京大兴'},
1010: {'name': '王语嫣', 'sex': False, 'age': 19},
1008: {'name': '钟灵', 'sex': False}
}
students.update(others)
print(students) # {1001: {...}, 1005: {...}, 1010: {...}, 1008: {...}}
跟列表一样,从字典中删除元素也可以使用del
关键字,在删除元素的时候如果指定的键索引不到对应的值,一样会引发KeyError
异常,具体的做法如下所示。
person = {'name': '王大锤', 'age': 25, 'sex': True}
del person['age']
print(person) # {'name': '王大锤', 'sex': True}
字典的应用及生成式语法
我们通过几个简单的例子来讲解字典的应用。
例子1:输入一段话,统计每个英文字母出现的次数。
sentence = input('请输入一段话: ')
counter = {}
for ch in sentence:
if 'A' <= ch <= 'Z' or 'a' <= ch <= 'z':
counter[ch] = counter.get(ch, 0) + 1
for key, value in counter.items():
print(f'字母{key}出现了{value}次.')
例子2:在一个字典中保存了股票的代码和价格,找出股价大于100元的股票并创建一个新的字典。
说明:可以用字典的生成式语法来创建这个新字典。
stocks = {
'AAPL': 191.88,
'GOOG': 1186.96,
'IBM': 149.24,
'ORCL': 48.44,
'ACN': 166.89,
'FB': 208.09,
'SYMC': 21.29
}
stocks2 = {key: value for key, value in stocks.items() if value > 100}
print(stocks2)
第13课:函数和模块
在讲解本节课的内容之前,我们先来研究一道数学题,请说出下面的方程有多少组正整数解。
x
1
+
x
2
+
x
3
+
x
4
=
8
x_1 + x_2 + x_3 + x_4 = 8
x1+x2+x3+x4=8
你可能已经想到了,这个问题其实等同于将8
个苹果分成四组且每组至少一个苹果有多少种方案,因此该问题还可以进一步等价于在分隔8
个苹果的7
个空隙之间插入三个隔板将苹果分成四组有多少种方案,也就是从7
个空隙选出3
个空隙放入隔板的组合数,所以答案是$ C_7^3=35 $。组合数的计算公式如下所示。
C
M
N
=
M
!
N
!
(
M
−
N
)
!
C_M^N = \frac {M!} {N!(M-N)!}
CMN=N!(M−N)!M!
循环做累乘的方式来计算阶乘,代码如下所示。
"""
输入M和N计算C(M,N)
Version: 0.1
Author: 骆昊
"""
m = int(input('m = '))
n = int(input('n = '))
# 计算m的阶乘
fm = 1
for num in range(1, m + 1):
fm *= num
# 计算n的阶乘
fn = 1
for num in range(1, n + 1):
fn *= num
# 计算m-n的阶乘
fk = 1
for num in range(1, m - n + 1):
fk *= num
# 计算C(M,N)的值 //是整除(取商的整数部分),/是浮点除法,保留小数
print(fm // fn // fk)
函数的作用
世界级的编程大师Martin Fowler先生曾经说过:“代码有很多种坏味道,重复是最坏的一种!”。对于上面的代码来说,我们可以将计算阶乘的功能封装到一个称为“函数”的代码块中,在需要计算阶乘的地方,我们只需要“调用函数”就可以了。
定义函数
数学上的函数通常形如y = f(x)
或者z = g(x, y)
这样的形式,在y = f(x)
中,f
是函数的名字,x
是函数的自变量,y
是函数的因变量;而在z = g(x, y)
中,g
是函数名,x
和y
是函数的自变量,z
是函数的因变量。Python中的函数跟这个结构是一致的,每个函数都有自己的名字、自变量和因变量。我们通常把Python中函数的自变量称为函数的参数,而因变量称为函数的返回值。
在Python中可以使用def
关键字来定义函数,和变量一样每个函数也应该有一个漂亮的名字,命名规则跟变量的命名规则是一致的(赶紧想一想我们之前讲过的变量的命名规则)。在函数名后面的圆括号中可以放置传递给函数的参数,就是我们刚才说到的函数的自变量,而函数执行完成后我们会通过return
关键字来返回函数的执行结果,就是我们刚才说的函数的因变量。一个函数要执行的代码块(要做的事情)也是通过缩进的方式来表示的,跟之前分支和循环结构的代码块是一样的。大家不要忘了def
那一行的最后面还有一个:
,之前提醒过大家,那是在英文输入法状态下输入的冒号。
我们可以通过函数对上面的代码进行重构。**所谓重构,是在不影响代码执行结果的前提下对代码的结构进行调整。**重构之后的代码如下所示。
"""
输入M和N计算C(M,N)
Version: 0.1
Author: 骆昊
"""
# 定义函数:def是定义函数的关键字、fac是函数名,num是参数(自变量)
def fac(num):
"""求阶乘"""
result = 1
for n in range(1, num + 1):
result *= n
# 返回num的阶乘(因变量)
return result
m = int(input('m = '))
n = int(input('n = '))
# 当需要计算阶乘的时候不用再写重复的代码而是直接调用函数fac
# 调用函数的语法是在函数名后面跟上圆括号并传入参数
print(fac(m) // fac(n) // fac(m - n))
说明:事实上,Python标准库的
math
模块中有一个名为factorial
的函数已经实现了求阶乘的功能,我们可以直接使用该函数来计算阶乘。将来我们使用的函数,要么是自定义的函数,要么是Python标准库或者三方库中提供的函数。
函数的参数
参数的默认值
如果函数中没有return
语句,那么函数默认返回代表空值的None
。另外,在定义函数时,函数也可以没有自变量,但是函数名后面的圆括号是必须有的。Python中还允许函数的参数拥有默认值,我们可以把之前讲过的一个例子“CRAPS赌博游戏”中摇色子获得点数的功能封装成函数,代码如下所示。
"""
参数的默认值
Version: 0.1
Author: 骆昊
"""
from random import randint
# 定义摇色子的函数,n表示色子的个数,默认值为2
def roll_dice(n=2):
"""摇色子返回总的点数"""
total = 0
for _ in range(n):
total += randint(1, 6)
return total
# 如果没有指定参数,那么n使用默认值2,表示摇两颗色子
print(roll_dice())
# 传入参数3,变量n被赋值为3,表示摇三颗色子获得点数
print(roll_dice(3))
我们再来看一个更为简单的例子。
def add(a=0, b=0, c=0):
"""三个数相加求和"""
return a + b + c
# 调用add函数,没有传入参数,那么a、b、c都使用默认值0
print(add()) # 0
# 调用add函数,传入一个参数,那么该参数赋值给变量a, 变量b和c使用默认值0
print(add(1)) # 1
# 调用add函数,传入两个参数,1和2分别赋值给变量a和b,变量c使用默认值0
print(add(1, 2)) # 3
# 调用add函数,传入三个参数,分别赋值给a、b、c三个变量
print(add(1, 2, 3)) # 6
# 传递参数时可以不按照设定的顺序进行传递,但是要用“参数名=参数值”的形式
print(add(c=50, a=100, b=200)) # 350
注意:带默认值的参数必须放在不带默认值的参数之后,否则将产生
SyntaxError
错误,错误消息是:non-default argument follows default argument
,翻译成中文的意思是“没有默认值的参数放在了带默认值的参数后面”。
def add(a, b=0, c=0): #a不带默认参数
"""三个数相加求和"""
return a + b + c
#如执行:
#add(b=5,c=2,10)
#提示错误:
# File "<ipython-input-42-fc2969185ee8>", line 1
# add(b=5,c=2,10)
^
#SyntaxError: positional argument follows keyword argument
#正常顺序,,带默认值往后放,先解决“无人认领”的变量
add(10,b=5,c=2) #17
可变参数
用可变参数实现对任意多个数求和的add
函数,args可以接收0个或任意多个参数,中间用,分隔。
"""
可变参数
Version: 0.1
Author: 骆昊
"""
# 用星号表达式来表示args可以接收0个或任意多个参数
def add(*args):
total = 0
# 可变参数可以放在for循环中取出每个参数的值
for val in args:
#如果类型是整数或浮点,则做加法
if type(val) in (int, float):
total += val
return total
# 在调用add函数时可以传入0个或任意多个参数
print(add())
print(add(1))
print(add(1, 2))
print(add(1, 2, 3))
print(add(1, 3, 5, 7, 9))
用模块管理函数(完全限定名、别名as)
不管用什么样的编程语言来写代码,给变量、函数起名字都是一个让人头疼的问题,因为我们会遇到命名冲突这种尴尬的情况。最简单的场景就是在同一个.py
文件中定义了两个同名的函数,如下所示。
def foo():
print('hello, world!')
def foo():
print('goodbye, world!')
foo() # 大家猜猜调用foo函数会输出什么
当然上面的这种情况我们很容易就能避免,但是如果项目是团队协作多人开发的时候,团队中可能有多个程序员都定义了名为foo
的函数,这种情况下怎么解决命名冲突呢?答案其实很简单,Python中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候我们通过import
关键字导入指定的模块再使用完全限定名的调用方式就可以区分到底要使用的是哪个模块中的foo
函数,代码如下所示。
module1.py
def foo():
print('hello, world!')
module2.py
def foo():
print('goodbye, world!')
test.py
import module1
import module2
# 用“模块名.函数名”的方式(完全限定名)调用函数,
module1.foo() # hello, world!
module2.foo() # goodbye, world!
在导入模块时,还可以使用as
关键字对模块进行别名,这样我们可以使用更为简短的完全限定名。
test.py
import module1 as m1
import module2 as m2
m1.foo() # hello, world!
m2.foo() # goodbye, world!
上面的代码我们导入了定义函数的模块,我们也可以使用from...import...
语法从模块中直接导入需要使用的函数,代码如下所示。
test.py
from module1 import foo
foo() # hello, world!
from module2 import foo
foo() # goodbye, world!
但是,如果我们如果从两个不同的模块中导入了同名的函数,后导入的函数会覆盖掉先前的导入,就像下面的代码中,调用foo
会输出hello, world!
,因为我们先导入了module2
的foo
,后导入了module1
的foo
。如果两个from...import...
反过来写,就是另外一番光景了。
test.py
from module2 import foo
from module1 import foo
foo() # hello, world!
如果想在上面的代码中同时使用来自两个模块中的foo
函数也是有办法的,大家可能已经猜到了,还是用as
关键字对导入的函数进行别名,代码如下所示。
test.py
from module1 import foo as f1
from module2 import foo as f2
f1() # hello, world!
f2() # goodbye, world!
标准库中的模块和函数
Python标准库中提供了大量的模块和函数来简化我们的开发工作,我们之前用过的random
模块就为我们提供了生成随机数和进行随机抽样的函数;而time
模块则提供了和时间操作相关的函数;上面求阶乘的函数在Python标准库中的math
模块中已经有了,实际开发中并不需要我们自己编写,而math
模块中还包括了计算正弦、余弦、指数、对数等一系列的数学函数。随着我们进一步的学习Python编程知识,我们还会用到更多的模块和函数。
Python标准库中还有一类函数是不需要import
就能够直接使用的,我们将其称之为内置函数,这些内置函数都是很有用也是最常用的,下面的表格列出了一部分的内置函数。
函数 | 说明 |
---|---|
abs | 返回一个数的绝对值,例如:abs(-1.3) 会返回1.3 。 |
bin | 把一个整数转换成以'0b' 开头的二进制字符串,例如:bin(123) 会返回'0b1111011' 。 |
chr | 将Unicode编码转换成对应的字符,例如:chr(8364) 会返回'€' 。 |
hex | 将一个整数转换成以'0x' 开头的十六进制字符串,例如:hex(123) 会返回'0x7b' 。 |
input | 从输入中读取一行,返回读到的字符串。 |
len | 获取字符串、列表等的长度。 |
max | 返回多个参数或一个可迭代对象中的最大值,例如:max(12, 95, 37) 会返回95 。 |
min | 返回多个参数或一个可迭代对象中的最小值,例如:min(12, 95, 37) 会返回12 。 |
oct | 把一个整数转换成以'0o' 开头的八进制字符串,例如:oct(123) 会返回'0o173' 。 |
open | 打开一个文件并返回文件对象。 |
ord | 将字符转换成对应的Unicode编码,例如:ord('€') 会返回8364 。 |
pow | 求幂运算,例如:pow(2, 3) 会返回8 ;pow(2, 0.5) 会返回1.4142135623730951 。 |
print | 打印输出。 |
range | 构造一个范围序列,例如:range(100) 会产生0 到99 的整数序列。 |
round | 按照指定的精度对数值进行四舍五入,例如:round(1.23456, 4) 会返回1.2346 。 |
sum | 对一个序列中的项从左到右进行求和运算,例如:sum(range(1, 101)) 会返回5050 。 |
type | 返回对象的类型,例如:type(10) 会返回int ;而 type('hello') 会返回str 。 |
第14课:函数的应用
接下来我们通过一些案例来为大家讲解函数的应用。
经典小案例
案例1:设计一个生成验证码的函数。
说明:验证码由数字和英文大小写字母构成,长度可以用参数指定。
import random
import string
#生成数字和英文大小写字母的集合字符串
#'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
ALL_CHARS = string.digits + string.ascii_letters
def generate_code(code_len=4):
"""生成指定长度的验证码
:param code_len: 验证码的长度(默认4个字符)
:return: 由大小写英文字母和数字构成的随机验证码字符串
"""
return ''.join(random.choices(ALL_CHARS, k=code_len))
可以用下面的代码生成10组随机验证码来测试上面的函数。
for _ in range(10):
print(generate_code())
random
模块的sample
和choices
函数区别
说明:
random
模块的sample
和choices
函数都可以实现随机抽样,sample
实现无放回抽样,这意味着抽样取出的字符是不重复的;choices
实现有放回抽样,这意味着可能会重复选中某些字符。这两个函数的第一个参数代表抽样的总体,而参数k
代表抽样的数量。
案例2:设计一个函数返回给定文件的后缀名。
说明:文件名通常是一个字符串,而文件的后缀名指的是文件名中最后一个
.
后面的部分,也称为文件的扩展名,它是某些操作系统用来标记文件类型的一种机制,例如在Windows系统上,后缀名exe
表示这是一个可执行程序,而后缀名txt
表示这是一个纯文本文件。需要注意的是,在Linux和macOS系统上,文件名可以以.
开头,表示这是一个隐藏文件,像.gitignore
这样的文件名,.
后面并不是后缀名,这个文件没有后缀名或者说后缀名为''
。
def get_suffix(filename, ignore_dot=True):
"""获取文件名的后缀名
:param filename: 文件名
:param ignore_dot: 是否忽略后缀名前面的点
:return: 文件的后缀名
"""
# 从字符串中逆向查找.出现的位置
pos = filename.rfind('.')
# 通过切片操作从文件名中取出后缀名
if pos <= 0:
return ''
return filename[pos + 1:] if ignore_dot else filename[pos:]
可以用下面的代码对上面的函数做一个简单的测验。
print(get_suffix('readme.txt')) # txt
print(get_suffix('readme.txt.md')) # md
print(get_suffix('.readme')) #
print(get_suffix('readme.')) #
print(get_suffix('readme')) #
上面的get_suffix
函数还有一个更为便捷的实现方式,就是直接使用os.path
模块的splitext
函数,这个函数会将文件名拆分成带路径的文件名和扩展名两个部分,然后返回一个二元组,二元组中的第二个元素就是文件的后缀名(包含.
),如果要去掉后缀名中的.
,可以做一个字符串的切片操作,代码如下所示。
from os.path import splitext
def get_suffix(filename, ignore_dot=True):
return splitext(filename)[1][1:]
思考:如果要给上面的函数增加一个参数,用来控制文件的后缀名是否包含
.
,应该怎么做?
from os.path import splitext
def get_suffix(filename, ignore_dot=True):
return splitext(filename)[1][1:] if ignore_dot else splitext(filename)[1]
案例3:写一个判断给定的正整数是不是质数的函数。
def is_prime(num: int) -> bool:
"""判断一个正整数是不是质数,只能被1整除,且不为1
:param num: 正整数
:return: 如果是质数返回True,否则返回False
"""
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0: #不能排除本身是1的情况
return False
return num != 1 #整数不为1,且不能被num的平方根以内的数整除
案例4:写出计算两个正整数最大公约数和最小公倍数的函数。
代码一:
def gcd_and_lcm(x: int, y: int) -> int:
"""求最大公约数和最小公倍数"""
a, b = x, y
while b % a != 0:
a, b = b % a, a
return a, x * y // a
代码二:
def gcd(x: int, y: int) -> int:
"""求最大公约数"""
while y % x != 0:
x, y = y % x, x
return x
def lcm(x: int, y: int) -> int:
"""求最小公倍数"""
return x * y // gcd(x, y)
思考:请比较上面的代码一和代码二,想想哪种做法是更好的选择。
案例5:写出计算一组样本数据描述性统计信息的函数。
import math
def ptp(data):
"""求极差(全距)"""
return max(data) - min(data)
def average(data):
"""求均值"""
return sum(data) / len(data)
def variance(data):
"""求方差"""
x_bar = average(data)
temp = [(num - x_bar) ** 2 for num in data]
return sum(temp) / (len(temp) - 1)
def standard_deviation(data):
"""求标准差"""
return math.sqrt(variance(data))
def median(data):
"""找中位数"""
temp, size = sorted(data), len(data)
if size % 2 != 0:
return temp[size // 2]
else:
return average(temp[size // 2 - 1:size // 2 + 1])
第15课:函数使用进阶
前面我们讲到了关于函数的知识,我们还讲到过Python中常用的数据类型,这些类型的变量都可以作为函数的参数或返回值,用好函数还可以让我们做更多的事情。
关键字参数
下面是一个判断传入的三条边长能否构成三角形的函数,在调用函数传入参数时,我们可以指定参数名,也可以不指定参数名,代码如下所示。
def is_triangle(a, b, c):
print(f'a = {a}, b = {b}, c = {c}')
return a + b > c and b + c > a and a + c > b
# 调用函数传入参数不指定参数名按位置对号入座
print(is_triangle(1, 2, 3))
# 调用函数通过“参数名=参数值”的形式按顺序传入参数
print(is_triangle(a=1, b=2, c=3))
# 调用函数通过“参数名=参数值”的形式不按顺序传入参数
print(is_triangle(c=3, a=1, b=2))
在没有特殊处理的情况下,函数的参数都是位置参数,也就意味着传入参数的时候对号入座即可,如上面代码的第7行所示,传入的参数值1
、2
、3
会依次赋值给参数a
、b
、c
。当然,也可以通过参数名=参数值
的方式传入函数所需的参数,因为指定了参数名,传入参数的顺序可以进行调整,如上面代码的第9行和第11行所示。
调用函数时,如果希望函数的调用者必须以参数名=参数值
的方式传参,可以用命名关键字参数(keyword-only argument)取代位置参数。所谓命名关键字参数,是在函数的参数列表中,写在*
之后的参数,代码如下所示。
def is_triangle(*, a, b, c):
print(f'a = {a}, b = {b}, c = {c}')
return a + b > c and b + c > a and a + c > b
#如果执行:is_triangle(3,4,5),会提示:
# TypeError: is_triangle() takes 0 positional arguments but 3 were given
# print(is_triangle(3, 4, 5))
# 传参时必须使用“参数名=参数值”的方式,位置不重要
print(is_triangle(a=3, b=4, c=5))
print(is_triangle(c=5, b=4, a=3))
注意:上面的
is_triangle
函数,参数列表中的*
是一个分隔符,*
前面的参数都是位置参数,而*
后面的参数就是命名关键字参数。
我们之前讲过在函数的参数列表中可以使用可变参数*args
来接收任意数量的参数,但是我们需要看看,*args
是否能够接收带参数名的参数。
def calc(*args):
result = 0
for arg in args:
if type(arg) in (int, float):
result += arg
return result
print(calc(a=1, b=2, c=3))
执行上面的代码会引发TypeError
错误,错误消息为calc() got an unexpected keyword argument 'a'
,由此可见,*args
并不能处理带参数名的参数。我们在设计函数时,如果既不知道调用者会传入的参数个数,也不知道调用者会不会指定参数名,那么同时使用可变参数和关键字参数。关键字参数会将传入的带参数名的参数组装成一个字典,参数名就是字典中键值对的键,而参数值就是字典中键值对的值,代码如下所示。
def calc(*args, **kwargs):
result = 0
for arg in args:
if type(arg) in (int, float):
result += arg
for value in kwargs.values():
if type(value) in (int, float):
result += value
return result
print(calc()) # 0
print(calc(1, 2, 3)) # 6
print(calc(a=1, b=2, c=3)) # 6
print(calc(1, 2, c=3, d=4)) # 10
提示:不带参数名的参数(位置参数)必须出现在带参数名的参数(关键字参数)之前,否则将会引发异常。例如,执行
calc(1, 2, c=3, d=4, 5)
将会引发SyntaxError
错误,错误消息为positional argument follows keyword argument
,翻译成中文意思是“位置参数出现在关键字参数之后”。
高阶函数的用法
函数的参数和返回值可以是任意类型的对象,这就意味着函数本身也可以作为函数的参数或返回值,这就是所谓的高阶函数。
如果我们希望上面的calc
函数不仅仅可以做多个参数求和,还可以做多个参数求乘积甚至更多的二元运算,我们就可以使用高阶函数的方式来改写上面的代码,将加法运算从函数中移除掉,具体的做法如下所示。
def calc(*args, init_value, op, **kwargs):
result = init_value
for arg in args:
if type(arg) in (int, float):
result = op(result, arg)
for value in kwargs.values():
if type(value) in (int, float):
result = op(result, value)
return result
注意,上面的函数增加了两个参数,其中init_value
代表运算的初始值,op
代表二元运算函数。经过改造的calc
函数不仅仅可以实现多个参数的累加求和,也可以实现多个参数的累乘运算,代码如下所示。
def add(x, y):
return x + y
def mul(x, y):
return x * y
print(calc(1, 2, 3, init_value=0, op=add, x=4, y=5)) # 15
print(calc(1, 2, x=3, y=4, z=5, init_value=1, op=mul)) # 120
Python内置函数中有不少高阶函数,我们前面提到过的filter
和map
函数就是高阶函数,前者可以实现对序列中元素的过滤,后者可以实现对序列中元素的映射,例如我们要去掉一个整数列表中的奇数,并对所有的偶数求平方得到一个新的列表,就可以直接使用这两个函数来做到,具体的做法是如下所示。
def is_even(num):
return num % 2 == 0
def square(num):
return num ** 2
numbers1 = [35, 12, 8, 99, 60, 52]
numbers2 = list(map(square, filter(is_even, numbers1)))
print(numbers2) # [144, 64, 3600, 2704]
当然,要完成上面代码的功能,也可以使用列表生成式,列表生成式的做法更为简单优雅。
numbers1 = [35, 12, 8, 99, 60, 52]
numbers2 = [num ** 2 for num in numbers1 if num % 2 == 0]
print(numbers2) # [144, 64, 3600, 2704]
Lambda函数
在使用高阶函数的时候,如果作为参数或者返回值的函数本身非常简单,一行代码就能够完成,那么我们可以使用Lambda函数来表示。Python中的Lambda函数是没有的名字函数,所以很多人也把它叫做匿名函数,匿名函数只能有一行代码,代码中的表达式产生的运算结果就是这个匿名函数的返回值。上面代码中的is_even
和square
函数都只有一行代码,我们可以用Lambda函数来替换掉它们,代码如下所示。
numbers1 = [35, 12, 8, 99, 60, 52]
numbers2 = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, numbers1)))
print(numbers2) # [144, 64, 3600, 2704]
通过上面的代码可以看出,定义Lambda函数的关键字是lambda
,后面跟函数的参数,如果有多个参数用逗号进行分隔;冒号后面的部分就是函数的执行体,通常是一个表达式,表达式的运算结果就是Lambda函数的返回值,不需要写return
关键字。
如果需要使用加减乘除这种简单的二元函数,也可以用Lambda函数来书写,例如调用上面的calc
函数时,可以通过传入Lambda函数来作为op
参数的参数值。当然,op
参数也可以有默认值,例如我们可以用一个代表加法运算的Lambda函数来作为op
参数的默认值。
def calc(*args, init_value=0, op=lambda x, y: x + y, **kwargs):
result = init_value
for arg in args:
if type(arg) in (int, float):
result = op(result, arg)
for value in kwargs.values():
if type(value) in (int, float):
result = op(result, value)
return result
# 调用calc函数,使用init_value和op的默认值
print(calc(1, 2, 3, x=4, y=5)) # 15
# 调用calc函数,通过lambda函数给op参数赋值
print(calc(1, 2, 3, x=4, y=5, init_value=1, op=lambda x, y: x * y)) # 120
提示:注意上面的代码中的
calc
函数,它同时使用了可变参数、关键字参数、命名关键字参数,其中命名关键字参数要放在可变参数和关键字参数之间,传参时先传入可变参数,关键字参数和命名关键字参数的先后顺序并不重要。
有很多函数在Python中用一行代码就能实现,我们可以用Lambda函数来定义这些函数,调用Lambda函数就跟调用普通函数一样,代码如下所示。
import operator, functools
# 一行代码定义求阶乘的函数
fac = lambda num: functools.reduce(operator.mul, range(1, num + 1), 1)
# 一行代码定义判断素数的函数
is_prime = lambda x: x > 1 and all(map(lambda f: x % f, range(2, int(x ** 0.5) + 1)))
# 调用Lambda函数
print(fac(10)) # 3628800
print(is_prime(9)) # False
提示1:上面使用的
reduce
函数是Python标准库functools
模块中的函数,它可以实现对数据的归约操作,通常情况下,过滤(filter)、映射(map)和归约(reduce)是处理数据中非常关键的三个步骤,而Python的标准库也提供了对这三个操作的支持。提示2:上面使用的
all
函数是Python内置函数,如果传入的序列中所有布尔值都是True
,all
函数就返回True
,否则all
函数就返回False
。
简单的总结
Python中的函数可以使用可变参数*args
和关键字参数**kwargs
来接收任意数量的参数,而且传入参数时可以带上参数名也可以没有参数名,可变参数会被处理成一个元组,而关键字参数会被处理成一个字典。Python中的函数是一等函数,可以赋值给变量,也可以作为函数的参数和返回值,这也就意味着我们可以在Python中使用高阶函数。如果我们要定义的函数非常简单,只有一行代码且不需要函数名,可以使用Lambda函数(匿名函数)。
TIPS1:分数符号(Latex)
\tfrac 设置分数为 textstyle
$ \tfrac {1}{2} $ 代码为: \tfrac 空格 {1}{2},前后加$,字体有缩放
\dfrac 设置分数为 displaystyle 单行模式,与行内字体高度一致
$ \dfrac {1}{2} $
\frac 根据上下文决定使用 \tfrac 还是 \dfrac
\cfrac 用于表示连续分数
$ \cfrac{2}{1+\cfrac{2}{1+\cfrac{2}{1}}}$ 代码为: \cfrac{2}{1+\cfrac{2}{1+\cfrac{2}{1}}}
$ x_1 , x^2 $ 的代码分别为: x_1 , x^2 前后加$
TIPS2:String库
import string
ascii = string.ascii_letters
print(ascii) #abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
ascii_lower= string.ascii_lowercase
print(ascii_lower) #abcdefghijklmnopqrstuvwxyz
ascii_upper= string.ascii_uppercase
print(ascii_upper) #ABCDEFGHIJKLMNOPQRSTUVWXYZ
digits = string.digits
print(digits) #0123456789
hexdigits= string.hexdigits
print(hexdigits) #0123456789abcdefABCDEF
printable = string.printable
print(printable) #0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
参数为:
TIPS3:operator库
TIPS4:处理数据三步骤
过滤(filter)、映射(map)和归约(reduce) 摘自:https://www.runoob.com/
- 过滤(filter)
用于过滤序列,过滤掉不符合条件的元素,返回一个迭代器对象,如果要转换为列表,可以使用 list() 来转换。
过滤出1~100中平方根是整数的数:
#!/usr/bin/python3
import math
def is_sqr(x):
return math.sqrt(x) % 1 == 0
tmplist = filter(is_sqr, range(1, 101))
newlist = list(tmplist)
print(newlist)
输出结果 :
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
- 映射(map)
根据提供的函数对指定序列做映射。
>>> def square(x) : # 计算平方数
return x ** 2
>>> map(square, [1,2,3,4,5]) # 计算列表各个元素的平方
<map object at 0x100d3d550> # 返回迭代器
>>> list(map(square, [1,2,3,4,5])) # 使用 list() 转换为列表
[1, 4, 9, 16, 25]
>>> list(map(lambda x: x ** 2, [1, 2, 3, 4, 5])) # 使用 lambda 匿名函数
[1, 4, 9, 16, 25]
- 归约(reduce)
reduce() 函数会对参数序列中元素进行累积。
函数将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给 reduce 中的函数 function(有两个参数)先对集合中的第 1、2 个元素进行操作,得到的结果再与第三个数据用 function 函数运算,最后得到一个结果。
注意:Python3.x reduce() 已经被移到 functools 模块里,如果我们要使用,需要引入 functools 模块来调用 reduce() 函数:
from functools import reduce
参数
function – 函数,有两个参数
iterable – 可迭代对象
initializer – 可选,初始参数
from functools import reduce
def add(x, y) : # 两数相加
return x + y
sum1 = reduce(add, [1,2,3,4,5]) # 计算列表和:1+2+3+4+5
sum2 = reduce(lambda x, y: x+y, [1,2,3,4,5]) # 使用 lambda 匿名函数
print(sum1) #15
print(sum2) #15