爱湃森学院

《Python入门》公开课 - 函数

2018-07-26

本节是 《Python入门》课程中的一节,课程购买链接(PC访问需要微信扫码)

购买课程请扫码:

现实世界的程序很快就会变得越来越大,越来越复杂。需要一些方法把它们分成较小的部分进行组织,这样更易于编写,也更容易阅读。这样有助于做代码复用,减少冗余代码,让结构清晰。

把程序分解成较小的部分,主要有3种方法。

  1. 函数(function)
  2. 对象(object)
  3. 模块(module)

本节我们先学习函数。函数是带名字的代码块,可以把多个逻辑封装起来。这样就可以在程序中可以不止一次的运行它。

函数的一般格式如下:

def <name>(arg1, arg2, ..., argN):
    <statements>
    return <value>

看一个真实的函数:

def hello():
    print('Hello World!')
    return True

这就是一个函数,def语句生成一个函数对象并赋值一个函数名,函数名字叫做hello。括号内可以传入一些参数内容,当然也可以不传,就是一个空括号,表示它不需要任何其他额外信息就能完成工作。另外在函数定义的最后要加上冒号。冒号告诉 Python 接下来是一个代码块,这个代码块是一个缩进的函数体,每次函数调用就是执行这部分内容,

代码块包含2个语句,先打印hello world,最后一句return表示返回,也就是函数返回值为True

调用函数是指运行函数里的代码。定义而不调用,那么函数内的代码块永远不会执行,调用函数时要使用函数名和一对括号。

In : hello()
Hello World!
Out: True

向函数传递参数

可以给函数传递参数,这样调用函数时可以让结果变得不一样

In : def hello(name):
....:     print(f'Hello, {name}!')
....:

In : hello('Amy')
Hello, Amy!

In : hello('Chris')
Hello, Chris!

调用时无论传入什么样的名字,都会打印相应的输出。

函数参数数量可以任意,完全看业务需要。

这里要引入实参和形参。

形参:函数定义中在内部使用的参数,这是函数完成其工作所需的一项信息,在没实际调用的时候,函数用形参来指代。 实参:是指调用函数时由调用者传入的参数,这个时候形参指代的内容就是实参了。

上面的例子中name就是形参,amy和chris是实参

实参类型

调用函数时,可以指定两种类型的参数:位置参数(positional argument)和关键字参数(keyword argument).

位置参数

位置参数又称为非关键字参数(non-keyword argument),这种参数的指定方式有两种:直接以值的形式和以*开头的可迭代对象:

In : def hello(name):
....:     print(f'Hell, {name}!')
....:

之前用的hello这个函数,name就是位置参数。一个*加上形参名的函数表示这个函数实参个数不定:

In : def hello(*names):
...:     print(names)
...:

In : hello()
()

In : hello(1)
(1,)

In : hello(1, 2)
(1, 2)

如果不能肯定参数的个数,就可以使用这种变长元组参数。另外,位置参数的顺序很重要,实参会直接对应形参位置。

强制关键字参数

Python 3.6 添加了一个新功能,就是强制关键字参数。使用强制关键字参数会比使用位置参数表意更加清晰,程序也更加具有可读性,那么可以让这些参数强制使用关键字参数传递,可以将强制关键字参数放到某个参数或者单个后面就能达到这种效果:

In : def recv(maxsize, *, block):
....:     pass
....:

In : recv(1024, True)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-53-8e61db2ef94b> in <module>()
----> 1 recv(1024, True)

TypeError: recv() takes 1 positional argument but 2 were given

In : recv(1024, block=True)

关键字参数

关键字参数的指定方式也有两种:以 name=value (名称=值)的方式和 以**开头的字典。关键字参数可以让函数更加清晰、容易使用,也无需考虑函数调用中的实参顺序,因为Python知道各个值该存储到哪个形参中。

In : def hello(name='World'):
....:     print(f'Hello, {name}!')
....:

In : hello()
Hello, World!

In : hello('Amy')
Hello, Amy!

In : hello(name='Chris')
Hello, Chris!

也就是name有个默认值,如果不传入name就使用默认的world。

**表示变长关键字参数。在常规参数和默认参数绑定完成后,如果还有额外(0个或多个)的关键字参数,则为变长关键字参数,将会把这些多余的“关键字参数” 以字典的形式搜集到一起。

