跳转至

2.11.函数

函数简介

关键字 def 引入了一个函数定义。后面必须跟上函数名和在圆括号里的参数序列。函数体从第一行开始,并且一定要缩进。

def func():
    pass

函式体的第一个语句可以是字符串。这个字符串就是函数的文档字符串(docstring)。有很多能将文档字串自动转换为在线或可打印文档的工具,或让用户在代码中交互地浏览它的工具。

执行函数会引入局部命名空间。所有在函数中被赋值的变量和值都将存储在局部命名空间中。

变量引用会首先在函数的局部命名空间里寻找,然后才是闭包函数的局部命名空间,再然后是全局命名空间,最后是内置命名空间。在函数中可以引用全局变量,但是不可直接对全局变量重新赋值(除非用 global 语句进行声明)。具体细节可参考《3.2.命名空间和作用域》

根据其它语言的经验,你可能会指出func不是一个函数,而是一个程序,因为它不返回值。事实上,即使没有写 return 语句的函数也会返回一个值——None。如果要唯一输出的值是None,那么解释器不会打印出来。如你实在想看看这个值,可以使用 print() 函数。

函数传参

刚学Python的人可能会疑惑,Python函数传参,到底是传值,还是传引用,甚至是传指针?可以确定的是,最后一种情况(即传指针)是不可能的,因为Python没有明显使用指针的地方,可以说Python本身不支持使用指针(除非通过导入ctypes模块来使用指针)。

排除了传指针的情况,这个问题依然有很多争论,有人认为是传值,有人认为是传引用,有人认为是两者都有,也有人认为两者都不是,而是传赋值。

这个问题的答案实际上取决于传值和传引用的定义。归根结底,对此的争论归结为对定义的分歧。在计算机领域中,人们争论某种东西往往是因为他们对术语的含义有不同的看法,人们往往会使用模糊且不一致的术语来定义一个概念。

如果根据C++中基于语义的定义:

  • 如果将变量传递给函数,则=对函数内部的参数进行简单赋值的效果与对函数外部的传递变量进行简单赋值的效果相同,即为按引用传参
  • 如果将变量传递给函数,=对函数内部的参数进行简单赋值对函数外部的传递变量没有影响,则为按值传参

那么,Python的函数传参可以认为是传引用和传值都有。

如果根据上述定义,并且对Python的内置类型按原子类型(值类型)和引用类型分类的话,那么Python的函数传参可以认为是传值。

如果基于以下事实:

  • Python赋值的本质是创建别名
  • 引用即是别名

则可以认为,Python的函数传参是传引用,也可以认为Python的函数传参即是赋值。

在这里,我们没必要关心使用哪个传参术语,只需要知道Python传参的本质是assignment即可。 传参的行为和赋值的行为是一样的,具体细节可以参考《2.10.赋值与深拷贝》。

在对可变对象的参数进行操作前,最好先深拷贝参数的值,否则可能会影响函数外部的命名空间。

参考
  1. Are arguments passed by value or by reference in Python?
  2. In Python, if I type a=1 b=2 c=a c=b, what is the value of c? What does c point to?
  3. 从符号表来理解指针和引用
  4. C以及Python中的引用,指针的区别
  5. Pointers in Python: What's the Point?

函数作为参数

在Python中,函数是可以直接作为参数传入另一个函数的。这似乎没什么特别的,如果您在学习C、C++、Python这些语言之前,就已经在使用R语言,您可能更会觉得这是很符合直觉的事情。但实际上,有一些语言是不支持直接将一个函数作为参数的,例如C、C++、C#等。

Python之所以可以直接将函数作为参数,是因为在Python中,函数也是一个对象。 如果在一个编程语言中,函数也是一个对象,我们可以说这个语言它具有 first-class function。这意味着,这门编程语言将函数视作 first-class citizen。first-class citizen 这个概念最早由英国计算机科学家 Christopher Strachey 在20世纪60年代提出。和大多计算机术语一样,first-class citizen 并不是一个明确的概念。first-class citizen最著名的定义可能是Gerald Jay Sussman 和 Harry Abelson 在《Structure and Interpretation of Computer Programs》中提出的:

如果一个编程元素具有如下特征,称之为 first-class citizen

  • 它可以赋值给变量。
  • 它可以作为参数传递给程序。
  • 它可以作为程序的返回值。
  • 它可以被包含在数据结构之中。

基本上,这意味着您可以使用此编程语言元素来完成您可以使用该编程语言中的所有其他元素来进行的所有操作。

参考
  1. What are first-class objects in Java and C#?

  2. What is first class function in Python

  3. Are functions first class objects in python?

  4. 头等公民

  5. First-class Everything

为什么在一些编程语言中,函数不是对象呢?本文无法对此问题进行深入的探究,但可以给出一个简短的解释。函数与对象的区别归结于指向函数的指针和指向对象的指针是严格区分的,这与计算机的设计方式有关。

参考
  1. Why is a function not an object?

参数

函数有四种参数概念:位置参数、默认参数、关键字参数、可变参数。

其中,位置参数、关键字参数是传入函数参数时的概念;默认参数、可变参数是定义函数参数时的概念。

函数有三种合法调用形式:

  1. 仅给出强制参数
  2. 给出所有强制参数和部分可选参数
  3. 给出所有参数

位置参数

调用函数时可以省略形参名字,根据函数定义的参数位置来传递参数。传入的实参顺序必须和函数定义的形参顺序一致,而且数量要相等。

