Python学习笔记(四) 函数

这篇文章介绍有关 Python 函数一些常被大家忽略的知识点,帮助大家更全面地掌握 Python


1、函数文档

def 语句后添加的字符串被称为 文档字符串,它是函数的说明,将作为函数的一部分储存起来

def multiply(x, y):
    '''Calculates the multiplication between x and y

    Args:
        x (Number): the multiplicand
        y (Number): the multiplier

    Returns:
        Number: the result of multiplication
    '''
    return x * y

既可以通过内置属性 __doc__ 访问文档字符串

print(multiply.__doc__)
# Calculates the multiplication between x and y
# 
#     Args:
#         x (Number): the multiplicand
#         y (Number): the multiplier
# 
#     Returns:
#         Number: the result of multiplication

也可以通过内置方法  help()  访问文档字符串

help(multiply)
# Help on function multiply in module __main__:
# 
# multiply(x, y)
#     Calculates the multiplication between x and y
# 
#     Args:
#         x (Number): the multiplicand
#         y (Number): the multiplier
# 
#     Returns:
#         Number: the result of multiplication

2、参数传递方式

一般来说,函数参数的传递方式有两种,一种是按值传递,另一种是按引用传递

那么在 Python 中,函数参数的传递方式是什么呢?要解释这个问题,我们要从变量开始讲起

(1)变量赋值

在 Python 中,变量赋值实际上并不是将值储存在变量中,反而有点像将变量名贴在值上

以下的图片展示了变量赋值的一种直观理解

在这里插入图片描述

(2)变量修改

在 Python 中,变量可以分为可变类型和不可变类型,当修改可变类型时,直接修改物体内的值

当修改不可变类型时,由于物体内的值不能改变,所以 Python 采用一种特殊的处理方式

即将标签贴在另一个储存了新内容的物体,储存了旧内容的物体将会在没有标签指向时被系统自动回收

以下的图片展示了变量修改的一种直观理解

在这里插入图片描述

(3)参数传递方式

传入函数的参数分两种情况考虑,一种是传入可变类型,另一种是传入不可变类型

常见的可变类型有 listdict,常见的不可变类型有 tuplestringintfloatbool

  • 如果的是传入 可变类型,那么直接修改形参会改变传入的实参
def change(strings):
    strings[0] = 'Function'

names = ['Andy']
change(names)

print(names)
# ['Function']
  • 如果传入的是 不可变类型,那么直接修改形参不会改变传入的实参
def change(string):
    string = 'Function'

name = 'Andy'
change(name)

print(name)
# 'Andy'

以下的图片展示了以上两种情况的一种直观理解

在这里插入图片描述

3、位置参数与关键字参数

我们一般使用的参数称为 位置参数,因为在调用函数时可以忽略它们的名称,只需要位置对应即可

def greet(greeting, name):
    print(greeting, name)
greet('Hello', 'World')
# Hello World

如果参数很多,难以记住每个参数的位置,可以在调用函数时指定参数的名称,这称为 关键字参数

def greet(greeting, name):
    print(greeting, name)
greet(name = 'World', greeting = 'Hello')
# Hello World

4、收集参数与分配参数

下面考虑一种场景,如果参数的数量是不确定的,我们该怎么处理呢?这时我们可以使用 收集参数

定义函数时,如果在参数前加上星号 *,那么调用函数时,自动把余下的参数作为一个元组储存在该参数

def test(a, b, *c):
    print(a, b, c)

test(1, 2, 3, 4, 5, 6)
# 1 2 (3, 4, 5, 6)

一般情况下,我们会把收集参数指定为最后一个参数

但假如我们把收集参数放在前面会发生什么呢?比如说像这样

def test(a, *b, c):
    print(a, b, c)

test(1, 2, 3, 4, 5, 6)
# TypeError: test() missing 1 required keyword-only argument: 'c'

参数前带有星号意味着,该形参会接收余下的所有实参,从而导致后面的形参无法接收实参而产生错误

但我们可以用关键字参数解决这个问题,即我们给收集参数后的所有形参直接指定实参

