30天Python入门到进阶——第7天:函数

时间:2022-09-29 14:54:59

30天Python入门到进阶——第7天:函数

接着上一期的继续,我们已经讲完了所有的流程控制,这一期,我们将详细了解Python中的函数和方法、文档字符串、以及作用域的实践。我会尽我所能用清晰简单的语言解释。

函数

函数是一个非常重要的概念,它们存在于所有编程语言中。函数允许我们定义一个动作(代码块),然后执行该动作任意次数,而无需遵循DRY原则重复自己。到目前为止,我一直在使用Python提供的一些内置函数,例如​​print​​​​input​​​​len​​等。

什么是函数?函数(function)是用于完成特定任务的程序代码的自包含单元。在面向对象编程的类中,函数通常被称作方法。不同的函数在程序中扮演着不同的角色,起着不同的作用,执行不同的动作。比如print()函数可以将对象打印到屏幕上;还有一些函数能够返回一个值以供程序使用,比如len()将可计算长度的对象的元素个数返回给程序。

那么,为什么要使用函数呢?

第一、函数的使用可以重用代码,省去重复性代码的编写,提高代码的重复利用率。如果程序中需要多次使用某种特定的功能,那么只需要编写一个合适的函数就可以了。程序可以在任何需要的地方调用该函数,并且同一个函数可以在不同的程序中调用,就像我们经常使用的print()和input()函数一样。

第二、函数能封装内部实现,保护内部数据,实现对用户的透明。很多时候,我们把函数看做“黑盒子”,即对应一定的输入会产生特定的结果或返回某个对象。往往函数的使用者并不是函数的编写者,函数的使用者对黑盒子的内部行为并不需要考虑,可以把精力投入到自身业务逻辑的设计而不是函数的实现细节。只有函数的设计者或者说编写者,才需要考虑函数内部实现的细节,如何暴露对外的接口,返回什么样的数据,也就是API的设计。

第三、即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序模块化,从“一团散沙”变成“整齐方队”,从而有利于程序的阅读、调用、修改和完善。例如,假设你正在编写一个实现下面功能的程序:

  • 读入一行数字
  • 对数字进行排序
  • 找到它们的平均值
  • 打印出一个柱状图

是时候创建一个函数了。

def blow_fire(): # 函数定义
print('fire ???? ???? ????')

blow_fire() # 函数调用
blow_fire() # 可以调用多次

参数

上面的函数看起来不错吧,但它也有一些限制。它只能执行相同的操作。让我们使它更具可扩展性,并通过向其传递一些数据来使其随意执行操作。

在定义函数时,当我们向其提供一些数据以基于该数据执行某些操作时,提供的数据称为参数。可以为函数提供任意数量的参数。

绝大多数函数接收一定数量的参数,然后根据实际调用时提供的参数的值的不同,输出不同的结果。前面我们说过,将函数内部的参数名字,定义得和外部变量的名字一样是一种不好的习惯,它容易混淆思维,甚至发生错误。通常我们定义和给函数传递参数是这样的:

x, y, z = 1, 2, 3

def add(a, b, c):

return a+b+c

add(x, y, x) # 使用变量,传递参数
add(4, 5, 6) # 直接传递值也是可以的。

在上面的例子中,a,b,c叫做形式参数,简称形参。而x,y,z和4,5,6叫做实际参数,简称实参,也就是实际要传递的值。而我们通常讨论的参数,指的都是形参。

定义函数时,参数的名字和位置确定下来,函数的接口就固定了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。Python函数的参数定义灵活度非常大。除了正常定义的位置参数外,还可以使用默认参数、动态参数和关键字参数,这些都是形参的种类。


位置参数

也叫必传参数,顺序参数,是最重要的,也是必须在调用函数时明确提供的参数!位置参数必须按先后顺序,一一对应,个数不多不少的传递!

上面例子中的a,b,c就是位置参数,我们在使用​​add(4, 5, 6)​​调用时,就是将4传给a,5传给b,6传给c的一一对应传递。类似​​add(4, 5, 6, 7)​​、​​add(4)​​和​​add(5, 4, 6)​​这种“画蛇添足”、“缺胳膊少腿”和“嫁错郎”类型的调用都是错误的。其中,​​add(5, 4, 6)​​的调用在语法上没问题,但是输出结果可能和预期的不一致。

注意: Python在做函数参数传递的时候不会对数据类型进行检查,理论上你传什么类型都可以!

def add(a, b, c):
return a+b+c

result = add("haha", 2, 3)

