Python3入门到精通——函数与变量作用域

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

函数

所谓函数,就是能执行特定任务的一段被命了名的程序。不管什么编程语言都会有函数这一概念,因为其有很重要的功能:

  • 功能性

如定义所说,一个函数一定具备解决某一特定场景下问题的能力。或者说一个函数就是一个小工具、一个套路

  • 封装性

一个良好的函数无论多么复杂,都能很好的隐藏自身的细节,暴露在外面的只是返回值、接受参数和函数说明而已,具体的功能实现外面的人都不用了解。

  • 重用性

函数的存在大大降低了重复编码带来的成本损耗,从而提升了效率。而经常用于解决某类问题的函数们通常会进一步封装成一个函数库供人们使用。

一个标准的函数定义形式如下:

In [ ]:
def funcname(parameter_list):
    pass 

这里注意几点:

  1. 参数列表部分parameter_list可以没有
  2. 如果函数有返回值,通过关键字return进行返回,如果没有返回值,python会返回一个None
  3. python中不需要像c,java语言那样明确每个函数的返回值类型

函数可以嵌套使用,但为了良好的代码阅读体验和编程规范,不推荐超过两层的嵌套调用

func(func1(func2(func(...))))

下面简单实现两个函数以作示例。

In [1]:
def add_two_num(x, y):
    result = x + y
    return result

def print_code(code):
    print(code)
    
result_add = add_two_num(1, 2)
result_print = print_code('Python')
print(result_add, result_print)
Python
3 None
In [3]:
# 无限递归的错误示例
def print(code):
    print(code)

print('Python')
---------------------------------------------------------------------------
RecursionError                            Traceback (most recent call last)
<ipython-input-3-f1cb3df65552> in <module>()
      3     print(code)
      4 
----> 5 print('Python')

<ipython-input-3-f1cb3df65552> in print(code)
      1 # 无限递归的错误示例
      2 def print(code):
----> 3     print(code)
      4 
      5 print('Python')

... last 1 frames repeated, from the frame below ...

<ipython-input-3-f1cb3df65552> in print(code)
      1 # 无限递归的错误示例
      2 def print(code):
----> 3     print(code)
      4 
      5 print('Python')

RecursionError: maximum recursion depth exceeded

这里注意几点:

  1. 先定义再使用,和变量一样。python是解释型语言,运行时编译,所以不能在定义之前调用函数,否则python解释器找不到。
  2. 自定义函数的名称避免和内置函数同名。如注释代码所示,会陷入无限递归,运行时会报错
  3. 函数参数赋值顺序和传参顺序一致,从左到右依次赋值。这里额外留意下,目前自定义的add_two_num()有且只能接受两个参数,多了少了都不行,而内置函数print()通过,分隔可以接受任意数量的参数

