定义函数

  • def 函数名 (参数(0个或多个))

    函数体

    return 返回值

  • 函数名其实就是指向一个函数对象的引用,完全可以把函数名赋给一个变量,相当于给这个函数起了一个“别名”

1
2
3
>>> a = abs #变a指向abs函数
>>> a(-1) #所以也可以通过a调用abs函数
1
  • 如果没有return语句,函数执行完毕后也会返回结果,只是结果为None

  • 单前置下划线的名字:

《流畅的python》作者称之为“受保护的”名字,有两种主要的用法:

1、作为类名或函数名时,

会阻止其他python脚本通过【from module import *】语句导入该名字,即该名字不会被星号匹配;

1
2
3
4
5
6
7
8
9
"""foo.py模块"""
def add(a, b):
"""待导入的函数,是个公有的名字"""
return a+b

def _add2(a, b):
"""待导入的函数,是个受保护的名字"""
return a+b

比如,对于上面的模块foo,如果我在另一个python脚本中使用【from foo import *】语句,实际上是访问不到_add2()函数的,但是如果我使用【from foo import add, _add2】语句,则两个函数都能被访问到。

2、作为类的属性名或方法名时,意为不希望下游的程序员直接访问该名字,而导致意外覆盖该属性,但是这只是一种【命名约定】,python解释器不会对这种属性名做特殊处理。只是表示类的定义者希望这些属性或者方法是”私有的”,但实际上并不会起任何作用

在Python中可以使用def关键字来定义函数,和变量一样每个函数也有一个响亮的名字,而且命名规则跟变量的命名规则是一致的。在函数名后面的圆括号中可以放置传递给函数的参数,这一点和数学上的函数非常相似,程序中函数的参数就相当于是数学上说的函数的自变量,而函数执行完成后我们可以通过return关键字来返回一个值,这相当于数学上说的函数的因变量。

在了解了如何定义函数后,我们可以对上面的代码进行重构,所谓重构就是在不影响代码执行结果的前提下对代码的结构进行调整,重构之后的代码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
输入M和N计算C(M,N)

Version: 0.1