但是,上面的add函数,如果你传递了一个字符串和两个数字,结果是弹出异常,因为字符串无法和数字相加。这就是Python的弱数据类型和动态语言的特点。在简单、方便的时候,需要你自己去实现数据类型检查。

Traceback (most recent call last):
File "F:/Python/pycharm/201705/func.py", line 33, in <module>
result = add("haha", 2, 3)
File "F:/Python/pycharm/201705/func.py", line 31, in add
return a+b+c
TypeError: must be str, not int

默认参数

在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。

def power(x, n = 2):
return x**n

ret1 = power(10) # 使用默认的参数值n=2
ret2 = power(10, 4) # 将4传给n,实际计算10**4的值

上面例子中的n就是个默认参数。默认参数可以简化函数的调用,在为最常用的情况提供简便调用的同时,还可以在特殊情况时传递新的值。但是在设置默认参数时,有几点要注意:

  • 默认参数必须在位置参数后面!

如果你违反了这点,在语法层面直接是通不过的。

# 这是一个错误的例子
def power(n = 2,x):
return x**n
  • 当有多个默认参数的时候,通常将更常用的放在前面,变化较少的放后面。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
pass
  • 在调用函数的时候,尽量给实际参数提供默认参数名。
def student(name, sex, age, classroom="101", tel="88880000", address="..."):
pass

student('jack','male',17) # 其它全部使用默认值
student('tom','male',18,'102','666666','beijing') # 全部指定默认参数的值
student('mary','female',18,'102',tel='666666') # 挑着来
student('mary','female',18,tel='666666','beijing') # 这是错误的参数传递方式
student("mary","female",18,tel="666666",address="beijing")

注意最后两种调用方式,倒数第二种是错误的,而最后一种是正确的。为什么会这样?因为一切没有提供参数名的实际参数,都会当做位置参数按顺序从参数列表的左边开头往右匹配!

  • 使用参数名传递参数

通常我们在调用函数时,位置参数都是按顺序先后传入,而且必须在默认参数前面。但如果在位置参数传递时,给实参指定位置参数的参数名,那么位置参数也可以不按顺序调用,例如:

def student(name, age, classroom, tel, address="..."):
pass

student(classroom=101, name="Jack", tel=66666666, age=20)

注意指定的参数名必须和位置参数的名字一样。

  • 默认参数尽量指向不变的对象!

​使用不可变的数据类型作为默认值!

def func(a=None):
# 注意下面的if语句
if a is None:
a = []
a.append("A")
return a

print(func())
print(func())
print(func())

将默认参数a设置为一个类似None,数字或字符串之类的不可变对象。在函数内部,将它转换为可变的类型,比如空列表。这样一来,不管调用多少次,运行结果都是['A']了。

动态参数

顾名思义,动态参数就是传入的参数的个数是动态的,可以是1个、2个到任意个,还可以是0个。在不需要的时候,你完全可以忽略动态函数,不用给它传递任何值。

Python的动态参数有两种,分别是​​*args​​和​​**kwargs​​,这里面的关键是一个和两个星号的区别,而不是​​args​​和​​kwargs​​在名字上的区别,实际上你可以使用​​*any​​或​​**whatever​​的方式。但就如self一样,默认大家都使用​​*args​​和​​**kwargs​​。

注意:动态参数,必须放在所有的位置参数和默认参数后面!

def func(name, age, sex='male', *args, **kwargs):
pass

*args

一个星号表示接收任意个参数。调用时,会将实际参数打包成一个元组传入形式参数。如果参数是个列表,会将整个列表当做一个参数传入。例如:

def func(*args):
for arg in args:
print(arg)

func('a', 'b', 'c')

li = [1, 2, 3]
func(li)

运行结果是:

a
b
c
[1, 2, 3]

通过循环args,我们可以获得传递的每个参数。但是li这个列表,我们本意是让它内部的1,2,3分别当做参数传递进去,但实际情况是列表本身被当做一个整体给传递进去了。怎么办呢?使用一个星号!调用函数,传递实参时,在列表前面添加一个星号就可以达到目的了。实际情况是,不光列表,任何序列类型数据对象,比如字符串、元组都可以通过这种方式将内部元素逐一作为参数,传递给函数。而字典,则会将所有的key逐一传递进去。

def func(*args):
for arg in args:
print(arg)

li = [1, 2, 3]
func(*li)

**kwargs

两个星表示接受键值对的动态参数,数量任意。调用的时候会将实际参数打包成字典。例如:

def func(**kwargs):
for kwg in kwargs:
print(kwg, kwargs[kwg])
print(type(kwg))

func(k1='v1', k2=[0, 1, 2])

