ExASIC
分享让工作更轻松

python的函数(三):参数传递

今天我们来继续学习python的函数,学习参数传递的一些基本规则。

可修改型参数与不可修改型参数的区别

我们在学习数据类型的时候,知道python的数据类型有两类,不可修改型(数字字符串等)和可修改型(列表字典等)(不记得的童鞋可以点击链接回忆一下)。可修改型和不可修改型作为函数参数时有较大的区别。我们下面通过几个简单的例子来说明。

示例一:数字作为参数
def func(a):
    print("id of a(initially):", id(a))
    a = 88
    print("id of a(assign):", id(a))
    print("value of a:", a)

b = 99
print("id of b:", id(b))
print("value of b (before):", b)

func(b)
print("value of b (after):", b)

输出结果:
id of b: 9086048
value of b (before): 99
id of a(initially): 9086048
id of a(assign): 9085696 #创建了新对象
value of a: 88
value of b (after): 99 #函数外部的b不变

从输出结果我们可以清楚看到,刚进入函数时,a指向了b,也就是说b的对象传递到了函数内部。当把a赋值成88时,a指向了一个新创建的对象,对象的值是88。当函数func执行结束后,b的值没有受到影响,仍是99。 是乎有点绕,我们来提取核心内容:

  1. 传递的是对象b,而非数值99。
  2. 由于b是不可修改的数据类型,对函数的参数a赋值时,在函数内新创建一个对象,然后用a指向这个对象。
  3. 这个新对象是函数内部变量,作用域仅在函数内部,对外部的全局变量没有影响。

那对于可修改类型的列表和字典有什么不一样的呢?我们通过下面的例子来看。

示例二:
def func(a):
    print("id of a(initially):", id(a))
    a[0] = 4
    print("id of a(assign):", id(a))
    print("value of a:", a)

b = [1, 2, 3]
print("id of b:", id(b))
print("value of b (before):", b)

func(b)
print("value of b (after):", b)

输出结果:
id of b: 140298248703240
value of b (before): [1, 2, 3]
id of a(initially): 140298248703240
id of a(assign): 140298248703240 #没有创建新对象
value of a: [4, 2, 3]
value of b (after): [4, 2, 3] #外部的值被修改

示例二的输出结果表明函数内部并没有创建新的列表。当修改列表元素时,实际上修改的就是函数外部的列表。函数执行结束后,看到外部的列表确实被修改。

可修改型参数的风险及解决办法

使用可修改型参数给我们提供了修改全局变量的方法。但如果使用不当,可能会误修改。我们要牢记这一点。那么有没有好的方法来避免这种风险呢?有!

一般有两种方法:

  1. 使用元组(Tuple)代替列表。
  2. 函数一开头把列表拷贝到本地。
示例三
def func(a):
    a[0] = 4

b = [1, 2, 3]
func(tuple(b))

输出结果:
TypeError: 'tuple' object does not support item assignment

示例三表明,把列表强制转换成元组再传递给函数,当函数内部有修改数组的操作时就会报错,提示元组不能赋值。这种方法虽简单暴力,但有局限性。局限性是列表数据类型提供的方法,如append、remove等,都不能使用。

再看下面的示例四,示例中提供了一种更优雅的解决方案。

示例四
def func(a):
    print("id of a(initially):", id(a))
    a = a[:]
    a[0] = 4
    print("id of a(assign):", id(a))
    print("value of a:", a)

b = [1, 2, 3]
print("id of b:", id(b))
print("value of b (before):", b)

func(b)
print("id of b:", id(b))
print("value of b (after):", b)

输出结果:
id of b: 140122896936264
value of b (before): [1, 2, 3]
id of a(initially): 140122896936264
id of a(assign): 140122896936328 #创建了新对象
value of a: [4, 2, 3]
id of b: 140122896936264
value of b (after): [1, 2, 3] #外部列表没有被修改

在函数一开头,通过a = a[:]创建了一个新的列表对象,a指向了这个新对象。那么函数内部接下来的所有操作都只针对这个新的列表对象。

两种参数调用模式:按位置顺序、按名称

我们知道verilog有两种实例化方式,按位置顺序和按端口名称。同样python也有这两种方式。例如下面的两个例子。

示例五:
def func(a, b, c):
    print(a, b, c)
func(1, 2, 3)
示例六:
def func(a, b, c):
    print(a, b, c)
func(a=1, b=2, c=3)
func(b=2, c=3, a=1)   #与上一行效果相同

示例六里,按名称调用时,变量的先后顺序就不重要了。按名称调用的好处是一目了然。

另外,函数定义时还可以指定参数默认值。如下面的例子:

示例七:
def func(a, b, c=3):
    print(a, b, c)
func(1, 2)

这个例子里,对于定义了默认值的参数,在调用时如果不需要修改就可以省略。这样可以让代码更简洁。

注意,函数定义时,没有默认值的在前,有默认值的在后。像def func(a, b=2, c)是不符合python语法的,会报错。

两种调用模式的混用

按位置顺序和按名称调用可以混用。如下面的示例:

示例八:
def func(a, b, c=3):
    print(a, b, c)
func(1, 2, c=4)

注意,混合调用时要讲究顺序,先按位置、后按名称调用。否则语法会报错。

print()函数的可变个数参数是怎么实现的?

我们都知道print()函数支持任意多个参数,倒底是怎么实现的?其实python的参数还有两种*和**。*表示把传递的参数看作一个元组,**表示把传递的参数看作是一个字典。例如:

示例九:
def func(*a):
    print(a)
func(1, 2, 3)   #输出(1, 2, 3)
示例十:
def func(**a):
    print(a)
func(a=1, b=2)   #输出{'a': 1, 'b': 2}

在示例九里,如果我们在函数内部做元组的解析和打印,是不是就可以实现自己的print()函数了呢?(习题2)

*、**和普通参数混用的顺序

由于*、**不确定参数的个数,所以一般放在参数列表的最后,表示剩下来的其它参数。

示例十一:
def func(a, *b):
    print(a, b)
func(1, 2, 3)   #输出1 (2, 3)

如果*不在最后,函数调用时*后面的参数需要按名称调用。不然没办法判断*的参数到哪里结束。如下面的例子:

示例十二:
def func(a, *b, c):
    print(a, b, c)
func(1, 2, 3, c=4) #输出1 (2, 3) 4
func(1, 2, 3, 4)   #会报错,c没有赋值

一般比较好习惯是,参数按位置 -> 名称 -> * -> ** 顺序定义及调用。

总结

函数的参数传递细节问题非常多,我们写代码时要多用简单、容易理解的编码风格。对于参数我们总结如下几点:

  1. 注意可修改型(如数字、字符串)和不可修改型(如列表、字典)参数的区别,并学会利用拷贝避免风险。
  2. 按位置和按名称两种调用方式,复杂的情况下多用按名称调用。
  3. 学会用*和**实现参数个数不确定的函数。
  4. 注意参数定义和调用时按位置、名称、*、**的顺序。

习题

  1. 实现一个函数args_parse(),解析命令行的参数,把结果存在一个字典变量中。
  2. 实现自己的print_anything(),支持任意个任意类型的参数。
参考答案

execrise

结果输出:
$ python3 func_args_ans1.py -R -full64 -sverilog -timescale=1ns/1ps -y rtl -f rtl.flist +warn=none +vpdfile+debug.vpd
{'f': 'rtl.flist', 'y': 'rtl', 'timescale': '1ns/1ps', 'sverilog': 'true', 'full64': 'true', 'R': 'true'}
{'vpdfile': 'debug.vpd', 'warn': 'none'}
exists sverilog option
vpd file name: debug.vpd

点评:主要的思路就是逆序判断参数前有无-或+。把解析后参数存在字典很方便后续脚本的识别和判断。

参考代码下载:http://www.exasic.com/example/func_args_ans1.py

execrise

结果输出:
$ python3 func_args_ans2.py
1
abc
[4, 5, 6]
{'name': 'dut', 'area': '40um2'}

参考代码下载:http://www.exasic.com/example/func_args_ans2.py

阅读数:
更多文章:文章目录
解惑专区
(支持markdown插入源代码)
欢迎使用ExASIC订阅服务
仅用于ExASIC最新文章通知,方便及时阅读。
友情链接: IC技术圈问答ReCclayCrazyFPGA