跳转到内容

User:Xyy23330121/Python/函数

来自维基学院


定义函数

[编辑 | 编辑源代码]

在 Python 中,使用 def 关键词来定义函数。以下是一种简单的定义函数和调用的方式:

def func():  #定义函数。函数名为"func"。
    print("运行了函数:func()")  #调用函数时,执行的语句组

func()       #调用函数,此处输出:运行了函数:func()

可以看出,对于需要多次执行的操作,使用函数可以缩减代码。如果要对这种操作进行更改,直接对函数进行更改也更容易。函数是增加代码可维护性、使代码简洁易读的手段。

带参数的函数

[编辑 | 编辑源代码]
def func1(arg):  #定义函数,带有参数 arg 。
    print("运行了函数:func1(),参数为", arg)
def func2(arg1, arg2):  #定义函数,带有参数 arg1, arg2 。
    print("运行了函数:func2(),参数为", arg1, arg2)

在函数名后面的括号内,可以添加任意个参数。这相当于在调用函数时,用传入的参数执行了赋值语句。如果调用上例中的 func1 函数:

func1("传入的参数字符串")

其运行结果,等同于:

arg = "传入的参数字符串"
print("运行了函数:func1(),参数为", arg)

注意,此时定义的参数是必须传入的,如果不传入,则会报错。我们看以下示例:

func1()   #报错:TypeError: func1() missing 1 required positional argument

参数与作用域

[编辑 | 编辑源代码]

作用域规则

[编辑 | 编辑源代码]

任何定义在函数中的参数,或在函数中被赋值的变量,都无法用在函数外。这些变量被称作“局部变量”。在函数中,对变量赋值时,会自动设置变量为“局部变量”,对函数外的变量无影响。我们看以下几个示例:

外部变量可以应用于内部,内部变量无法应用于外部

[编辑 | 编辑源代码]
def argFunc(arg):
    var = 0   #在函数内,给变量 var 赋值
    print(a)  #使用了函数外的变量 a 
    print(arg)

a = 1
argFunc(123)   #输出:1。
print(var)     #报错:NameError: name 'var' is not defined.
print(arg)     #报错:NameError: name 'arg' is not defined.

可以看出,函数内赋值的变量,或函数的参数不可以用于函数外。而在函数调用前赋值的函数外变量可以使用于函数内。

在内部对变量赋值,对外部变量无影响

[编辑 | 编辑源代码]
def argFunc2():
    a = 0
    print(a)

a = 1
argFunc2()  #输出:0
print(a)    #输出:1

特别情况

[编辑 | 编辑源代码]

只要在函数内,有给变量 a 赋值的语句。变量 a 就会被视为局部变量,而非外部变量。

def argFunc3():
    print(a)
    a = 0

a = 1
argFunc3()    #报错:UnboundLocalError: cannot access local variable 'a' where it is not associated with a value

在执行 argFunc3 的时候,print(a) 查询了局部变量 a 的值。但此时局部变量 a 尚未赋值,从而导致了报错。

作用域查询变量的规则

[编辑 | 编辑源代码]

上面已经说明了几种作用域的简单情况,这里讨论更复杂的情况。

在查询变量名时,会先查询同层作用域是否有同名变量,随后查询上一层作用域。如果上一层作用域没有,会再查询更上一层。我们看以下示例:

a = b = c = "Global"

def first():
    b = c = "First"
    def second():
        c = "Second"
        print(a,b,c)
    second()

first()    #输出:Global First Second

在输出时,c 优先输出定义在 second 中的版本;而 b 由于在 second 中没有,所以往上一层,输出定义在 first 中的版本;而 a 由于在 first 中也没有,从而输出了全局版本。

作用域和 def 子句的位置相关,而不与运行函数的位置相关。我们把上面示例改为:

a = b = c = "Global"

def first():
    b = c = "First"
    second()
    
def second():
    c = "Second"
    print(a,b,c)

first()    #输出:Global Global Second

可以发现输出改变了。这是因为 second 的上一层直接就是全局作用域。因此,在运行 second 时,根本就没查询 first 的作用域。

global 语句

[编辑 | 编辑源代码]

global 语句用于说明变量为全局变量。

a = b = c = d = "Global"

def first():
    b = c = d = "First"
    def second():
        global c,d
        c = d = "Second"
    second()
first()
print(a,b,c,d)  #输出:Global Global Second Second

通过最后的输出,我们看出函数直接对全局变量进行了更改。

对于 global 语句声明的变量,不能在 global 语句之前调用该变量。以下的示例会报错:

def causeError():
    a = 1
    Global a

causeError()

nonlocal 语句

[编辑 | 编辑源代码]

nonlocal 语句用于说明变量不是局部变量。此时,Python 会按照查询规则,将变量设置为查询到的变量——到查询到全局作用域之前为止。我们看以下示例:

a = b = "Global"
def first():
    a = b = "First"
    def second():
        b = "Second"
        def third():
            nonlocal a,b
            a = b = "Third"
        third()
        print(a,b)
    second()
    print(a,b)