def test(a, *b ,c):
    print(a, b, c)

test(1, 2, 3, 4, 5, c = 6)
# 1 (2, 3, 4, 5) 6

到目前为止,使用 单星号 * 还存在一个问题,即它无法收集关键字参数

def test(*paras):
    print(paras)

test(1, 2, 3)              # 收集位置参数,正常
# (1, 2, 3)

test(x = 1, y = 2, z = 3)  # 收集关键字参数,错误
# TypeError: test() got an unexpected keyword argument 'x'

解决方案就是使用 双星号 **,此时得到的将会是一个字典,而非元组

def test(**paras):
    print(paras)

test(x = 1, y = 2, z = 3)  # 收集关键字参数,正常
# {'x': 1, 'y': 2, 'z': 3}

test(1, 2, 3)              # 收集位置参数,错误
# TypeError: test() takes 0 positional arguments but 3 were given

使用单星号可以收集位置参数但不能收集关键字参数,使用双星号可以收集关键字参数但不能收集位置参数

所以我们可以使用单星号和双星号同时收集位置参数和关键字参数

def test(*args, **kwargs):
    print(args, kwargs)

test(1, 2, 3, x = 4, y = 5, z = 6)
# (1, 2, 3) {'x': 4, 'y': 5, 'z': 6}

分配参数 与收集参数执行相反的操作

在调用函数时用单星号 * 或双星号 ** 将元组或字典拆成独立的参数后赋值

def test(x, y):
    print(x, y)  

paras1 = (1, 2)
test(*paras1)  # 将元组变成独立的位置参数
# 1 2

paras2 = {'x': 1, 'y': 2}
test(**paras2) # 将字典变成独立的关键字参数
# 1 2

5、作用域与作用域链

变量是什么?其实可以将变量视为指向值的名称,这或许有点像字典中的键值对(在字典中键指向值)

实际上,变量的确是在使用一种看不见的字典,我们可以使用内置函数返回这个看不见的字典

  • globals():返回模块名称空间的字典
  • locals()  :返回当前名称空间的字典
a = 0

def func():
    b = 0
    print('1', locals())
    print('2', globals())

func()
print('3', locals())
print('4', globals())

# 结果如下(以下结果省略了部分内置的变量)
# 1 {'b': 0}
# 2 {'a': 0, 'func': <function func at 0x038C0810>}
# 3 {'a': 0, 'func': <function func at 0x038C0810>}
# 4 {'a': 0, 'func': <function func at 0x038C0810>}

# 由此可知
# 1. 如果在同一模块下,globals() == globals() 永远成立
# 2. 如果在全局环境下,locals()  == globals() 也会成立

这个看不见的字典被称为 作用域,作用域可以分为内建作用域、全局作用域和局部作用域

每个函数都有一个独立的 局部作用域,当函数出现嵌套时,就会出现层层嵌套的作用域,称为 作用域链

# 内建作用域:内置函数的命名空间

# 全局作用域
a = 1
print('a', a) # 1
print('b', b) # NameError
print('c', c) # NameError
def outer():
    # 局部作用域 (outer)
    b = 2
    print('a', a) # 1
    print('b', b) # 2
    print('c', c) # NameError
    def inner():
        # 局部作用域 (inner)
        c = 3
        print('a', a) # 1
        print('b', b) # 2
        print('c', c) # 3
    inner()
outer()

内层作用域可以访问外层作用域的变量,但外层作用域不能访问内层作用域的变量,这是一个单向的过程

在访问一个变量时,首先在当前作用域搜索,如果没有找到,则在下一个外层作用域搜索,直到最后一层

若在最外层作用域也没有找到该变量,就会抛出引用错误,下图是一个直观的理解

在这里插入图片描述

下面来看几个例子,理解一下这个概念

p = 'F'

def func1():
    p = 'W'
    q = 'H'
    print('func1 p:', p)
    print('func1 q:', q)
func1()

def func2():
    print('func2 p:', p)
    print('func2 q:', q)
func2()

# 结果如下
# func1 p: W
# func1 q: H
# func2 p: F
# func2 q: NameError

