Python学习笔记(五)函数

“Life is short, You need Python” – Bruce Eckel

Environment

  • OS: macOS Mojave
  • Python version: 3.7
  • IDE: Jupyter Notebook

文章目录

  • 0. 写在前面
  • 1. 函数的定义
    • 1.1 设置参数
      • 1.1.1 位置参数和默认参数
      • 1.1.2 关键字参数
      • 1.1.3 仅位置参数
      • 1.1.4 可变长参数
    • 1.2 函数体
      • 1.2.1 直接赋值
      • 1.2.2 global 关键字
    • 1.3 返回值
  • 2. 匿名函数
  • 3. 高阶函数
  • 4. 函数的嵌套
    • 4.1 概念
    • 4.2 nonlocal 关键字
  • 5. 闭包
    • 5.1 闭包的概念
    • 5.2 闭包的变量
    • 5.3 闭包的意义
    • 5.4 闭包与装饰器



0. 写在前面

函数体现的是面向过程的编程,如可以将训练模型的过程写成一个函数;类体现的是面向对象的编程,创造具有属性和方法的对象,如可以将一个模型定义为一个类实例化而来的对象。这里做一点儿学习笔记,内容包括函数相关的操作,需要关注变量的作用域问题,并且理解函数闭包。

使用函数的意义

  • 对一块代码进行封装,模块化地解决问题
  • 使需要重复使用的代码能够方便地被调用,且利于管理和维护

1. 函数的定义

def 语句 实现函数的定义

def 函数名(参数):
	""" 函数文档 """
	函数体
	return 返回值

返回值 = 函数名(参数) # 调用

对于在函数中调用另个函数的情况, Python 允许被调用的函数在之后定义

def bar():
    print('bar')
    
def foo():
    bar() # 之前定义
    baz() # 之后定义
    
def baz():
    print('baz')
    
foo()
  • 使用 “"" xxx """ , 在函数的开头编写函数文档,便于代码的维护。调用 函数名.__doc__ 得到函数文档的内容

计算 sigmoid 函数自变量分别为 0 , 1 0, 1 0,1 − 10 -10 10 的结果
σ ( x ) = 1 1 + e − x \sigma(x) = \frac{1}{1 + e^{-x}} σ(x)=1+ex1

import math

def sigmoid(x):
    """sigmoid 函数"""
    return 1 / (1 + math.e**(-x))

print(sigmoid(0)) # 0.5
print(sigmoid.__doc__) # sigmoid 函数



执行一个定义好的函数可分为三步:

  1. 接收输入 —— 参数
  2. 运行处理 —— 函数体
  3. 输出结果 —— 返回值

1.1 设置参数

参数可分为形式参数(形参)和实际参数(实参)

  • 形参是指定义函数时使用的参数
  • 实参是指调用函数时传入的参数

在之前的 sigmoid 函数中

import math

def sigmoid(x): # 此处的 x 为形参
    """sigmoid 函数"""
    return 1 / (1 + math.e**(-x)) # 此处的 x 为形参

res = sigmoid(0) # 传入的 0 为实参



根据函数定义时的形式,参数可以分为位置参数默认参数可变长参数;根据函数调用时的形式,参数可以为关键字参数


1.1.1 位置参数和默认参数

定义函数时,

  • 不设置参数的默认值,则为位置参数
  • 设置参数的默认值,则为默认参数,在调用时可不对该参数传值
  • 使用 *args**kwargs ,则为可变长参数
  • 有严格的顺序要求:位置参数在最前,之后为默认参数,最后为可变长参数

# 其中,x 和 w 为位置参数,b 为默认参数,后俩为可变长参数
def foo(x, w, b=0):
    return w * x + b

# 调用,将 1 传给 x,将 2 传给 w
print(foo(1, 2)) # 2

注意 ⚠️ 勿将默认参数的值设置为可变类型,包括列表、字典和字符串,会搞出 bug!

# 在列表最后添加一个元素
def foo(ls=[]):
    ls.append(6)
    return ls

print(foo()) # [6]
print(foo()) # [6, 6]
print(foo()) # [6, 6, 6]

每次调用函数时,分配给形参的内存地址都是相同的,函数存在,形参就存在,这块内存不会被 Python 的垃圾回收机制清除

def foo(ls=[0, 1]):
    print(id(ls))
    ls.append(2)
    print(id(ls))

print(foo())
# 5187260680
# 5187260680
# [0, 1, 2]
print(foo())
# 5187260680
# 5187260680
# [0, 1, 2, 2]

1.1.2 关键字参数

调用函数时,

  • 若未指定形参,则为位置参数,严格按一一对应的顺序传入值
  • 若指定了形参,则为关键字参数,除去已传入的位置参数,剩余的参数数量上必须相同(指定参数后允许顺序打乱)
  • 有严格的顺序要求:位置参数在前,关键字参数在后

def foo(x, w, b=0):
    return w * x + b

# 使用关键字参数,允许传入的顺序与定义的顺序不一致
print(foo(2, b=3, w=1)) # 5
print(foo(w=1, 2, b=3)) # 报错 SyntaxError

1.1.3 仅位置参数

一些 Python 内置函数被定义为仅接受位置参数,如 float(x=0, /) ,在 / 之前的参数在调用函数时仅能以位置参数的形式传入,若以关键字参数形式传入,则报错。

print(float('1.1')) # 1.1

float(x='1.1') # 报错

不过从 Python 3.8 开始才允许用户在使用 def 语句定义函数时使用 / 定义仅位置参数。

1.1.4 可变长参数

  • 可变长(位置)参数 *args ,用于接收一些可能需要的额外参数

定义函数时,该参数放在最后(**kwargs 之前)。调用时先传入位置参数和关键字参数,之后剩余的参数以元组的形式打包传递给 args

def foo(a, b, c, *args):
    print(args) # 元组形式
    print((a, b, c, args))
    print((a, b, c, *args)) # 星号解包
    return None

baz(0, 1, 2, 3, 4)
# (3, 4)
# (0, 1, 2, (3, 4))
# (0, 1, 2, 3, 4)

  • 可变长关键字参数 **kwargs

定义函数时,该参数放在最后。调用时先传入前面的参数,剩余的以关键字形式传入的参数将以字典的形式打包传递给 kwargs

def bar(a, b, c, **kwargs):
    print(kwargs)
    print(a, b, c, kwargs)
    return None

bar(1, 2, 3, d=3, e=4)
# {'d': 3, 'e': 4}
# 1 2 3 {'d': 3, 'e': 4}

*args**kwargs 一齐使用

def forw(x, *args, **kwargs):
    if len(args) == 0:
        y = x
    elif len(args) == 1:
        w = args[0]
        y = w * x
    elif len(args) == 2:
        w, b = args
        y = w * x + b
    else:
        print('Too many elements in *args')
        return None
    
    if len(kwargs) > 0:
        if kwargs['nonlinearity'] == 'relu':
            return max(0, y)
    
    return y

print(forw(2, -2, 3)) # -1
print(forw(2, -2, 3, nonlinearity='relu')) # 0

1.2 函数体

函数体是封装在函数内部的代码块,只有在调用该函数时才会被执行。

根据变量的作用域,可讲变量分为两类

  • 定义在函数内部的变量仅在该函数内部起作用,称为局部变量
  • 相对地,定义在函数外部的变量能够在全局起作用,称为全局变量

Python 函数体可以访问在其外部定义的变量(访问变量的顺序 LEGB:Local → \rightarrow Enclosing → \rightarrow Global → \rightarrow Built-in),但无法修改不可变类型的变量(注意 ⚠️ 可变类型的变量可以修改,如列表、集合)

x = 6
def foo():
    y = x + 60 # 访问了全局变量
    print(y)
bar() # 66

def bar():
    x += 60 # 尝试修改全局变量
    print(x)
bar() # 报错
# UnboundLocalError: local variable 'x' referenced before assignment

dc = {1: 'one', 2: 'two'}
def baz():
    dc[3] = 'three'
baz()
print(dc) # {1: 'one', 2: 'two', 3: 'three'}

在 Python 函数体中定义变量时,可以用三种方式

  • 直接赋值,得到局部变量
  • 采用 global 关键字进行声明
  • 采用 nonlocal 关键字进行声明

1.2.1 直接赋值

该变量仅在函数体内部定义和作用

def foo():
    a = 6
    
foo()
print(a) # 报错 NameError

1.2.2 global 关键字

函数内部可以访问外部的全局变量,但无法对其进行修改;若需要修改,则需要使用 global 关键字进行声明

a = 6

def foo():
    global a
    a += 60
    print(a)
    
foo() # 66

1.3 返回值

使用 return 语句设定函数的返回值,该语句的执行代表函数执行的结束,这句完事后退出函数。

若一个函数内无 return 语句,则执行完所有函数体代码后返回 Nonereturn 后跟多个变量以逗号隔开,则以元组的形式返回多个值

def foo():
    """返回为 None"""
    print()
    
def bar():
    """这是一个返回单个值的函数"""
    return 6

def baz():
    """这是一个返回元组的函数"""
    return 66, 666

print(foo()) # None
print(bar()) # 6
print(baz()) # (66, 666)

2. 匿名函数

匿名函数使用 lambda 变量: 函数体 定义并调用,简化了代码。

# 如判断三个样本属于第一类和第二类的概率
y = [
    [0.9, 0.1],
    [0.3, 0.7],
    [0.4, 0.6]
]

# 按照判断属于第一类的概率进行排列
y.sort(key=lambda x: x[0], reverse=True)
print(y)

3. 高阶函数

Python 中,函数为第一类对象,其可以赋值给一个变量、可以作为元素添加到组合类型中、可作为参数值传入其它函数,还可以当作其他函数的返回值。

以函数为参数返回值为函数的函数称为高阶函数(High-order function)

def high_order_func(func):
    return func


f = high_order_func(print)
f('hello') # hello

4. 函数的嵌套

4.1 概念

在一个函数中定义定义另一个函数,称为函数的嵌套

def outer():
    print('outer is being called.')
    
    def inner():
        print('inner is being called.')
        print('inner done.')
        
    inner()
    print('outer done.')
    
    
outer()
# outer is being called.
# inner is being called.
# inner done.
# outer done.

4.2 nonlocal 关键字

若需要修改嵌套中外层函数的变量,则 global 关键字声明无法实现(global 只能修改到全局),此时需要 nonlocal 关键字声明

def outer():
    x = 6
    
    def inner():
        nonlocal x
        
        x += 60
        print(x)
    
    inner()
        
outer() # 66
print(x) # 报错 NameError

5. 闭包

5.1 闭包的概念

外层函数以内层函数为返回值,内层函数中引用了外层函数的变量,称为闭包(closure)

例 1 内层函数无返回值

def affine_weight(w):
    print('Setting affine transform config...')
    b = 10
    
    def affine(x):
        print('result:', w * x + b)
        
    print('Config done.')
    return affine
    
    
transform = affine_weight(2)
# Setting affine transform config...
# Config done.

transform(3)
# result: 16

例 2 内层函数有返回值

def affine_weight(w):
    print('Setting affine transform config...')
    b = 10
    
    def affine(x):
        return w * x + b
        
    print('Config done.')
    return affine
    
    
transform = affine_weight(2)
# Setting affine transform config...
# config done.

print(transform(3)) # 16

5.2 闭包的变量

闭包的 __closure__ 属性是一个以 cell 对象为元素的元组,cell 对象的 cell_contents属性就是闭包中引用的变量。

transform.__closure__
# (,
# )
for elem in transform.__closure__:
    print(elem.cell_contents)
# 10
# 2

5.3 闭包的意义

闭包由一个函数与其可引用变量的外部环境所组成。

以下是维基百科的解释是

A closure, also lexical closure or function closure, is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created. Unlike a plain function, a closure allows the function to access those captured variables through the closure’s copies of their values or references, even when the function is invoked outside their scope.


从面向对象编程的角度理解(相关内容参考Python基础(六)类和对象):如果将闭包视作一个对象,那么

  1. 其可引用的外部环境的变量就是它的属性;

  2. 函数所执行的操作就是这个对象唯一的方法。

如下例所示,定义了三个进行仿射变换(方法)的闭包,而它们的参数(属性)不同。

# 定义三种不同的线性变换
def affine_params(w, b):
    def affine(x):
        res = w * x + b
        return res
    
    return affine

    
transform_1 = affine_params(3, 4)
transform_2 = affine_params(5, 10)
transform_3 = affine_params(8, 14)

print(transform_1(2)) # 10
print(transform_2(2)) # 20
print(transform_3(2)) # 30

5.4 闭包与装饰器

Python 中闭包的主要应用就是装饰器,内容参考Python基础(十)深浅拷贝与三大器 Part 4。

你可能感兴趣的:(Python学习笔记)