Python实战之函数的一些奇技淫巧

不妨大胆一点,有很多事没有答案。——–大鱼海棠

写在前面


  • 博文为《Python Cookbook》读书后笔记整理

  • 书很不错,感兴趣小伙伴可以去拜读下

  • 博文涉及内容包括:

  • 语法方面

    • 定义接受任意数量参数的函数
    • 定义只允许接受字典参数的函数
    • 定义函数参数类型注释,函数体注释信息打印
    • 定义返回多个值的函数
    • 定义有默认参数的函数
    • 定义匿名或内联函数
    • 匿名函数如何捕获变量值
  • 函数调优方面:

    • 减少可调用对象的参数个数
    • 将单方法的类转换为函数
    • 带额外状态信息的回调函数
    • 优雅的访问闭包中定义的变量
  • 食用方式

    • 本文适合初学python的小伙伴,需要了解Python基础知识
    • 可能小伙伴们觉得pytohn函数有什么可讲的,只要会基本语法,用的时候灵活运用就可以了
    • 实际上真的是这样么?希望通过本文认识不一样的 Python 函数
  • 理解不足小伙伴帮忙指正

不妨大胆一点,有很多事没有答案。——–大鱼海棠


名词解释

  • 位置参数: 直接传递变量值
  • 关键字参数:给指定变量名传递一个变量值
  • 默认参数: 一般为定值的关键字参数,赋值在函数定义时完成,必须为不可变类型

函数

如何定义接受任意数量参数的函数

你想构造一个可接受任意数量参数的函数。

让一个函数接受任意数量的位置参数,python 可以使用一个 * 参数

1
2
3
4
5
def avg(first, *rest):
return (first + sum(rest)) / (1 + len(rest))

avg(1, 2) # 1.5
avg(1, 2, 3, 4) # 2.5

在函数内部的处理机制中,rest会转化为所有其他位置参数组成的元组。所以我们可以直接当成了一个序列来使用

在其他的语言中,这种语法也叫做可变参数,JavaScript的可变参数函数定义

1
2
3
4
5
function fun(a,...b){
console.log(a,b)
}

fun(1,23,4,5) //1 [ 23, 4, 5 ]

为了接受任意数量的k-v关键字参数参数,使用一个以**开头的参数。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import html


def make_element(name, value, **attrs):
keyvals = [' %s="%s"' % item for item in attrs.items()]
attr_str = ''.join(keyvals)
element = '<{name}{attrs}>{value}</{name}>'.format(
name=name,
attrs=attr_str,
value=html.escape(value))
return element;

#<item size="large" quantity="6">Albatross</item>
print( make_element('item', 'Albatross', size='large', quantity=6))
#<p>&lt;spam&gt;</p>
print(make_element('p', '<spam>'))

如果希望某个函数能同时接受任意数量的位置参数和关键字参数,可以同时使用*和**。比如:

1
2
3
def anyargs(*args, **kwargs):
print(args) # A tuple
print(kwargs) # A dict

所有位置参数会被放到args元组中,所有关键字参数会被放到字典kwargs中。

一个*参数只能出现在函数定义中最后一个位置参数后面,而**参数只能出现在最后一个参数。有一点要注意的是,在*参数后面仍然可以定义其他参数。这里有点不太理解

1
2
3
4
5
6
def a(x, *args, y):
pass

def b(x, *args, y, **kwargs):
pass

如何定义只允许接受关键字参数的函数

你希望函数的某些参数强制使用关键字参数传递

将强制关键字参数放到某个 * 参数后面就能达到这种效果。

1
2
3
4
5
6
7
def recv(maxsize, *, block):

'Receives a message'
pass

recv(1024, True) # TypeError: recv() takes 1 positional argument but 2 were given
recv(1024, block=True) # Ok

利用这种技术,我们还能在接受任意多个位置参数的函数中指定关键字参数。

1
2
3
4
5
6
7
8
9
def mininum(*values, clip=None):
m = min(values)
if clip is not None:
m = clip if clip > m else m
return m


