函数

我们编写计算机程序时,常常碰到很多重复性的代码,它们通常用于实现某一功能,代码往往完全相同或仅有部分变量的值不同,对于这种情况,计算机程序设计语言都提供 "函数" 这一功能,以把代码重复而功能相同的程序源代码抽象出来,通过 "函数调用" 的形式实现总代码量的缩减,并且简化程序的结构。下面我们先从Python内置函数开始,讨论Python中关于函数的初级主题。

8.1 Python内置函数

Python中调用函数有两种形式,分别是:

函数名([逗号分隔的参数列表])       # 第一种形式
对象名.成员函数名([逗号分隔的参数列表]) # 第二种形式

Python的内置函数既有第一种形式的,也有第二种形式的。第一种形式的函数又叫做全局函数,我们以前讲过作用域的概念,知道作用域分为全局作用域、函数作用域、类作用域、块作用域等。"全局函数"中名词"全局"的意思是它在Python代码的全局作用域内是可见的。第二种形式的函数调用对于定义了成员函数的类的实例可用 ( 如列表类型的一个实例就是一个列表对象,如[ 1, 2, 3 ], 它的一个成员函数就是 list.append(item),这就是一个内置类型的成员函数调用 ),由于对象中的成员函数通常又叫作方法,因此,第二种形式也可写成:

对象名.方法名([逗号分隔的参数列表])

[ ]表示它括起来的内容可以省略,因此函数是可以没有参数的。下面是以上函数调用形式的一个例子:

li=list()
li.append(1)  # 第二种形式的函数调用
li.append(2)
len(li)       # 第一种形式的函数调用

这两种形式的函数调用的区别是:

只要提供了恰当的参数,第一种形式的函数调用都可顺利执行。而特定类型的对象只支持该类型所定义的成员函数,在给出对象名、方法名的同时还需给出恰当的参数。比如:

s="Jack is Henry's father"
li=["Jack", "Henry", "Jane"]
li.append("Mike")  # correct: list类型定义了append()方法
s.append("Mike")   # error: str类型未定义append()方法
len(s)    # correct: str可用作len的参数
len(li)   # correct: list可用作len的参数

Python 3.6中,内置的全局函数有70多个,可用如下方法找到这些内置全局函数的名字:

>>> import sys
>>> dir(sys.modules['builtins'])  # builtins: 内置的
>>>['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__', 'abs', 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']

我们看到,dir(sys.modules['builtins'])命令返回了一个列表,包含了Python中的全部内置名字, 其中包括全部内置全局函数,它们是这个列表中以小写字母开头的名字,上面的例子中共有72个。我们看看它们中的第一个: abs( ) 函数。在Python交互式命令行下,用help( )命令可以查看这个函数的帮助信息:

>>> help(abs)
Help on built-in function abs in module builtins:
# 模块builtins中的内置函数abs的帮助信息

abs(x, /)  # 函数原型
    Return the absolute value of the argument.
    # 返回参数的绝对值
(END) # 结束

这个函数的帮助信息其实很简单,对应英文的中文翻译都在例子中给出了。需要解释的是,内置全局函数都定义在一个名字为builtins的模块中,在Python代码中无须使用import导入这个模块,它是默认就被导入的,调用abs这样的全局函数无须使用builtins.abs(x) 这样的第二种函数调用形式,而直接使用abs(x)这样的第一种函数调用形式即可。

我们可以使用类似的方法查看Python中内置数据类型的方法名及其帮助信息。如:

>>> dir(list)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']

这个命令返回的结果中以小写字母开头的名字,都是list类型的方法名,可以用listobject_name.method_name([arguments])的形式调用。我们看看它的append方法的帮助信息:

>>> help(list.append)
Help on method_descriptor:
# 方法描述符的帮助信息
append(...)  # 函数原型
    L.append(object) -> None -- append object to end
    # L.append(object) 是函数调用的形式
    # -> 后面跟的是返回值类型,这里是None
    # -- 后面跟的是函数的功能,这里是: 将object添加到尾部

8.2 自定义函数

我们很容易就可实现自己的自定义函数:

def myabs( x ):
  #下面是文档字符串, 在Python交互式命令行下,可以用
  #help(myabs)看到这个信息,如上面我们对内置函数做的一样
  #代码运行过程中,可用print(myabs.__doc__)打印这个信息
  # myabs__doc__是这个文档字符串的内置变量名
  '''
  函数原型: myabs(x)
  功能: 求x的绝对值
  参数: x 
  返回值: 当x为int或float类型时,返回x的绝对值
         当x为其它类型时,返回None
  '''
  if isinstance(x, (int, float)):# isinstance()函数
                                 # 判断x是否为int或float
                                 # 类型,是:返回值为True
                                 #      否:返回值为False
    if x >= 0: return x
    else: return -x
  return None

请你运行下面的代码,看看结果是什么:

def myabs( x ):
  '''
  函数原型: myabs(x)
  功能: 求x的绝对值
  参数: x 
  返回值: 当x为int或float类型时,返回x的绝对值
         当x为其它类型时,返回None
  '''
  if isinstance(x, (int, float)):
    if x >= 0: return x
    else: return -x
  return None

print(myabs.__doc__)
print(myabs(5))
print(myabs(5.0))
print(myabs(-5))
print(myabs(-5.0))
print(myabs("str"))

这里我们实现了一个简单的自定义函数,代码中的def 表示这是一个函数定义,def 后面跟一个空格,再后面是函数名。Python中的函数名和变量名一样,都是以字母或下划线开头的,后面跟字母、数字或下划线的一个或多个字符的组合,字母区分大小写。函数名或变量名一般没有长度的限制,但由于实际的使用中,人们阅读长名字有困难,因此通常这些名字比较短小,但可采用一些命名规则使名字蕴含的信息比较丰富。函数名后面是一个圆括号括起来的参数列表,此例中只有一个参数。

函数名、圆括号和形参列表加在一起被称作函数原型,它们给出了调用函数所需的全部信息,也被称作函数的接口。所谓"接口",可以把它想象成中国古代木制家具中的榫卯结构的连接部,具有可接合的凹凸特征的连接部连接在一起,构成了整个家具。同样,设计合适的函数,使用适当的接口调用代码中的函数,将代码组合起来,就形成了整个计算机程序。在面向对象的程序设计方法出现以前,即过程式程序设计方法统治世界的时候,这就是计算机程序设计的全部任务。即使到了今天,过程式编程仍占据计算机程序设计的大部分内容。

可为函数提供默认参数,比如,下面的函数就提供了默认参数:

def cal(year=2017, month=1) # 一个日历函数,
                            # 默认返回2017年,1月的日历
    ...

调用上述函数时,既可给出全部参数,也可不带参数或给出部分参数:

cal()  # 返回2017年1月的日历
cal(2018) # 返回2018年1月的日历
cal(2018, 9) # 返回2018年9月的日历
# 但这样调用不会达到想要的效果
cal(10) # 这不会返回2017年10月的日历,
        # 而是会返回公元10年1月的日历
# 可以在调用函数时给出参数的名字
cal(year=2018)  # 返回2018年1月的日历
cal(year=2018, month=10) # 返回2018年10月的日历
cal(month=10, year=2018) # 给出名字的情况下,
                         # 可以打乱参数的顺序
cal(month=10) # 调用错误,不可以省略排在前面的默认参数

默认参数的用途,除可指定默认情况下的参数值之外,一个重要的用途是可以扩展原有的程序代码功能,而维持原有的用户代码不变。例如,在我们编写的函数库中,有一个求矩形面积的函数:

def area(length, width):
  return length*width

当用户对我们的函数库有新的需求,需要我们用同一个函数名( area ) 求长方体的表面积,但基于我们的函数库原来已经编写了大量的用户代码,即area()函数被已被用户大量用于求矩形面积,因此不能对我们的函数库的area函数的接口进行改动时,可以通过增加一个默认参数的方式修改area的代码:

def area(length, width, height=0):
  if height==0:
    return length*width
  else:
    return 2*(area(length, width)+
              area(length, height)+
              area(width, height))

这样,原来调用area() 求矩形面积的用户代码无须改动 ( 它们仍旧使用area( length, width) 进行调用 ) ,而用户也可以调用area(length, width, height)的新接口求长方体表面积。

根据函数的功能,函数可带或不带返回值。不带返回值时,返回None。

8.3 函数的参数

1. 位置参数

按照函数参数定义的位置和顺序进行传递的参数。如

def funcB(a, b):
    print(a)
    print(b)

调用的时候,我们需要使用函数名,加上圆括号扩起来的参数列表,比如 funcB(100, 99),执行结果是:

100
99

很明显,参数的顺序和个数要和函数定义中一致,如果执行funcB(100),Python会报错的:

TypeError: funcB() takes exactly 2 arguments (1 given)

2. 默认参数

我们可以在函数定义中使用参数默认值,比如

def funcC(a, b=0):
     print(a)
     print(b)

在函数funcC的定义中,参数b有默认值,是一个可选参数,如果我们调用funcC(100),b会自动赋值为0。

3. 可变参数

OK,目前为止,我们要定义一个函数的时候,必须要预先定义这个函数需要多少个参数(或者说可以接受多少个参数)。一般情况下这是没问题的,但是也有在定义函数的时候,不能知道参数个数的情况(想一想C语言里的printf函数),在Python里,带*的参数就是用来接受可变数量参数的。看一个例子

def funcD(a, b, *c):
    print(a)
    print(b)
    print("length of c is: %d " % len(c))
    print(c)

调用funcD(1, 2, 3, 4, 5, 6)结果是

1
2
length of c is: 4
(3, 4, 5, 6)

我们看到,前面两个参数被a、b接受了,剩下的4个参数,全部被c接受了,c在这里是一个tuple。我们在调用funcD的时候,至少要传递2个参数,2个以上的参数,都放到c里了,如果只有两个参数,那么c就是一个empty tuple。

当已经有一个tuple,将它作为参数传递给定义了可变参数的函数时,不需要将tuple的元素一个一个的写入,而是只需在tuple前加上一个*即可,如:

t = ( 3, 4, 5, 6 )
funcD(1, 2, *t)

程序运行的结果和前面的例子相同。

4. 关键字参数

上面的例子里,调用函数的时候,传递的参数都是根据位置来跟函数定义里的参数表匹配的,比如funcB(100, 99)和funcB(99, 100)的执行结果是不一样的。在Python里,还支持一种用关键字参数(keJaneord argument)调用函数的办法,也就是在调用函数的时候,明确指定参数值赋给哪个形参。比如还是上面的funcB(a, b),我们通过这两种方式调用

funcB(a=100, b=99)

funcB(b=99, a=100)

结果跟funcB(100, 99)都是一样的,因为我们在使用关键字参数调用的时候,指定了把100赋值给a,99赋值给b。也就是说,关键字参数可以让我们在调用函数的时候打乱参数传递的顺序!

另外,在函数调用中,可以混合使用基于位置匹配的参数和关键字参数,前题是先给出固定位置的参数,比如

def funcE(a, b, c):
    print(a)
    print(b)
    print(c)

调用funcE(100, 99, 98)和调用funcE(100, c=98, b=99)的结果是一样的。

如果一个函数定义中的最后一个形参有**(双星号)前缀,所有正常形参之外的其他的关键字参数都将被放置在一个字典中传递给函数,比如:

def funcF(a, **b):
    print(a)
    for x in b:
        print(x + ": " + str(b[x]))

调用funcF(100, c='你好', b=200),执行结果

100
c: 你好
b: 200

大家可以看到,b是一个dict对象实例,它接受了关键字参数b和c。

和关键字参数可以接收一个tuple作为参数类似,定义了可变关键字参数的函数也可以接收一个dict作为参数,如,定义以下函数:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

可以像下面这样调用:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

5. 命名关键字参数(不是可变参数,但需要一个可变参数作为分隔)

对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。仍以person()函数为例,我们希望检查是否有city和job参数:

def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

但是调用者仍可以传入不受限制的关键字参数:

>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):
    print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符后面的参数被视为命名关键字参数。调用方式如下:

>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

def person(name, age, *args, city, job):
    print(name, age, args, city, job)

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
  File "", line 1, in 
TypeError: person() takes 2 positional arguments but 4 were given

由于调用时缺少参数名city和job,Python解释器把这4个参数均视为位置参数,但person()函数仅接受2个位置参数。

命名关键字参数可以有缺省值,从而简化调用:

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)

由于命名关键字参数city具有默认值,调用时,可不传入city参数:

>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer

使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个作为特殊分隔符。如果缺少,Python解释器将无法识别位置参数和命名关键字参数:

def person(name, age, city, job):
    # 缺少 *,city和job被视为位置参数
    pass

6. 参数组合

在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
比如定义一个函数,包含上述若干种参数:

def f1(a, b, c=0, *args, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
    print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

在函数调用的时候,Python解释器自动按照参数位置和参数名把对应的参数传进去。

>>> f1(1, 2)
a = 1 b = 2 c = 0 args = () kw = {}
>>> f1(1, 2, c=3)
a = 1 b = 2 c = 3 args = () kw = {}
>>> f1(1, 2, 3, 'a', 'b')
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {}
>>> f1(1, 2, 3, 'a', 'b', x=99)
a = 1 b = 2 c = 3 args = ('a', 'b') kw = {'x': 99}
>>> f2(1, 2, d=99, ext=None)
a = 1 b = 2 c = 0 d = 99 kw = {'ext': None}

最神奇的是通过一个tuple和dict,你也可以调用上述函数:

>>> args = (1, 2, 3, 4)
>>> kw = {'d': 99, 'x': '#'}
>>> f1(*args, **kw)
a = 1 b = 2 c = 3 args = (4,) kw = {'d': 99, 'x': '#'}
>>> args = (1, 2, 3)
>>> kw = {'d': 88, 'x': '#'}
>>> f2(*args, **kw)
a = 1 b = 2 c = 3 d = 88 kw = {'x': '#'}

所以,对于任意函数,都可以通过类似func(*args, **kw)的形式调用它,无论它的参数是如何定义的。

8.4 函数返回值的语义和作用

Python规定函数必须有返回值,当函数没有显式返回时,它默认返回None。返回值通常有两种语义,其一是通过返回值返回函数的运算结果,其二是通过返回值返回函数的错误代码。实践中,常常综合使用这两种语义。下面举一个返回值为函数运算结果的例子:

def segment(x):
    if x>0: return 1
    elif x=0: return 0
    else: return -1

上述函数代码定义了一个分段函数:

下面再给出一个综合使用两种语义的例子:

def frac(x):
    if x != 0: return 1/x
    else: return None

该范例定义了一个反比例函数,

当x不等于0时,该函数有意义,将返回1/x,当x等于0时,该函数返回None以表示计算出错。该函数的返回值兼具返回正常运算结果和表示错误状态的语义。

下面再举一个仅返回错误状态的例子:

def recivedMsgOk(receivedMsg):
    if len(receivedMsg): return True
    else: return False

该函数测试receivedMsg是否有效,如果有效,返回True,否则,返回False。关于这个函数,还需要注意函数名和参数名等名字的命名方式,它们都由若干独立的词组成,receivedMsgOk中有三个词,receivedMsg中有两个词,它们的第一个词由小写字母开头,后面跟小写字母,后续的词都由大写字母开头,后面跟小写字母。这种命名法使得名字更易读且富有含义。名字中的Msg是Message的简写,程序源代码中常用这种方法简写词汇,使名字更为短小精悍。

你可能感兴趣的:(函数)