函数

五十岚2020年6月6日
大约 11 分钟

把函数视为 一等对象

函数

1. 概念

1.1 函数定义

  • 函数,不应该理解为数学中的函数 != function()
  • 计算机的函数,则应该理解为== subroutine ,这在 BASIC 中称为 子程序,在 Pascal 中叫做 过程procedures
  • C / Python 中通常称为 function 函数 ,而 Java 中则叫 method 方法

创建格式为

def function_name():	# 需根据功能来下划线形式的命名
    pass
  • 其中 def 表示 define(定义) ,在函数名称前声明,为 Python 的关键字

1.2 函数地址

函数是有地址的,可以直接打印出函数本身的地址

def func():
    pass

print(func)
### 输出结果
# <function func at 0x0000025DD40B28B8>

提示

  • 变量名 不能与函数名相同
  • 函数还有返回的地址,当函数 赋值给变量 时,变量中存储的便是函数的返回地址

1.3 函数的作用

函数只要修改一处,所有引用该函数的地方,都会统一被修改,因此好处在于

  • 减少重复代码
  • 方便日后改写扩展
  • 保持代码的一致性


2. 函数参数

2.1 参数类别

  • 形参: 形式上的, 不占内存空间的 参数变量,用于接收值
  • 实参: 实际上的, 有实际的地址|指针的 参数变量,占用内存的实际传入值

关键字参数

形如 key=value 的传参,表示使用 关键字 的形式来传参

def info(name, age):
    print(f"age: {age}, name: {name}")
    pass


info(age=18, name="Warira")

## 输出结果
# age: 18, name: Warira
  • 若正常按顺序传参,name, age 便是 位置参数
  • age=18 | name="Warira" 这种形式的传参, age=18 | name="Warira" 则为关键字参数,可无序传参,可以不用像 位置参数 那样,按位置传

默认参数

当一个函数值大多数参数相同,便可以设置一个 默认参数 进行优化

def info(name, age, sex="female"):
    pass
  • 这里的 sex 就是默认参数,必须 放在最后

警告

1.6 亿操作! 千万别在 默认参数 中写 xx = list() 或者 xx = []

def foo(list1=[]):
    list1.append("2")
    print("list1", list1)

foo()  # list1 ['2']
foo()  # list1 ['2', '2']
foo()  # list1 ['2', '2', '2']
  • 若函数内部有 append() 操作,仅第一次(且只一次)的调用正常外,但凡再被调用,就会不断内存泄漏

  • 一旦 多次调用,且参数中 没有默认参数,函数则会 直接拿第一次调用的默认参数因为地址相同,没有传入新地址)继续 append() ,业务量一上来,百分百炸裂

  • 因此以后需要 可变类型的默认参数 时,直接初始化 None,不会有地址问题

不定长参数

*args
def add(*args):
    print(args, type(args))


add(1, 2, 3, 4, 5)

## 输出结果
# (1, 2, 3, 4, 5) <class 'tuple'>
  • *args 可以接收 不定长 的任意多个位置参数
  • add(1, 2, 3, 4, 5) 这里形参接收实参的赋值后,会返回元组 tuple
def add(*args):
    print(args, type(args))
    list1 = list(*args)
    print(list1)


add([1, 2, 3, 4])

## 输出结果
# ([1, 2, 3, 4],) <class 'tuple'>		# 返回元组,但第一个值为传入的列表
# [1, 2, 3, 4]
  • *args 不能直接赋值,但若使用内置类型转换,如使用 list() 则能原样取到传入的值
**kwargs
def info(*args, **kwargs):
    print(args, kwargs, type(kwargs))


info("Shiki", age=18, sex="female")

## 输出结果
# ('Shiki',) {'age': 18, 'sex': 'female'} <class 'dict'>
  • **kwargs 可以接收 不定长 的任意多个 关键字参数
  • 返回字典 dict 类型
  • 位置参数: "Shiki"*args接收的,关键字参数: age=18sex="female"**kwargs 接收的

提示

  • 这里的 **kwargs 作为接收参数的默认值,仅接收 string 为键的值

  • 若有针对如上,需要不同类型,可通过如下方式转为 string, 转化后才能接受该参数

    **{str(k): v for k, v in kwargs.items() if isinstance(k,str)}
    
  • 过滤非字符串参数,生成新的字符串。直接暴力的将非字符串转化为字符串,存在覆盖(同字符串)键的风险

  • 实际开发中,直接传字典 的形式十分常见

    def info(**kwargs):
        print(kwargs, type(kwargs))
    
    info(**{"key": "value"})
    
    ## 输出结果
    # {'key': 'value'} <class 'dict'>
    
def info(name, sex="female", *args, **kwargs):
    pass

【优先级】: 位置参数 > 默认参数 > *args > **kwargs

特殊参数

Python 通常能按 位置关键字 形式给函数传参,但为了提高代码易读性,可以使用特殊标识来 限制参数传递

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only
  • / 可选,用来 限制位置,意思是 / 前面 的,只能用位置传参,禁用 kwarg=value 这种关键字形式
  • * 可选,用来限制关键字,意思是 * 后面 的,只能用关键字传参,必须以 kwarg=value 形式


3. 函数返回值(return)

  • Python 默认情况,无返回值时 会返回None

  • Python 可返回多个对象,编译器不会报错,而是默认返回如 (1,"qw",3) 这种封装后的 tuple 对象

    def foo():
        return 1, "qw", 3
    
    a, b, _ = foo()
    print(a, b, _)				# _ 是有意义的,不仅仅是占位
    
    ## 输出结果
    # 1 qw 3
    

用处:

  • 结束函数,return 之后便结束,后续代码无任何意义
  • 返回对象


4. 函数的作用域

如下禁止

def f():
  a = 10

f()
print(a)

### 输出结果
# Traceback (most recent call last):
# ...
# NameError: name 'a' is not defined

变量 a,声明在函数 f()作用域 里,一旦函数执行完后,就会销毁,故变量 a 只作用于函数内部,是无法在全局中引用的

4.1 作用域 LEGB

  • Llocal局部作用域,即函数中定义的变量
  • Eenclosing嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域,但不是全局的
  • Gglobal全局变量,模块级别定义的全局变量
  • Bbulid_in系统固定模块里的变量, 比如 intbytearray

优先级顺序依次:

局部作用域 > 外层作用域 > 模块的全局变量 > Python 内置作用域变量,也就是 LEGB

x = int(2.9)				# int built_in 最外一层

o_count = 0					# global 全局变量

def outer():
    o_count = 1				# enclosing  父级函数的局部作用域
    i_count = 8

    def inner():
        i_count = 2			# local 局部作用域
        print(o_count)
        print(i_count)		# 有优先级 仅本地没有,才去找上层

    inner()

outer()

## 输出结果
# 1
# 2

4.2 全局变量和局部变量

全局变量,在局部里,只读不可改

count = 10

def f():
    print(count)
    count = 5

f()

## 输出结果
# UnboundLocalError: local variable 'count' referenced before assignment
  • 报错,赋值前引用了局部变量
  • 执行时,会 先在局部中找 这个变量,若存在,便不再去全局查
  • 找到后,又发现是使用在 print(count) 后,故报错
函数内部生成
  1. 函数一创建,便加载到内存,会在内存开辟了一段空间,把局部的内容存储

  2. 程序是顺序执行(不论全局还是局部),但先在局部函数地址域中执行变量检索

  3. 局部中没有,再去全局检索,若依然没有找到引用的变量 ,则报错

在局部变量里面修改全局变量

如果遇到需要改写全局变量的需求,需要如下方式

count = 10

def f():
    global count
    print(count)
    count = 5
    print(count)

f()
## 输出结果
# 10
# 5
  • global 可以在局部直接获取到全局变量 count 的地址指针
  • globalopen in new window 是对解析器的指令,慎用

在局部变量里面修改嵌套作用域

def foo():
    count = 10

    def inner():
        nonlocal count
        count = 20
        print(count)

    print(count)
    return inner

foo()()
## 输出结果
# 10
# 20
  • nonlocal 可以在局部获取到嵌套作用域变量 count 的地址指针

4.3 命名空间

Python 的命名空间,通过 dict字典)形式体现,内置函数 globals()locals() 分别对应 全局命名空间局部命名空间,经常在需要 动态进行变量赋值 时使用

globals()

返回 当前模块全局命名空间 的字典,在任何位置调用,返回都不变,可通过如下形式修改全局变量

a = 3

def foo():
    globals()['a'] = 4

foo()
print(a)

## 输出结果
# 4
locals()

既然 globals() 可以改全局变量,那么 locals() 也理所当然的可以修改局部变量吗?

注意: 不要用 locals() 去修改变量,尽管 Python 高版本已经没有报错,建议 依然作为只读使用

def foo():
    print(locals())
    for i in ['a', 'b', 'c']:
        locals()[i] = 1
    print(locals())

foo()
## 输出结果: Python2 中报错

# {}
# {'i': 'c', 'a': 1, 'b': 1, 'c': 1}

参考open in new window



5. 进阶

5.1 函数为参

函数名可作为 参数返回值

def f():
    print("ok")