first()
print(a,b)

输出为:

Third Third
Third First
Global Global

third 中,遇到的各变量与作用域如下:

全局 first 的作用域 second 的作用域
a "Global" "First"
b "Global" "First" "Second"

nonlocal 语句查询到 first 中的 asecond 中的 b——从而之后更改的也是这两个。于是,各变量及作用域变为如下的情况:

全局 first 作用域 second 作用域
a "Global" "Third"
b "Global" "First" "Third"

所以输出会变成上面那样。

注意,nonlocal 语句不会查询全局作用域。如果一个变量只有全局作用域才有,此时使用 nonlocal 会报错。

global 语句相同,nonlocal 语句声明的变量,不能在 nonlocal 语句之前调用该变量。

return 语句

[编辑 | 编辑源代码]

return 语句可以返回一个值,从而向函数外传递信息。

def argFunc4():
    return 0

a = argFunc5()
print(a)    #输出:0

由于返回的可以是元组、列表、字典等任何数据结构。所以想返回多少个信息就能返回多少个信息。

特殊:可变序列

[编辑 | 编辑源代码]

函数在对外部的可变序列变量进行更改(而非直接赋值)时,在函数内的更改会表现在函数外。比如:

def add1(s):
    s.append(1)

iter = [0, 0]
add1(iter)
print(iter)    #输出:[0, 0, 1]

def badAdd1(s):
    s = s + [1]

badAdd1(iter)
print(iter)    #输出:[0, 0, 1]

由于函数 badAdd1 里面是一个赋值语句,所以对外部的序列变量没有影响。而 Add1 里面是一个更新列表的语句(而非直接赋值),所以它可以影响到外部的序列变量。

传入参数的方法

[编辑 | 编辑源代码]

我们考虑以下函数:

def func3(arg1, arg2, arg3):
    print(arg1, arg2, arg3)

传入参数的简单方法

[编辑 | 编辑源代码]

该函数有几种传入参数的方法。

按顺序传入

[编辑 | 编辑源代码]

我们可以不管变量名,直接按顺序传入参数。Python 会按顺序给参数赋值。

func3(1,2,3)  #输出:1 2 3

按关键字传入

[编辑 | 编辑源代码]

可以按参数名传入参数。此时,Python 会根据参数名给参数赋值。

func3(arg1 = 1, arg2 = 2, arg3 = 3)  #输出:1 2 3
func3(arg2 = 2, arg3 = 3, arg1 = 1)  #输出:1 2 3

混合按序、按关键字传入

[编辑 | 编辑源代码]
func3(1, arg3 = 3, arg2 = 2)  #输出:1 2 3
func3(1, 2, arg3 = 3)         #输出:1 2 3

可以混合上面的方法来传入参数。此时,按序传入的参数必须在按关键字传入的参数前面。

参数解包

[编辑 | 编辑源代码]

除上述方法外,以下还有两种方法,可以分别变换为按顺序传入与按关键字传入。

解包参数序列

[编辑 | 编辑源代码]

有时,我们希望把一个序列中的所有元素分别作为参数按序传入到函数中。此时,我们可以使用:

a = [1,2]
func3(*a,3)  #输出:1 2 3

在调用函数时,*a,3等同于1,2,3

解包参数字典

[编辑 | 编辑源代码]

类似,如果需要把一个字典中的所有键-值对分别作为参数传入到函数中。我们可以使用:

s = {arg2: 2, arg3: 3}
func3(arg1 = 1, **s}  #输出:1 2 3

处理参数的方法

[编辑 | 编辑源代码]

规定传入方式

[编辑 | 编辑源代码]

规定按顺序传入

[编辑 | 编辑源代码]

若在参数列表中加入 / ,则位于 / 之前的参数只能按序传入。比如,以下的代码会报错:

def func4(arg1, arg2, /, arg3):
    print(arg1, arg2, arg3)

func4(arg1 = 1, arg2 = 2, arg3 = 3)  #报错:TypeError: func4() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

规定按关键字传入

[编辑 | 编辑源代码]

若在参数列表中加入 * ,则位于 * 之后的参数只能按关键字传入。比如,以下的代码会报错:

def func4(arg1, arg2, *, arg3):
    print(arg1, arg2, arg3)

func4(1, 2, 3)  #报错:TypeError: func4() got some positional-only arguments passed as keyword arguments: 'arg1, arg2'

同时使用

[编辑 | 编辑源代码]

带默认值的参数(可选参数)

[编辑 | 编辑源代码]
def func5(arg1, arg2=2, arg3):
    print(arg1, arg2, arg3)

func5(1, arg3 = 3)  #输出:1 2 3

带默认值的参数可以不传入。如果不传入,则会直接使用默认值。这里 arg2 带有默认值 2,从而不用传入它。