"""
def fac(num):
"""求阶乘"""
result = 1
for n in range(1, num + 1):
result *= n
return result


m = int(input('m = '))
n = int(input('n = '))
# 当需要计算阶乘的时候不用再写循环求阶乘而是直接调用已经定义好的函数
print(fac(m) // fac(n) // fac(m - n))

说明: Python的math模块中其实已经有一个名为factorial函数实现了阶乘运算,事实上求阶乘并不用自己定义函数。下面的例子中,我们讲的函数在Python标准库已经实现过了,我们这里是为了讲解函数的定义和使用才把它们又实现了一遍,实际开发中并不建议做这种低级的重复劳动

参数传递

位置参数

函数可以有参数,也可以没有,但必须保留括号;实参顺序与形参顺序一致

关键字实参

  1. 关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict

  2. 传递给函数名称-值对,实参顺序无关紧要,但要指定函数定义中的形参名;

    1
    2
    3
    4
    def describe pet(animal type, pet_name)
    调用:
    describe pet(animal type=“harry”, pet_name= “haster”)

  3. 传递任意数量的关键字实参:**形参名

    1
    2
    3
    4
    5
    6
    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'}
    #**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
  4. 命名关键字参数

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

      1
      2
      def person(name, age, * , city, job):
      print(name, age, city, job)
    • 和关键字参数kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。调用方式如下:

      1
      2
      >>> person('Jack', 24, city='Beijing', job='Engineer')
      Jack 24 Beijing Engineer
    • 如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

      1
      2
      def person(name, age, *args, city, job):
      print(name, age, args, city, job)
    • 命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:

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

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

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

      1
      2
      3
      4
      5
      6
      def person(name, age, *, city='Beijing', job):
      print(name, age, city, job)
      #由于命名关键字参数city具有默认值,调用时,可不传入city参数:
      >>> person('Jack', 24, job='Engineer')
      Jack 24 Beijing Engineer

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

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

默认参数

在形参列表中必须列出没有默认值的形参,再列出有默认值的形参;

实参可选

将形参指定成空字符串

传递列表

  1. 在函数中对列表的修改是永久性的;
  2. 禁止函数修改列表:向函数传递副本 eg function_name(list_name[:]);

传递任意数量的实参

  1. *形参名;任意数量****实参的形参应放在最后;Python先匹配位置实参和关键字实参;
  2. Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去

参数组合

参数定义的顺序必须是**:必选参数、默认参数、可变参数、命名关键字参数和关键字**参数。

返回值

  1. return ;可返回任何类型的值;
  2. return可以传递0个返回值,也可以传递任意多个返回值;在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

用模块管理函数

  1. 导入整个模块:import module_name(文件名)
  2. 导入特定的函数:from module_name import function_name,通过逗号隔开导入多个函数;
  3. 使用as给函数指定别名
    • from module_name import function_name as fun;
  4. 使用as给模块指定别名:import module_name as Mn;
  5. 导入模块中所有函数:from module_name import *;

对于任何一种编程语言来说,给变量、函数这样的标识符起名字都是一个让人头疼的问题,因为我们会遇到命名冲突这种尴尬的情况。最简单的场景就是在同一个.py文件中定义了两个同名函数,由于Python没有函数重载的概念,那么后面的定义会覆盖之前的定义,也就意味着两个函数同名函数实际上只有一个是存在的。

1
2
3
4
5
6
7
8
def foo():
print('hello, world!')

def foo():
print('goodbye, world!')

# 下面的代码会输出什么呢?
foo()

当然上面的这种情况我们很容易就能避免,但是如果项目是由多人协作进行团队开发的时候,团队中可能有多个程序员都定义了名为foo的函数,那么怎么解决这种命名冲突呢?答案其实很简单,Python中每个文件就代表了一个模块(module),我们在不同的模块中可以有同名的函数,在使用函数的时候我们通过import关键字导入指定的模块就可以区分到底要使用的是哪个模块中的foo函数,代码如下所示。

module1.py

1
2
def foo():
print('hello, world!')

module2.py

1
2
def foo():
print('goodbye, world!')

test.py

1
2
3
4
5
6
7
8
9
from module1 import foo

# 输出hello, world!
foo()

from module2 import foo

# 输出goodbye, world!
foo()

也可以按照如下所示的方式来区分到底要使用哪一个foo函数。

test.py

1
2
3
4
5
import module1 as m1
import module2 as m2

m1.foo()
m2.foo()

但是如果将代码写成了下面的样子,那么程序中调用的是最后导入的那个foo,因为后导入的foo覆盖了之前导入的foo

test.py

1
2
3
4
5
from module1 import foo
from module2 import foo

# 输出goodbye, world!
foo()

test.py

1
2
3
4
5
from module2 import foo
from module1 import foo

# 输出hello, world!
foo()

需要说明的是,如果我们导入的模块除了定义函数之外还中有可以执行代码,那么Python解释器在导入这个模块时就会执行这些代码,事实上我们可能并不希望如此,因此如果我们在模块中编写了执行代码,最好是将这些执行代码放入如下所示的条件中,这样的话除非直接运行该模块,if条件下的这些代码是不会执行的,因为只有直接执行的模块的名字才是"__main__"。

module3.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def foo():
pass


def bar():
pass


# __name__是Python中一个隐含的变量它代表了模块的名字
# 只有被Python解释器直接执行的模块的名字才是__main__
if __name__ == '__main__':
print('call foo()')
foo()
print('call bar()')
bar()

test.py

1
2
3
import module3

# 导入module3时 不会执行模块中if条件成立时的代码 因为模块的名字是module3而不是__main__

变量的作用域

局部变量和全局变量是不同变量

  • 局部变量是函数内部占位符,与全局变量可能重名但不同
  • 函数运算结束后,局部变量被释放
  • 可以使用global保留字在函数内部使用全局变量
    global 全局变量名

局部变量为组合数据类型且未创建,等同于全局变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def foo():
b = 'hello'

# Python中可以在函数内部再定义函数
def bar():
c = True
print(a)
print(b)
print(c)

bar()
# print(c) # NameError: name 'c' is not defined


if __name__ == '__main__':
a = 100
# print(b) # NameError: name 'b' is not defined
foo()
1
2
3
100
hello
True

bar函数的内部并没有定义ab两个变量,那么ab是从哪里来的。

  • 我们在上面代码的if分支中定义了一个变量a,这是一个全局变量(global variable),属于全局作用域,因为它没有定义在任何一个函数中。
  • 在上面的foo函数中我们定义了变量b,这是一个定义在函数中的局部变量(local variable),属于局部作用域,在foo函数的外部并不能访问到它;但对于foo函数内部的bar函数来说,变量b属于嵌套作用域,在bar函数中我们是可以访问到它的。
  • bar函数中的变量c属于局部作用域,在bar函数之外是无法访问的。

事实上,Python查找一个变量时会按照“局部作用域”、“嵌套作用域”、“全局作用域”和“内置作用域”的顺序进行搜索,前三者我们在上面的代码中已经看到了,所谓的“内置作用域”就是Python内置的那些标识符,我们之前用过的inputprintint等都属于内置作用域。

再看看下面这段代码,我们希望通过函数调用修改全局变量a的值,但实际上下面的代码是做不到的。

1
2
3
4
5
6
7
8
9
def foo():
a = 200
print(a) # 200


if __name__ == '__main__':
a = 100
foo()
print(a) # 100
1
2
200
100

在调用foo函数后,我们发现a的值仍然是100,这是因为当我们在函数foo中写a = 200的时候,是重新定义了一个名字为a的局部变量,它跟全局作用域的a并不是同一个变量,因为局部作用域中有了自己的变量a,因此foo函数不再搜索全局作用域中的a。如果我们希望在foo函数中修改全局作用域中的a,代码如下所示。

1
2
3
4
5
6
7
8
9
10
def foo():
global a
a = 200
print(a) # 200


if __name__ == '__main__':
a = 100
foo()
print(a) # 200

我们可以使用global关键字来指示foo函数中的变量a来自于全局作用域,如果全局作用域中没有a,那么下面一行的代码就会定义变量a并将其置于全局作用域。同理,如果我们希望函数内部的函数能够修改嵌套作用域中的变量,可以使用nonlocal关键字来指示变量来自于嵌套作用域,请大家自行试验。

在实际开发中,我们应该尽量减少对全局变量的使用,因为全局变量的作用域和影响过于广泛,可能会发生意料之外的修改和使用,除此之外全局变量比局部变量拥有更长的生命周期,可能导致对象占用的内存长时间无法被垃圾回收。事实上,减少对全局变量的使用,也是降低代码之间耦合度的一个重要举措,同时也是对迪米特法则的践行。减少全局变量的使用就意味着我们应该尽量让变量的作用域在函数的内部,但是如果我们希望将一个局部变量的生命周期延长,使其在定义它的函数调用结束后依然可以使用它的值,这时候就需要使用闭包。

说了那么多,其实结论很简单,从现在开始我们可以将Python代码按照下面的格式进行书写,这一点点的改进其实就是在我们理解了函数和作用域的基础上跨出的巨大的一步。

1
2
3
4
5
6
def main():
# Todo: Add your code here
pass

if __name__ == '__main__':
main()