xuzhengfu / pilot

进入编程世界的第一课
1 stars 0 forks source link

p2-1-function-def 第一章 函数定义再探 #30

Open xuzhengfu opened 4 years ago

xuzhengfu commented 4 years ago

1. 再探函数定义的原因

我们已经见过不少函数,也自己写过一些函数。我们已经理解函数的概念来自代数:从输入参数出发,计算出函数的返回值;我们也知道可以用 def foo(): 来定义函数。其实函数的定义非常复杂,我们不太能够在第一次介绍时就讲清楚,所以之前我们就采取 “先引入用起来” 的方法,这是一种知识上的 “提前引用”。

这一章我们就围绕函数定义深入看看。

2. 为函数命名

哪怕一个函数内部什么都不干,它也得有个名字,然后名字后面要加上圆括号 (),以明示它是个函数,而不是某个变量。

给函数命名(给变量命名也一样)需要遵循的一些规则如下:

  • 首先,名称不能以数字开头,能用在名称开头的只有大小写字母和下划线 _
  • 其次,名称中不能有空格,如果一个名字里有好几个词汇,可以用下划线来分割,也可以用所谓 Camel Case 风格(doNothing),习惯上更推荐使用下划线;
  • 最后,绝对不能与 Python 语言的关键字(keyword)重复。

关键字也叫保留字(reserved),是编程语言保护起来内部使用的,如果用这些词做变量、函数 或 类型的名字,编译器或者解释器就无法正确工作了。

Python 提供了一个模块叫 keyword 来帮助我们了解语言有哪些关键字:

import keyword
keyword.kwlist

keyword.kwlist 就是当前你使用的 Python 解释器中不可使用的关键字列表,如果我们不记得这个列表,可以随时用 keyword.iskeyword('xxx') 来查询某个词是不是关键字。

在程序里给变量、函数命名是个挺重要的事情,影响到程序的可读性,最好能有一种流畅清晰、又始终一致的风格(style)。为了让全世界的 Python 程序员都有相对一致的风格,Python 社区有专门的一套建议规范,放在专门维护 Python 语言特性的社区 PEP 上:

PEP,是 Python enhancement proposal 的缩写,每当有重要的语言特性、新需求、新想法,就放在这里,经过广大 Python 用户和开发者的讨论完善,在某个版本放进 Python 中。很多 PEP 早已从 proposal 毕业变成官方特性,但也还在这里保留着。PEP 8 就是一个古老的 proposal,现在已为大多数 Python 用户采纳。

3. “没有、一个和多个参数” 的情况

函数可以没有参数,也可以有一个或者多个参数。

没有参数就意味着这个函数执行不依赖于输入。

注意:即使没有参数,无论定义还是调用时,函数名后面的括号都是不可省略的,这是函数身份的标志。

调用时,输入参数的值 是严格按照 参数的顺序 去匹配的。比如我们写一个函数输出某年到某年之间的所有闰年:

def leap_year(year_s, year_e):
    a = list(range(year_s, year_e))
    for year in a:
        if (year%4==0 and year%100!=0) or (year%400==0):
            print(year)
leap_year(2000, 2020)

当我们调用 leap_year(2000, 2020) 时,输入两个参数值 2000 和 2020,按照顺序匹配函数定义 leap_year(year_s, year_e),于是 year_s = 2000 year_e = 2020。所以参数的顺序是不能搞错的,有些函数参数很多,要是开发过程中还调整过顺序的话,那简直就是灾难,所以一般情况下还是保持函数参数不要乱动为好。

这三个条件 year%4==0year%100!=0year%400==0 之间有何关系?若其中某个条件可以用其他条件表示,不就可以简化了吗?

  1. year%4!=0 成立,year%100!=0year%400!=0 一定成立。
  2. year%100!=0 成立,year%400!=0 一定成立。
  3. year%100==0 成立,year%4==0 一定成立。
  4. year%400==0 成立,year%4==0year%100==0一定成立。

4. “没有、一个和多个返回值” 的情况

和参数一样,Python 的函数可以没有返回值,也可以有一个或者多个返回值。

实际上,没有返回语句的函数,等价于在其最后有一句 return None,表示函数返回了一个空值 NoneNone 在 Python 中是一个合法的值,表示什么都没有,它在逻辑上等价于 False

bool(None)

结果为:

False

所以即使没有返回值的函数,也可以用在 if 后面做逻辑表达式,不过我们并不推荐这么做,因为可读性很差。

大部分情况下函数是有返回值的,因为绝大部分情况下函数的作用都是做 “数据处理”,从输入出发得到输出。

一般情况下函数都只有一个返回值,但 Python 也允许多返回值,比如我们想用一个函数来计算两个整数相除的商和余数,可以这么写:

def idiv(a, b):
    quotient = a // b
    remainder = a % b
    return quotient, remainder

q, r = idiv(50, 6)
print(q, r)

结果如下:

8 2

和多参数的情况类似,多返回值的情况下,赋值也是按照顺序匹配的,上面的代码中赋值语句左边的 q 匹配到第一个返回值,r 匹配第二个。

5. 函数内与函数外:变量的作用域

在不同地方出现的同名变量和函数,可能是完全不同的两个东西。

函数定义体中的变量的作用域是该函数内,程序的其他部分不知道其存在,这种变量叫局部变量(local variable);函数的输入参数也是局部变量,也只在函数定义体中有效;

不在任何函数、类定义体中的变量的作用域是全局的,在任何地方都可以访问,这种变量称为全局变量(global variable);

如果局部变量和全局变量同名,函数定义体内会优先局部变量,不会把它当做全局变量。