在,本例中,如果想只传入两个参数,只用按顺序传入会导致 arg3 不被传入任何值,从而导致报错。因此,如果只传入两个参数,此处的 arg3 必须是用关键字传入的。

一般情况下,带默认值的参数会放在必须传入的参数后。这是为了在不传入可选参数的情况下,必须的参数也可以选择传入方式。

参数打包

[编辑 | 编辑源代码]

我们看以下示例:

def func6(arg, *args):
    print(args)

func6(1, 2, 3)  #输出:(2, 3)

可以看出,在按序传入参数1之后,对于多出来的、按序传入的参数,Python 把它们弄成一个元组,传入了*后面跟着的参数名称args里面。

我们看以下示例:

def func7(arg, **kwargs):
    print(kwargs)

func7(0, one = 1, two = 2)  #输出:{'one': 1, 'two': 2}

可以看出,在按序传入参数1之后,对于多出来的、按关键字传入的参数,Python 把它们弄成一个字典,传入了**后面跟着的参数名称args里面。

参数打包的提示

[编辑 | 编辑源代码]

由于元组和字典打包的是不同传入方式的参数,一个函数在处理时可以同时打包元组和字典。同时,按关键字传入的参数可以打乱顺序。因此,以下的方式也是可行的:

def func8(arg0, *args, arg1, **kwargs):
    print(arg0, args, arg1, kwargs)

func8(0, 1, 2, one = 1, arg1 = True, two = 2)
#输出:0 (1, 2) True {'one': 1, 'two': 2}

函数对象

[编辑 | 编辑源代码]

函数对象不过是支持在名称后添加 () 并输入参数进行操作的对象:函数也是对象。对于一般对象适用的操作,对函数对象也适用。比如:

函数可以作为参数传入另一个函数:

def do(func):
    func(1)

def todo(arg):
    print(list(range(arg,10)))

do(todo)  #输出:[1, 2, 3, 4, 5, 6, 7, 8, 9]

以上方式向函数 do 中传入函数 todo 作为参数。并执行了作为参数的函数。

函数可以打印,或作为变量进行赋值:

def MyFunc(): return "调用了函数"

a = MyFunc
print(a)      #输出:<function MyFunc at *>
print(MyFunc) #输出:<function MyFunc at *>
print(a())    #输出:调用了函数

MyFunc = "更改"
print(a)      #输出:<function MyFunc at *>
print(MyFunc) #输出:更改

赋值后,a 也支持 MyFunc 的一切操作。而 MyFunc 作为变量也可以用赋值语句修改。

函数可以作为返回值:

def getFunc():
	def f(): return "返回的函数"
	return f

a = getFunc()
print(a()) #输出:返回的函数

用于 list.sort()

[编辑 | 编辑源代码]

我们在学习列表的排序时,提到过 list.sort() 方法。注意到该方法在 Python 文档中写为: sort(*, key=None, reverse=False)。这代表该函数的所有参数必须是按参数名传入。

我们将在此处讲解该方法中,参数 key 的作用。

参数 key 需要传入的是一个函数,它以列表中的元素为参数,并返回一个可比较的值。sort 方法将用输出的、可比较的值来进行排序。比如:

def key0(elem):
    return elem[0]
def key1(elem):
    return elem[1]

s = [(2,2), (0,1), (1,0)]
s.sort(key=key0)  #用 key0 输出的值进行排序
print(s)          #输出:[(0, 1), (1, 0), (2, 2)]
s.sort(key=key1)  #用 key1 输出的值进行排序
print(s)          #输出:[(1, 0), (0, 1), (2, 2)]

装饰器

[编辑 | 编辑源代码]

注意到有些函数可以取函数作为参数,装饰器就是这样的一类函数。以下是一个简单的装饰器。

def SimpleDecorator(func):
    return "String"

装饰器的使用方法如下:

def MyFunc(): pass

MyFunc = SimpleDecorator(MyFunc)

对于需要对一些函数作统一的处理时,可以使用装饰器。

为了简便起见,Python 也支持用这种方法:

@SimpleDecorator
def MyFunc(): pass

这种方法和上面的方法没什么区别。

lambda 表达式

[编辑 | 编辑源代码]

lambda 表达式可以用于创建简单函数。我们看以下示例:

square_sum = lambda x,y=0: x**2+y**2

print(square_sum(3,4))  #输出:25

其中,冒号左边的内容为参数列表,参数列表可以设置默认值。而冒号右边的内容为函数的返回值。

这对于某些只使用一次的函数很有用。比如上面的 list.sort() 的示例,我们可以改为:

s = [(2,2), (0,1), (1,0)]
s.sort(key=lambda elem: elem[0])  #用 key0 输出的值进行排序
print(s)          #输出:[(0, 1), (1, 0), (2, 2)]
s.sort(key=lambda elem: elem[1])  #用 key1 输出的值进行排序
print(s)          #输出:[(1, 0), (0, 1), (2, 2)]

这种方法除了看起来更简洁美观之外,其作用和上面的“定义函数”一模一样。