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

Posted 山河已无恙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python实战之函数的一些奇技淫巧相关的知识,希望对你有一定的参考价值。

写在前面


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

  • 书很不错,感兴趣小伙伴可以去拜读下,博文涉及内容包括:

  • 语法方面

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

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

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

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


名词解释

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

函数

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

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

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

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的可变参数函数定义

function fun(a,...b)
  console.log(a,b)


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

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

import html


def make_element(name, value, **attrs):
    keyvals = [' %s="%s"' % item for item in attrs.items()]
    attr_str = ''.join(keyvals)
    element = '<nameattrs>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>'))

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

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

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

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

def a(x, *args, y):
    pass

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

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

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

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

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

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

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方法也可以直接输出注释信息

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)

嗯,执行输出

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里面去声明类型应该如何处理

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

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__ 可以打印函数的注解信息

>>> add.__annotations__
'x': <class 'int'>, 'y': <class 'int'>, 'return': <class 'int'>
>>>

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

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

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

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

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

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

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

func (ip IP) MarshalText() ([]byte, error) 
	if len(ip) == 0 
		return []byte(""), nil
	
	if len(ip) != IPv4len && len(ip) != IPv6len 
		return nil, &AddrErrorErr: "invalid IP address", Addr: hexString(ip)
	
	return []byte(ip.String()), nil

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

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

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

普通的默认参数函数

>>> def spam(a, b=42):
...     print(a, b)
...
>>> spam(1)
1 42
>>> spam(1, 2)
1 2
>>>

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

>>> def spam(a, b=None):
...     if b is None:
...        print(b)
...
>>> spam(a)
None
>>> spam(a,12)
>>>

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

我们可以像下面这样写:

>>> _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 值不传值两种情况是有差别的。

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

>>> 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、数字或字符串。特别的,千万不要像下面这样写代码:

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

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

>>> 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 操作符是很重要的,不要使用下面的方式

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

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

>>> 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表达式某种意义上讲,是函数式编程的体现,行为参数化的思想,本质上是匿名函数的语法糖。

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

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

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

python看一个具体的Demo

>>> 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 定义了一个匿名函数,并想在定义时捕获到某些变量的值。

>>> 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的时候,外部的局部变量值在内部使用时是不应该改变的。

>>> x = 10
>>> a = lambda y: x + y
>>> a(10)
20
>>> x = 3
>>> a(10)
13
>>>

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

>>> 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()函数允许你给一个或多个参数设置固定的值,减少接下来被调用时的参数个数。为了演示清楚,假设你有下面这样的函数:

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

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

>>> 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接受未赋值的参数,然后跟之前已经赋值过的参数合并起来,最后将所有参数传递给原始函数。

def distance(p1, p2):
    x1, y1 = p1
    x2, y2 = p2

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

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

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

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

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

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 参数的值来初始化即可

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表达式也能实现。比如,之前的几个例子可以使用下面这样的表达式:

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的函数式接口特别像

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 python奇技淫巧

20个Python奇技淫巧,终极干货,建议收藏!

20个Python奇技淫巧,终极干货,建议收藏!

Excel之如何使用VLOOKUP函数合并两张表

Python中迭代器&生成器的“奇技淫巧“

Python中迭代器&生成器的“奇技淫巧“