《Python 基础篇》五:函数

Author: ACatSmiling

Since: 2024-09-27

函数简介

函数:也是一个对象,对象是内存中专门用来存储数据的一块区域。函数可以用来保存一些可执行的代码,并且可以在需要时,对这些语句进行多次的调用。

创建函数:

def 函数名([形参1, 形参2, ... 形参n]):
    代码块
  • 函数名必须要符号标识符的规范(可以包含字母、数字、下划线,但是不能以数字开头)。
  • 函数中保存的代码不会立即执行,需要调用函数代码才会执行。

调用函数:

函数名()
  • 定义函数一般都是要实现某种功能的。

示例:

# 定义一个函数
def fn():
    print('这是我的第一个函数!')
    print('hello')
    print('今天天气真不错!')


# 打印 fn
print(fn)  # <function fn at 0x00000175FD0261F0>
print(type(fn))  # <class 'function'>

# fn 是函数对象,fn() 调用函数
# print 是函数对象,print() 调用函数
fn()


# 定义一个函数,可以用来求任意两个数的和
def sum():
    a = 123
    b = 456
    print(a + b)


sum()  # 579


# 定义函数时指定形参
def fn2(a, b):
    print(a, "+", b, "=", a + b)


# 调用函数时,来传递实参
fn2(10, 20)  # 10 + 20 = 30
fn2(123, 456)  # 123 + 456 = 579

函数的参数

在定义函数时,可以在函数名后的()中定义数量不等的形参,多个形参之间使用,隔开。

  • 形参(形式参数):定义形参就相当于在函数内部声明了变量,但是并不赋值。
  • 实参(实际参数):调用函数时,传入的真正的参数值。
  • 如果函数定义时,指定了形参,那么在调用函数时也必须传递实参, 实参将会赋值给对应的形参,简单来说,有几个形参就得传几个实参。
# 求任意三个数的乘积
def mul(a, b, c):
    print(a * b * c)


mul(1, 2, 3)  # 6


# 根据不同的用户名显示不同的欢迎信息
def welcome(username):
    print('欢迎 ', username, ' 光临')


welcome('孙悟空')  # 欢迎 孙悟空 光临

参数的传递方式

函数的传参方式有多种:

  1. 默认值参数:定义形参时,可以为形参指定默认值。指定了默认值以后,如果用户传递了参数,则默认值没有任何作用;如果用户没有传递参数,则默认值就会生效。
  2. 位置参数:即将对应位置的实参复制给对应位置的形参
  3. 关键字参数:可以不按照形参定义的顺序去传递,而直接根据参数名去传递参数
    • 位置参数和关键字参数可以混合使用,混合使用时,必须将位置参数写到前面。
  4. 函数在调用时,解析器不会检查实参的类型,实参可以传递任意类型的对象
  5. 在函数中对形参进行重新赋值,不会影响其他的变量。
  6. 如果形参指向的是一个对象,当我们通过形参去修改对象时,会影响到所有指向该对象的变量。

示例:

# 定义函数,并为形参指定默认值
def fn(a=5, b=10, c=20):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn(1, 2, 3)
fn(1, 2)
fn()

# 位置参数,根据位置传递对应的实参
fn(1, 2, 3)

# 关键字参数,根据形参名传递对应的实参
fn(b=1, c=2, a=3)
print('hello', end='')

# 位置参数和关键字参数混合使用,位置参数必须在关键字参数前面
fn(1, c=30)


def fn2(a):
    print('a =', a)


# 函数在调用时,解析器不会检查实参的类型
# 实参可以传递任意类型的对象
b = 123
fn2(b)  # a = 123
b = True
fn2(b)  # a = True
b = 'hello'
fn2(b)  # a = hello
b = None
fn2(b)  # a = None
b = [1, 2, 3]
fn2(b)  # a = [1, 2, 3]
fn2(fn)  # a = <function fn at 0x0000025AB8B561F0>


# 类型不检查的缺陷,在传参时需要额外注意
def fn3(a, b):
    print(a + b)


# fn3(123, "456")  # TypeError: unsupported operand type(s) for +: 'int' and 'str'

# 在函数中对形参进行重新赋值,不会影响其他的变量
def fn4(a):
    a = 20
    print('a =', a, id(a))  # a = 20 140708519029120


c = 10
fn4(c)
print(c)  # 10


# 如果形参指向的是一个对象,当我们通过形参去修改对象时,会影响到所有指向该对象的变量
def fn5(a):
    # a 是一个列表,尝试修改列表中的元素
    a[0] = 30
    print('a =', a, id(a))  # a = [30, 2, 3] 2056358531968


c = [1, 2, 3]
fn5(c)
print('c =', c, id(c))  # c = [30, 2, 3] 2056358531968

# 通过浅复制,或者切片,实现不修改 c 本身
fn4(c.copy())
fn4(c[:])

Java 的参数传递机制

在 Java 中,基本数据类型(如 int、double、char 等)是按值传递的,即传递的是变量的值的副本。对于引用数据类型(如对象、数组等),实际上传递的是对象引用的副本,但从效果上看,可以通过这个引用副本去操作对象本身。

Python 的参数传递机制

Python 的参数传递通常被认为是 "对象引用传递"。不可变对象(如整数、字符串、元组等)在函数内部的行为看起来像是值传递,因为不能直接修改不可变对象,一旦尝试修改,实际上是创建了新的对象。而可变对象(如列表、字典、集合等)在函数内部的修改会影响到外部的对象,因为传递的是对象的引用。

二者对比

基本数据类型的传递:

  • Java:对于基本数据类型(如 int、double、char 等),Java 是严格的值传递。这意味着当把一个基本数据类型的变量作为参数传递给方法时,会将该变量的值复制一份传递过去,在方法内部对这个参数的修改不会影响到原始变量的值。

     public class JavaValuePassing {
         public static void main(String[] args) {
             int num = 10;
             changeNumber(num);
             System.out.println(num); // 输出仍然是 10
         }
    
         public static void changeNumber(int n) {
             n = 20;
         }
     }
    
  • Python:Python 中没有严格意义上的基本数据类型和引用数据类型的区分。但对于不可变的基本数据类型类似物(如整数、字符串等),在函数调用时的行为有点类似于值传递。然而,实际上是对象引用的传递,只是因为不可变对象一旦被修改就会创建新的对象,所以看起来像是值传递。

     def change_number(num):
         num = 20
    
     a = 10
     change_number(a)
     print(a) # 输出仍然是 10
    

引用数据类型的传递:

  • Java:对于引用数据类型(如对象、数组等),Java 传递的是对象引用的副本。这意味着在方法内部可以通过这个引用副本去操作对象本身,修改对象的属性会影响到外部的对象。但是,如果将这个引用重新指向一个新的对象,不会影响到原始的引用。

     class Person {
         String name;
    
         public Person(String name) {
             this.name = name;
         }
    
         public String getName() {
             return name;
         }
    
         public void setName(String name) {
             this.name = name;
         }
     }
    
     public class JavaValuePassing {
         public static void main(String[] args) {
             Person person = new Person("Alice");
             changePerson(person);
             System.out.println(person.getName()); // 可能输出修改后的名字
         }
    
         public static void changePerson(Person p) {
             p.setName("Bob");
             // 如果重新创建一个新的对象并赋值给参数 p
             // p = new Person("Charlie"); 这样不会影响到外部的 person 引用
         }
     }
    
  • Python:对于可变的引用数据类型类似物(如列表、字典、集合等),传递的是对象引用。在函数内部对这个对象的修改会影响到外部的对象。对于不可变的引用数据类型类似物(如元组),虽然传递的也是对象引用,但由于元组本身不可变,在函数内部无法修改其内容。

     def change_list(lst):
         lst.append(20)
    
     my_list = [1, 2, 3]
     change_list(my_list)
     print(my_list) # 输出为 [1, 2, 3, 20]
    

小结:

  • Java:
    • Java 的值传递机制相对较为明确和严格,基本数据类型和引用数据类型的传递方式有明显的区分,并且对于引用数据类型的操作有一定的限制,不会因为意外的重新赋值而影响到外部的引用。
    • 开发者在使用时可以比较清楚地知道参数传递的效果,不容易出现意外的错误。
  • Python:
    • Python 的参数传递机制更加灵活,但也可能因为这种灵活性而导致一些难以察觉的错误。开发者需要根据对象的可变性来理解参数传递的行为,并且要注意在函数内部对可变对象的修改可能会影响到外部的对象。
    • Python 的这种特性在某些情况下可以使代码更加简洁和高效,但也需要开发者更加小心地处理参数传递和对象的修改。

不定长的参数

在定义函数时,可以在形参前边加上一个*,这样这个形参将会获取到所有的实参,并将所有的实参保存到一个元组中。

  • *的形参只能有一个。
  • *的参数,可以和其他参数配合使用。
  • 可变参数不是必须写在最后,但是注意,带*的参数后的所有参数,必须以关键字参数的形式传递,否则报错。
  • 如果在形参的开头直接写一个*,则要求所有的参数必须以关键字参数的形式传递。
  • *形参只能接收位置参数,而不能接收关键字参数。
  • **形参可以接收其他的关键字参数,它会将这些参数统一保存到一个字典中。字典的 key 就是参数的名字,字典的 value 就是参数的值。
  • **形参只能有一个,并且必须写在所有参数的最后。
  • 传递实参时,也可以在序列类型的参数前添加*,这样会自动将序列中的元素依次作为参数传递给函数,但要求序列中元素的个数必须和形参的个数一致。
  • 如果是字典,通过**来进行解包操作。
# 定义一个函数,可以求任意个数字的和
def sum(*nums):
    # 定义一个变量,来保存结果
    result = 0
    # 遍历元组,并将元组中的数进行累加
    for n in nums:
        result += n
    print(result)


sum(10, 20, 30, 40)  # 100
sum(10, 20, 30, 40, 50, 60, 70)  # 280


# *a 会接受所有的位置实参,并且会将这些实参统一保存到一个元组中(装包)
def fn(*a):
    print("a =", a, type(a))


fn(1, 2, 3)  # a = (1, 2, 3) <class 'tuple'>
fn(1, 2, 3, 4, 5)  # a = (1, 2, 3, 4, 5) <class 'tuple'>


# 带星号的形参只能有一个
# 带星号的参数,可以和其他参数配合使用
# 下面的函数,第一个参数给a,第二个参数给b,剩下的都保存到c的元组中
def fn2(a, b, *c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


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


# 可变参数不是必须写在最后,但是注意,带 * 的参数后的所有参数,必须以关键字参数的形式传递
# 下面的函数,第一个参数给 a,剩下的位置参数给 b 的元组,c 必须使用关键字参数
def fn3(a, *b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn3(1, 2, 3, 4, c=5)


# 下面的函数,所有的位置参数都给 a,b 和 c 必须使用关键字参数
def fn4(*a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn4(1, 2, 3, b=4, c=5)


# 如果在形参的开头直接写一个 *,则要求我们的所有的参数必须以关键字参数的形式传递
def fn5(*, a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


fn5(a=3, b=4, c=5)


# * 形参只能接收位置参数,而不能接收关键字参数
# def fn3(*a) :
#     print('a =',a)

# ** 形参可以接收其他的关键字参数,它会将这些参数统一保存到一个字典中
#   字典的 key 就是参数的名字,字典的 value 就是参数的值
# ** 形参只能有一个,并且必须写在所有参数的最后
def fn6(b, c, **a):
    print('a =', a, type(a))
    print('b =', b)
    print('c =', c)


fn6(b=1, c=2, h=3, e=10, f=20)
fn6(6, 7, g=1, d=2, h=3, e=10, f=20)

print('##########################################################')


# 参数的解包(拆包)

def fn7(a, b, c):
    print('a =', a)
    print('b =', b)
    print('c =', c)


# 传递实参时,也可以在序列类型的参数前添加星号,这样他会自动将序列中的元素依次作为参数传递给函数
# 这里要求序列中元素的个数必须和形参的个数的一致
t = (10, 20, 30)
fn7(*t)

# 通过 **来对一个字典进行解包操作
d = {'a': 100, 'b': 200, 'c': 300}
fn7(**d)

函数的返回值

函数的返回值就是函数执行以后返回的结果,可以通过return来指定函数的返回值。

  • return 后边可以跟任意的对象,甚至可以是一个函数。return 后边跟什么值,函数就会返回什么值。
  • 如果仅仅写一个 return 或者不写 return,则相当于 return None。
  • 在函数中,return 后的代码都不会执行,return 一旦执行函数自动结束。

示例:

# return 后边跟什么值,函数就会返回什么值
# return 后边可以跟任意的对象,返回值甚至可以是一个函数
def fn():
    # return 'Hello'
    # return [1, 2, 3]
    # return {'k': 'v'}
    def fn1():
        print('hello')

    return fn1  # 返回值也可以是一个函数


r = fn()  # 这个函数的执行结果就是它的返回值
print(r)  # <function fn.<locals>.fn1 at 0x000001F3430BCF70>
r()  # hello


# 如果仅仅写一个 return 或者不写 return,则相当于 return None
# 在函数中,return 后的代码都不会执行,return 一旦执行函数自动结束
def fn2():
    a = 10
    return
    print('abc')  # 不会执行


r = fn2()
print(r)  # None


def fn3():
    for i in range(5):
        if i == 3:
            # break 用来退出当前循环
            # continue 用来跳过当次循环
            return  # return 用来结束函数
        print(i)
    print('循环执行完毕!')


fn3()


def fn4():
    return 10


# fn4 和 fn4()的区别
print(fn4)  # fn4 是函数对象,打印 fn4 实际是在打印函数对象:<function fn5 at 0x00000229B3CFD670>
print(fn4())  # fn4() 是在调用函数,打印 fn4() 实际上是在打印 fn4() 函数的返回值:10

文档字符串

文档字符串:在定义函数时,可以在函数内部编写文档字符串,文档字符串就是函数的说明。文档字符串非常简单,其实直接在函数的第一行写一个字符串就是文档字符串,当我们编写了文档字符串时,就可以通过help()函数来查看函数的说明。

示例:

# help() 是 Python 中的内置函数
# 通过 help() 函数可以查询 Python 中的函数的用法
# 语法:help(函数对象)
help(print)  # 获取 print() 函数的使用说明


# 文档字符串(doc str)
def fn(a: int, b: bool, c: str = 'hello') -> int: # 函数参数后跟着类型,返回值是一个 int
    """
    这是一个文档字符串的示例

    函数的作用:。。。。。。
    函数的参数:
        a,作用,类型,默认值。。。
        b,作用,类型,默认值。。。
        c,作用,类型,默认值。。。
    """
    return 10


help(fn)

作用域

作用域(scope):指的是变量生效的区域。在 Python 中一共有两种作用域:

  1. 全局作用域:全局作用域在程序执行时创建,在程序执行结束时销毁。
    • 所有函数以外的区域都是全局作用域。
    • 在全局作用域中定义的变量,都属于全局变量,全局变量可以在程序的任意位置被访问。
  2. 函数作用域:函数作用域在函数调用时创建,在调用结束时销毁。
    • 函数每调用一次就会产生一个新的函数作用域。
    • 在函数作用域中定义的变量,都是局部变量,它只能在函数内部被访问。
    • 如果希望在函数内部修改全局变量,则需要使用 global 关键字,来声明变量。

变量的查找过程:当我们使用变量时,会优先在当前作用域中寻找该变量,如果有则使用;如果没有则继续去上一级作用域中寻找,如果有则使用;如果依然没有则继续去上一级作用域中寻找,以此类推,直到找到全局作用域,依然没有找到,则会抛出异常 "NameError: name 'a' is not defined"。

示例:

b = 20  # 全局变量


def fn():
    a = 10  # a 定义在了函数内部,所以他的作用域就是函数内部,函数外部无法访问
    print('函数内部:', 'a =', a)
    print('函数内部:', 'b =', b)


fn()

# print('函数外部:', 'a =', a)  # NameError: name 'a' is not defined
print('函数外部:', 'b =', b)



def fn1():
    def fn2():
        print('fn3中:', 'a =', a)

    fn2()


# fn1() # fn1 中的嵌套函数 fn2,找不到 a,报错 NameError: name 'a' is not defined

a = 20


def fn3():
    # a = 10 # 在函数中为变量赋值时,默认都是为局部变量赋值
    # 如果希望在函数内部修改全局变量,则需要使用 global 关键字,来声明变量
    global a  # 声明在函数内部使用的 a 是全局变量,此时再去修改 a 时,就是在修改全局的 a
    a = 10  # 修改全局变量
    print('函数内部:', 'a =', a)


fn3()
print('函数外部:', 'a =', a)  # 函数外部: a = 10

命名空间

命名空间(namespace):指的是变量存储的位置,每一个变量都需要存储到指定的命名空间当中。

  • 每一个作用域都会有一个它对应的命名空间。
  • 全局命名空间,用来保存全局变量;函数命名空间,用来保存函数中的变量。
  • 命名空间实际上就是一个字典,是一个专门用来存储变量的字典
  • 使用locals()函数,可以获取当前作用域的命名空间,其返回值是一个字典。
    • 如果在全局作用域中调用 locals() 则获取全局命名空间,如果在函数作用域中调用 locals() 则获取函数命名空间。
    • 使用globals()函数,可以用来在任意位置获取全局命名空间。

示例:

scope = locals()  # 当前命名空间
print(type(scope))  # <class 'dict'>
# 下面两个打印效果相同
a = 20
print(a)  # 20
print(scope['a'])  # 20


# 向 scope 中添加一个 key-value
# scope['c'] = 1000  # 向字典中添加 key-value 就相当于在全局中创建了一个变量(一般不建议这么做)
# print(c)  # 1000


def fn():
    a = 10
    scope = locals()  # 在函数内部调用 locals() 会获取到函数的命名空间
    print(type(scope))  # <class 'dict'>

    # scope['b'] = 90  # 可以通过 scope 来操作函数的命名空间,但是也是不建议这么做
    # print(b)

    # globals() 函数可以用来在任意位置获取全局命名空间
    # 函数外面无法获得函数的命名空间
    global_scope = globals()
    print(global_scope['a'])  # 20
    # global_scope['a'] = 30 # 不建议这么做
    # print(global_scope['a'])  # 30


fn()

递归

递归:递归是解决问题的一种方式,它和循环很像,它的整体思想是,将一个大问题分解为一个个的小问题,直到问题无法分解时,再去解决问题。

递归式函数的两个要件:

  1. 基线条件:问题可以被分解为的最小问题,当满足基线条件时,递归就不在执行了。
  2. 递归条件:将问题继续分解的条件。

示例:

# 10 的阶乘
n = 10
for i in range(1, 10):
    n *= i

print(n)


# 创建一个函数,可以用来求任意数的阶乘
def factorial(n):
    '''
        该函数用来求任意数的阶乘

        参数:
            n 要求阶乘的数字
    '''

    # 创建一个变量,来保存结果
    result = n

    for i in range(1, n):
        result *= i

    return result


print(factorial(10))


# 递归简单理解就是自己去引用自己!
# 递归式函数,在函数中自己调用自己!

# 下面这个是无穷递归,如果这个函数被调用,程序的内存会溢出,效果类似于死循环
# def fn():
#     fn()
# fn()


# 递归和循环类似,基本是可以互相代替的,
#   循环编写起来比较容易,阅读起来稍难
#   递归编写起来难,但是方便阅读

def factorial(n):
    # 基线条件:判断 n 是否为 1,如果为 1,则此时不能再继续递归
    if n == 1:
        # 1 的阶乘就是 1,直接返回 1
        return 1
    # 递归条件
    return n * factorial((n - 1))


print(factorial(10))


# 创建一个函数 power,来为任意数字做幂运算 n ** i
def power(n, i):
    '''
        power() 用来为任意的数字做幂运算

        参数:
            n 要做幂运算的数字
            i 做幂运算的次数
    '''
    # 基线条件
    if i == 1:
        # 求1次幂
        return n
    # 递归条件
    return n * power(n, i - 1)


print(pow(3, 4))


# 创建一个函数,用来检查一个任意的字符串是否是回文字符串,如果是返回 True,否则返回 False
# 回文字符串,字符串从前往后念和从后往前念是一样的
# abcba
# abcdefgfedcba
# 先检查第一个字符和最后一个字符是否一致,如果不一致则不是回文字符串
#     如果一致,则看剩余的部分是否是回文字符串
# 检查 abcdefgfedcba 是不是回文
# 检查 bcdefgfedcb 是不是回文
# 检查 cdefgfedc 是不是回文
# 检查 defgfed 是不是回文
# 检查 efgfe 是不是回文
# 检查 fgf 是不是回文
# 检查 g 是不是回文
def hui_wen(s):
    '''
        该函数用来检查指定的字符串是否回文字符串,如果是返回 True,否则返回 False

        参数:
            s:就是要检查的字符串
    '''
    # 基线条件
    if len(s) < 2:
        # 字符串的长度小于 2,则字符串一定是回文
        return True
    elif s[0] != s[-1]:
        # 第一个字符和最后一个字符不相等,不是回文字符串
        return False
        # 递归条件
    return hui_wen(s[1:-1])


# def hui_wen(s):
#     '''
#         该函数用来检查指定的字符串是否回文字符串,如果是返回 True,否则返回 False

#         参数:
#             s:就是要检查的字符串
#     '''
#     # 基线条件
#     if len(s) < 2 :
#         # 字符串的长度小于 2,则字符串一定是回文
#         return True
#     # 递归条件
#     return s[0] == s[-1] and hui_wen(s[1:-1])

print(hui_wen('abba'))
print(hui_wen('abcdefgfedcba'))

高阶函数

高阶函数:接收函数作为参数,或者将函数作为返回值的函数是高阶函数。当我们使用一个函数作为参数时,实际上是将指定的代码传递进了目标函数。

在 Python 中,函数是一等对象。一等对象一般都会具有如下特点:

  • 对象是在运行时创建的。
  • 能赋值给变量或作为数据结构中的元素。
  • 能作为参数传递。
  • 能作为返回值返回。

高阶函数至少要符合以下两个特点中的一个:

  • 能接收一个或多个函数作为参数。
  • 能将函数作为返回值返回。

示例:

# 定义一个函数,功能:可以将指定列表中的所有的偶数,保存到一个新的列表中返回

# 待提取列表
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


# 常规写法

def fn(lst):
    '''
        fn() 函数可以将指定列表中的所有偶数提取出来,并保存到一个新列表中返回

        参数:
            lst:指定的要筛选的列表
    '''
    new_list = []
    for n in lst:
        if n % 2 == 0:
            new_list.append(n)
    return new_list


print(fn(l))  # [2, 4, 6, 8, 10]


# 高阶函数

# 功能 1:定义一个函数,用来检查一个任意的数字是否是偶数
def fn1(i):
    if i % 2 == 0:
        return True

    return False


# 功能 2:定义一个函数,用来检查指定的数字是否大于 5
def fn2(i):
    if i > 5:
        return True
    return False


# 功能 2:定义一个函数,用来检查一个任意的数字是否能被 3 整除
def fn3(i):
    return i % 3 == 0


# 多功能的高阶函数

def fn(func, lst):
    '''
        fn() 函数可以将指定列表中的数据按指定函数要求提取出来,并保存到一个新列表中返回

        参数:
            func:指定的提取要求
            lst:指定的要筛选的列表
    '''
    new_list = []
    for n in lst:
        if func(n):
            new_list.append(n)
    return new_list


print(fn(fn1, l))  # 获取偶数:[2, 4, 6, 8, 10]
print(fn(fn2, l))  # 获取大于 5 的数:[6, 7, 8, 9, 10]
print(fn(fn3, l))  # 获取能被 3 整除的数:[3, 6, 9]

# filter() 函数的功能,就如上面自定义的 fn() 函数
# filter() 可以从序列中过滤出符合条件的元素,保存到一个新的序列中
# 参数:
#  1. 函数,根据该函数来过滤序列(可迭代的结构)
#  2. 需要过滤的序列(可迭代的结构)
# 返回值:
#   过滤后的新序列(可迭代的结构)

iterator = filter(fn1, l)
# for n in iterator:
#     print(n)
print(list(iterator))  # 返回的是一个可迭代的结构,需要转换成 list 才能直接打印出来数据
print(list(filter(fn2, l)))
print(list(filter(fn3, l)))

匿名函数

匿名函数:将一个或多个函数作为参数来接收。匿名函数一般都是作为参数使用,其他地方一般不会使用。

语法:lambda 参数列表 : 返回值

示例:

# 在前面的示例中,fn1 ~ fn3 是作为参数传递进 filter() 函数中
#   而 fn1 ~ fn3 实际上只有一个作用,就是作为 filter() 的参数
#   filter() 调用完毕以后,fn1 ~ fn3 就已经没用
#   这种情况可以用匿名函数简化
# 匿名函数 lambda 函数表达式(语法糖)
#   lambda 函数表达式专门用来创建一些简单的函数,他是函数创建的又一种方式
#   语法:lambda 参数列表 : 返回值
#   匿名函数一般都是作为参数使用,其他地方一般不会使用

# 待提取列表
l = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]


# 常规写法
def fn5(a, b):
    return a + b
print(fn5(10, 20))  # 常规写法

print((lambda a, b: a + b)(10, 20))  # lambad 表达式写法

fn6 = lambda a, b: a + b  # 也可以将匿名函数赋值给一个变量,一般不会这么做
print(fn6(10, 20))

# filter() 函数中可以很方便的使用 lambda 表达式
#   此时,lambda 表达式只会使用一次,使用完后内存中自动回收
r = filter(lambda i: i % 2 == 0, l) # lambda i: i % 2 == 0 是匿名函数
print(list(r))  # [2, 4, 6, 8, 10]
print(list(filter(lambda i: i > 5, l)))  # [6, 7, 8, 9, 10],lambda i: i > 5 是匿名函数

# map() 函数可以对可迭代对象中的所有元素做指定的操作,然后将其添加到一个新的对象中返回

print(list(map(lambda i: i ** 2, l)))  # 对列表中的每一个元素求平方,[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

# sort() 函数用来对列表中的元素进行排序:
#   sotr() 只能排序列表
#   默认是直接比较列表中的元素的大小
# 在 sort() 可以接收一个关键字参数 key:
#   key 需要一个函数作为参数,当设置了函数作为参数
#   每次都会以列表中的一个元素作为参数来调用该函数
#   并且使用函数的返回值来比较元素的大小
l = ['bb', 'aaaa', 'c', 'ddddddddd', 'fff']
l.sort()
print(l)  # 默认比较:['aaaa', 'bb', 'c', 'ddddddddd', 'fff']

l = ['bb', 'aaaa', 'c', 'ddddddddd', 'fff']
l.sort(key=len)
print(l)  # 按长度比较:['c', 'bb', 'fff', 'aaaa', 'ddddddddd']

l = [2, 5, '1', 3, '6', '4']
l.sort(key=int)
print(l)  # 把每一个元素转换成整形后再比较:['1', 2, 3, '4', 5, '6']

l = [2, 5, '1', 3, '6', '4']
l.sort(key=str)
print(l)  # 把每一个元素转换成字符串后再比较:['1', 2, 3, '4', 5, '6']

# sorted() 函数和 sort() 的用法基本一致,但是 sorted() 可以对任意的序列进行排序
#   并且使用 sorted() 排序不会影响原来的对象,而是返回一个新对象

l = [2, 5, '1', 3, '6', '4']  # 排序列表
print('排序前:', l)  # 排序前:[2, 5, '1', 3, '6', '4']
print('排序中:', sorted(l, key=int))  # 排序中:['1', 2, 3, '4', 5, '6']
print('排序后:', l)  # 排序后:[2, 5, '1', 3, '6', '4']

l = '123765816742634781'  # 排序字符串
print('排序前:', l)  # 排序前:123765816742634781
print('排序中:', sorted(l, key=int))  # 排序中:['1', '1', '1', '2', '2', '3', '3', '4', '4', '5', '6', '6', '6', '7', '7', '7', '8', '8']
print('排序后:', l)  # 排序后:123765816742634781

闭包

闭包:是指在一个函数内部定义另一个函数,并且内部函数可以访问外部函数的局部变量。即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的局部变量。

形成闭包的要件:

  1. 函数嵌套。
  2. 将内部函数作为返回值返回。
  3. 内部函数必须要使用到外部函数的变量。

示例:

# 将函数作为返回值返回,也是一种高阶函数
# 这种高阶函数我们也称为叫做闭包,通过闭包可以创建一些只有当前函数能访问的变量
#   可以将一些私有的数据藏到闭包中

def fn():
    a = 10

    # 函数内部再定义一个函数
    def inner():
        print('我是fn2', a)

    # 将内部函数 inner 作为返回值返回
    return inner


# r 是一个函数,是调用 fn() 后返回的函数
# 而且这个函数是在 fn() 内部定义,并不是全局函数
# 所以这个函数总是能访问到 fn() 函数内的变量
r = fn()
print(r)  # <function fn.<locals>.inner at 0x000001CEC1142430>
r()  # 我是fn2 10

# 求多个数的平均值
nums = [50, 30, 20, 10, 77]

# 常规写法:sum() 用来求一个列表中所有元素的和
print(sum(nums) / len(nums))  # 37.4


# 如果 nums 中的数据是变化的,可以使用闭包
def make_averager():
    # 创建一个列表,用来保存数值
    nums = []

    # 创建一个函数,用来计算平均值
    def averager(n):
        # 将n添加到列表中
        nums.append(n)
        # 求平均值
        return sum(nums) / len(nums)

    return averager


# 函数返回的是 make_averager() 中定义的 averager() 函数
# 并创建了一个 nums 列表,这个列表外界无法访问,只有 averager 对象可以访问
averager = make_averager()

print(averager(10))  # 10/1=10
print(averager(20))  # (10+20)/2=15
print(averager(30))  # (10+20+30)/3=20
print(averager(40))  # (10+20+30+40)/4=25

装饰器

# 创建几个函数

def add(a, b):
    '''
        求任意两个数的和
    '''
    r = a + b
    return r


def mul(a, b):
    '''
        求任意两个数的积
    '''
    r = a * b
    return r


print(add(123, 456))  # 579
print(mul(10, 20))  # 200


# 现在,希望函数可以在计算前,打印开始计算,计算结束后打印计算完毕
#  我们可以直接通过修改函数中的代码来完成这个需求,但是会产生以下一些问题
#   ① 如果要修改的函数过多,修改起来会比较麻烦
#   ② 并且不方便后期的维护
#   ③ 并且这样做会违反开闭原则(OCP)
#           程序的设计,要求开发对程序的扩展,要关闭对程序的修改


# 我们希望在不修改原函数的情况下,来对函数进行扩展
def fn():
    print('我是fn函数....')


# 只需要根据现有的函数,来创建一个新的函数
def fn2():
    print('函数开始执行~~~')
    fn()
    print('函数执行结束~~~')


fn2()


# 创建新函数,扩展 add()
def new_add(a, b):
    print('计算开始~~~')
    r = add(a, b)
    print('计算结束~~~')
    return r


r = new_add(111, 222)
print(r)

print('##############################################################')


# 上边的方式,已经可以在不修改源代码的情况下对函数进行扩展了
#   但是,这种方式要求我们每扩展一个函数就要手动创建一个新的函数,实在是太麻烦了
#   为了解决这个问题,我们创建一个函数,让这个函数可以自动的帮助我们生产函数

def begin_end(old):
    '''
        用来对其他函数进行扩展,使其他函数可以在执行前打印开始执行,执行后打印执行结束

        参数:
            old 要扩展的函数对象
    '''

    # 创建一个新函数,参数的个数是不确定的,使用*和**
    def new_function(*args, **kwargs):
        print('开始执行~~~~')
        # 调用被扩展的函数
        result = old(*args, **kwargs)
        print('执行结束~~~~')
        # 返回函数的执行结果
        return result

    # 返回新函数
    return new_function


f = begin_end(fn)  # 包装 fn()
f2 = begin_end(add)  # 包装 add()
f3 = begin_end(mul)  # 包装 mul()

r = f()
print(r)
r = f2(123, 456)
print(r)
r = f3(123, 456)
print(r)

print('##############################################################')


# 像 begin_end() 这种函数我们就称它为装饰器
#   通过装饰器,可以在不修改原来函数的情况下来对函数进行扩展
#   在开发中,我们都是通过装饰器来扩展函数的功能的
# 在定义函数时,可以通过 @装饰器,来使用指定的装饰器,来装饰当前的函数
#   可以同时为一个函数指定多个装饰器,这样函数将会安装从内向外的顺序被装饰

def fn3(old):
    '''
        用来对其他函数进行扩展,使其他函数可以在执行前打印开始执行,执行后打印执行结束

        参数:
            old 要扩展的函数对象
    '''

    # 创建一个新函数
    # *args:old 函数中的位置参数(形如:'a, b'),全都存放其中
    # **kwargs:old 函数中的字典参数(形如:'a=x, b=y'),全部存放其中
    def new_function(*args, **kwargs):
        print('fn3装饰~开始执行~~~~')
        # 调用被扩展的函数
        result = old(*args, **kwargs)
        print('fn3装饰~执行结束~~~~')
        # 返回函数的执行结果
        return result

    # 返回新函数
    return new_function


@fn3  # 第一层:fn3 装饰
@begin_end  # :第二层:begin_end 装饰
def say_hello():
    print('大家好~~~')


say_hello()

原文链接

https://github.com/ACatSmiling/zero-to-zero/blob/main/PythonLanguage/python.md

posted @ 2024-09-29 23:44  ACatSmiling  阅读(159)  评论(0)    收藏  举报