运行结果是:

k1 v1
<class 'str'>
k2 [0, 1, 2]
<class 'str'>

而如果我们这样传递一个字典dic呢?我们希望字典内的键值对能够像上面一样被逐一传入。

def func(**kwargs):
for kwg in kwargs:
print(kwg, kwargs[kwg])

dic = {
'k1': 'v1',
'k2': 'v2'
}

func(dic)

实际结果却是弹出错误,为什么?

Traceback (most recent call last):
File "F:/Python/pycharm/201705/func.py", line 10, in <module>
func(dic)
TypeError: func() takes 0 positional arguments but 1 was given

因为这时候,我们其实是把dic当做一个位置参数传递给了func函数。而func函数并不接收任何位置函数。那怎么办呢?使用两个星号

def func(**kwargs):
for kwg in kwargs:
print(kwg, kwargs[kwg])

dic = {
'k1': 'v1',
'k2': 'v2'
}

func(**dic)

有了前面一个星号的基础,这里我们应该很好理解了。两个星号能将字典内部的键值对逐一传入​​**kwargs​​。


“万能”参数

当​​*args​​和​​**kwargs​​组合起来使用,理论上能接受任何形式和任意数量的参数,在很多代码中我们都能见到这种定义方式。需要注意的是,​​*args​​必须出现在​​**kwargs​​之前。

def func(*args, **kwargs):

for arg in args:
print(arg)

for kwg in kwargs:
print(kwg, kwargs[kwg])


lis = [1, 2, 3]
dic = {
'k1': 'v1',
'k2': 'v2'
}

func(*lis, **dic)

现在我们结合一下普通参数和万能参数,看看会有什么情况发生:

def func(a, b, c=1, *args, **kwargs):
for arg in args:
print(arg)

for kwg in kwargs:
print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
'k1': 'v1',
'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果是:

bbb
ccc
k1 v1
k2 v2

列表lis中的第一个元素‘aaa’怎么没有打印出来?

我们改一下代码,打印一下参数c的结果就知道了:

def func(a, b, c=1, *args, **kwargs):
print('c的值是:', c)
for arg in args:
print(arg)

for kwg in kwargs:
print(kwg, kwargs[kwg])


lis = ['aaa', 'bbb', 'ccc']
dic = {
'k1': 'v1',
'k2': 'v2'
}

func(1, 2, *lis, **dic)

打印结果为:

c的值是: aaa
bbb
ccc
k1 v1
k2 v2

原来,lis的第一个元素被传递给参数c了!这就是Python的参数传递规则之一。


关键字参数

对于​​*args​​和​​**kwargs​​参数,函数的调用者可以传入任意不受限制的参数。比如:

def func(*args):
pass

func("haha", 1, [], {})
func(1,2,3,4,5,6)

对于这样的参数传递方式,虽然灵活性很大,但是风险也很大,可控性差,必须自己对参数进行过滤和判定。例如下面我只想要姓名、年龄和性别,就要自己写代码检查:

def student(name, age, **kwargs):
if 'sex' in kwargs:
student_sex = kwargs['sex']

但是实际上,用户任然可以随意调用函数,比如​​student("jack", 18, xxx='male')​​,并且不会有任何错误发生。而我们实际期望的是类似​​student("jack", 18, sex='male')​​的调用。那么如何实现这种想法呢?

可以用关键字参数!关键字参数前面需要一个特殊分隔符​​*​​和位置参数及默认参数分隔开来,​​*​​后面的参数被视为关键字参数。在函数调用时,关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。不同于默认参数,关键字参数必须传递,但是关键字参数也可以有缺省值,这时就可以不传递了,从而简化调用。

我们把前面的函数改写一下:

def student(name, age, *, sex):
pass

student(name="jack", age=18, sex='male')

注意函数的定义体首行。

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

def student(name, age=10, *args, sex, classroom, **kwargs):
pass

student(name="jack", age=18, sex='male', classroom="202", k1="v1")

Python的函数参数种类多样、形态多变,既可以实现简单的调用,又可以传入非常复杂的参数。需要我们多下功夫,多写实际代码,多做测试,逐步理清并熟练地使用参数。


返回

​return​​​是 Python 中的一个关键字,用于从函数返回值。为了使函数更有用,它需要根据表达式的计算返回一些值。如果未指定 return 语句,或者 return 语句的表达式的计算结果未计算为数据类型,则该函数将返回​​None​​​ 。在 JavaScript 世界中,这个​​None​​​可以链接到​​void​​​ 。

​return​​语句终止该函数并退出该函数。​​return​

def multiplier(num1, num2):
return num1 * num2

