文章目录
一、创建和调用函数
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: 可以是类名、基本类型(int,float,bool,complex,str,list,dict,set,tuple)或者由它们组成的元组
# 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层),而且每次函数的调用都需要压栈、弹栈、保存和恢复寄存器的栈的操作,所以在这个上边是非常消耗时间和空间的。
- 典型应用:阶乘、斐波那契数列、汉诺塔
六、常见内置函数
- 详细介绍见:Python 常见的内置函数