混合使用

调用函数时,可以混合使用位置参数和关键字参数,但是位置参数必须位于关键字参数之前。

In : def hello(name, default='World'):
....:     print(f'Hello, {name or default}!')
....:

要混合使用上述四种类型的形参,则这几种形参类型的排列顺序必须从左到右依次为:常规参数,默认参数,变长元组参数,变长关键字参数。

In : def func(a, b=0, *args, **kwargs):
   ....:     print('a =', a, 'b =', b, 'args =', args, 'kwargs =', kwargs)
   ....:

In : func(1, 2)
a = 1 b = 2 args = () kwargs = {}

In : func(1, 2, d=4)
a = 1 b = 2 args = () kwargs = {'d': 4}

In : func(1, 2, 3)
a = 1 b = 2 args = (3,) kwargs = {}

In : func(1, 2, 3, d=4)
a = 1 b = 2 args = (3,) kwargs = {'d': 4}

返回值

之前的例子中都是打印输出,实际开发中通常会让函数在执行一系列逻辑之后,返回一个或一组值。函数返回的值被称为返回值。在函数中,可使用return语句将值返回到调用函数的代码行。之前看到的函数都没有显式的使用return,可以理解为返回值为None。

In : def add(a, b):
....:     return a + b
....:

In : add(1, 2)
Out: 3

返回值就是函数的执行结果。返回值不仅可以是单个,也可以是一组:

In : def partition(string, sep):
....:     return string.partition(sep)
....:

In : partition('/home/dongwm/bran', '/')
Out: ('', '/', 'home/dongwm/bran')

参数为函数:

函数实参除了可以是常见的数据结构。也可以是函数:

In : def hello(name):
print(f'Hello {name}!')
....:

In : def test(func, name='World'):
....:     func(name)
....:

In : test(hello, 'Amy')
Hello Amy!

本地变量/全局变量

本地变量

在函数定义内声明的变量就是本地变量,也叫局部变量,它们与函数外具有相同名称的其他变量没有任何关系,即变量只是在函数内可见的。我们看下面的例子

In : def run(name):
...:     s = f'{name}'
...:     for x in range(5):
...:         if x == 3:
...:             return
...:     print(s)
...:

In : run('Test')

第二行的s是被赋值过的,所以s是一个本地变量, 第三行for循环将元素赋值给变量x, 所以x是一个本地变量, 第一行参数name也是通过赋值被传入的,所以也是本地变量。

这些本地变量会在函数调用时出现,在函数退出时消失。

全局变量

全局变量有更大的作用域,它可以在程序的任何地方使用这个变量。

In : g = 0

In : def run():
...:     print(g)
...:

In : run()
0

In : def run():
....:     g = 2
....:

In : g
Out: 0

可以感受到g在函数内也可以访问的到。另外函数内的修改没有影响这个全局变量。

现在我演示一个初学者常见错误使用全局变量的例子:

In : g = 0

In : def run():
....:     print(g)
....:     g = 2
....:     print(g)
....:

In : run()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-14-157c9bda2cd6> in <module>()
----> 1 run()

<ipython-input-13-8b2ff1ac73b1> in run()
1 def run():
----> 2     print(g)
3     g = 2
4     print(g)
5

UnboundLocalError: local variable 'g' referenced before assignment

在函数内操作全局变量很常见,但现在Python抛出了一个异常,错误提示局部变量g在赋值前被应用,也就是该变量没有定义就使用它,错误发生在第二行,也就是第一次print g的时候。但是一开始已经对g赋值了。但是为了print时候说是本地变量g没有被定义呢?这是因为在函数内第二行,想把2赋值给g,如果函数内部的变量名第一次出现,且出现在=前面,即被视为定义一个局部变量,不管全局域中有没有用到该变量名,函数中使用的将是局部变量。我们在感受下:

In : def run():
....:     g += 2
....:     print(g)
....:

In : run()
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-16-157c9bda2cd6> in <module>()
----> 1 run()

<ipython-input-15-573471f9c3b9> in run()
    1 def run():
    ----> 2     g += 2
    3     print(g)
    4

UnboundLocalError: local variable 'g' referenced before assignment

如果确实想这么用,怎么办呢?可以使用global关键字:

In : def run():
....:     global g
....:     g += 2
....:     print(g)
....:

In : run()
2

In : g
Out[19]: 2

In : run()
4

In : g
Out[21]: 4

但是请注意,global语句会让函数内的赋值影响到全局,这种修改是很隐晦的,不具备可读和可追溯性。global语句我是非常不推荐使用的,除非你很清楚确实需要使用它,而且我实际工作中总结,真的基本不需要用它,如果你不得不用它,往往是由于程序设计有问题,或者在滥用。

变量在全局域中有定义,而在局部没有定义,则会使用全局变量,如果局部要定义,定义前不要使用这个变量。否则需要引入关键字global

作用域(scope)

上面说的local和global是不同的作用域。作用域简单说就是一个变量的命名空间,这个空间里面可以创建改变和查找变量名。所以变量赋值的地方决定了它的作用域。Python的变量作用域中分为四个级别,简称为:BGEL,作用域的级别依次升高,级别最高的是Local,如果该变量在Local中已经声明并赋值,将优先使用Local中的变量对应的值:

  1. B:build-in 系统固定模块里面的变量,也叫系统变量,比如int,这些变量可以通过builtins模块获取
  2. G:global 全局变量,在单个程序文件里面都可用, 它位于文件代码的顶级
  3. E:enclosing 嵌套的父级函数的局部作用域,就是包含此函数的上层函数的局部作用域
  4. L:local 局部作用域,即为函数中定义的变量
In : import builtins

In : ', '.join((i for i in dir(builtins) if i.islower() and '_' not in i))
Out[33]: 'abs, all, any, ascii, bin, bool, bytearray, bytes, callable, chr, classmethod, compile, complex, copyright, credits, delattr, dict, dir, divmod, dreload, enumerate, eval, exec, 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, range, repr, reversed, round, set, setattr, slice, sorted, staticmethod, str, sum, super, tuple, type, vars, zip'

这个列表太长了,省略了一些,在这里只展示一些比较常见的。builtins模块里面的这些函数和类等内容构成了内置作用域。这些函数和类可以直接使用,Python会从这里找到他们

在Python2里面builtins模块叫做__builtin__,

>>> import __builtin__
>>> dir(__builtin__)
...

在Python3中双下划线的这种用法依然可以使用,而且不需要导入就能使用.

E和L是相对的,E中的变量相对上层来说也是L.

E嵌套作用域,在local中取值,但是local中没有,就会去E里面找。我们感受一个例子:

In : g = 0

In : def run():
...:     g = 2
...:     def run2():
...:         print(g)
...:     return run2
...:

In : f = run()

In : f()
2

这个例子值得回味,第一函数内嵌套了函数run2, run2内使用变量g,但是run2里面没有,所以它从内向外找,就找到了run的作用域。第二注意run函数的最后一句,它返回了run2这个函数,所以 f = run(),就是把run2函数赋值给f,执行f就是执行run2函数,但是可以访问run函数的作用域。

闭包Closure

上个例子里面出现了一个函数返回了嵌套在它里面的函数的用法,我们再看一个重复的帮助记忆:

In : def maker(n):
...:     def action(m):
...:         return m * n
...:     return action
...:

In : f = maker(3)

In : f(2)
Out: 6

In : g = maker(10)

In : g(2)
Out: 20

这个maker函数有一个特点,它返回的是内嵌函数action的一个引用,它记忆了嵌套作用域里面形参n,这个n不是action的本地变量。在调用中,赋给f的函数记忆了n的实参3,赋给g的函数记忆了n的实参10,f和n这2个函数有自己的状态

action函数就是闭包。我引用流畅的Python里面对闭包的定义:

闭包指延伸了作用域的函数,其中包含函数定义体中引用,但是不在定义体中定义的非全局变量... 它能访问定义体之外定义的非全局变量。

另外一个知识点,maker函数叫作工厂函数,就像一个生产函数的工厂,如上例,f和g都是它生产的函数。

nonlocal

上面我们说到了,如果不用global关键字,函数内对全局变量的定义不会影响函数外。包含在嵌套作用域里面:

In : def run():
....:     g = 2
....:     def run2():
....:         g = 4
....:         print('inner ---> ', g)
....:     run2()
....:     print('outer --->', g)
....:

In : run()
inner --->  4
outer ---> 2