python会监测递归动态,根据机器性能状况的差异,当发现一段代码自循环超过一定次数(995,998,1000等),就会报错。可以通过`sys.setrecursionlimit(recur_nums)指定递归深度。但python内部有额外机制,并不会允许特别大的深度

对于返回值,传统语言一般只允许返回一个值,也有通过out/ref返回多值的方法。python想要返回多个值,方法很简单,通过,分隔开即可,如下代码所示:

In [2]:
def damage(skill_one, skill_two):
    damage_one = skill_one * 3
    damage_two = skill_two + 2
    return damage_one, damage_two

# 下标解包
damages = damage(4,8)
print(type(damages),damages[0],damages[1])

# 序列解包
skill_damage1,skill_damage2 = damage(4,8)
print(skill_damage1,skill_damage2)
<class 'tuple'> 12 10
12 10

多返回值的函数会将多个返回值封装成一个元组。注意样例代码中获取返回值的方式,第一种具有传统语言和新手风格,但并不赞成使用,当返回值众多,每个都用下标访问调用,日后维护代码时很难以理解。

第二种就是一种 pythonic 风格的解包方式了,通过明晰、有意义的变量接收返回值,易于理解且代码简介。这种方式叫做序列解包

更好的理解序列解包,先来了解下pythonic的赋值方式

In [3]:
# 传统方式
a = 1
b = 2
c = 3

# pythonic
a,b,c = 1,2,3

两种方式的效果等价,但后一种更显“优美”。而进一步的

In [4]:
d = 1,2,3
print(type(d))

a,b,c = d
print(a,b,c)
<class 'tuple'>
1 2 3

d = 1,2,3这样的也是一种快速获得一个序列变量的方式。而a,b,c = d这样,通过和序列内元素数目相等的变量获取各个元素值,这样的方式就是序列解包。

当多个变量的赋值相同时,可以使用链式赋值——a=b=c=1

参数

python中参数类型很多,主要是这么几种:

必须参数

In [ ]:
def add(x, y):
    result = x + y
    return result

对上述函数定义,指定了函数接收的两个参数xy,定义里指定的参数也叫形式参数。当调用该函数时,如c = add(x_value,y_value),必须传入和函数定义中数量一致的参数,默认从左向右依次赋值。函数调用时传入的参数也叫实际参数,即这里的x_value,y_value

这种函数定义多少就必须传入多少否则报错的参数类型就是必须参数

关键字参数

上面提到调用函数时传入参数默认是自左向右依次赋值,对函数add来说就是先给x赋值,再给y赋值。如果想要改变赋值顺序,或者说自定义赋值顺序,可以这样:

c = add(y=3, x=2)

这样的参数形式就是关键字参数,通过参数名 = 参数赋值的形式明确各个参数的传入值,可阅读性良好,也能改变传参顺序。

默认参数

很多时候对于一个函数的调用,多数参数值是一样的,重复输入相同的内容开发效率很低。这时候就可以使用默认参数了,义同其名,当没有明确指定参数值时,解释器会将默认值赋值给参数

In [5]:
def print_student_files(name, gender, age, college):
    print('我叫' + name)
    print('我今年' + str(age) + '岁')
    print('我是' + gender + '生')
    print('我在' + college + '上学')

print_student_files('小旋风', '男', 18, '狮驼岭')
我叫小旋风
我今年18岁
我是男生
我在狮驼岭上学

比如对如上函数定义,在狮驼岭上学的学生在gender,age,college一栏或几栏里内容都是一致的。这时就可以修改函数定义如下

In [6]:
def print_student_files(name, gender='男', age=18, college='狮驼岭'):
    print('我叫' + name)
    print('我今年' + str(age) + '岁')
    print('我是' + gender + '生')
    print('我在' + college + '上学')

这时如果个人数据和默认值无异,就可以只传入唯一的必须参数print_student_files('奔波灞')。如果有数据不同,则将对应值传入即可print_student_files('蜘蛛精','女',16,'盘丝洞')

In [7]:
print_student_files('奔波灞')
我叫奔波灞
我今年18岁
我是男生
我在狮驼岭上学
In [8]:
print_student_files('蜘蛛精', '女', 16, '盘丝洞')
我叫蜘蛛精
我今年16岁
我是女生
我在盘丝洞上学

目前为止默认参数看起来很简单,但有这么一些坑必须小心:

  • 不能跳过默认值赋值

当不显式的给参数赋值时,默认从左到右依次赋值,所以不能自以为是的跳跃赋值。比如对上面的函数,有一个学生糊涂虫,它的数据只有年龄和默认值不同,但函数调用不能这么写print_student_files('糊涂虫',17),解释器会将实参17赋值给gender一栏。

In [10]:
print_student_files('糊涂虫', 17)
我叫糊涂虫
我今年18岁
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-bfc83b4de022> in <module>()
----> 1 print_student_files('糊涂虫', 17)

<ipython-input-6-938facb24f9a> in print_student_files(name, gender, age, college)
      2     print('我叫' + name)
      3     print('我今年' + str(age) + '岁')
----> 4     print('我是' + gender + '生')
      5     print('我在' + college + '上学')

TypeError: must be str, not int

正确的方式是print_student_files('糊涂虫', age = 17)

In [11]:
print_student_files('糊涂虫', age=17)
我叫糊涂虫
我今年17岁
我是男生
我在狮驼岭上学
  • 默认参数和非默认参数不能混杂

函数定义时,默认参数和非默认参数不能穿插,下面的定义形式是非法的:

In [12]:
def print_student_files(name, gender='男', age=18, college='狮驼岭',teacher):
    print('我叫' + name)
    print('我今年' + str(age) + '岁')
    print('我是' + gender + '生')
    print('我在' + college + '上学')
  File "<ipython-input-12-71526d0e917e>", line 1
    def print_student_files(name, gender='男', age=18, college='狮驼岭',teacher):
                           ^
SyntaxError: non-default argument follows default argument

如果要加入一个必须参数,其应该在默认参数之前

In [ ]:
def print_student_files(name, num_id, gender='男', age=18, college='狮驼岭'):
    print('我叫' + name)
    print('我今年' + str(age) + '岁')
    print('我是' + gender + '生')
    print('我在' + college + '上学')
    print('我的id是' + num_id)
  • 调用传参时定位参数和关键字参数不能混杂

和上一个坑类似,调用时默认的传参和关键字传参不能穿插,下面的调用是非法的:

In [13]:
print_student_files('糊涂虫', gender='男', 17, college='魔王寨')
  File "<ipython-input-13-72741bab2308>", line 1
    print_student_files('糊涂虫', gender='男', 17, college='魔王寨')
                                                  ^
SyntaxError: positional argument follows keyword argument

可变参数

在我们已接触的函数中,有一个很特殊的函数print(),它可以接受任意多个参数。那么我们能否自己设计一个这样的函数,每次接收一组参数且数量不限,答案是肯定的。

一般的实现思路是通过元组()和列表[]进行传参,但Python内通过*param_name形式定义的可变参数,可以自动的将一组参数平铺式的逐个传入函数中

In [2]:
def var_param(*param):
    print(param)
    print(type(param))

var_param(1,2,3,4,5)
(1, 2, 3, 4, 5)
<class 'tuple'>

Python自动将可变参数列表中给出的实参封装成一个元组,如果不喜欢这样的形式,可以自行将要传递的参数封装成一个元组,然后传给函数

In [3]:
def other_var_param(param):
    print(param)
    print(type(param))

other_var_param((1,2,3,4,5)) # 直接传入封装后的元组
(1, 2, 3, 4, 5)
<class 'tuple'>

如果已经定义了可变参数*param_name,如果将元组传入,将会构成二维元组

In [4]:
var_param((1,2,3,4,5))
((1, 2, 3, 4, 5),)
<class 'tuple'>

*param_name*的作用类似于解包,所以如果自己想要传递的参数中有序列类型,不想构成二维元组,可以将其赋值给一个变量,然后将变量当做参数传递给函数,并在其前加上*

In [6]:
seris_var = (1,2,3,4,5)

var_param(*seris_var)
(1, 2, 3, 4, 5)
<class 'tuple'>
In [11]:
seris_var = [1,2,3,4,5]

var_param(*seris_var)
(1, 2, 3, 4, 5)
<class 'tuple'>

当一个函数的参数中既有可变参数,也有其他类型参数时,必须参数放在前面,其他类型参数顺序没有强制要求,但要注意与赋值方式匹配

In [7]:
def mixed_param(m_param, def_param=2, *var_param):
    print(m_param)
    print(def_param)
    print(var_param)  # 逐类打印参数

mixed_param('a', 1,2,3)
a
1
(2, 3)

如上例所示,定了一个同时接收多种参数类型的函数mixed_param,我们希望逐类打印参数,并跳过默认参数,只赋值必须采参数和可变参数,但Python还是默认依次赋值

In [8]:
def mixed_param(m_param, *var_param, def_param=2):
    print(m_param)
    print(var_param)
    print(def_param)  # 逐类打印参数

mixed_param('a', 1,2,3, 'param')
a
(1, 2, 3)
2

调换可变参数和默认参数的位置,还是逐类打印,希望var_param接收1,2,3def_param接收param。但是因为var_name是可以接收任意多个参数的,所以除了必须参数拿走的,其余参数都被Python分配给了var_param,以至于def_name只能使用默认值。

如果想让Python按照我们的意图赋值,必须显示的使用关键字参数给默认参数赋值

In [9]:
mixed_param('a', 1,2,3, def_param='param')
a
(1, 2, 3)
param

传参时是无法预知实际参数都会分配给什么类型的参数的,所以再次注意传参顺序

In [10]:
mixed_param('a', def_param='param', 1,2,3)
  File "<ipython-input-10-42557d71b452>", line 1
    mixed_param('a', def_param='param', 1,2,3)
                                       ^
SyntaxError: positional argument follows keyword argument

上述案例只是展示不同参数类型在一起的场合,编程实践中,强烈建议参数列表尽可能的简单,不要混合多种类型的参数

在实际应用中,可变参数通常通过for循环来遍历使用

In [12]:
def square_sum(*nums):  # 求数列平方和
    num_sum = 0
    for num in nums:
        num_sum += num * num
    return num_sum

square_sum(1,2,3)
Out[12]:
14

上面的传参都还只是序列类型的实参,那么当传入参数是任意多个关键字参数,或者干脆是字典类型会怎么样呢?

In [13]:
def city_temp(*citys): # 输出城市气温
    pass

city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-564f69151207> in <module>()
      2     pass
      3 
----> 4 city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')

TypeError: city_temp() got an unexpected keyword argument 'Beijing'

可以看到,*param_name形式的可变参数不能应付任意多个的关键字参数列表。这种情况下需要**param_name形式

In [15]:
def city_temp(**citys): # 输出城市气温
    print(citys)
    print(type(citys))

city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
{'Beijing': '36c', 'Shanghai': '32c', 'Wuhai': '27c'}
<class 'dict'>

可以看到Python将关键字参数列表转换成了字典进行存储,当我们使用的时候需要遍历字典来使用。

字典的遍历方式很pythonic,类似于序列解包,大多是key,value in dict.items()形式

In [27]:
def city_temp(**citys): # 输出城市气温
    print('Weather Now')
    for city, temp in citys.items():
        print('%s: %s' % (city, temp))

city_temp(Beijing='36c', Shanghai='32c', Wuhai='27c')
Weather Now
Beijing: 36c
Shanghai: 32c
Wuhai: 27c

当字典作为可变参数的接收对象的时候,类似于序列,可以将字典赋值给一个变量,然后以**param的方式使用

In [28]:
eg_citys={'Beijing':'36c', 'Shanghai':'32c', 'Wuhai':'27c'}

city_temp(**eg_citys)
Weather Now
Beijing: 36c
Shanghai: 32c
Wuhai: 27c

可变参数既然可以接收任意多个参数,那么一个都不传自然也是能够接受的

In [29]:
eg_citys={}

city_temp(**eg_citys)
Weather Now

因此,就有了常见的参数接收形式(must_param, **var_param),可以只传递必须参数。

In [31]:
def func_show(func_name, **des):
    print(func_name)
    print(des)
    
func_show(print)
<built-in function print>
{}

变量作用域

每一个变量都有自己能够起作用的范围,我们称之为作用域,作用域的概念所有编程语言内都有。通常在变量作用域之外访问不到该变量

In [32]:
c = 0

def add(x, y):
    c = x + y
    print(c)

add(1, 2)
print(c)
3
0

函数内定义的 c 外部无法访问,同名变量优先使用内部定义的变量。所以函数内的 c 打印值为3,外部 c 的打印值为0。

还有更加直观方式的展示作用域

In [33]:
def demo_func():
    a = 10

print(a)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-33-bfda2886468c> in <module>()
      2     a = 10
      3 
----> 4 print(a)

NameError: name 'a' is not defined

在一个模块内,.py文件内定义的变量称之为全局变量,像函数内定义的变量称之为局部变量。全局变量覆盖整个模块,能被多个函数调用;局部变量只能作用在自己所处的局部

所谓局部是一个相对概念,比如对于函数内的语句来说,函数内定义的变量当然是可以访问的

In [37]:
def demo_func():
    b = 0
    for x in range(1,10):
        b += 1
    print(b)

demo_func()
9

注意,Python没有块级作用域的概念,即if elif else,for while这样的选择、循环语句不构成作用域,即期内定义的变量,在语句外部是可以访问的。有其他语言经验的人要格外注意

In [38]:
def demo_func():
    for x in range(0, 9):
        a = 'a'
    print(a)
    
demo_func()
a

变量的作用域有链式特性,也叫做作用域链,即局部同名变量会屏蔽全局同名变量,嵌套层数多的作用域内的同名变量会屏蔽掉嵌套层数少的作用域内的变量,访问变量时从最近的作用域开始寻找变量

In [39]:
d = 0

def func1():
    d = 1
    def func2():  # 函数内定义函数是允许的
        d = 2
        print(d)
    func2()

func1()
2
In [40]:
d = 0

def func1():
    d = 1
    def func2():
        # d = 2
        print(d)  # 本作用域没有,向上一级寻找
    func2()

func1()
1
In [41]:
d = 0

def func1():
    # d = 1  # 本作用域没有,向上一级寻找
    def func2():
        # d = 2
        print(d)  # 本作用域没有,向上一级寻找
    func2()

func1()
0

上面全局变量作用于整个模块内的函数,实际上全局变量在整个应用程序内都是可以被访问到的,即不同的包间也可以互相访问彼此定义的全局变量。比如说aaa.bbb内定义的全局变量,在ccc.ddd内,在导入aaa.bbb模块后即可访问

此外 Python 还提供了global关键字申明变量是全局变量,这时即使变量是在aaa.bbb下某一函数内定义的,ccc.ddd导入后依旧可以访问到,前提是该函数至少执行过一次以初始化变量

In [42]:
def demo_func():
    global e
    e = 2

demo_func()  # 至少执行一次函数,否则 e 是不存在的
print(e)
2
In [ ]: