蒙珣的博客

活好当下,做好今天该做的事情。

0%

Python函数进阶

动态参数

  • *args
1
2
3
4
5
6
7
8
def func(*args):
print(args) # 元组类型 (22,)(22,33,99)()

# 只能按照位置传参
func(22)
func(22,33)
func(22,33,99)
func()
  • **kwargs
1
2
3
4
5
6
7
8
def func(**kwargs):
print(kwargs) # 字典类型 {"n1":"蒙珣"} {'n1':'蒙珣','age':18,'email':'mengxun@11.com'} {}

# 只能按关键字传参
func(n1="蒙珣")
func(n1="蒙珣",age=18)
func(n1="蒙珣",age=18,email="mengxun@11.com")
func()
  • *args,**kwargs
1
2
3
4
5
6
7
def func(*args,**kwargs):
print(args,kwargs) # (22,33,99) (22,){'n1':'蒙珣','age':18} (){}

func(22,33,99)
func(n1="蒙珣",age=18)
func(22,n1="蒙珣",age=18)
func()

提示:是否还记得字符串格式化时的format功能

1
2
3
4
5
v1 = "我叫{},今年{}".format("蒙珣",18)

v2 = "我叫{name},今年{age}".format(name="蒙珣",age=18)

# def format(*args,**kwargs):

参数内存地址相关

如果想查看某个值在内存中的地址

1
2
3
4
>>> v1 = "蒙珣"
>>> addr = id(v1)
>>> print(addr)
4347695248
1
2
3
4
5
6
>>> v1 = [11,22,33]
>>> v2 = [11,22,33]
>>> print(id(v1))
4347824448
>>> print(id(v2))
4347825216
1
2
3
4
5
6
>>> v1 = [11,22,33]
>>> v2 = v1
>>> print(id(v1))
4346072768
>>> print(id(v2))
4346072768

记住一句话:函数执行传参时,传递的是内存地址

即:python传参默认传递的是内存地址

Python参数的这一特性有两个好处:

  • 节省内存
  • 对于可变类型且函数中修改元素的内容,所有的地方都会修改。可变类型:列表、字典、集合。
1
2
3
4
5
6
7
8
9
10
11
# 可变类型 & 修改内部
>>> def func(data):
... data.append(999)

>>> v1 = [11,22,33]
>>> func(v1)

>>> print(v1)

# 结果
[11,22,33,999]
1
2
3
4
5
6
7
8
9
10
11
# 特殊情况:可变类型 & 重新赋值
>>> def func(data):
... data = ["蒙珣","william"]

>>> v1 = [11,22,33]
>>> func(v1)

>>> print(v1)

# 结果
[11, 22, 33]
1
2
3
4
5
6
7
8
9
10
# 特殊情况:不可变类型,无法修改内部元素,只能重新赋值
>>> def func(data):
... data = "William"
... print(data)

>>> v1 = "蒙珣"
>>> func(v1)

# 结果
William

函数的返回值是内存地址

其他很多编程语言执行函数时,默认传参时会将数据重新拷贝一份,会浪费内存

当然,如果你不想让外部的变量和函数内部参数的变量一致,也可以选择将外部值拷贝一份,再传给函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import copy

# 可变类型 & 修改内部修改
def func(data):
data.append(999)

v1 = [11,22,33]
# 拷贝一份数据
new_v1 = copy.deepcopy(v1)
func(new_v1)

print(v1)

# 结果
[11,22,33]

函数内存地址引用的变化

1
2
3
4
5
6
7
8
def func():
data = [11,22,33]

v1 = func()
print(v1) # [11,22,33]

v2 = func()
print(v2) # [11,22,33]

上述代码的执行过程:

  • 执行func函数
  • data = [11,22,33] 创建一块内存区域,内部存储 [11, 22, 33],data变量指向这块内存地址 10000001110。
  • return data 返回data指向的内存地址
  • v1接收返回值,所以v1和data都指向[11,22,33]的内存地址(两个变量质量此内存,引用计数器为2)
  • 由函数执行完毕后,函数内部的变量都会被释放。(即:删除data变量,内存地址的引用计数器-1)

所以,最终v1只想的函数内部创建的那块内存地址。(v1指向的1000001110内存地址)

  • 执行func函数
  • data = [11,22,33] 创建一块内存区域,内部存储 [11, 22, 33],data变量指向这块内存地址 11111001110。
  • return data 返回data指向的内存地址
  • v2接收返回值,所以v1和data都指向[11,22,33]的内存地址(两个变量质量此内存,引用计数器为2)
  • 由函数执行完毕后,函数内部的变量都会被释放。(即:删除data变量,内存地址的引用计数器-1)

所以,最终v1指向的函数内部创建的那块内存地址。(v1指向的 11111001110)

参数的默认值【面试题】

这个知识点在面试题中出现的概率比较高,但真正实际开发中用的比较少

1
2
def func(a1,a2=18):
print(a1,a2)

原理:Python在创建函数(未执行)时,如果发现函数的参数中有默认值,则在函数内部会创建一块区域并维护这个值

  • 执行函数未传值时,则让a2指向函数维护的那个值的地址

    1
    func("root")
  • 执行函数传值时,则让a2指向新传入的值的地址

    1
    func("admin",20)

在特定情况【默认参数的值是可变类型】&【函数内部会修改这个值】

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 在函数内存中会维护一块区域存储 [1,2,666] 100010001
    def func(a1,a2=[1,2]):
    a2.append(666)
    print(a1,a2)

    # a1 = 100
    # a2 -> 100010001
    func(100) # 100 [1,2,666]

    # a1 = 200
    # a2 -> 100010001
    func(200) # 200 [1,2,666,666]

    # a1 = 99
    # a2 -> 111111101
    func(99,[77,99]) # 99 [77,99,666]

    # a1 = 300
    # a2 -> 100010001
    func(300) # 300 [1,2,666,666,666]
  • 大坑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 在内部会维护一块区域存储[1,2],内存地址 1010101010
    def func(a1,a2=[1,2]):
    a2.append(a1)
    return a2

    v1 = func(10)
    print(v1) # [1,2,10]

    v2 = func(20)
    print(v2) # [1,2,10,20]

    v3 = func(30,[11,22])
    print(v3) # [11,22,30]

    v4 = func(40)
    print(v4) # [1,2,10,20,40]
  • 深坑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    def func(a1,a2=[1,2]):
    a2.append(al)
    return a2

    v1 = func(10)
    v2 = func(20)
    v3 = func(30,[11,22])
    v4 = func(40)

    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]

    V1,V2,V4用的都是同一块内存

动态参数补充

实参使用*,即在执行函数时也可以用*

  • 形参固定,实参用*和**

    1
    2
    3
    4
    5
    6
    7
    8
    def func(a1,a2):
    print(a1,a2)

    func(11,22) # 11 22
    func(a1=1,a2=2) # 1 2

    func(*[11,22]) # 11 22
    func(**{"a1":11,"a2":22}) # 11 22
  • 形参用*和** ,实参也用*和**

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    def func(*args,**kwargs):
    print(args,kwargs)

    func(11,22)
    func(11,22,name="蒙珣",age=18)

    func([11,22,33],{"k1":1,"k2":2}) # ([11, 22, 33], {'k1': 1, 'k2': 2}) {}
    func(*[11,22,33],**{"k1":1,"k2":2}) # (11, 22, 33) {'k1': 1, 'k2': 2}

    # 值得注意:按照这个方式将数据传给args和kwargs时,数据是会重新拷贝的(可以理解为内部循环每个元素并设置到args和kwargs中)

所以,在使用format字符串格式化时,可以这样使用:

1
2
3
4
5
v1 = "我是{},年龄:{}".format("蒙珣",18)
v2 = "我是{name},年龄:{age}".format(name="蒙珣",age=18)

v3 = "我是{},年龄:{}".format(*["蒙珣",18])
v4 = "我是{name},年龄:{age}".format(**{"name":"蒙珣","age":18})

函数做元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func():
return 123

data_list = ["mengxun","蒙珣",func,func()]

print(data_list[0]) # mengxun
print(data_list[1]) # 蒙珣
print(data_list[2]) # func函数
print(data_list[3]) # 123

res = data_list[2]()
print(res) # 123

print(data_list[2]()) # 123

注意:函数同时也可以被哈希,所以函数名通常也可以当做集合的元素、字典的键

