3、Python 函数


一、创建和调用函数

1. 创建函数

  • 函数定义(函数名、参数、函数体、返回值):
    • 使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后在缩进块中编写函数体,函数的返回值用return 返回
    • 当执行到return语句时,Python 认为函数到此结束,需要返回了,当未设置返回值时,返回的是None;可同时返回多个不同类型值,默认以逗号隔开,则是一个tuple的形式返回,也可以用列表包含起来,返回一个 list
  • 函数注释:
    • 函数体最上面的开始位置经常会放置一个文档字符串(三引号''' ''',注意缩进)来说明函数的功能和调用方法
    • 最好写出参数的类型 str、int、float....,然后再加上相应的解释
    • 可通过函数的 .__doc__ 属性获取函数注释

2. 函数调用

  • 要调用一个函数,需要知道函数的名称和参数即可;在调用时,使用 _ 来接收不需要使用的返回值
  • 函数调用中的参数传递及影响:
    • 不可变对象(传值):函数体中可以使用实参的值,但不能改变实参(int, string, tuple...
    • 可变对象(传引用):函数体中可以对实参进行修改(list、dict、set、ndarray...
  • 函数本身也是一个对象
    • 有自己的方法和属性,可通过 dir(func) 来查看,其中函数的名字可以通过.__name__属性获得
    • 可将函数分配给变量、存储在数据结构中、作为参数传递给其它函数或进行函数嵌套、作为其它函数的返回值
  • 空函数:
    • def nop(): pass
    • pass 可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个 pass,让代码能运行起来

3. 函数提出的目的:代码重用,实现特定功能

  • 代码块重复,这时候必须考虑到函数,降低程序的冗余度
  • 代码块复杂,这时候必须考虑到函数,降低程序的复杂度

二、函数的参数

1. 固定参数:定义时固定了参数顺序和数量

  • 位置参数
    • 调用函数时,传入的实参值 按照位置顺序 依次赋给形参
    • 形参(parameters):是指函数创建和定义过程中小括号里的参数,形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只在函数内部有效。函数调用结束返回主调用函数后则不能再使用该形参变量
    • 实参(arguments):是指函数在调用过程中传递进去的参数,实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使参数获得确定值
  • 关键字参数
    • 调用函数时,带上形参的名字去指定具体调用的是哪个参数,从而可以不用按照参数的顺序调用函数。
    • eg: hi(words='Hello', name='Tom') 使用关键字参数,可以有效的避免搞乱参数顺序导致BUG的出现。
  • 默认参数
    • 形参定义的过程中为其赋初值
    • 调用函数时,默认使用形参的初始值代替需要传入的实参值,此时括号中无需再指定实参数值,若指定实参数值,则指定的实参数值会替代初始形参值。
    • 使用默认参数最大的好处是能降低调用函数的难度
    • 默认参数一定要用不可变对象,如果是可变对象,程序运行时会有逻辑错误!

2. 可变参数:定义时还不知道有多少参数传入

  • 可变位置参数(将实参收集为元组):def func(*args)
    • 允许传入任意多个实参,在函数调用的时候自动将 args 组装(packing)成一个tuple,函数体中不能对参数进行修改,否则会报错
    • 实参可以直接传入:func(1, 2, 3)
    • 实参也可以先组装成list或tuple,再通过*list/tuple传入:func(*(1, 2, 3))
      • 注意:此时必须要加 * 号先将其解包(unpacking)为位置参数(*args),然后再自动打包(packing)为一个 tuple 传给 args,要不然就会当成一个参数传给函数
      • 星号*可以把序列/集合解包(unpacking)成位置参数
  • 可变关键字参数(将实参收集为字典):def func(**kwargs)
    • 允许传入任意个含参数名的实参,在函数调用的时候自动将 kwargs 组装(packing)成一个dict
    • 实参可以直接传入:func(a=1, b=2),注意此时 key 无需加引号
    • 实参也可以先组装成dict eg: kw = {'a':1, 'b':2},再通过**kw传入:func(**kw)
      • 注意,此时必须要加 ** 号,先将其解包(unpacking)为关键字参数(**kwargs),然后再自动打包(packing)为一个 dict 传给 kwargs,要不然就会当成一个参数传给函数
      • 两个星号**可以把字典解包(unpacking)成关键字参数
  • 可变参数的作用:扩展函数的功能
    • 使用可变位置参数可以让你在使用时再决定传入多少个参数
    • 试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用可变关键字参数来定义这个函数就能满足注册的需求
  • 注意*** 号既可以解包(调用时),又可以打包(定义时)
def func(*args, **kwargs):
    print(type(args), type(kwargs))  # <type 'tuple'>   <type 'dict'>
    print(args, kwargs)
    for i, arg in enumerate(args):
        print("arg{}={}".format(i, arg))

    for key, val in kwargs.items():
        print("{}={}".format(key, val))

l_0 = [4, 5, 6]
d_0 = {'name':'Tom', 'age':12}

# 不加星号时,输入的 l_0 和 d_0 被组装(packing)成了一个 tuple, 送进了第一个形参中,第二个形参没有数据传入
func(l_0, d_0)
<type 'tuple'>   <type 'dict'>
([4, 5, 6], {'age': 12, 'name': 'tom'})   {}
arg0=[4, 5, 6]
arg1={'age': 12, 'name': 'tom'}

# 加上星号后,l_0 和 d_0 分别被解包(upacking)成位置参数和关键字参数,然后分别被组装(packing)成 tuple 和 dict 送进函数中
# 即:* 和 ** 号既可以解包(调用时),又可以打包(定义时)
func(*l_0, **d_0)
<type 'tuple'>   <type 'dict'>
(4, 5, 6)   {'age': 12, 'name': 'tom'}
arg0=4
arg1=5
arg2=6
age=12
name=Tom

# 也可以这样调用,位置参数自动组装为一个 tuple, 关键字参数自动组装为一个 dict
func(4, 5, 6, name='Tom', age=12)
<type 'tuple'>   <type 'dict'>
(4, 5, 6)   {'age': 12, 'name': 'tom'}
arg0=4
arg1=5
arg2=6
age=12
name=Tom

# 至少需要一个名为 required 的参数,但也可接受额外的位置参数和关键字参数
def func(required, *args, **kwargs):
    print('usage of {}'.format(required))
    if args:
        pass
    if kwargs:
        pass



def my_function(x, y):
    return x + y

# 当可变关键字参数通过字符串来传递时,可以像下面那样使用
param_str = "{'x': 3, 'y': 4}"
params = eval(param_str)
result = my_function(**params)  
print(result)  # 7

3. 参数组合

  • 在 Python 中定义函数,可以用位置参数、默认参数、关键字参数、可变位置参数以及可变关键字参数,这5种参数都可以组合使用
  • 但是请注意,如果混用,所有位置参数必须在前,关键字参数必须在后。
    • 元组解包参数在前,单个形参在后时,调用语句必须指定形参名称(使用关键字参数传递参数值)
    • 字典解包参数、元组解包参数、单个形参三者一起使用时,必须保证字典的解包参数放在最后

4. 参数类型检查

isinstance(obj, class_or_tuple):
	obj: 实例对象
	class_or_tuple: 可以是类名、基本类型(intfloatboolcomplexstrlistdictsettuple)或者由它们组成的元组

# eg1
isinstance(512, int)  # 返回 True

if not isinstance('512', (int, float)):
	raise TypeError('bad operand type')   # 触发异常

# eg2
class A:
    pass
 
class B(A):
    pass

#  type() 不会认为子类是一种父类类型,不考虑继承关系;isinstance() 会认为子类是一种父类类型,考虑继承关系
isinstance(A(), A)    # returns True
type(A()) == A        # returns True
isinstance(B(), A)    # returns True
type(B()) == A        # returns False

5. 函数注释

def f(text: str, max_len: 'int>0' = 80) -> str:
    """这个是函数的帮助说明文档,help时会显示"""
    print("函数注释", f.__annotations__)
    print("参数值打印", text, max_len)
    print("参数类型打印", type(text), type(max_len))

    return "hello annotations"

f('test')
# 输出如下所示:
函数注释 {'text': <class 'str'>, 'max_len': 'int>0', 'return': <class 'str'>}
参数值打印 test 80
参数类型打印 <class 'str'> <class 'int'>
    
>>> 参数 text:无默认值,冒号后面的 str 是参数的注释
>>> 参数 max_len:默认值为 80, 冒号后面的 'int>0' 是参数的注释
>>> -> str:是函数返回值的注释
>>> 这些注释信息都是函数的元信息,保存在 f.__annotations__ 字典中,需要注意的是 Python 对注释信息和 f.__annotations__ 的一致性,不做任何检查

三、局部&全局、动态&静态变量

  • 局部变量&全局变量

    • 局部变量:只有在特定过程或函数中可以访问的变量(作用域是定义该变量的子程序)
    • 全局变量:作用域是整个程序,所以可以直接在函数内部使用(Note:顶层函数名以及导入的函数名也是全局变量)
    • 当局部变量和全局变量重名时:在定义局部变量的子程序内,局部变量起作用;在其它地方全局变量起作用
    • 在面向对象语言中,一般只使用局部变量
  • 动态变量&静态变量

    • 动态变量在程序执行时分配存储单元,当所在程序段结束时,自动将这些存储单元释放
    • 静态变量永久性的存储在存储单元中,在下次执行该程序段时,仍然使用原来的存储单元
  • global:通过将变量定义为全局变量,实现在内部函数中改变全局变量的值,但要注意的是此函数的外部函数中局部变量的值并不会改变

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    # eg1
    user_status = False  # 用户登录了就把这个改成 True
    _username = "man"  # 假装这是 DB 里存的用户信息
    _password = "123"  # 假装这是 DB 里存的用户信息
    
    
    def login():  # 把要执行的模块从这里传进来
    	global user_status
    
    	if not user_status:
    		username = input("user:")
    		password = input("pasword:")
    
    		if username == _username and password == _password:
    			print("welcome login....")
    			user_status = True
    		else:
    			print("wrong username or password!")
    
    	return user_status
    
    
    print(login())
    
    # 执行过程
    user:man
    pasword:123
    welcome login....
    True
    
    # eg2
    x = 0
    def outer():
        x = 1
        def inner():
            global x
            x = 2
            print("inner:", x)
    
        inner()
        print("outer:", x)
    
    outer()
    print("global:", x)
    
    
    # 执行过程
    inner: 2
    outer: 1
    global: 2
    
  • nonlocal:python3 中新加入的特性,在内部函数里修改外部函数里局部变量的值,注意此时全局变量的值不会改变

    x = 0
    def outer():
        x = 1
        def inner():
            nonlocal x
            x = 2
            print("inner:", x)
    
        inner()
        print("outer:", x)
    
    outer()
    print("global:", x)
    
    # 执行过程
    inner: 2
    outer: 2
    global: 0
    
  • 变量名解析机制及生命周期
    在这里插入图片描述
    在这里插入图片描述

  • 变量的生命周期:

    • built-in,解释器在则在,解释器亡则亡
    • global,导⼊模块时创建,直到解释器退出
    • local,函数调⽤时才创建

四、函数式编程

函数式编程的一个特点就是:允许把 函数本身作为参数 传入另一个函数,还允许 返回一个函数

1、高阶函数

函数作为参数传入,这样的函数称为高阶函数,用 List Comprehension 可轻松替代 map 和 filter(reduce替代起来⽐较困难)!

( a )、filter(func or None, seq)
  • filter() 函数用于过滤序列,它会将传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃原序列中所对应的元素。
  • filter() 函数有两个参数:第一个参数是一个函数名(即,函数对象)或者 None,第二个参数为可迭代序列。
    • 若第一个参数是一个函数的话,则将可迭代序列里的每一个元素作为函数的实参传入进行计算,把计算后值为 True 所对应的可迭代序列中的项筛选出来
    • 若第一个参数为None,则直接将 可迭代序列中每一个值为True 的项筛选出来
    • 返回一个filter object,可以用list将其中元素转化为列表。
( b )、map(func, seq)
  • map() 函数有两个参数:第一个参数是一个函数名(即,函数对象),第二个参数为可迭代序列。
  • 可迭代序列里的每一个元素作为函数的实参传入并进行计算,返回所有加工完成后的元素构成的 Iterator,可以用list将其转化为列表。
  • 一般用于映射性任务
( c )、reduce(func(x, y), seq)
  • func必须有两个参数
  • 每次func计算的结果继续和seq的下一个元素做累积计算
  • 一般用于归并性任务
    lst = [a1, a2, a3, ..., an]
    reduce(func(x, y), lst) = func(func(func(a1, a2), a3), ... , an) 
    
    eg: 
    reduce(lambda x, y : x + y, range(5))
    >>> 10
    

2、闭包

  • 闭包函数的必要条件:
    • 闭包函数嵌套存在于函数体内
    • 闭包函数必须引用外部变量(一般不能是全局变量),不一定要 return
    • 闭包函数必须作为对象被逐级 return,直至作为主函数(最外层函数)的返回值
  • 使用注意事项:
    • 闭包在被返回时,它的所有变量就已经固定,形成了一个封闭的对象,这个对象包含了其引用的所有外部、内部变量和表达式。当然,闭包的参数除外
    • 返回闭包时牢记一点:返回函数不要引用任何循环变量,或者后续会发生变化的变量
  • 代码示例
    # eg1
    def line_conf(a, b):
        def line(x):
            return a * x + b
        return line
        
    # 嵌套捕获并携带父函数的一些状态(a, b 的值)
    f = line_conf(1, 1)
    f(2) >>> 3
    
    
    # eg2
    def line_conf():
        a = 1
        b = 1
    
        def line(x):
            print(a * x + b)
        return line
        
    # 嵌套捕获并携带父函数的一些状态(a, b 的值)
    f = line_conf()
    f(2) >>> 3	
    
    # eg3
    def make_averager():
        count = 0
        total = 0
    
        def averager(new_value):
            nonlocal count, total
            count += 1
            total += new_value
            return total / count
    
        return averager
    
    # 嵌套捕获并携带父函数的一些状态(count, total 的值)
    # 若想要在子函数中修改其外层不可变对象的值,需要使用 nonlocal 进行声明;
    # 亦可将子函数外层变量设置为可变对象(list,set,array等)来解决
    ave = make_averager()
    print(ave(10)) >>> 10	
    print(ave(11)) >>> 10.5	
    print(ave(12)) >>> 11.0	
    

3、装饰器

所谓函数装饰器,就是通过装饰器函数,在不修改原函数的前提下,来对函数的功能进行合理的扩充。Python 内置了 3 种函数装饰器,分别是 @staticmethod@classmethod@property

  • 为什么需要装饰器?

    • 在软件工程中,一个项目的多个版本间迭代要尽量遵循开放-封闭的原则
    • 已经实现的功能代码不允许被修改(封闭),但可以被扩展(开放)
    • 应用:参数检查、添加日志、调试、用户授权、注册等等
  • 装饰器的本质?

    • 是一个闭合函数,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能
    • 一般传入的是被装饰函数,返回的是装饰函数最内层),若要实现带参数的装饰器,可以在外层再封装一层带参函数
    • 装饰器在模块加载时立即执行(不调用也会执行装饰器中的内容)
  • 装饰器的小 trick ?

    • @ 修饰符:

      • 相当于执行了func = decorator(func) 进行覆盖,只对增强了功能的最终函数感兴趣
      • 装饰器和被装饰函数的关系更加明显(就近调用)且能实现名称复用(不引入多余变量)
    • 能够接收任何参数的通用参数装饰器

      • wrapper 函数中使用元组和字典的解包参数来作为形参
      • 这样得到的装饰器便可以适用于各种不同参数的函数
    • 装饰器返回函数的名称修复

      • 通常装饰器在装饰函数时,会改变函数本身的的元信息,比如函数的 docstring、__name__、参数列表
      • 可使用 @functools.wraps(func) 语句将函数的名称等元信息恢复过来
      import functools
      
      def funA(fn):
          # 定义一个嵌套函数
          @functools.wraps(fn)
          def say(arc):
              print("hello:", arc)
      
          return say
      
      
      # 等价于执行了 funB = funA(funB), 装饰函数中不加 @functools.wraps(fn) 则返回 <function funA.<locals>.say at 0x7f8245242790>
      # 装饰函数中加了 @functools.wraps(fn) 则会修复函数的名称,返回 <function funB at 0x7fcbeb4f4790>
      @funA
      def funB(arc):
          print("funB():", arc)
      
      
      funB("world")
      print(funB)
      
    • 多装饰器的调用顺序

      • 实现组合装饰时,只需要将不同装饰器使用@符号一行一行的堆叠起来即可
      • 多装饰器的载入顺序是从下往上的;调用时,其执行的函数顺序是从上到下
      import functools
      
      # 1、通用装饰器(传入的是被装饰函数,返回的是装饰函数)
      def logger(func):
          @functools.wraps(func)
          def wrapper(*args, **kw):
              print('call %s():' % func.__name__)  # 装饰内容
              return func(*args, **kw)
      
          return wrapper
      
      
      # 2、可接收参数的通用装饰器
      def log(text):
          def decorator(func):
              @functools.wraps(func)
              def wrapper(*args, **kw):
                  print('%s %s():' % (text, func.__name__))
                  return func(*args, **kw)
      
              return wrapper
      
          return decorator
      
      
      @logger
      def now(hour, minute, second):
          print('Time is {}-{}-{}'.format(hour, minute, second))
      
      
      @log('execute function')  # 注意:log('execute function') 才是装饰器
      def today(year, month, day):
          print('Today is {}-{}-{}'.format(year, month, day))
      
      
      today(2020, 5, 12)
      now(17, 30, 30)
      
      
      # 输出内容如下:
      execute function today():
      Today is 2020-5-12
      call now():
      Time is 17-30-30
      
      
      -----------------------------------------------
      # 3、将不同级别的功能模块封装在不同的文件中
      # DecorateToolBox.py
      class DecorateToolBox:
          @classmethod  # @classmethod 装饰器,使类无需实例化而直接调用其方法
          def decorate(self, func):
              func.__doc__ += '\nDecorated by decorate.'
              return func
      
      # test.py
      from DecorateToolBox import DecorateToolBox
      
      @DecorateToolBox.decorate
      def add(x, y):
          '''Return the sum of x and y.'''
          return x + y
      
      help(add)
      # 输出如下被装饰后的 doc
      Help on function add in module __main__:
      add(x, y)
          Return the sum of x and y.
          Decorated by decorate.
      
      
      
       @staticmethod 装饰器可以将方法绑定到类本身上,实现在类未实例化之前,直接调用其方法
       相比 @classmethod ,其调用方式更加宽松,对第一个参数没有任何要求(不需要加 cls 或 self),与普通的函数参数一样
       使用类名或者类的实例化对象都可以对其进行调用
       
       @property:将方法当作属性来调用
      -----------------------------------------------
      # 4、装饰器堆叠及导入时立即执行示例
      def deco_1(func):
          print("running deco_1...")
          return func
      
      
      def deco_2(func):
          print("running deco_2...")
          return func
      
      
      @deco_1
      @deco_2
      def f():
          print("running f...")
      
      
      if __name__ == '__main__':
          pass
      
      # 模块载入时,等效于执行了 f = deco_1(deco_2(f)),输出如下
      running deco_2...
      running deco_1...
      
  • 使用 numba 对 python 函数进行加速(尤其是对循环等函数加速效果明显)

    from numba import jit
    
    @jit
    def f(x, y):
        return x + y
    

五、lambda 表达式和递归函数

1. lambda 表达式

  • 使用 lambda 关键字来创建匿名函数可以省下函数定义过程,节省变量内存占用空间,通常与 map、reduce、filter 结合使用
  • 格式:dy = lambda 参数1,参数2,... : 返回值(可以加入条件语句)
  • 调用:dy(参数1, 参数2, ...)
  • eg:dy = lambda x : x if x%2 else None # dy 为一个函数对象,必须要有else,要不会报错

2. 递归函数

  • 在函数内部,可以调用其他函数。如果一个函数在内部调用自身,这个函数就是递归函数,递归必备条件:
    • 函数调用自身:将问题分解为 规模更小的相同问题,持续分解,直到可用非常简单直接的方式来解决
    • 设置了正确的退出条件
    • 必须要减小规模,改变状态,向基本结束条件演进
  • 在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出
  • 递归函数的优缺点
    • 优点:逻辑简单清晰
    • 缺点:过深的调用会导致栈溢出(Python3 处于保护,默认最深100层),而且每次函数的调用都需要压栈、弹栈、保存和恢复寄存器的栈的操作,所以在这个上边是非常消耗时间和空间的。
  • 典型应用:阶乘、斐波那契数列、汉诺塔

六、常见内置函数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值