result = multiplier(2,3)
print(result) # 6

是时候使用​​return​​语句做一些很酷的事情了。

def sum(num1):
def child(num2):
return num1 + num2
return child

add_10 = sum(10)
print(add_10(20)) # 30 (Closure!!!)
print(add_10(50)) # 60
print(add_10(100)) # 110

我刚刚验证了Python中也有闭包的概念,就像在JavaScript中一样。它在创建工厂功能方面非常有效。在上面的代码块中,我能够创建一个通用函数add_10,并向其传递动态参数以生成不同的结果。这是多么酷啊!

下周将在学习Python中的函数式编程概念时对此进行更多探讨。

方法只是在对象内部定义的函数,或者换句话说,它们由对象“拥有”。使用对象名后跟​​.​​运算符调用它们以执行或调用它们。

return可以返回什么?

  • 什么都不返回,仅仅return:return
  • 数字/字符串/任意数据类型:return 'hello'
  • 一个表达式:return 1+2
  • 一个判断语句:return 100 > 99
  • 一个变量:return a
  • 一个函数调用:return func()
  • 甚至是返回自己!:return self
  • 多个返回值,以逗号分隔:return a, 1+2, "hello"

简而言之,函数可以return几乎任意Python对象。


文档注释

在某些特定的位置,用三引号包括起来的部分,也被当做注释。但是,这种注释有专门的作用,用于为__doc__提供文档内容,这些内容可以通过现成的工具,自动收集起来,形成帮助文档。比如,函数和类的说明文档:

def func(a, b):
"""
这个是函数的说明文档。
:param a: 加数
:param b: 加数
:return: 和
"""
return a + b


class Foo:
"""
这个类初始化了一个age变量
"""
def __init__(self, age):
self.age = age

需要强调的是这类注释必须紧跟在定义体下面,不能在任意位置。


作用域

简单来说,作用域意味着“我有权访问哪些变量?这是解释器在读取代码以查找变量范围时提出的问题。在Python中,变量具有函数作用域,这意味着在函数内部定义的变量不能在函数外部访问。

作用域指的是变量的有效范围。变量并不是在哪个位置都可以访问的,访问权限取决于这个变量是在哪里赋值的,也就是在哪个作用域内的。

通常而言,在编程语言中,变量的作用域从代码结构形式来看,有块级、函数、类、模块、包等由小到大的级别。但是在Python中,没有块级作用域,也就是类似if语句块、for语句块、with上下文管理器等等是不存在作用域概念的,他们等同于普通的语句。

num = 1
def confusing_function():
num = 10
return num

print(num) # 1 => Global Scope
print(confusing_function()) # 10 => Local Scope

这些是Python解释器遵循的作用域规则:

  • 从本地开始。变量是否存在?然后获取该值。如果没有,请继续
  • 变量是否在父函数本地作用域中定义?如果存在,则获取值,否则继续
  • 全局范围内是否存在该变量?如果存在,则获取值,否则继续
  • 该变量是内置函数吗?获取值否则退出

通常,函数内部的变量无法被函数外部访问,但内部可以访问;类内部的变量无法被外部访问,但类的内部可以。通俗来讲,就是内部代码可以访问外部变量,而外部代码通常无法访问内部变量。

变量的作用域决定了程序的哪一部分可以访问哪个特定的变量名称。Python的作用域一共有4层,分别是:

  • L (Local) 局部作用域
  • E (Enclosing) 闭包函数外的函数中
  • G (Global) 全局作用域
  • B (Built-in) 内建作用域
x = int(2.9)  # 内建作用域,查找int函数

global_var = 0 # 全局作用域
def outer():
out_var = 1 # 闭包函数外的函数中
def inner():
inner_var = 2 # 局部作用域

前面说的都是变量可以找得到的情况,那如果出现本身作用域没有定义的变量,那该如何寻找呢?

Python以​​L –> E –> G –>B​​的规则查找变量,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,最后去内建中找。如果这样还找不到,那就提示变量不存在的错误。例如下面的代码,函数func内部并没有定义变量a,可是print函数需要打印a,那怎么办?向外部寻找!按照​​L –> E –> G –>B​​的规则,层层查询,这个例子很快就从外层查找到了a,并且知道它被赋值为1,于是就打印了1。

a = 1

def func():
print(a)

全局变量和局部变量

定义在函数内部的变量拥有一个局部作用域,被叫做局部变量,定义在函数外的拥有全局作用域的变量,被称为全局变量。(类、模块等同理)

所谓的局部变量是相对的。局部变量也有可能是更小范围内的变量的外部变量。