掌握这个知识后,对后续的项目开发有很大的帮助,例如,在项目中遇到根据选择不同操作时:

  • 情景1,例如:开发一个类似微信的功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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("输入错误")

    其实这种大量使用 if 判断不是一个很好的方式,我们可以将其放入字典中再使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    def send_message():
    """发送信息"""
    pass

    def send_image():
    """发送图片"""
    pass

    def send_emoji():
    """发送表情"""
    pass

    def send_file():
    """发送文件"""
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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,['1513121255089','你好']],
    "2":[send_image,['xxx/xxx/xx.png','消息内容']],
    "3":[send_emoji,["😊"]],
    "4":[send_file,['xx.zip']]
    }

    print("欢迎使用xx系统")
    print("请选择:1.发送消息;2.发送图片;3.发送表情;4.发送文件")
    choice = input("输入选择的序号")

    item = function_dict.get(choice)
    if not item:
    print("输入错误")
    else:
    # 执行函数
    func = item[0]
    param_list = item[1]
    func(*param_list)
  • 情景2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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': "17610837191", "content":"您有新短息"}},
    {"name": send_email, "params": {'to_emial': "mengxu@117.com", "subject": "报警消息", "content": "硬盘容量不够用了"}},
    {"name": send_wechat, "params": {'user_id': 1, 'content': "出去玩吗"}}
    ]

    for item in func_list:
    func = item['name']
    param_dict = item['params']
    func(**param_dict)

函数嵌套

上述示例中的函数均定义在全局作用域,其实函数也可以定义在局部作用域,这样函数呗局部作用域和其子作用域中调用(函数的嵌套)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def func():
print("AAAA")

def handler():
print("BBBB")
def inner():
print("CCCC")
inner()
func()
print("DDDD")

handler()
-------------------
BBBB

到现在你会发现,只要理解数据定义时所存在的作用域,并根据从上到下代码执行过程进行分析,再怎么嵌套都可以搞定

现在的你可能有疑问:为什么要这么嵌套定义?把函数都定义在全局不好吗?

其实,大多数情况下我们都会将函数定义在全局,不会嵌套着定义函数。不过,当我们定义一个函数去实现某功能,想要将内部功能拆分成N个函数,又担心这个N个函数放在全局会与其他函数名冲突时(尤其多人协同开发)可以选择使用函数的嵌套

1
2
3
4
5
6
7
8
9
def f1():
pass

def f2():
pass

def func():
f1()
f2()
1
2
3
4
5
6
7
8
9
def func():
def f1():
pass

def f2():
pass

f1()
f2()

![image-20230227222215296](/Users/william/Library/Application Support/typora-user-images/image-20230227222215296.png)

![image-20230227222321268](/Users/william/Library/Application Support/typora-user-images/image-20230227222321268.png)

![image-20230227222345169](/Users/william/Library/Application Support/typora-user-images/image-20230227222345169.png)

嵌套引发作用域问题

函数作用域分析1

1
2
3
4
5
6
7
8
9
name = "蒙珣"

def run():
name = "mengxun"
def inner():
print(name)
inner()

run()
  • 函数在执行过程中,先创建了一个全局作用域 name = 蒙珣 ,然后创建了一个全局作用域run函数

  • 之后在我们调用函数run()时,该函数在内部声明了一个局部作用域 name = mengxun,而后又声明了一个局部作用域的函数为inner函数

  • 在函数内部中,他自己调用 inner(),先从函数内部寻找,是否有 inner 函数。如果有则调用内部函数,如果没有则调用外部函数。

  • 最后执行 inner函数,结果为mengxun

函数作用域分析1

函数作用域分析2

1
2
3
4
5
6
7
8
9
10
11
12
13
name = "蒙珣"

def run():
name = "mengxun"
def inner():
print(name)
return inner

v1 = run()
v1()

v2 = run()
v2()
  • 函数在执行过程中,先创建了一个全局作用域 name = 蒙珣,然后创建了一个全局作用域 run函数
  • 之后我们调用函数run(),并将其声明成全局变量 v1
  • 此时,函数run()内部,开始声明局部变量name = mengxun,并返回局部函数inner()的内存地址
  • 接着,我们调用全局变量v1,于是开始执行run函数内部的inner函数
  • inner函数先从内部找name变量,如果内部没有name变量,就开始在上一级函数中寻找name变量,并使用。
  • 最后打印 mengxun
  • 声明全局变量v2,同上。不同的是,v2是创建的不同于v1的另一个调用栈。

函数作用域分析2

函数作用域分析3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
name = "蒙珣"

def run():
name = "mengxun"
def inner():
print(name)
return [inner,inner,inner]

func_list = run()
func_list[2]()
func_list[1]()

funcs = run()
funcs[2]()
funcs[1]()

三句话搞定作用域:

  • 优先在自己的作用域找,自己没有就去上级作用域
  • 在作用域中寻找值时,要确保此次此刻值是什么
  • 分析函数的执行,并确定函数作用域链(函数嵌套)

![image-20230227224253973](/Users/william/Library/Application Support/typora-user-images/image-20230227224253973.png)

闭包