可以看到run2里面g为4,在run里面g为2. 怎么修改嵌套作用域的变量呢,Python3新增了一个关键字nonlocal:

In : def run():
....:     g = 2
....:     def run2():
....:         nonlocal g
....:         g = 4
....:         print('inner ---> ', g)
....:     run2()
....:     print('outer --->', g)
....:

In : run()
inner --->  4
outer ---> 4

这样在run2中就可以对父级作用域里面的变量的g做修改了。

到这里,我再重复的进一步总结,赋值的变量名如果不使用global和nonlocal关键字声明为全局变量或者非本地变量,均为本地变量。

匿名函数

有些时候,不需要显式地定义函数,直接传入匿名函数更方便。可以用关键字lambda创建一个匿名函数,也就是没有名称的函数。匿名函数的格式是这样的:

lambda 参数: 表达式

关键字lambda说明它是一个匿名函数,冒号前面的变量是该匿名函数的参数,冒号后面是函数的返回值,注意这里不需使用return关键字。

使用匿名函数不用写def语句 不用费力的去想名字,很适用于创建一些临时性的,小巧的函数。

我们举个例子,现在有一个列表,全部元素都是数字,想把每个元素都乘以2最后成为一个新的列表。最传统的函数写法:

In : def double(n):
...:     return n * 2
...:
In : double(10)
Out: 20

如果是匿名函数就写成下面这样:

In : f = lambda n: n * 2

In : f(10)
Out: 20

这种对一个序列每个元素做一些处理生成新序列的需求很常见的,那么可不可以写的更简洁和优美呢。答案显然是有的,用匿名函数之前,我们先学习几个常见的高阶函数。

高阶函数英文叫Higher-order function,是指把函数作为参数传入的函数,顺便插一句函数式编程就是指这种高度抽象的编程范式。本节先聊5个最常用的高阶的函数,分别是map/filter/sum/zip/reduce。

map

In : l1 = [1, 3, 4]

In : l2 = []

In : for i in l1:
....:     l2.append(double(i))
....:

In : l2
Out: [2, 6, 8]

map 接收一个函数和一个可迭代的对象,并通过把函数依次作用在序列的每个元素上,得到一个新的可迭代的对象并返回。比如上例可以这么写

In : rs = map(double, l1)

In : rs
Out: <map at 0x105986748>

In : list(rs)
Out: [2, 6, 8]

用map就是一句话搞定,它返回的是一个迭代器,如果想看到全部内容可以转换成列表。map接收的可迭代的对象不仅是列表,元组,字典等都可以:

In : list(map(double, {'a':1, 'b': 2}))
Out: ['aa', 'bb']

filter

filter函数接收一个函数和一个可迭代的对象,这个函数的作用是对每个元素进行判断,返回True或False,返回False会被自动过滤掉,返回由符合条件元素组成的新可迭代的对象。

In : def is_odd(x):
....:    return x % 2 == 1
....:

In : rs = filter(is_odd, l1)

In : rs
Out: <filter at 0x105986d68>

In : list(rs)
Out: [1, 3]

由于Python布尔值的设置,如果像过滤那些布尔值为False的对象,可以用下面的方法:

In : list(filter(None, [1, '', {}, (), False, None, set()]))
Out: [1]

reduce

reduce函数接收一个函数和一个可迭代的对象,但行为和 map()不同,reduce()传入的函数必须接收两个参数,reduce对可迭代的对象的每个元素反复调用函数,并返回最终结果值。求合可以这些写:

In : def add(a, b):
....:     return a + b
....:

In : from functools import reduce

In : reduce(add, [1, 2, 3])
Out: 6

reduce在Python2可以直接使用,但是在Python3中被放进了functools模块,需要先导入这个模块, 模块里面就包含了很多高阶函数。这个求合的例子里面reduce,先把1和2当做add的参数,执行返回3,再把3当做a, 列表最后一个元素3当做参数b, 再调用add函数,最后返回总结果6

reduce还可以接受三个参数,作为计算的初始值10。

In : reduce(add, [1, 2, 3], 10)
Out: 16

匿名函数续

回到匿名函数问题上,现在我们可以用map了,但是要创建函数double

In : def double(n):
....:     return n * 2
....:
In : map(double, l1)

用匿名函数会更直观:

In : list(map(lambda x: x * 2, l1))
Out: [2, 6, 8]

匿名函数试用场景很多,比如之前介绍列表的sort方法时候没有说它接受参数key来决定排序方案. 我们看个复杂一点的列表

In : l = [[2, 4], [1, 1], [9, 3]]

In : sorted(l)
Out: [[1, 1], [2, 4], [9, 3]]

这个列表每个元素都是一个列表,sorted函数默认就按元素内容从左到右对比,由于这三个元素的第一项的值各不相同,会按照每项第一个元素从小到大这种升序排了。

如果希望安装元素的第二项的大小来拍呢?用匿名函数就很方便了

In : sorted(l, key=lambda x:x[1])
Out: [[1, 1], [9, 3], [2, 4]]

key描述的就是用来做排序的每项元素的那个部分。匿名函数的x,就是指代每项元素,x[1] 就是元素的第二项的意思。匿名函数的表达式部分非常灵活,还可以使用对象属性,调用方法甚至混用:

In : l3 = ['/boot/grub', '/usr/local', '/home/dongwm']

In : sorted(l3, key=lambda x: x.rsplit('/')[2])
Out: ['/home/dongwm', '/boot/grub', '/usr/local']

zip

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。

如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同,多出来的部分元素被忽略掉。

利用*号操作符,可以将元组解压为列表。

In : list(zip(*zip(a, b)))
Out: [(1, 2, 3), (4, 5, 6)]

第二次用zip可理解为解压,返回二维矩阵式

sum

sum是求合函数。

In : sum([1, 2, 3])
Out: 6

In : sum([1, 2, 3], 10)
Out: 16

他接收第二个参数,可以传入一个初始值,默认是0,求合的结果基于这个初始值。另外sum一个有意思的用法是可以把嵌套类型的元素扁平化,就是从嵌套结构中剥离出来

In : sum([[1, 2], [3, 4]], [])
Out: [1, 2, 3, 4]

开发陷阱

可变默认参数

In : def append_to(element, to=[]):
....:    to.append(element)
....:    return to
....:

In : my_list = append_to(12)

In : my_list
Out: [12]

In : my_other_list = append_to(42)

In : my_other_list
Out: [12, 42]

可以看到,影响了第2次执行的结果。当默认参数值是可变对象的时候,那么每次使用该默认参数的时候,其实更改的是同一个变量。为了防止出现这种情况,通常使用一个完全不预期的值,比如None,在逻辑中检查,如果是这个预期的值就初始化。

In : def append_to(element, to=None):
....:    if to is None:
....:        to = []
....:    to.append(element)
....:    return to
....:

闭包变量绑定

In : def create_multipliers():
....:    return [lambda x : i * x for i in range(5)]
....:

In : for multiplier in create_multipliers():
....:    print(multiplier(2))
....:
8
8
8
8
8

但是本来我们希望的应该是[0, 2, 4, 6, 8]这个列表。这是因为闭包中用到的变量的值,是在内部函数被调用时查询得到的,也就是延迟绑定,i在range(5)最后一个循环时被设置为了4。

如果希望这个需要正常有2个办法。第一是用 函数默认值:

In : def create_multipliers():
.....:    return [lambda x, i=i : i * x for i in range(5)]
.....:

第二种是用之后要讲的偏函数。创建一个新的函数,这个新函数可以固定住函数的参数i,从而让结果正确:

In : from functools import partial
In : from operator import mul

In : def create_multipliers():
.....:    return [partial(mul, i) for i in range(5)]
.....:

关于本课程

上面就是爱湃森系列课程《Python入门》中的《函数》一节内容了,如果你对这套课程有兴趣可以了解如下内容:

  1. 为什么要做爱湃森学院
  2. 为什么你应该选爱湃森
  3. 爱湃森视频教程制作方法
  4. 爱湃森课程咨询常见问题汇总

课程下有一些免费内容可以试看:

不过我强烈推荐购买会员产品,购买会员产品是已非常低的折扣获得全部付费视频,另外还能看一些直播和专栏文章,其实除了教授知识,我的学习方法和经验、对一些事情的观点等才是最宝贵的。直播过程中还可以问我一些有兴趣的问题,大部分问题在外面(包含值乎等付费产品)我也是不回答的。

标签: python

扫描二维码,分享此文章

还没有评论
空空如也