局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。

a = 1               # 全局变量

def func():
b = 2 # 局部变量
print(a) # 可访问全局变量a,无法访问它内部的c

def inner():
c = 3 # 更局部的变量
print(a) # 可以访问全局变量a
print(b) # b对于inner函数来说,就是外部变量
print(c)

global和nonlocal关键字

我们先看下面的例子:

total = 0                        # total是一个全局变量

def plus( arg1, arg2 ):
total = arg1 + arg2 # total在这里是局部变量.
print("函数内局部变量total= ", total)
print("函数内的total的内存地址是: ", id(total))
return total

plus(10, 20)
print("函数外部全局变量total= ", total)
print("函数外的total的内存地址是: ", id(total))

很明显,函数plus内部通过​​total = arg1 + arg2​​语句,新建了一个局部变量total,它和外面的全局变量total是两码事。而如果我们,想要在函数内部修改外面的全局变量total呢?使用global关键字!

global:指定当前变量使用外部的全局变量

total = 0                        # total是一个全局变量

def plus( arg1, arg2 ):
global total # 使用global关键字申明此处的total引用外部的total
total = arg1 + arg2
print("函数内局部变量total= ", total)
print("函数内的total的内存地址是: ", id(total))
return total

plus(10, 20)
print("函数外部全局变量total= ", total)
print("函数外的total的内存地址是: ", id(total))

打印结果是:

函数内局部变量total=   30
函数内的total的内存地址是: 503494624
函数外部全局变量total= 30
函数外的total的内存地址是: 503494624

我们再来看下面的例子:

a = 1
print("函数outer调用之前全局变量a的内存地址: ", id(a))

def outer():
a = 2
print("函数outer调用之时闭包外部的变量a的内存地址: ", id(a))
def inner():
a = 3
print("函数inner调用之后闭包内部变量a的内存地址: ", id(a))
inner()
print("函数inner调用之后,闭包外部的变量a的内存地址: ", id(a))
outer()
print("函数outer执行完毕,全局变量a的内存地址: ", id(a))

如果你将前面的知识点都理解通透了,那么这里应该没什么问题,三个a各是各的a,各自有不同的内存地址,是三个不同的变量。打印结果也很好的证明了这点:

函数outer调用之前全局变量a的内存地址:  493204544
函数outer调用之时闭包外部的变量a的内存地址: 493204576
函数inner调用之后闭包内部变量a的内存地址: 493204608
函数inner调用之后,闭包外部的变量a的内存地址: 493204576
函数outer执行完毕,全局变量a的内存地址: 493204544

那么,如果,inner内部想使用outer里面的那个a,而不是全局变量的那个a,怎么办?用global关键字?先试试看吧:

a = 1
print("函数outer调用之前全局变量a的内存地址: ", id(a))
def outer():
a = 2
print("函数outer调用之时闭包外部的变量a的内存地址: ", id(a))
def inner():
global a # 注意这行
a = 3
print("函数inner调用之后闭包内部变量a的内存地址: ", id(a))
inner()
print("函数inner调用之后,闭包外部的变量a的内存地址: ", id(a))
outer()
print("函数outer执行完毕,全局变量a的内存地址: ", id(a))

运行结果如下,很明显,global使用的是全局变量a。

函数outer调用之前全局变量a的内存地址:  494384192
函数outer调用之时闭包外部的变量a的内存地址: 494384224
函数inner调用之后闭包内部变量a的内存地址: 494384256
函数inner调用之后,闭包外部的变量a的内存地址: 494384224
函数outer执行完毕,全局变量a的内存地址: 494384256

那怎么办呢?使用​​nonlocal​​关键字!它可以修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量。将​​global a​​改成​​nonlocal a​​,代码这里我就不重复贴了,运行后查看结果,可以看到我们真的引用了outer函数的a变量。

函数outer调用之前全局变量a的内存地址:  497726528
函数outer调用之时闭包外部的变量a的内存地址: 497726560
函数inner调用之后闭包内部变量a的内存地址: 497726592
函数inner调用之后,闭包外部的变量a的内存地址: 497726592
函数outer执行完毕,全局变量a的内存地址: 497726528

总结

今天就讲到这里。我现在已经能够涵盖Python的大部分基础知识。现在是时候深入研究高级主题了,但在此之前,我想探索和设置开发人员环境以及明天可用的所有不同类型的工具和程序,以完成第一周的路线图。

我希望通过易于理解的阐述来解释我对Python的学习过程。如果有什么令人困惑的事情,或者你有任何其他建议,想法,请在评论区让我知道。