目标:掌握函数相关易错点&项目开发必备技能
概要:
- 参数的补充
- 函数名,函数名到底是什么?
- 返回值和print,傻傻分不清楚
- 函数的作用域
1、参数的补充
在函数基础部分,我们掌握函数和参数基础知识,掌握这些其实完全就可以进行项目的开发
今天补充的内容属于进阶知识,包含:内存地址相关、面试题相关等,在特定情况下也可以让代码更简洁,提升开发效率。
1.1、参数内存地址相关【面试题】
再开始讲参数内存地址相关之前,先学习一个技能:如果想要查看某个值在内存中的地址?
v1 = '武沛齐'
addr = id(v1)
print(addr) # 2307800736944
v1 = [11, 22, 33]
v2 = [11, 22, 33]
print(id(v1)) # 2855833700288
print(id(v2)) # 2855833513664
v1 = [11, 22, 33]
v2 = v1
print(id(v1)) # 2674044772480
print(id(v2)) # 2674044772480
记住一句话:函数执行传参时,传递的是内存地址
def func(data):
print(data, id(data))
v1 = '武沛齐'
print(id(v1)) # 2304215327920
func(v1) # 武沛齐 2304215327920
面试题:请问Python的参数默认传递的是什么?
Python参数的这一特性有两个好处:
- 节省内存
- 对于可变类型且函数中次改元素的内容,所有的地方都会修改。可变类型:列表、字典、集合
# 可变类型 & 修改内部元素
def func1(data):
data.append(666)
v1 = [11, 22, 33]
func1(v1)
print(v1) # [11, 22, 33, 666]
# 特殊情况:可变类型 & 重新赋值
def func2(data):
data = ['武沛齐', 'alex']
v1 = [11, 22, 33]
func2(v1)
print(v1) # [11, 22, 33]
# 特殊情况,不可变类型,无法修改内部元素,只能重新赋值
def func3(data):
data = 'alex'
v1 = '武沛齐'
func3(v1)
print(v1) # 武沛齐
其他很多变成语言执行函数时,默认传参时会将数据重新拷贝一份,会浪费内存。
当然,如果你不想让外部的变量和函数内部参数的变量一致,也可以选择将外部值拷贝一份,再传给参数。
import copy
# 可变类型 & 修改内部元素
def func(data):
data.append(666)
v1 = [11, 22, 33]
new_v1 = copy.deepcopy(v1) # 拷贝一份数据
func(new_v1)
print(v1) # [11, 22, 33]
print(new_v1) # [11, 22, 33, 666]
1.2、函数的返回值是内存地址
def func():
data = [11, 22, 33]
return data
v1 = func()
print(v1) # [11, 22, 33]
上述代码的执行过程:
- 执行func函数
- data = [11,22,33]创建一块内训区域,内部存储[11,22,33],data变量指向这块内存地址
- return data返回data指向的内存地址
- v1接收返回值,所以v1和data都指向[11,22,33]的内存地址(两个变量指向此内存,引用计数器为2)
- 由函数执行完毕后,函数内部的变量都会被释放(即:删除data变量,内存地址的引用计数器-1)
所以,最终v1指向的函数内部创建的那块内存地址
def func():
data = [11, 22, 33]
print(id(data))
return data
v1 = func()
print(v1, id(v1)) # 2693005379712 # [11, 22, 33] 2693005379712
v2 = func()
print(v2, id(v2)) # 2693005378624 # [11, 22, 33] 2693005378624
上述代码的执行过程:
- 执行func函数
- data = [11,22,33]创建一块内训区域,内部存储[11,22,33],data变量指向这块内存地址
2693005379712
- return data返回data指向的内存地址
- v1接收返回值,所以v1和data都指向[11,22,33]的内存地址(两个变量指向此内存,引用计数器为2)
- 由函数执行完毕后,函数内部的变量都会被释放(即:删除data变量,内存地址的引用计数器-1)
此时,v1指向的函数内部创建的那块内存地址(v1指向的2693005379712
内存地址)
- 执行func函数
- data = [11,22,33]创建一块内训区域,内部存储[11,22,33],data变量指向这块内存地址2693005378624
- return data返回data指向的内存地址
- v2接收返回值,所以v2和data都指向[11,22,33]的内存地址(两个变量指向此内存,引用计数器为2)
- 由函数执行完毕后,函数内部的变量都会被释放(即:删除data变量,内存地址的引用计数器-1)
此时,最终v2指向的函数内部创建的那块内存地址(v2指向的2693005378624内存地址)
注意:如果data是数字或字符串,会因为Python的缓存/驻留机制(只创建一个地址)导致v1和v2指向一样的内存地址
1.3、参数的默认值【面试题】
该知识点在面试题中出现概率比较高,但真正实际开发中用的比较少
def func(a1,a2=18):
print(a1,a2)
原理:Python在创建函数(未执行)时,如果发现函数中的参数中有默认值,则在函数内部会创建一块区域并维护这个默认值。
- 执行函数未传值时,则让a2指向函数维护的那个值的地址
func('root')
- 执行函数传值时,则让a2指向新传入的值的地址
func('admin',20)
在特定情况【默认参数的值是可变类型list/dict/set】&【函数内部会修改这个值】
下,参数的默认值 有坑。
- 坑
# 在函数内存中会维护一块区域存储[1,2] 地址:100001001 def func(a1, a2=[1, 2]): a2.append(666) print(a1, a2) # a1 = 100 # a2 -> 100001001 地址的内容在本次调用中修改为[1, 2, 666] func(100) # 100 [1, 2, 666] # a1 = 200 # a2 -> 100001001 依然指向该地址,该地址的内容在本次调用中修改为[1, 2, 666, 666] func(200) # 200 [1, 2, 666, 666] # a1 = 99 # a2 = [77, 88] -> 11111101 传值,指向新地址 func(99, [77, 88]) # 99 [77, 88, 666] # a1 = 300 # a2 -> 100001001 依然指向该地址,该地址的内容在本次调用中修改为[1, 2, 666, 666, 666] func(300) # 300 [1, 2, 666, 666, 666]
- 大坑
# 在函数内存中会维护一块区域存储[1,2] 地址:1010101010 def func(a1, a2=[1, 2]): a2.append(a1) return a2 # a1 = 10 # a2 -> 1010101010 # v1 -> 1010101010 该地址的内容在本次调用中修改为[1, 2, 10] v1 = func(10) print(v1) # [1, 2, 10] # a1 = 20 # a2 -> 1010101010 依然指向该地址 # v2 -> 1010101010 该地址的内容在本次调用中修改为[1, 2, 10, 20] v2 = func(20) print(v2) # [1, 2, 10, 20] # a1 = 30 # a2 = [11, 22] -> 111111111 传值,指向新地址 # v3 -> 111111111 v3 = func(30, [11, 22]) print(v3) # [11, 22, 30] # a1 = 40 # a2 -> 1010101010 依然指向该地址 # v4 -> 1010101010 该地址的内容在本次调用中修改为[1, 2, 10, 20, 40] v4 = func(40) print(v4) # [1, 2, 10, 20, 40]
- 深坑
# 在函数内存中会维护一块区域存储[1,2] 地址:100000001 def func(a1, a2=[1, 2]): a2.append(a1) return a2 # a1 = 10 # a2 -> 100000001 # v1 -> 100000001 该地址的内容在本次调用中修改为[1, 2, 10] v1 = func(10) # a1 = 20 # a2 -> 100000001 依然指向该地址 # v2 -> 100000001 该地址的内容在本次调用中修改为[1, 2, 10, 20] v2 = func(20) # a1 = 30 # a2 = [11, 22] -> 111100001 传值,指向新地址 # v3 -> 111100001 该地址的内容在本次调用中修改为[11, 22, 30] v3 = func(30, [11, 22]) # a1 = 40 # a2 -> 100000001 依然指向该地址 # v4 -> 100000001 该地址的内容在本次调用中修改为[1, 2, 10, 20, 40] v4 = func(40) # 以上函数运行结束后 v1 v2 v4 -> 100000001 该地址的内容最后一次修改为[1, 2, 10, 20, 40] v3 -> 111100001 指向传值的地址,内容修改为[11, 22, 30] print(v1) # [1, 2, 10, 20, 40] print(v2) # [1, 2, 10, 20, 40] print(v3) # [11, 22, 30] print(v4) # [1, 2, 10, 20, 40]
1.4、动态参数
动态参数,定义函数时在形参位置用*
或**
可以接任意个参数
def func(*args, **kwargs):
print(args, kwargs)
func('宝强', '杰伦', n1='alex', n2='eric') # ('宝强', '杰伦') {'n1': 'alex', 'n2': 'eric'}
在定义函数时可以用*
或**
,其实在执行函数时,也可以用
- 形参固定,实参用
*
或**
def func(a1, a2): print(a1, a2) func(11, 22) # 11 22 func(a1=1, a2=2) # 1 2 func(*[11, 22]) # 1 2 func(**{'a1': 1, 'a2': 2}) # 1 2
- 形参用
*
或**
,实参也用*
或**
def func(*args, **kwargs): print(args, kwargs) func(11, 22) # (11, 22) {} func(11, 22, name='武沛齐', age=18) # (11, 22) {'name': '武沛齐', 'age': 18} # 小坑 func([11, 22, 33], {'k1': 1, 'k2': 2}) # ([11, 22, 33], {'k1': 1, 'k2': 2}) {} # 值得注意:按照这个方式将数据传递给args和kwargs时,数据是会重新拷贝一份的(可理解为内部循环每个元素并设置到args和kwargs中) func(*[11, 22, 33], **{'k1': 1, 'k2': 2}) # (11, 22, 33) {'k1': 1, 'k2': 2}
所以,在使用format字符串格式化时,可以如下这般:
v1 = '我是{},年龄:{}。'.format('武沛齐', 18)
v2 = '我是{name},年龄:{age}。'.format(name='武沛齐', age=18)
v3 = '我是{},年龄:{}。'.format(*['武沛齐', 18])
v4 = '我是{name},年龄:{age}。'.format(**{'name': '武沛齐', 'age': 18})
练习题:
读取文件中的URL和标题,根据URL下载视频到本地(以标题作为文件名)
模仿,https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0300f570000bvmace0gvch7lo53oog&ratio=720P&line=0
卡特,https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f3e0000bv52fpn5t6p007e34qlg&ratio=720P&line=0
罗斯,https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f240000buuer5aa4tij4gv6ajqg&ratio=720P&line=0
# 下载视频实例
import requests
res = requests.get(
url="https://www.bilibili.com/video/BV1qh411273b?p=127&spm_id_from=pageDriver",
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac os x 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
}
)
# 视频的文件内容
with open('123.mp4', mode='wb') as file_object:
file_object.write(res.content)
import requests
def download(title, url):
"""下载并保存视频"""
res = requests.get(
url=url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac os x 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 FS"
}
)
# 视频保存至本地
with open('{}.mp4'.format(title), mode='wb') as f:
f.write(res.content)
# 读取文件
with open('files/db.csv', mode='r', encoding='utf-8') as file_object:
# 循环文件的每一行
for line in file_object:
# 去除空格/换行符
line = line.strip()
# 根据","分割
row_list = line.split(',')
# 下载视频
# download(row_list[0], row_list[1])
download(*row_list)
2、函数和函数名
函数名其实就是一个变量,这个变量只不过代指的函数而已
name = '武沛齐'
def add(n1,n2):
return n1 + n2
注意:函数必须先定义才能被调用(解释型语言)
# 正确
def add(n1,n2):
return n1 + n2
ret = add(1,2)
print(ret)
# 错误
ret = add(1,2)
print(ret)
def add(n1,n2):
return n1 + n2
2.1、函数做元素
既然函数就相当于是一个变量,那么在列表等元素中是否可以把行数当做元素呢?
def func():
return 123
data_list = ['武沛齐', 'func', func, func()]
print(data_list[0]) # 字符串‘武沛齐’
print(data_list[1]) # 字符串‘func’
print(data_list[2]) # 函数 func
print(data_list[3]) # 整数 123
res = data_list[2]()
print(res) # 123 # 执行函数func,并获取返回值;print再输出返回值
print(data_list[2]()) # 123 # 执行函数func,并获取返回值;print再输出返回值
注意:函数同时也可被哈希,所以函数名同时也可以当做集合的元素、字典的键
掌握这个知识后,对后续项目的开发有很大的帮助,例如,在项目中遇到根据选择做不同操作时:
- 情景1,例如:开发一个类似于微信的功能
def send_message(): """发送消息""" pass def send_image(): """发送图片""" pass def send_emoji(): """发送表情""" pass def send_file(): """发送文件""" pass print('欢迎使用XX系统') print("请选择:1、发送消息;2、发送图片;3、发送表情;4、发送文件") choice = input('输入选择的序号') if choice == '1': send_message() elif choice == '2': send_image() elif choice == '3': send_emoji() elif choice == '4': send_file() else: print('输入错误')
def send_message(): """发送消息""" pass def send_image(): """发送图片""" pass def send_emoji(): """发送表情""" pass def send_file(): """发送文件""" pass def XXX(): """收藏""" pass function_dict = { '1': send_message, '2': send_image, '3': send_emoji, '4': send_file } print('欢迎使用XX系统') print("请选择:1、发送消息;2、发送图片;3、发送表情;4、发送文件") choice = input('输入选择的序号') func = function_dict.get(choice) if not func: print('输入错误') else: # 执行函数 func()
- 情景2,例如:某个特定情况,要实现发送短信、微信、邮件
def send_msg(): """发送短信""" pass def send_email(): """发送邮件""" pass def send_wechat(): """发送微信""" pass # 执行函数 send_msg() send_email() send_wechat()
def send_msg(): """发送短信""" pass def send_email(): """发送邮件""" pass def send_wechat(): """发送微信""" pass func_list = [send_msg, send_email, send_wechat] for item in func_list: item() # 执行函数
上述两种情景,在参数相同时才可用
,如果参数不一致,会出错。所以,在项目设计师就要让程序满足这一点,如果无法满足,也可以通过其他手段实现,例如:
- 情景1补充:
def send_message(phone, content): """发送消息""" pass def send_image(img_path, content): """发送图片""" pass def send_emoji(emoji): """发送表情""" pass def send_file(path): """发送文件""" pass function_dict = { '1': [send_message, ['15131255089', '你好呀']], '2': [send_image, ['XXX/xxx/xx.png', '消息内容']], '3': [send_emoji, ['☺']], '4': [send_file, ['xx.zip']], } print('欢迎使用XX系统') print("请选择:1、发送消息;2、发送图片;3、发送表情;4、发送文件") choice = input('输入选择的序号') # 1 item = function_dict.get(choice) # [send_message, ['15131255089', '你好呀']] if not item: print('输入错误') else: # 执行函数 func = item[0] # send_message param_list = item[1] # ['15131255089', '你好呀'] func(*param_list) # send_message(*['15131255089', '你好呀'])
- 情景2补充:
def send_msg(mobile, content): """发送短信""" pass def send_email(to_email, subject, content): """发送邮件""" pass def send_wechat(user_id, content): """发送微信""" pass func_list = [ {'name': send_msg, 'params': {'mobile': '15131255089', 'content': '你有新短消息'}}, {'name': send_email, 'params': {'to_email': 'wupeiqi@live.com', 'subject': '报警消息', 'content': '硬盘容量不够用了'}}, {'name': send_wechat, 'params': {'user_id': '1', 'content': '约吗'}}, ] for item in func_list: # 执行函数 func = item['name'] # send_msg param_list = item['params'] # {'mobile': '15131255089', 'content': '你有新短消息'} func(*param_list) # send_msg(**{'mobile': '15131255089', 'content': '你有新短消息'})
2.2、函数名赋值
- 将函数名赋值给其他变量,函数名其实就是个变量,代指某函数;如果将函数名赋值给另外一个变量,则此变量也会代指该函数,例如:
def func(a1, a2): print(a1, a2) xxxx = func # 此时,xxxx和func都代指上面的函数,所以都可以被执行 func(1, 1) # 1 1 xxxx(2, 2) # 2 2
def func(a1, a2): print(a1, a2) func_list = [func, func, func] func(11, 22) # 11 22 func_list[0](11, 22) # 11 22 func_list[1](33, 44) # 33 44 func_list[2](55, 66) # 55 66
- 对函数名重新赋值,如果将函数名修改为其他值,函数名便不再代指函数,例如:
def func(a1, a2): print(a1, a2) # 执行func函数 func(11, 22) # 11 22 # func重新赋值成一个字符串 func = '武沛齐' print(func) # 武沛齐
def func(a1, a2): print(a1 + a2) func(1, 2) # 3 # 重新定义函数,覆盖掉上面定义的函数 def func(): print(666) func() # 666
注意:由于函数名被重新定义后,就会变成被定义的值,所以在自定义函数时,不要与Python内置的函数同名
,否则会覆盖掉内置函数的功能,例如:
id,bin,hex,oct,len....
# 新定义函数覆盖内置函数示例
# len内置函数用于计算值的长度
v1 = len('武沛齐')
print(v1) # 3
# len重新定义成另外一个函数
def len(a1,a2):
return a1 + a2
# 以后执行len函数,只能按照重新定义的来使用
v3 = len(4,2)
print(v3) # 6
2.3、函数名做参数和返回值
函数名其实就是一个变量,代指某个函数,所以,也可以当做函数的参数和返回值。
- 参数
def plus(num): return num + 100 def handler(func): res = func(10) # 110 msg = '执行func,并获取到的结果为:{}'.format(res) print(msg) # 执行handler函数,并将plus作为参数传递给handler的形式参数func handler(plus) # 执行func,并获取到的结果为:110
- 返回值
def plus(num): return num + 100 def handler(): print('执行handler函数') return plus result = handler() # 执行handler函数,并将返回值plus赋值给result,此时result也指向函数plus data = result(20) # 120 print(data) # 120
3、返回值和print
对于初学者,很多人对print和返回值分不清楚,例如;
def add(n1, n2):
print(n1 + n2)
v1 = add(1, 3)
print(v1) # 4 # None
def plus(a1, a2):
return a1 + a2
v2 = plus(1, 2)
print(v2) # 3
这两个函数是完全不同的
- 在函数中使用print,只是用于在某个位置输出内容而已
- 在函数汇总使用return,是为了将函数的执行结果返回给调用者,以便于后续其他操作
在调用函数并执行函数时,要学会分析函数的执行步骤
def f1():
print(123)
def f2(arg):
ret = arg()
return ret
# f1作为参数传给函数f2()的参数arg,此时arg也指向函数f1(),执行函数f1()输出‘123’,再将函数f1()返回值None赋值给变量ret,最后将返回值ret值赋值给调用者v1
v1 = f2(f1)
print(v1) # 123 # None
def f1():
print(123)
def f2(arg):
ret = arg()
return f1
# f1作为参数传给函数f2()的参数arg,此时arg也指向函数f1(),执行函数f1()输出‘123’,再将函数f1()返回值None赋值给变量ret,最后将返回值f1值赋值给调用者v1,此时v1也指向函数f1()
v1 = f2(f1) # 123 # 123
# v1指向函数f1(),将返回值None赋值给变量v2
v2 = v1()
print(v2) # None
4、作用域
作用域,可以理解为一块空间,这块空间的数据是可以共享的。通俗点讲,作用域就类似于一个房子,房子内的东西归里面的所有人共享,其他房子的人无法获取。
4.1、函数为作用域
Python以函数为作用域,所以在函数内创建的所有数据,可以此函数中被使用,无法在其他函数中使用。
def func():
name = '武沛齐'
data_list = [11, 22, 33, 44]
print(name, data_list)
age = 20
print(age)
def handler():
age = 18
print(age)
func() # 武沛齐 [11, 22, 33, 44] # 20
handler() # 18
学会分析代码,了解变量到底属于哪个作用域且是否可以被调用:
def func():
name = '武沛齐'
age = 29
print(age)
data_list = [11, 22, 33, 44]
print(name, data_list)
for num in range(10):
print(num) # 运行到最后,num=9
print(num) # 9
if 1 == 1:
value = 'admin' # 运行,value='admin'
print(value) # admin
print(value) # admin
if 1 > 2:
max_num = 10 # 不运行,不存在max_num
print(max_num)
print(max_num) # 报错
def handler():
age = 18
print(age)
handler()
func()
4.2、全局和局部
4.3、global关键字