def print_hello(name, sex):
    sex_dict = {1: u'先生', 2: u'女士'}
    print('hello %s %s, welcome to python world!' \
          %(name, sex_dict.get(sex, '先生')))

print_hello('Jack', 1)
hello Jack 先生, welcome to python world!

关键字参数

函数调用时,可以通过key=value形式指定,即"形参=实参",此时关键字参数不需要按顺序指定。良好的形参命名加上关键字的形式传参,可以让代码更易读。

# 以下是用关键字参数正确调用函数的实例
print_hello('tanggu', sex=1)
print_hello(name='tanggu', sex=1)
print_hello(sex=1, name='tanggu')
hello tanggu 先生, welcome to python world!
hello tanggu 先生, welcome to python world!
hello tanggu 先生, welcome to python world!
# 以下是错误的调用方式
print_hello(1, name='tanggu')
print_hello(name='tanggu', 1)
print_hello(sex=1, 'tanggu')

通过上面的代码可以发现:有位置参数时,位置参数必须在关键字参数的前面,但关键字参数之间不存在先后顺序

默认参数值

定义函数时,为参数提供了默认值,调用函数时该参数可传可不传。注意:在函数定义和调用函数时,所有位置参数必须写在带默认值的参数前面。

# 正确的默认参数定义方式--> 位置参数在前,默认参数在后
def print_hello(name, sex=1):
    ....

# 错误的定义方式
def print_hello(sex=1, name):
    ....

# 调用时不传sex的值,则使用默认值1
# print_hello('tanggu')

# 调用时传入sex的值,并指定为2
# print_hello('tanggu', 2)

可变参数

有时候在定义函数时,我们不确定函数调用时会传入多少个参数(包括传入0个参数的情况)。此时,可以*args**kargs的形式收集可变数量的位置参数或关键字参数。

收集可变位置参数

def func(arg1, arg2, *args):
    ....

# 调用形式 
func(a, b)
func(a, b, c)
func(a, b, c, d)

args是一个元组,传入的可变位置参数都会被按顺序放入args之中。之后,我们可以从args变量中获取传入的参数。

包裹关键字传递

def func(arg1, arg2, *args, **kargs):
    ....

# 调用形式 
func(a, b, c=1)
func(a, b, c, d=1, e=2)

kargs是一个字典,kargs收集了所有的可变关键字参数。

解包可变参数列表

***也可以在函数调用的时候使用,称之为解包裹(unpacking)。在传入元组时,元组每个元素按顺序对应一个位置参数。

def print_hello(name, sex):
    print name, sex

# 调用形式 
args = ('tanggu', '男')
print_hello(*args)

在传入字典时,字典的每个键值对作为一个关键字参数传递给函数。

def print_hello(kargs):
    print kargs

# 调用形式 
kargs = {'name': 'tanggu', 'sex', '男'}
print_hello(**kargs)

位置参数、默认参数、可变参数的混合使用

顺序是:位置参数、默认参数、可变位置参数、(可变)关键字参数,定义和调用都应遵循这个顺序。

def func(name, age, sex=1, *args, **kargs):
    print name, age, sex, args, kargs

# 调用形式 
func('tanggu', 25, 2, 'music', 'sport', class=2)

限定传参形式

有时候,我们可能想要限定传入参数的形式:

  1. 只能传入位置参数(Positional-Only Parameters),只使用位置参数,程序性能可能会更好。
  2. 只能传入关键字参数(Keyword-Only Arguments),只使用关键字参数,代码可读性会更好。

那么,可以在函数定义的形参列表中加入/*进行限制。/表示正斜杠之前的参数必须传入位置参数;*表示星号之后的参数必传入关键字参数。

# arg的传参形式没有限制
def standard_arg(arg):
    print(arg)

# arg必须以位置参数的形式传入
def pos_only_arg(arg, /):
    print(arg)

# arg必须以关键字参数的形式传入
def kwd_only_arg(*, arg):
    print(arg)

# pos_only必须传入位置参数 standard没有限制 kwd_only必须传入关键字参数
def combined_example(pos_only, /, standard, *, kwd_only):
    print(pos_only, standard, kwd_only)

文档字符串

Python 的文档字符串有一些约定俗成的规范,可以参考 Python 的官方文档。Python 的文档字符串不只一种风格,主要有三种风格:

  1. NumPy style
  2. Google style
  3. reStructuredText style

reStructuredText风格的docstring(Pycharm默认的风格):

def func(arg1, arg2):
    """Summary line.

    Extended description of function.

    :param arg1: Description of arg1
    :type arg1: int
    :param arg2: Description of arg2
    :type arg2: str
    :returns: Description of return value
    :rtype: bool

    """
    return True

Google风格的docstring(倾向水平,短而简单的文档):

def func(arg1, arg2):
    """Summary line.

    Extended description of function.

    Args:
        arg1 (int): Description of arg1
        arg2 (str): Description of arg2

    Returns:
        bool: Description of return value

    Raises:
        Possible exception: Description of exception
    """
    return True

NumPy风格的docstring(倾向垂直,长而深的文档;Spyder默认的风格):

def func(arg1, arg2):
    """Summary line.

    Extended description of function.

    Parameters
    ----------
    arg1 : int
        Description of arg1
    arg2 : str
        Description of arg2

    Returns
    -------
    bool
        Description of return value

    """
    return True

具体可以参考Support for NumPy and Google style docstrings,这里是中文版本