# 分析如下
# 在 func1 中访问 p:局部作用域 func1 (√)
# 在 func1 中访问 q:局部作用域 func1 (√)
# 在 func2 中访问 p:局部作用域 func2 (×) -> 全局作用域 (√)
# 在 func2 中访问 q:局部作用域 func2 (×) -> 全局作用域 (×) -> 内建作用域 (×) -> 报错
#                                                          ×
#                                                         /
#   |— — — — — — — — — — — — — — — — — — — — — — — — — —|/
#   | global                                            /
#   | p = 'F'                                     √    /|
#   |                                            /    / |
#   |   |— — — — — — — — — —|   |— — — — — — — —/— —|/  |
#   |   | func1        √    |   | func2        /    /   |
#   |   | p = 'W'     /     |   |             /    /|   |
#   |   | q = 'H'    /   √  |   |            /    / |   |
#   |   |           /   /   |   |           /    /  |   |
#   |   |   print(p)   /    |   |   print(p)    /   |   |
#   |   |             /     |   |              /    |   |
#   |   |     print(q)      |   |     print(q)      |   |
#   |   |— — — — — — — — — —|   |— — — — — — — — — —|   |
#   |— — — — — — — — — — — — — — — — — — — — — — — — — —|

# 由此可知
# 1. 访问变量的顺序从当前作用域开始向外搜索,直到最外层作用域为止
# 2. 在局部作用域中的变量会覆盖全局作用域中的同名变量
# 3. 不同局部作用域间的变量互不影响
p = 'Hi'
def func1():
    p = 'Hello'
    print('func1  p:', p)
func1()
print('global p:', p)

q = 'Hi'
def func2():
    global q
    q = 'Hello'
    print('func2  q:', q)
func2()
print('global q:', q)

# 结果如下
# func1  p: Hello
# global p: Hi
# func2  q: Hello
# global q: Hello

# 分析如下
# 在 func1 中,p = 'Hello'  在局部作用域创建变量 p 并将其赋值为 'Hello'
# 因此,在局部作用域中访问变量 p 时,能直接引用局部作用域中的变量 p
# 并且,在全局作用域中访问变量 p 时,其值不变,因为全局作用域中的 p 没有被修改
# 在 func2 中,q = 'Hello'  前通过 global 关键字声明 q 直接引用全局变量,后将其修改为 'Hello'
# 因此,在局部作用域中访问变量 q 时,其引用的是全局作用域中的变量 q
# 并且,在全局作用域中访问变量 q 时,其值改变,因为全局作用域中的 q 在局部作用域被修改

# 由此可知
# 1. 局部作用域中的赋值语句左边出现与全局变量同名的变量,则会在局部作用域创建变量而非引用全局变量
# 2. 局部作用域中使用 global 关键字能明确声明该变量引用全局变量

6、lambda 函数

lambda 函数即 匿名函数,可以省去函数命名的烦恼,对于一些功能简单的函数尤为合适

其基本语法如下:lambda parameters: expression

  • parameters:用逗号分隔的变量列表,可选,代表传入函数的参数
  • expression:简单语句,不用包含 return,代表函数的返回结果

示例 1:实现简单的加法

# 输入 x、y,输出 x + y
add = lambda x, y: x + y
# 能像使用正常函数一样使用 lambda 函数
z = add(1, 2) # 3

示例 2:使用在 sorted 函数中,指定排序规则

# 按绝对值排序
result = sorted([3, -2, 4, -1, 5], key = lambda x: abs(x))
print(result) # [-1, -2, 3, 4, 5]

示例 3:使用在 filter 函数中

# 过滤奇数
result = list(filter(lambda x: x % 2 == 0, range(10)))
print(result) # [0, 2, 4, 6, 8]

示例 4:使用在 map 函数中

# 对每个元素求平方
result = list(map(lambda x: x**2, range(10)))
print(result) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

示例 5:使用在 reduce 函数中

# 累加求和
from functools import reduce 
result = reduce(lambda x, y: x + y, range(10))
print(result) # 45
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值