mininum(1, 5, 2, -5, 10) # Returns -5
mininum(1, 5, 2, -5, 10, clip=0) # Returns 0

那为什么要使用关键字参数,而不用位置参数?

很多情况下,使用关键字参数会比使用位置参数表意更加清晰,另外,使用强制关键字参数也会比使用**kwargs 参数更好,因为在使用函数help的时候输出也会更容易理解:

通过help方法也可以直接输出注释信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def mininum(*values, clip=None):
"""
@Time : 2022/07/08 23:08:07
@Author : Li Ruilong
@Version : 1.0
@Desc : None
Args:
*values
clip=None
Returns:
m
"""
m = min(values)
if clip is not None:
m = clip if clip > m else m
return m
help(mininum)

嗯,执行输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Help on function mininum in module __main__:

mininum(*values, clip=None)
@Time : 2022/07/08 23:08:07
@Author : Li Ruilong
@Version : 1.0
@Desc : None
Args:

Returns:
void


Process finished with exit code 0

如何定义函数参数类型注释,函数体注释信息打印

关于函数体注释信息打印可以看上面的Demo

写好了一个函数,然后想为这个函数的参数增加一些额外的信息,这样的话其他使用者就能清楚的知道这个函数应该怎么使用。一般的编译型语言都会强制的声明,解释型语言则没有那么多要求,那如果我希望在python里面去声明类型应该如何处理

使用函数参数注解是一个很好的办法,它能提示程序员应该怎样正确使用这个函数。例如,下面有一个被注解了的函数:

1
2
3
4
5
6
7
8
9
Python 3.9.0 (tags/v3.9.0:9cf6752, Oct  5 2020, 15:23:07) [MSC v.1927 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> def add(x:int, y:int) -> int:
... return x + y
...
>>> help(add)
Help on function add in module __main__:

add(x: int, y: int) -> int

add.__annotations__ 可以打印函数的注解信息

1
2
3
>>> add.__annotations__
{'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>}
>>>

python解释器不会对这些注解添加任何的语义。它们不会被类型检查,运行时跟没有加注解之前的效果也没有任何差距。然而,对于那些阅读源码的人来讲就很有帮助啦。第三方工具和框架可能会对这些注解添加语义。同时它们也会出现在文档中。

尽管你可以使用任意类型的对象给函数添加注解 (例如数字,字符串,对象实例等等),不过通常来讲使用或着字符串会比较好点

如何定义返回多个值的函数

希望构造一个可以返回多个值的函数

为了能返回多个值,函数直接 return 一个元组就行了,默认情况下回返回一个元组。

1
2
3
4
5
6
>>> def myfun():
... return 1,2,3
...
>>> myfun()
(1, 2, 3)
>>>
1
2
3
4
5
6
>>> a,b,c = myfun()
>>> a
1
>>> c
3
>>>

相比来讲,这里 GO就要方便的很多,不但可以传递多个参数,同时可以返回异常信息,自动拆包,不同的是GO需要定义返回值

1
2
3
4
5
6
7
8
9
func (ip IP) MarshalText() ([]byte, error) {
if len(ip) == 0 {
return []byte(""), nil
}
if len(ip) != IPv4len && len(ip) != IPv6len {
return nil, &AddrError{Err: "invalid IP address", Addr: hexString(ip)}
}
return []byte(ip.String()), nil
}

如何定义有默认参数的函数

你想定义一个函数或者方法,它的一个或多个参数是可选的并且有一个默认值

这个没啥可说的,小伙伴应该都很熟悉,需要注意这里的默认参数和关键字参数有相似的地方,当关键字参数的值为不可变得,即为默认参数,但是行为是不同的,默认参数一般会给一个默认值,是不可变得,而关键字参数是传递的变量给一个定义好的变量名

普通的默认参数函数

1
2
3
4
5
6
7
8
>>> def spam(a, b=42):
... print(a, b)
...
>>> spam(1)
1 42
>>> spam(1, 2)
1 2
>>>

需要注意的是 如果默认参数是一个可修改的容器比如一个列表、集合或者字典,可以使用None作为默认值,就像下面这样:

1
2
3
4
5
6
7
8
>>> def spam(a, b=None):
... if b is None:
... print(b)
...
>>> spam(a)
None
>>> spam(a,12)
>>>

但是这样写的话会有一个问题,我们如何确认当前关键字变量使用的是默认参数,还是传递的关键字参数

我们可以像下面这样写:

1
2
3
4
5
6
7
8
9
10
>>> _no_value = object()
>>> def spam(a, b=_no_value):
... if b is _no_value:
... print('No b value supplied')
...
>>> spam(1)
No b value supplied
>>> spam(1, 2)
>>> spam(1, None)
>>>

通过执行我们可以看到,传递一个 None 值不传值两种情况是有差别的。

默认参数的值仅仅在函数定义的时候赋值一次

1
2
3
4
5
6
7
8
9
10
>>> x = 42
>>> def spam(a,b = x):
... print(a,b)
...
>>> spam(1)
1 42
>>> x= 23
>>> spam(1)
1 42
>>>

注意到当我们改变x的值的时候对默认参数值并没有影响,这是因为在函数定义的时候就已经确定了它的默认值了,这里类似于pythonvars(),可以默认获取当前上下文的变量值。

其次,默认参数的值应该是不可变的对象,比如None、True、False、数字或字符串。特别的,千万不要像下面这样写代码:

1
def spam(a, b=[]): # NO!

如果你这么做了,当默认值在其他地方被修改后你将会遇到各种麻烦。这些修改会影响到下次调用这个函数时的默认值。换句话讲,这其实是一个共享变量,随着使用在不断变化,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> def spam(a, b=[]):
... print(b)
... return b
...
>>> x = spam(1)
>>> x
[]
>>> x.append(99)
>>> x.append('Yow!')
>>> x
[99, 'Yow!']
>>> spam(1) # Modified list gets returned!
[99, 'Yow!']
>>>

最好是将默认值设为None,然后在函数里面检查它,前面的例子就是这样做的。

在测试 None 值时使用 is 操作符是很重要的,不要使用下面的方式

1
2
3
def spam(a, b=None):
if not b: # NO! Use 'b is None' instead
b = []

这么写的问题在于尽管None值确实是被当成False,但是还有其他的对象(比如长度为0的字符串、列表、元组、字典等)都会被当做False。因此,上面的代码会误将一些其他输入也当成是没有输入。比如:

1
2
3
4
5
6
>>> spam(1) # OK
>>> x = []
>>> spam(1, x) # Silent error. x value overwritten by default
>>> spam(1, 0) # Silent error. 0 ignored
>>> spam(1, '') # Silent error. '' ignored
>>>

所以在默认参数中,判断参数是否为空的清理。唯一能做的就是测试同一性。这个刚好符合要求。

如何定义匿名或内联函数

你想为sort()操作创建一个很短的回调函数,但又不想用def 去写一个单行函数,而是希望通过某个快捷方式以内联方式来创建这个函数。

当一些函数很简单,仅仅只是计算一个表达式的值的时候,就可以使用lambda表达式来代替了。比如:

lambda表达式某种意义上讲,是函数式编程的体现,行为参数化的思想,本质上是匿名函数的语法糖。

1
2
3
4
5
6
>>> add = lambda x, y: x + y
>>> add(2,3)
5
>>> add("li",'ruilong')
'liruilong'
>>>

这里和JS里的语法很类似,但是JavaScript中的lambad要强大的多,和Java里的相类似,不单单可以写一行语句,甚至可以嵌套。但是Java的lambad表达式接收的外接的共享变量必须为不可变得。

1
2
var arr = [2,15,8,11,7,4];
arr.sort((a,b) => a < b ? 1:a > b ? -1:0)

python看一个具体的Demo

1
2
3
4
>>> names = ['David Beazley', 'Brian Jones','Raymond Hettinger', 'Ned Batchelder']
>>> sorted(names, key=lambda name: name.split()[-1].lower())
['Ned Batchelder', 'David Beazley', 'Raymond Hettinger', 'Brian Jones']
>>>

有人编写大量计算表达式值的短小函数或者需要用户提供回调函数的程序的时候,会常常使用lambda表达式。

匿名函数如何捕获变量值

你用 lambda 定义了一个匿名函数,并想在定义时捕获到某些变量的值。

1
2
3
4
5
6
7
8
9
>>> x = 10
>>> a = lambda y: x + y
>>> x = 20
>>> b = lambda y: x + y
>>> a(10)
30
>>> b(10)
30
>>>

这其中的奥妙在于lambda表达式中的x是一个自由变量,在运行时绑定值而不是定义时就绑定,这跟函数的默认值参数定义是不同的。因此,在调用这个lambda表达式的时候,x的值是执行时的值。

这里值得一提的是Java中lambda表达式也有需要注意的,当表达式内部使用外部的共享(引用)变量要单独赋值或者定义为final,java通过这样一种方式,在语法层面强制lambad表达式引用的局部变量不可被改变,引起局部变量的语义冲突,类似上面python那样。我最初以为java中代码编译后会涉及指令重排,执行lambad的时候,变量为执行到这里的值,所以为了保证表达式中的变量是自己想要,需要强制设定,其实和指令重排没关系,只是为了避免上面的那种语法混淆,提醒 coder 在使用lambad的时候,外部的局部变量值在内部使用时是不应该改变的。

1
2
3
4
5
6
7
8
>>> x = 10
>>> a = lambda y: x + y
>>> a(10)
20
>>> x = 3
>>> a(10)
13
>>>

如果你想让某个匿名函数在定义时就捕获到值,可以将那个参数值定义成默认参数即可,就像下面这样:

1
2
3
4
5
6
7
8
9
>>> x = 10
>>> a = lambda y, x=x: x + y
>>> x = 20
>>> b = lambda y, x=x: x + y
>>> a(10)
20
>>> b(10)
30
>>>

减少可调用对象的参数个数

你有一个被其他 python代码使用的callable 回调对象,可能是一个回调函数或者是一个处理器,但是它的参数太多了,导致调用时出错。

如果需要减少某个函数的参数个数,你可以使用functools.partial()
partial()函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。为了演示清楚,假设你有下面这样的函数:

1
2
def spam(a, b, c, d):
print(a, b, c, d)

现在我们使用partial()函数来固定某些参数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> from functools import partial
>>> s1 = partial(spam, 1) # a = 1
>>> s1(2, 3, 4)
1 2 3 4
>>> s1(4, 5, 6)
1 4 5 6
>>> s2 = partial(spam, d=42) # d = 42
>>> s2(1, 2, 3)
1 2 3 42
>>> s2(4, 5, 5)
4 5 5 42
>>> s3 = partial(spam, 1, 2, d=42) # a = 1, b = 2, d = 42
>>> s3(3)
1 2 3 42
>>> s3(4)
1 2 4 42
>>> s3(5)
1 2 5 42
>>>

可以看出partial()固定某些参数并返回一个新的callable对象。这个新的callable接受未赋值的参数,然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。

1
2
3
4
5
def distance(p1, p2):
x1, y1 = p1
x2, y2 = p2

return math.hypot(x2 - x1, y2 - y1)

现在假设你想以某个点为基点,根据点和基点之间的距离来排序所有的这些点。列表的 sort()方法接受一个关键字参数来自定义排序逻辑,但是它只能接受一个单个参数的函数(distance()很明显是不符合条件的)。现在我们可以通过使用 partial()来解决这个问题:

1
2
3
4
5
>>> pt = (4, 3)
>>> points.sort(key=partial(distance,pt))
>>> points
[(3, 4), (1, 2), (5, 6), (7, 8)]
>>>

partial() 通常被用来微调其他库函数所使用的回调函数的参数

使用 multiprocessing 来异步计算一个结果值,然后这个值被传递给一个接受一个 result 值和一个可选 logging 参数的回调函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def output_result(result, log=None):
if log is not None:
log.debug('Got: %r', result)

# A sample function
def add(x, y):
return x + y


if __name__ == '__main__':

import logging
from multiprocessing import Pool
from functools import partial
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('test')
p = Pool()
p.apply_async(add, (3, 4), callback=partial(output_result, log=log))
p.close()
p.join()

当给apply_async()提供回调函数时,通过使用partial()传递额外的logging参数。而multiprocessing对这些一无所知——它仅仅只是使用单个值来调用回调函数。

作为一个类似的例子,考虑下编写网络服务器的问题,socketserver ,使用 partial() 就能很轻松的解决——给它传递 ack 参数的值来初始化即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from socketserver import StreamRequestHandler, TCPServer
from functools import partial

class EchoHandler(StreamRequestHandler):
def __init__(self, *args, ack, **kwargs):
self.ack = ack
super().__init__(*args, **kwargs)

def handle(self):
for line in self.rfile:
self.wfile.write(self.ack + line)


#serv = TCPServer(('', 15000), EchoHandler)
serv = TCPServer(('', 15000), partial(EchoHandler, ack=b'RECEIVED:'))
serv.serve_forever()

很多时候partial()能实现的效果,lambda表达式也能实现。比如,之前的几个例子可以使用下面这样的表达式:

1
2
3
4
points.sort(key=lambda p: distance(pt, p))
p.apply_async(add, (3, 4), callback=lambda result: output_result(result,log))
serv = TCPServer(('', 15000),lambda *args, **kwargs: EchoHandler(*args, ack=b'RECEIVED:', **kwargs))

将单方法的类转换为函数

你有一个除 init () 方法外只定义了一个方法的类。为了简化代码,你想将它转换成一个函数。

哈,这个和Java的函数式接口特别像

1
2
3
4
5
6
7
8
9
10
11
12
from urllib.request import urlopen

class UrlTemplate:
def __init__(self, template):
self.template = template
def open(self, **kwargs):
return urlopen(self.template.format_map(kwargs))


baidu = UrlTemplate('https://kaifa.baidu.com/searchPage?wd={names}&module={fields}')
for line in baidu.open(names='python', fields='SUG'):
print(line.decode('utf-8'))

这个类可以被一个更简单的函数来代替:

1
2
3
4
5
6
7
8
9
from urllib.request import urlopen
def urltemplate(template):
def opener(**kwargs):
return urlopen(template.format_map(kwargs))
return opener

baidu = urltemplate('https://kaifa.baidu.com/searchPage?wd={names}&module={fields}')
for line in baidu(names='python', fields='SUG'):
print(line.decode('utf-8'))

使用一个内部函数或者闭包的方案通常会更优雅一些。简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境。因此,在我们的解决方案中,opener()函数记住了template参数的值,并在接下来的调用中使用它。

任何时候只要你碰到需要给某个函数增加额外的状态信息的问题,都可以考虑使用闭包。相比将你的函数转换成一个类而言,闭包通常是一种更加简洁和优雅的方案。Python 装饰器

1

带额外状态信息的回调函数

你的代码中需要依赖到回调函数的使用(比如事件处理器、等待后台任务完成后的回调等),并且你还需要让回调函数拥有额外的状态值,以便在它的内部使用到。

会不会很熟悉,好多的js里面也会这样写,

1
2
3
4
5
def apply_async(func, args, *, callback):
# Compute the result
result = func(*args)
# Invoke the callback with the result
callback(result)

这里的callback必须为一个关键字参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def apply_async(func, args, *, callback):
# Compute the result
result = func(*args)
# Invoke the callback with the result
callback(result)

def print_result(result):
print('Got:', result)

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

apply_async(add, (2, 3), callback=print_result)
apply_async(add, ('hello', 'world'), callback=print_result)

print result() 函数仅仅只接受一个参数 result 。不能再传入其他信息,而当你想让回调函数访问其他变量或者特定环境的变量值的时候就会遇到麻烦,为了让回调函数访问外部信息,一种方法是使用一个绑定方法来代替一个简单函数。

1
2
3
4
5
6
7
class ResultHandler:
def __init__(self):
self.sequence = 0

def handler(self, result):
self.sequence += 1
print('[{}] Got: {}'.format(self.sequence, result))

使用这个类的时候,你先创建一个类的实例,然后用它的 handler() 绑定方法来

1
2
3
4
5
6
7
class ResultHandler:
def __init__(self):
self.sequence = 0

def handler(self, result):
self.sequence += 1
print('[{}] Got: {}'.format(self.sequence, result))

使用闭包方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14

def resultHandler():
sequence = 0
def handler( result):
# nonlocal 关键字用于在嵌套函数内部使用变量,其中变量不应属于内部函数。
nonlocal sequence
sequence += 1
print('[{}] Got: {}'.format(sequence, result))

return handler

r = resultHandler();
apply_async(add, (2, 3), callback=r)
apply_async(add, ('hello', 'world'), callback=r)

使用协程来完成同样的事情:

1
2
3
4
5
6
def make_handler():
sequence = 0
while True:
result = yield
sequence += 1
print('[{}] Got: {}'.format(sequence, result))

对于协程,你需要使用它的 send() 方法作为回调函数

1
2
3
4
5
6
7
>>> handler = make_handler()
>>> next(handler) # Advance to the yield
>>> apply_async(add, (2, 3), callback=handler.send)
[1] Got: 5
>>> apply_async(add, ('hello', 'world'), callback=handler.send)
[2] Got: helloworld
>>>

这里 nonlocal声明语句用来指示接下来的变量会在回调函数中被修改。如果没有这个声明,代码会报错。

访问闭包中定义的变量

你想要扩展函数中的某个闭包,允许它能访问和修改函数的内部变量。

通常来讲,闭包的内部变量对于外界来讲是完全隐藏的。但是,你可以通过编写访问函数并将其作为函数属性绑定到闭包上来实现这个目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def sample():
n = 0
# Closure function
def func():
print('n=', n)
# Accessor methods for n
def get_n():
return n

def set_n(value):
nonlocal n
n = value
# Attach as function attributes
func.get_n = get_n
func.set_n = set_n
return func
f = sample()
f()
f.set_n(10)
f()
print(f.get_n())

nonlocal声明可以让我们编写函数来修改内部变量的值,函数属性允许我们用一种很简单的方式将访问方法绑定到闭包函数上,这个跟实例方法很像(尽管并没有定义任何类)。

还可以进一步的扩展,让闭包模拟类的实例。你要做的仅仅是复制上面的内部函数到一个字典实例中并返回它即可。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import sys

class ClosureInstance:
def __init__(self, locals=None):
if locals is None:
#
locals = sys._getframe(1).f_locals

# Update instance dictionary with callables
self.__dict__.update((key,value) for key, value in locals.items() if callable(value) )

# Redirect special methods
def __len__(self):
return self.__dict__['__len__']()

# Example use
def Stack():
items = []

def push(item):
items.append(item)

def pop():
return items.pop()

def __len__():
return len(items)

return ClosureInstance()

s = Stack()
print(s)
s.push(10)
s.push(20)
s.push('Hello')
len(s)
s.pop()
s.pop()

结果显示,闭包的方案运行起来要快大概 8%,大部分原因是因为对实例变量的简化访问,闭包更快是因为不会涉及到额外的 self 变量。

总体上讲,在配置的时候给闭包添加方法会有更多的实用功能,比如你需要重置内部状态、刷新缓冲区、清除缓存或其他的反馈机制的时候。

发布于

2022-07-12

更新于

2023-06-21

许可协议

评论
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×