def foo(a, b, func): 		# 接收函数参数
    func()
    print(f"a: {a}, b: {b}, func: {func} - id: {hex(id(func))}")

foo(1, 2, f) 				# 传入函数,后文回调思想

# ok
# a: 1, b: 2, func: <function f at 0x000001C0EA826550> - id: 0x1c0ea826550
  • 函数地址(1928079019888)因此一定义,即使没调用,也会在内存开辟一块地址来初始化,并等待调用
  • 在内存是以 str字符串)形式存储的函数对象
  • 函数名即是引用这个函数对象,因此存放函数对象的 地址指针

5.2 嵌套

函数内部可以 定义函数,还可以 返回内部函数

def foo():
    def inner():
        return 8

    return inner   	# 返回函数对象的地址指针

print(foo()())

# 8
-------------------------------------
# 后文[闭包/装饰器]思想
# foo() == inner
# foo()() == inner()

5.3 递归

凡递归程序,都可改循环,建议不用递归,效率低、用不好就是灾难

特点:
  • 递归函数 自己调自己

  • 有出口,结束条件

def fibo(n):
   if n <= 1:
       return n
   else:
       return fibo(n-1) + fibo(n-2)

for i in range(10):
    print(f"fibo({i}) = {fibo(i)}")

## 输出结果
# fibo(0) = 0
# fibo(1) = 1
# fibo(2) = 1
# fibo(3) = 2
# ...
# fibo(8) = 21
# fibo(9) = 34

注意:

  • 递归层次数过多,会导致 栈溢出

    • 计算机中,函数是通过 stack)来实现的
    • 函数调用,系统栈就会增加一层栈帧,函数返回,减少一层栈帧
    • 由于栈的大小 不是无限的,递归调用次数过多会使栈溢出
  • 递归 效率低,栈入栈出操作频繁,因此效率十分低下

    • 每次都自调一层后 进栈,直到条件成立
    • 直到退出条件成立,再层层 return 出栈

5.4 常用内置函数

Python 对常用函数进行了封装 build-in Functions

lambda

Python 支持 lambda 匿名函数

  • 其扩展的 BNF 表示法是 lambda_expr ::= "lambda" [parameter_list] ":" expression

    • 若翻译成函数,则是如下形式

      def <lambda>(parameter_list):
          return expression
      
  • lambda 参数序列 : 表达式

优点:

  • 单行简洁,不需要函数命名与换行缩进

缺点:

  • 只支持单行表达式,不适合复杂逻辑
  • 可读性差

示例

add = lambda a, b : a + b

add(1, 2)

# 3

(lambda a, b: print(a + b))(1, 2)	# lambda的另一种执行方式,类似js的自执行函数(连开销都省了)
  • lambda: 左侧接收参数 ab ,右侧则是执行的表达式后的返回值
  • 创建时不需要命名,故也称为 匿名函数

lambda 函数通常的用法是结合 map()reduce()filter()sorted() 等函数一起使用,这些函数的共性是:都可以接收其它函数作为参数

map(function, iterable, ...)

iterable可迭代对象)的每一项,传入 function 执行,返回一个包含所有结果的 迭代器

def foo(x):
    return x * x

ret = map(foo, [1, 2, 3, 4, 5, 6]) # => map(lambda x: x * x, [1, 2, 3, 4, 5, 6])

print([i for i in ret])
# [1, 4, 9, 16, 25, 36]

特殊用法

利用 map() 做类型转换

[i for i in map(int, "12345")]

# [1, 2, 3, 4, 5]

filter(function, iterable)

iterable可迭代对象)的每一项,传入 function 执行,返回一个包含所有 真值迭代器

# 当 function 不为 None 的时是	(item for item in iterable if function(item))

li = [1, 2, 3, 4, 5]
[i for i in filter(lambda x: x > 2, li)]	# => [i for i in li if i > 2]

## [3, 4, 5]


# function 为 None 的时是	(item for item in iterable if item)

data = {"name": "zz", "age": 18}
[i for i in filter(None, data)]				# => [i for i in data if i]

reduce

Python 3.4 中改到 functools 模块中实现

5.5 题外话

函数式编程

起源于 范畴论Category Theory)的数学分支,认为世界上所有概念体系,都成抽象成一个个 范畴category

  • 范畴: 使用箭头连接的物体,彼此存在某种关系
  • 集合 + 函数: 所有成员是一个集合、变形关系是函数,因此通过函数,可以从范畴的一个成员,算出其他成员

因此可以简单理解为是 范畴类 Category + value + 函数 function

这玩意前端大放异彩,包括一些合成、柯里化、函子啥的,但不怎么符合 Python 哲学

上次编辑于: 2022/9/20 06:19:59