def increase_one(n):
    n += 1
    return n

n = 1
print(increase_one(n))
print(n)
  • 第一个 print() 打印的是 函数调用 increase_one(n) 的返回值,increase_one(n) 这个语句不在任何函数定义体中,所以它里面用到的变量都是全局变量:
    • 在调用 increase_one() 时,参数 n,按照作用域原理,是全局变量 n 当时的值,也就是 1;
    • increase_one() 函数定义内,参数 n 是输入参数即局部变量,带着传进来的值 1,经过加 1 之后返回,返回值是 2;
    • print 打印这个返回值,输出 2;
    • 这个过程中处理的都是局部变量,完全不影响全局变量 n 的值;
  • 第二个 print() 打印的是全局变量 n 的值,输出 1。

以上的文字,可能需要反复阅读若干遍;几遍下来,消除了疑惑,以后就彻底没问题了;若是这个疑惑并未消除,或者关键点并未消化,以后则会反复被这个疑惑所坑害,浪费无数时间。

顺便说一句,上面这个例子用来说明作用域的概念很有用,但是平时写程序最好别这么写,减少重名的变量可以提升代码的清晰度和可读性。

6. 带缺省值的参数

在函数定义中可以在某个参数后面用等号 = 给它一个缺省值,调用时可以省略传入这个参数的值,直接采用缺省值;当然也可以在调用时传入这个参数的值来覆盖掉缺省值。这种特性相当于给了这个函数两个版本,一个带某个参数,一个不带,不带的版本就当该参数是某个缺省值。

一个函数可以有多个带缺省值的参数,但有一个限制:所有这些带缺省值的参数只能堆在参数表的最后,也就是说你定义的参数表里,出现一个带缺省值的参数,则它后面的都必须带缺省值。如果在函数定义时不遵守此规则,则会扔出一个 SyntaxError: non-default argument follows default argument 的异常。

7. 指定参数名来调用函数

例如下面这个函数:

def greeting(name, msg='Hi', punc='!'):
    print(f'{msg}, {name}{punc}')

在这个版本的 greeting() 函数中,包含一个普通参数 name 和两个带缺省值的参数 msg punc,如果我们想跳过 msg 只传入 name(这个是必须的,因为没有缺省值)和 punc 的值,那么就可用下面的语法:

greeting('zhengfu', punc='.')

在上面这个例子中,第一个值按照顺序位置匹配到参数变量 name,这叫 “positional argument”(即 “按照位置顺序匹配的参数” ),而按照位置下一个是 msg,是我们想跳过的,所以要注明参数变量名,说明下一个传入的值 '.' 是给 punc 参数变量的,这叫 “keyword argument”(即 “按照参数名匹配的参数” )。

由于所有带缺省值的参数都在普通参数的后面,所以我们只要记住:

  • 调用函数时先传入所有 不带缺省值的参数的值,严格按照函数定义的位置顺序(positional);
  • 然后想指定哪些带缺省值参数的值,就用 变量名=值 这样的格式在后面列出(keyword),未列出的就还用缺省值了。

8. 变长参数

所谓 变长参数 就是函数定义时名字前面带个星号 * 的参数变量,这表示这个变量其实是一组值,多少个都可以。我们先来看个简单的例子:

def say_hi(*names):
    for name in names:
        print('Hi,', name)

在这个例子里,*names 是一个变长参数(arbitrary argument),调用时可以传入一个或者多个值,函数会把这些值看做一个列表,赋给局部变量 names —— 后面我们会知道,其实不是列表(list),而是一个元组(tuple)—— 然后我们在函数体中可以用 for...in 来对这个 names 做循环。

在使用 arbitrary argument 的场合,有几点需要注意:

  • 参数变量名最好用复数单词,一看就知道是一组数据;这个变量在函数里通常都会被 for...in 循环处理,用复数名词在写类似 for name in names 的循环语句时会很舒服、很地道(idiomatic);
  • 这种参数变量只能有一个,因为从它开始后面的输入值都会被当做它的一部分,多了就不知道怎么分了,显然,如果有这种参数,必须放在参数表的最后。

上面的第二点,有一个不太常见的例外,那就是一个函数既有 arbitrary arguments 又有 arguments with default values 的情况,那么可以有两个 arbitrary arguments,其中第二个必须带缺省值,然后参数表排列成这样:

def monstrosity(*normal arguments*, *normal arbitrary argument*, *arguments with defaults*, *arbitrary argument with default*)

这样是完全符合语法要求的,调用时 传入参数值 还是按照前面讲的规则,先按照位置顺序匹配前两部分,多出来的都归 normal arbitrary argument;然后按照参数变量名指定对应值,没指定的都用缺省值。不过这实在是太麻烦了,不知道什么情况下才必须用这么可怕的函数!

当然,只有上面列出的前三个部分的情况还是有的,比如下面的例子:

def say_hi(msg, *names, punc='!'):
    for name in names:
        print(f'{msg}, {name}{punc}')

say_hi('hello', 'b', 'c', 'd', punc='.')

结果如下:

hello, b.
hello, c.
hello, d.

小结

  • 函数定义四要素:函数名、参数表、函数体 和 返回值,本章对每一个部分都进行了更深入的说明,尤其是一些特殊的用法;
  • 函数定义内外是两个不同的 “作用域(scope)”,区分出 全局变量 和 局部变量,需要充分理解其运作原理;
  • 参数表可以分为四段(正常情况下最多只会用到前三段),需要充分理解每一段的特点,如何定义和使用,以及为什么。

Logging

2020-03-03 14:39:19 initialize