《Effective Python》笔记 第三章-函数
阅读Effective Python(第二版)的一些笔记
第19条 不要把函数返回的多个值拆分到三个以上的变量中
Python的unpacking机制允许函数返回一个以上的值,如下所示:
# coding:utf-8
def get_one_result():
return "data"
def get_two_result():
return "data_1", "data_2"
def get_three_result():
return "data_1", "data_2", "data_3"
def get_four_result():
return "data_1", "data_2", "data_3", "data_4"
data_1 = get_one_result()
data_1, data_2 = get_two_result()
data_1, data_2, data_3 = get_three_result()
data_1, data_2, data_3, data_4 = get_four_result()
上面例子中,有返回3、4个值的情况,如果调用方要同时接受这么多个甚至更多的返回值,可能会搞错顺序,增加出错的风险;另外,当变量名和方法名比较长时,返回多个变量就可能需要将代码拆分为多行,看起来很别扭。
为了避免这种问题,不应该把函数的返回值拆分到3个以上的变量中,如果返回值确实比较多,可以考虑返回一个对象。
第20条 遇到意外状况时应该抛出异常,不要返回None
下面一个查询数据操作的方法
def query_data(condition):
try:
return self.db_session.query_by_condition(condition)
except:
# 抛出异常后返回None
return None
调用query_data后的结果为None,这个时候并不能从返回值上区分是正常查询的结果是None还是说遇到了异常返回None。
既然不知道是正常处理还是遇到异常,那么可以返回两个数据,一个状态code,一个数据data:
def query_data_v2(condition):
try:
data = query_by_condition(condition)
return Status.SUCCESS, data
except:
return Status.FAILED, None
# 调用
status, data = query_data_v2()
if status == Status.SUCCESS:
# 处理data
pass
else:
# 失败处理
pass
在遇到异常的时候,是否需要抛出异常,这个需要根据自己实际情况进行处理:
- 流程不太重要的步骤如果出现异常,可以不用抛出来,这样不会中断流程;
- 如果出现的异常会引起后面处理的失败,那么可以抛出异常,快速失败;
- ...
第21条 了解如何在闭包中使用外围作用域中的变量
第22条 用数量可变的位置参数给函数设计清晰的参数列表
位置参数
位置参数就是按照函数定义的参数顺序,依次进行传参,比如第一个位置的参数是xxx,第二个位置的参数时yyyy。
函数的参数不要太多
如果定义的方法参数比较多(这里指的位置参数),那么将每个参数都罗列出来的话,在调用和浏览代码的时候比较恶心,因为参数数量比较多,那么参数的位置数量就可能传错(除非指定参数名来传值);另外如果参数不固定,每次新增参数都得改函数逻辑。
比如下面这样的:
# coding:utf-8
# 有很多参数的函数
def test_compute_sum(name, a1, a2, a3, a4, a5, a6, a7):
# do action
return "sum({}) is {}".format(name, (a1 + a2 + a3 + a4 + a5 + a6 + a7))
# 调用的时候,要么严格按照方法参数顺序并传参,要么指定参数名来传参(此时参数位置可调整)
res = test_compute_sum("abc", 1, 2, 3, 4, 5, 6, 7)
res = test_compute_sum("xyz", a1=1, a2=2, a3=3, a4=4, a5=5, a7=7, a6=6)
# 注意:如果参数没有缺省的默认值,那么参数就是必须要指定值的,否则报错
使用可变位置参数
上面由于参数位置太多,并且参数不固定,可以使用*args
位置参数来接收可变参数。用*args
来改写上面的例子:
# 使用位置参数改写
def test_compute_sum_v2(name, *numbers):
print("numbers :%s", numbers)
return "sum({}) is{}".format(name, sum(numbers))
if __name__ == '__main__':
res = test_compute_sum_v2("abc", 1, 2, 3, 4, 5, 6, 7)
# ('numbers :%s', (1, 2, 3, 4, 5, 6, 7))
-
使用可变位置参数,其实是将传入的参数先转换为一个元组,然后将该元组传给函数的可变位置参数。
-
在调整有可变位置参数的函数时,需要注意新加的参数要在可变位置参数之前。
第23条 用关键字参数来表示可选的行为
关键字参数
关键字参数就是明确指定参数名和参数值的方式。使用关键字参数来传参,那么参数的数据可以和函数定义的参数顺序不同。
如下面所示:
# coding:utf-8
def test_diff_order(name, age, s):
print("name:{}, age:{}".format(name, age))
test_diff_order("abc", 99)
test_diff_order(name="abc", age=99) # 使用关键字参数,指定参数名和参数值
test_diff_order(age=99, name="abc") # 使用关键字参数,指定参数名和参数值
但是需要注意,使用关键字参数是,关键字参数后面不能有位置参数(也就是不指定参数名和参数值的参数)
test_diff_order("abc", age=99) # 正确,关键字参数前面可以有位置参数
# test_diff_order(name="abc", 99) # 错误,关键字参数后面不能有位置参数
# test_diff_order(age=99, "abc") # 错误,关键字参数后面不能有位置参数
可变关键字参数
也就是定义函数的时候只指定了一个类似map的参数,函数内部会自己获取map的k-v进行处理,参数的列表就比较干净一些。比如下面这样:
# coding:utf-8
# 测试可变关键参数
def test_changeable_key_words_args(logger_name, **data_dict):
return "logger_name:{}, time:{}, logID:{}, msg:{}" \
.format(logger_name, data_dict.get("time"), data_dict.get("logID"), data_dict.get("msg"))
data = {
"time": time.time(),
"logID": "12345678",
"msg": "hello"
}
# 在dict前面加俩*,表示将其转换为可变位置参数
res = test_changeable_key_words_args("main_logger", **data)
print(res) # logger_name:main_logger, time:1648980410.15, logID:12345678, msg:hello
可变位置参数和可变关键字参数配合使用
可变位置参数和可变关键字参数是可以配合使用的,比如下面这样:
# 可变位置参数和可变关键字参数配合使用
def test_changeable_position_and_key_word_args(*position_args, **key_word_args):
print("position args:{}".format(position_args))
print("key word args:{}".format(key_word_args))
test_changeable_position_and_key_word_args(1, 2, 3, 4, name="abc", age="456")
# position args:(1, 2, 3, 4)
# key word args:{'age': '456', 'name': 'abc'}
第24条 用None和docstring来描述默认值会变的参数
在定义函数的时候,可以给参数设置默认值;而且当参数有设置默认值,那么该参数就是非必填的。
参数的默认值可以是一些确定的值,比如下面的例子:
# coding:utf-8
def test_handle(name, need_handle=False):
if not need_handle:
print("name:{} skiped".format(name))
return
# handle
print("name:{} handled".format(name))
test_handle("job_1", True) # name:job_1 handled
test_handle("job_2", False) # name:job_2 skiped
test_handle("job_3") # name:job_3 skiped
一些存在问题的默认值,比如下面这样例子,希望run_time参数缺少时,就使用当前的时间,但是这样的写法是错误的:
def record_run_time(name, run_time=time.time()):
print("name:{} run when {}".format(name, run_time))
record_run_time("job_4", 1234567890) # name:job_4 run when 1234567890
record_run_time("job_5") # name:job_5 run when 1648981470.28
time.sleep(10)
record_run_time("job_6") # name:job_6 run when 1648981470.28
注意上面,job_5和job_6打印的run_time都是1648981470.28,这是由于run_time的参数默认值在这个方法被加载的时候就计算好了,后面每次缺省时,都是使用之前计算好的值,不会每次重新计算。
正确的方式应该使用None替换,并且使用注释来解释清楚当run_time为空时,会使用当前时间戳。
# coding:utf-8
# 改进后
def record_run_time_v2(name, run_time=None):
"""
记录执行时间
:param name: job_name
:param run_time: 执行时间,如果缺省的话,默认为当前时间戳
:return:
"""
if not run_time:
run_time = time.time()
print("name:{} run when {}".format(name, run_time))
record_run_time_v2("job_7", 1234567890) # name:job_7 run when 1234567890
record_run_time_v2("job_8") # name:job_8 run when 1648981898.77
time.sleep(10)
record_run_time_v2("job_9") # name:job_9 run when 1648981908.77
第25条 用只能以关键字指定和只能按位置传入的参数来设计清晰的参数列表
前面几条介绍了位置参数、可变位置参数、关键字参数、可变关键字参数,以及他们的组合使用。在Python3里面,又多了两种参数:只能以关键字指定 和 只能按位置传入的参数,也就是这个参数的传入方式已经确定了,不能更改。
使用这两种关键字参数的方式也比较简单,在定义函数时,使用*
来分隔参数,前面的就是位置参数,后面的就是关键字参数,位置参数不能以关键字参数形式传值,关键参数也不能以位置参数方式传值。
示例如下:
#coding:utf-8
# 使用*号来分隔开位置参数和关键字参数,后面的关键字参数必须以关键字形式调用
def test_example(name, age, *, height, weight):
print("name:{}, age:{}, height:{}, weight:{}".format(name, age, height, weight))
test_example("abc", 99, 200, 250)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: test_example() takes 2 positional arguments but 4 were given
test_example("abc", 99, 200, weight=250)
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# TypeError: test_example() takes 2 positional arguments but 3 positional arguments (and 1 keyword-only argument) were given
test_example("abc", 99, height=200, weight=250)
# name:abc, age:99, height:200, weight:250
上面是两种参数同时使用,也是可以单独使用关键字参数的,例子如下:
def test_only_key_word_args(*, height, weight):
print("height:{}, weight:{}".format(height, weight))
test_only_key_word_args(height=200, weight=250) # height:200, weight:250
# 错误的方式,不支持
def test_only_position_args(height, weight, *):
print("height:{}, weight:{}".format(height, weight))
第26条 用functools.wraps定义函数修饰器
Python里面,使用定义的装饰器增强方法后,原来的方法其实是已经被替换掉了,如下所示:
#coding:utf-8
from functools import wraps
def log(func):
def new_func(*args, **kwargs):
res = func(*args, **kwargs)
print("args:{}, kwargs:{}".format(args, kwargs))
return res
return new_func
@log
def test_func():
pass
print(test_func) # <function new_func at 0x105d9ec50>
上面打印出来的是new_func,也就是被包装后的函数。除此之外,被装饰方法的一些元信息其实都无法获取了,都变成new_func的元信息了,要想利用反射获取原始的元信息就会失败,要想实现被装饰后的方法元信息不变,那么可以使用functionals.wraps注解,他会将被装饰的方法元信息拷贝到新的方法,示例如下:
#coding:utf-8
from functools import wraps
def log(func):
# 使用functools.wraps装饰器,会将传入的func元信息拷贝到返回的方法上
@wraps(func)
def new_func(*args, **kwargs):
res = func(*args, **kwargs)
print("args:{}, kwargs:{}".format(args, kwargs))
return res
return new_func
@log
def test_func():
pass
print(test_func) # <function test_func at 0x103072250>
这个时候看打印出来的就是原始的test_func了。