python进阶-你是否真的懂函数,装饰器,闭包,一等对象

2023-11-04

函数的定义:
函数是一段具有特定功能的、可重用的语句组,通过函数名来表示和调用。经过定义,一组语句等价于一个函数,在需要使用这组语句的地方,直接调用函数名称即可。

函数的使用包括两部分:函数的定义和函数的使用。
在这里插入图片描述

以上是函数的通用定义,无可厚非。那面向对象中的函数,又有什么独特的功能呢,首先,它是函数也是对象。

在 Python 中,函数是一等对象。编程语言理论家把“一等对象”定义为满足下述条件的程序实体:
在运行时创建
能赋值给变量或数据结构中的元素
能作为参数传给函数
能作为函数的返回结果

看来函数和其他对象的属性并没有什么不同,以下将重点讨论,在python中,函数作为一个一等对象,所具有的特性。

>>> def func(n):
    '''这是一个递归阶乘函数'''
    return  1 if n<2 else n*func(n-1)

>>> func(10)
3628800
>>> func.__doc__
'这是一个递归阶乘函数'
>>> type(func)
<class 'function'>
>>> help(func)
Help on function func in module __main__:

func(n)
    这是一个递归阶乘函数
__doc__ 是函数对象众多属性中的一个。
func 是 function 类的实例。
__doc__ 属性用于生成对象的帮助文本。

把函数作为对象,传入另一个函数map

>>> abc = func
>>> type(abc)
<class 'function'>
>>> abc
<function func at 0x7f5661eba1e0>
>>> map(abc,range(10))
<map object at 0x7f5660d76940>
>>> list(map(abc,range(10)))
[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]

有了一等函数,就可以使用函数式风格编程。函数式编程的特点之一是使用高阶函数。
lambda 关键字在 Python 表达式内创建匿名函数。
Python 简单的句法限制了 lambda 函数的定义体只能使用纯表达式。换句话
说,lambda 函数的定义体中不能赋值,也不能使用 while 和 try 等 Python 语句。

使用 lambda 表达式反转拼写,然后依此给单词列表排序,如下:

>>> fruits = ['strawberry', 'fig', 'apple', 'cherry', 'raspberry', 'banana']
>>> sorted(fruits, key=lambda word: word[::-1])
['banana', 'apple', 'fig', 'raspberry', 'strawberry', 'cherry']

再思考下一个段代码,想想结果是多少,然后自己试一试吧,相信你会有所发现:

>>> x = 10
>>> a = lambda y: x + y
>>> x = 20
>>> b = lambda y: x + y

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

>>> a(10)
30
>>> b(10)
30
>>>

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

>>> x = 10
>>> a = lambda y, x=x: x + y
>>> x = 20
>>> b = lambda y, x=x: x + y
>>> a(10)
20
>>> b(10)
30
>>>

在这里列出来的问题是新手很容易犯的错误,有些新手可能会不恰当的使用lambda 表达式。比如,通过在一个循环或列表推导中创建一个lambda 表达式列表,并期望函数能在定义时就记住每次的迭代值。例如:

>>> funcs = [lambda x: x+n for n in range(5)]
>>> for f in funcs:
... print(f(0))
...
4
4
4
4
4
>>>

现在我们用另一种方式修改一下:

>>> funcs = [lambda x, n=n: x+n for n in range(5)]
>>> for f in funcs:
... print(f(0))
...
0
1
2
3
4
>>>

lambda 句法只是语法糖:与 def 语句一样,lambda 表达式会创建函数对象。这是Python 中几种可调用对象的一种。

可调用对象

除了用户定义的函数,调用运算符(即 ())还可以应用到其他对象上。如果想判断对象能否调用,可以使用内置的 callable() 函数。Python 数据模型文档列出了 7 种可调用对象。
用户定义的函数
  使用 def 语句或 lambda 表达式创建。
内置函数
  使用 C 语言(CPython)实现的函数,如 len 或 time.strftime。
内置方法
  使用 C 语言实现的方法,如 dict.get。
方法
  在类的定义体中定义的函数。

  调用类时会运行类的 new 方法创建一个实例,然后运行 init 方法,初始化实例,最后把实例返回给调用方。因为 Python 没有 new 运算符,所以调用类相当于调用函数。(通常,调用类会创建那个类的实例,不过覆盖 new 方法的话,也可能出现其他行为。)
类的实例
  如果类定义了 call 方法,那么它的实例可以作为函数调用。
生成器函数
  使用 yield 关键字的函数或方法。调用生成器函数返回的是生成器对象。

>>> abs, str, 13
(<built-in function abs>, <class 'str'>, 13)
>>> [callable(obj) for obj in (abs,str,int,12)]
[True, True, True, False]

用户定义的可调用类型

不仅 Python 函数是真正的对象,任何 Python 对象都可以表现得像函数。为此,只需实现实例方法 call


import random

class BingoCage:
    '''
    BingoCage 类。这个类的实例使用任何可迭代对象构建,
    而且会在内部存储一个随机顺序排列的列表。调用实例会取出一个元素。
    '''
    def __init__(self, items):
         self._items = list(items) 
         random.shuffle(self._items) 
    def pick(self): 
         try:
             return self._items.pop()
         except IndexError:
             raise LookupError('pick from empty BingoCage') 
    def __call__(self): 
         return self.pick()
>>> bingo = BingoCage(range(3))
>>> bingo.pick()
1
>>> bingo()
0
>>> callable(bingo)
True

实现 call 方法的类是创建函数类对象的简便方式,此时必须在内部维护一个状态,让它在调用之间可用,例如 BingoCage 中的剩余元素。装饰器就是这样。装饰器必须是函数,而且有时要在多次调用之间“记住”某些事 [ 例如备忘(memoization),即缓存消耗大的计算结果,供后面使用 ]。创建保有内部状态的函数,还有一种截然不同的方式——使用闭包。

函数内省

除了 doc,函数对象还有很多属性。使用 dir 函数可以探知 factorial 具有下述属性:

>>> dir(func)
['__annotations__', '__call__', '__class__', '__closure__','__code__', '__defaults__', 
'__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__get__', 
'__getattribute__', '__globals__', '__gt__', 
'__hash__', '__init__', '__init_subclass__', 
'__kwdefaults__', '__le__', '__lt__', '__module__', 
'__name__', '__ne__', '__new__', '__qualname__', 
'__reduce__', '__reduce_ex__', '__repr__', 
'__setattr__', '__sizeof__', '__str__', '__subclasshook__']
>>> 

其中大多数属性是 Python 对象共有的。本节讨论与把函数视作对象相关的几个属性,先从 dict 开始。
与用户定义的常规类一样,函数使用 dict 属性存储赋予它的用户属性。这相当于一种基本形式的注解。一般来说,为函数随意赋予属性不是很常见的做法,但是 Django 框架这么做了。
把 short_description 属性赋予一个方法,Django 管理后台使用这个方法时,在记录列表中会出现指定的描述文本:

def upper_case_name(obj):
 	return ("%s %s" % (obj.first_name, obj.last_name)).upper()
upper_case_name.short_description = 'Customer name'

下面重点说明函数专有而用户定义的一般对象没有的属性。计算两个属性集合的差集便能得到函数专有属性列表.

>>> class User: pass

>>> obj=User()
>>> def func():pass

>>> sorted(set(dir(func))-set(dir(obj)))
['__annotations__', '__call__', '__closure__',
 '__code__', '__defaults__', '__get__',
  '__globals__', '__kwdefaults__', 
  '__name__', '__qualname__']
>>> 
创建一个空的用户定义的类。
创建一个实例。
创建一个空函数。
计算差集,然后排序,得到类的实例没有而函数有的属性列表。

在这里插入图片描述在这里插入图片描述

从定位参数到仅限关键字参数

Python 最好的特性之一是提供了极为灵活的参数处理机制,而且 Python 3 进一步提供了仅限关键字参数(keyword-only argument)。与之密切相关的是,调用函数时使用 * 和**“展开”可迭代对象,映射到单个参数。

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

def avg(first, *rest):
	return (first + sum(rest)) / (1 + len(rest))

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

在这个例子中,rest 是由所有其他位置参数组成的元组。然后我们在代码中把它当成了一个序列来进行后续的计算。为了接受任意数量的关键字参数,使用一个以 ** 开头的参数。

import json

def func(name,**kwargs):
    return {name:json.dumps(kwargs)}


print(func("http",h1="标题",key="abcd"))

**kwargs是一个包含所有被传入进来的关键字参数的字典
如果你还希望某个函数能同时接受任意数量的位置参数和关键字参数,可以同时使用 * 和 **。比如:

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

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

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(1024, block=True) # Ok

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

def add(x:int, y:int) -> int:
		return x + y

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

>>> help(add)
Help on function add in module __main__:

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

函数注解只存储在函数的 annotations 属性中

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

尽管注解的使用方法可能有很多种,但是它们的主要用途还是文档。因为 python并没有类型声明,通常来讲仅仅通过阅读源码很难知道应该传递什么样的参数给这个函数。这时候使用注解就能给程序员更多的提示,让他们可以正确的使用函数。
**

获取关于参数的信息

**
函数对象有个 defaults 属性,它的值是一个元组,里面保存着定位参数和关键字参数的默认值。仅限关键字参数的默认值在 kwdefaults 属性中。然而,参数的名称在 code 属性中,它的值是一个 code 对象引用,自身也有很多属性。


def clip(text, max_len=80):
 """在max_len前面或后面的第一个空格处截断文本
 """
     end = None
     if len(text) > max_len:
         space_before = text.rfind(' ', 0, max_len)
         if space_before >= 0:
             end = space_before
         else:
             space_after = text.rfind(' ', max_len)
             if space_after >= 0:
                 end = space_after
      if end is None: # 没找到空格
          end = len(text)
      return text[:end].rstrip()

查看__defaults__、code.co_varnames 和 code.co_argcount 的值。

>>> from clip import clip
>>> clip.__defaults__
(80,)
>>> clip.__code__ # doctest: +ELLIPSIS
<code object clip at 0x...>
>>> clip.__code__.co_varnames
('text', 'max_len', 'end', 'space_before', 'space_after')
>>> clip.__code__.co_argcount
2

可以看出,这种组织信息的方式并不是最便利的。参数名称在 __code__.co_varnames中,不过里面还有函数定义体中创建的局部变量。
因此,参数名称是前 N 个字符串,N的值由 __code__.co_argcount 确定。
顺便说一下,这里不包含前缀为 *** 的变长参数。参数的默认值只能通过它们在 __defaults__ 元组中的位置确定,因此要从后向前扫描才能把参数和默认值对应起来。在这个示例中 clip 函数有两个参数,text 和max_len,其中一个有默认值,即 80,因此它必然属于最后一个参数,即 max_len。

使用 inspect 模块。提取函数的签名

>>> from clip import clip
>>> from inspect import signature
>>> sig = signature(clip)
>>> sig 
<inspect.Signature object at 0x...>
>>> str(sig)
'(text, max_len=80)'
>>> for name, param in sig.parameters.items():
... print(param.kind, ':', name, '=', param.default)
...
POSITIONAL_OR_KEYWORD : text = <class 'inspect._empty'>
POSITIONAL_OR_KEYWORD : max_len = 80

inspect.signature 函数返回一个 inspect.Signature 对象,它有一个parameters 属性,这是一个有序映射,把参数名和 inspect.Parameter 对象对应起来。各个 Parameter 属性也有自己的属性,例如 name、default 和 kind。特殊的inspect._empty 值表示没有默认值,考虑到 None 是有效的默认值(也经常这么做),而且这么做是合理的。

kind 属性的值是 _ParameterKind 类中的 5 个值之一,列举如下。
POSITIONAL_OR_KEYWORD
可以通过定位参数和关键字参数传入的形参(多数 Python 函数的参数属于此类)。
VAR_POSITIONAL
  定位参数元组。
VAR_KEYWORD
  关键字参数字典。
KEYWORD_ONLY
  仅限关键字参数(Python 3 新增)。
POSITIONAL_ONLY
  仅限定位参数;目前,Python 声明函数的句法不支持,但是有些使用 C 语言实现且不接受关键字参数的函数(如 divmod)支持。

函数装饰器和闭包

函数装饰器用于在源码中“标记”函数,以某种方式增强函数的行为。这是一项强大的功能,但是若想掌握,必须理解闭包。
nonlocal 是新近出现的保留关键字,在 Python 3.0 中引入。作为 Python 程序员,如果严格遵守基于类的面向对象编程方式,即便不知道这个关键字也不会受到影响。
然而,如果你想自己实现函数装饰器,那就必须了解闭包的方方面面,因此也就需要知道nonlocal。除了在装饰器中有用处之外,闭包还是回调式异步编程和函数式编程风格的基础。

我们思考以下几个问题:

Python 如何计算装饰器句法
Python 如何判断变量是不是局部的
闭包存在的原因和工作原理
nonlocal 能解决什么问题

装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

def times(func):
    def real(*args):
        print("我被装饰了")
        return func(*args)
    return real

@times
def foo(*args):
    return sum(args)

以上效果等同于:

foo=times(foo)
print(foo(1,2,3,4,5,6))

两种写法的最终结果一样:上述两个代码片段执行完毕后得到的 foo不一定是原来那个 foo函数,而是 times(target) 返回的函数。

Python何时执行装饰器

装饰器的一个关键特性是,它们在被装饰的函数定义之后立即运行。这通常是在导入时(即 Python 加载模块时)

erji.py 文件内容

li=[]

def reg(func):
    print("运行装饰函数{}".formate(func))
    li.append(func)
    return func
@reg
def f1():
    print("运行f1")

@reg   
def f2():
    print("运行f2")
    
def f3():
    print("运行f3")

def main():
    print("运行main")
    print("列表li——》",li)
    f1()
    f2()
    f3()

if __name__=="__main__":
    main()

在命令行中导入:

>>> import erji
运行装饰函数<function f1 at 0x7f2f5d3d22f0>
运行装饰函数<function f2 at 0x7f2f5d3d2378>
>>> 

 

函数装饰器在导入模块时立即执行,而被装饰的函数只在明确调用
时运行。这突出了 Python 程序员所说的导入时和运行时之间的区别。

通过脚本运行,注意执行顺序:

fujp@fujp-PC:~/Documents$ python3 erji.py
运行装饰函数<function f1 at 0x7f9248dd7c80>
运行装饰函数<function f2 at 0x7f9248dd7d08>
运行main
列表li——》 [<function f1 at 0x7f9248dd7c80>, <function f2 at 0x7f9248dd7d08>]
运行f1
运行f2
运行f3

考虑到装饰器在真实代码中的常用方式:
装饰器函数与被装饰的函数在同一个模块中定义。实际情况是,装饰器通常在一个模块中定义,然后应用到其他模块中的函数上。
reg装饰器返回的函数与通过参数传入的相同。实际上,大多数装饰器会在内部定义一个函数,然后将其返回。

变量作用域规则

>>> def f1(a):
	print(a)
	print(b)

	
>>> f1(10)
10
Traceback (most recent call last):
  File "<pyshell#4>", line 1, in <module>
    f1(10)
  File "<pyshell#3>", line 3, in f1
    print(b)
NameError: name 'b' is not defined
>>> b='abc'
>>> f1(10)
10
abc
>>> 

出现错误并不奇怪。如果先给全局变量 b 赋值,然后再调用 f1,那就不
会出错.

>>>b="abc"
>>> def f2(a):
	print(a)
	print(b)
	b=100

	
>>> f2(11)
11
Traceback (most recent call last):
  File "<pyshell#12>", line 1, in <module>
    f2(11)
  File "<pyshell#11>", line 3, in f2
    print(b)
UnboundLocalError: local variable 'b' referenced before assignment

为什么会出错呢?已经定义了全局变量b?

Python 编译函数的定义体时,它判断 b 是局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,Python 会尝试从本地环境获取 b。后面调用 f2(11)时, f2 的定义体会获取并打印局部变量 a 的值,但是尝试获取局部变量 b 的值时,发现b 没有绑定值。这不是缺陷,而是设计选择:Python 不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。这比 JavaScript 的行为好多了,JavaScript 也不要求声明变量,但是如果忘记把变量声明为局部变量(使用 var),可能会在不知情的情况下获取全局变量。如果在函数中赋值时想让解释器把 b 当成全局变量,要使用 global 声明:

>>> b=6
>>> def f3(a):
	global b
	print(a)
	print(b)
	b=9

	
>>> f3(3)
3
6
>>> b
9
>>> 

以上是python的作用域,下面我们看闭包:

人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。因此,很多人是同时知道这两个概念的。其实,闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。

>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

avg的行为是如何做到保存值的呢,看看常规方法:

lass Avg():
    def __init__(self):
        self.li=[]
    def __call__(self,newvalue):
        self.li.append(newvalue)
        return sum(self.li)/len(self.li)

avg=Avg()

print(avg(10))
print(avg(11))

10.0
10.5
>>> 

利用函数实现:

def avg():
    li=[]
    def add(num):
        li.append(num)
        return sum(li)/len(li)
    return add

avg=avg()   

print(avg(10))
print(avg(11))


10.0
10.5
>>> 

avg的闭包延伸到那个函数的作用域之外,包含自由变量 li 的绑定审查返回的 avg 对象,我们发现 Python 在 code 属性(表示编译后的函数定义
体)中保存局部变量和自由变量的名称

print(avg.__code__.co_varnames)
print(avg.__code__.co_freevars)
print(avg.__closure__)
print(avg.__closure__[0].cell_contents)


('num',)
('li',)
(<cell at 0x7f8be63410a8: list object at 0x7f8be716b8c8>,)
[]
>>> 

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。

nonlocal声明

def avg():
    count=0
    total=0
    def add(num):
        count +=1
        total +=num
        return total/count
    return add

avg=avg()   

print(avg(10))



Traceback (most recent call last):
  File "/home/fujp/Documents/erji.py", line 12, in <module>
    print(avg(10))
  File "/home/fujp/Documents/erji.py", line 5, in add
    count +=1
UnboundLocalError: local variable 'count' referenced before assignment
>>> 

为了解决这个问题,Python 3 引入了 nonlocal 声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

def avg():
    count=0
    total=0
    def add(num):
        nonlocal count, total
        count +=1
        total +=num
        return total/count
    return add

avg=avg()   

print(avg(10))

10.0
>>> 

实现一个简单的装饰器

import time

def clock(func):
    def clocked(*args):
        start = time.perf_counter()
        res = func(*args)
        elapsed = time.perf_counter()-start
        name = func.__name__
        arg_str = ','.join(repr(i) for i in args)
        print("{:.8f}s {}({}) ->{!r}".format(elapsed,name,arg_str,res))
        return res
    return clocked


@clock
def sno(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n<2 else n*factorial(n-1)


if __name__ == "__main__":
    print("*"*40,'Calling sno(.123)')
    sno(.123)
    print("*"*40,"Calling factorial(6)")
    print("6!=", factorial(6))
fujp@fujp-PC:~/Documents$ python3 erji.py
**************************************** Calling sno(.123)
0.12325145s sno0.123 ->None
**************************************** Calling factorial(6)
0.00000039s factorial1 ->1
0.00001483s factorial2 ->2
0.00002399s factorial3 ->6
0.00003201s factorial4 ->24
0.00004375s factorial5 ->120
0.00005503s factorial6 ->720
6!= 720

clock 装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的 namedoc 属性。使用 functools.wraps 装饰器把相关的属性从 func 复制到 clocked 中。此外,这个新版还能正确处理关键字参数。

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args,**kwargs):
        start = time.perf_counter()
        res = func(*args,**kwargs)
        elapsed = time.perf_counter()-start
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(','.join(repr(i) for i in args))
        if kwargs:
            pairs = ["{i}={k}" for i, k in sorted(kwargs.items())]
            arg_lst.append(",".join(pairs))
        arg_str = ",".join(arg_lst)
        print("{:.8f}s {}({}) ->{!r}".format(elapsed,name,arg_str,res))
        return res
    return clocked


@clock
def sno(seconds):
    time.sleep(seconds)

@clock
def factorial(n):
    return 1 if n<2 else n*factorial(n-1)


if __name__ == "__main__":
    print("*"*40,'Calling sno(.123)')
    sno(.123)
    print("*"*40,"Calling factorial(6)")
    print("6!=", factorial(6))

functools.wraps 只是标准库中拿来即用的装饰器之一。

标准库中的装饰器

functools.wraps,它的作用是协助构建行为良好的装饰器。我们在示例中用过。标准库中最值得关注的两个装饰器是 lru_cache 和全新的singledispatch。这两个装饰器都在 functools 模块中定义。接下来分别讨论它们。

使用functools.lru_cache做备忘

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args,**kwargs):
        start = time.perf_counter()
        res = func(*args,**kwargs)
        elapsed = time.perf_counter()-start
        name = func.__name__
        arg_lst = []
        if args:
            arg_lst.append(','.join(repr(i) for i in args))
        if kwargs:
            pairs = ["{i}={k}" for i, k in sorted(kwargs.items())]
            arg_lst.append(",".join(pairs))
        arg_str = ",".join(arg_lst)
        print("{:.8f}s {}({}) ->{!r}".format(elapsed,name,arg_str,res))
        return res
    return clocked

@clock
def fibonacci(n):
    if n<2:
        return n
    return fibonacci(n-2)+fibonacci(n-1)


if __name__ == "__main__":
    
    print(fibonacci(8))

0.00000044s fibonacci(0) ->0
0.00000058s fibonacci(1) ->1
0.00518868s fibonacci(2) ->1
0.00000032s fibonacci(1) ->1
0.00000068s fibonacci(0) ->0
0.00000076s fibonacci(1) ->1
0.00519792s fibonacci(2) ->1
0.00972343s fibonacci(3) ->2
0.01918018s fibonacci(4) ->3
0.00000031s fibonacci(1) ->1
0.00000029s fibonacci(0) ->0
0.00000071s fibonacci(1) ->1
0.00400207s fibonacci(2) ->1
0.01008527s fibonacci(3) ->2
0.00000038s fibonacci(0) ->0
0.00000081s fibonacci(1) ->1
0.00441932s fibonacci(2) ->1
0.00000044s fibonacci(1) ->1
0.00000085s fibonacci(0) ->0
0.00000077s fibonacci(1) ->1
0.00463588s fibonacci(2) ->1
0.00855313s fibonacci(3) ->2
0.01659546s fibonacci(4) ->3
0.03134774s fibonacci(5) ->5
0.05440020s fibonacci(6) ->8
0.00000035s fibonacci(1) ->1
0.00000044s fibonacci(0) ->0
0.00000067s fibonacci(1) ->1
0.00483554s fibonacci(2) ->1
0.00914232s fibonacci(3) ->2
0.00000021s fibonacci(0) ->0
0.00000107s fibonacci(1) ->1
0.00460196s fibonacci(2) ->1
0.00000044s fibonacci(1) ->1
0.00000051s fibonacci(0) ->0
0.00000068s fibonacci(1) ->1
0.00738260s fibonacci(2) ->1
0.01360563s fibonacci(3) ->2
0.02402176s fibonacci(4) ->3
0.03901143s fibonacci(5) ->5
0.00000024s fibonacci(0) ->0
0.00000096s fibonacci(1) ->1
0.00670099s fibonacci(2) ->1
0.00000074s fibonacci(1) ->1
0.00000044s fibonacci(0) ->0
0.00000068s fibonacci(1) ->1
0.00676779s fibonacci(2) ->1
0.01379754s fibonacci(3) ->2
0.02743604s fibonacci(4) ->3
0.00000023s fibonacci(1) ->1
0.00000039s fibonacci(0) ->0
0.00000096s fibonacci(1) ->1
0.00588801s fibonacci(2) ->1
0.01215024s fibonacci(3) ->2
0.00000035s fibonacci(0) ->0
0.00000065s fibonacci(1) ->1
0.00608444s fibonacci(2) ->1
0.00000039s fibonacci(1) ->1
0.00000072s fibonacci(0) ->0
0.00000070s fibonacci(1) ->1
0.00756144s fibonacci(2) ->1
0.01338449s fibonacci(3) ->2
0.02543876s fibonacci(4) ->3
0.04436490s fibonacci(5) ->5
0.07847265s fibonacci(6) ->8
0.12640754s fibonacci(7) ->13
0.18563432s fibonacci(8) ->21
21
>>> 

可见递归的效率非常低。修改一下

@functools.lru_cache()
@clock
def fibonacci(n):
    if n<2:
        return n
    return fibonacci(n-2)+fibonacci(n-1)


if __name__ == "__main__":
    
    print(fibonacci(8))

执行结果:

0.00000044s fibonacci(0) ->0
0.00000059s fibonacci(1) ->1
0.00550505s fibonacci(2) ->1
0.00000103s fibonacci(3) ->2
0.00951667s fibonacci(4) ->3
0.00000104s fibonacci(5) ->5
0.01483862s fibonacci(6) ->8
0.00000108s fibonacci(7) ->13
0.01925801s fibonacci(8) ->21
21
>>> 

对比两次的执行结果,可见用lru_cache装饰器之后,调用的重复次数大幅度减低,每个值仅调用了一次,所用时间降低10倍。

lru_cache 可以使用两个可选的参数来配置。
functools.lru_cache(maxsize=128, typed=False)

maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能,maxsize 应该设为 2 的幂。typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。顺便说一下,因为 lru_cache 使用字典存储结果,而且键根据调用时传入的定位参数和关键字参数创建,所以被 lru_cache 装饰的函数,它的所有参数都必须是可散列的。

实现单分派泛函数,functools.singledispatch 装饰器


def out(obj):
    content = '{!r}'.format(obj)
    return "<h1>{}</h1>".format(content)

这个函数适用于任何 Python 类型,但是现在我们想做个扩展,让它使用特别的方式显示某些类型。

在这里插入代码片

@singledispatch 标记处理 object 类型的基函数。
各个专门函数使用 @«base_function».register(«type») 装饰。
专门函数的名称无关紧要;_ 是个不错的选择,简单明了。
为每个需要特殊处理的类型注册一个函数。
numbers.Integral 是 int 的虚拟超类。
可以叠放多个 register 装饰器,让同一个函数支持不同类型。

from functools import singledispatch
from collections import abc
import numbers

@singledispatch
def out(obj):
    content = '{!r}'.format(obj)
    return "<h1>{}</h1>".format(content)

@out.register(str)
def _(text):
    content="我是字符串{}".format(text)
    return content

@out.register(numbers.Integral)
def _(num):
    num=num**2
    content="我是数字{}".format(num)
    return content
    
  
@out.register(tuple)
@out.register(abc.MutableSequence)
def _(seq):
    return list(seq)


print(out("abcdefg"))
print(out(100))
print(out((1,2,3,4,5,6,)))

只要可能,注册的专门函数应该处理抽象基类(如 numbers.Integral 和
abc.MutableSequence),不要处理具体实现(如 int 和 list)。这样,代码支持的兼容类型更广泛。例如,Python 扩展可以子类化 numbers.Integral,用固定的位数实现 int 类型。
使用抽象基类检查类型,可以让代码支持这些抽象基类现有和未来的具体子类
或虚拟子类。
singledispatch 机制的一个显著特征是,你可以在系统的任何地方和任何模块中注册专门函数。如果后来在新的模块中定义了新的类型,可以轻松地添加一个新的专门函数来处加粗样式理那个类型。此外,你还可以为不是自己编写的或者不能修改的类添加自定义函数。

简单来讲,一个闭包就是一个函数,只不过在函数内部带上了一个额外的变量环境。闭包关键特点就是它会记住自己被定义时的环境。任何时候只要你碰到需要给某个函数增加额外的状态信息的问题,都可以考虑使用闭包。相比将你的函数转换成一个类而言,闭包通常是一种更加简洁和优雅的方案。
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

python进阶-你是否真的懂函数,装饰器,闭包,一等对象 的相关文章

随机推荐

  • 在Windows 11 中安装和使用 WSL 2

    文章目录 列出可安装的发行版 分发 安装WSL 2 常用命令 显示帮助 启动分发 从powershell中退出分发 关闭正在运行的分发 立即终止所有正在运行的分发和 WSL 2轻型虚拟机 更新wsl 使用VSCode连接WSL 设置代理 换
  • QEMU-从buildroot里面编译kernel(7)

    上面是我的微信和QQ群 欢迎新朋友的加入 下载交叉编译工具 https snapshots linaro org gnu toolchain 选一个最新的 选择压缩包 解压 sudo apt get install g sudo mv gc
  • 网狐棋牌:数据库

    jeefwtwo 账号数据库 QPAccountsDB 账号账号数据库 QPGamescoreDB 游戏积分数据库 QPGameMatchDB 比赛数据库 QPPlatformDB 平台数据库 QPRecordDB 记录数据库 QPTrea
  • vue-video-player基本使用

    下载 npm install vue video player 如果不使用vue的话 可以直接去官网 或者cdn获取对应js即可 在vue中的基本使用 main js中 全局 import Vue from vue import VueVi
  • 线程与进程的对比、互斥锁和条件变量的使用-多线程编程

    线程与进程的对比 线程的概念是共享CPU的需要 进程概念是共享内存的需要 一个进程里代码段 数据段 堆是共享的 但是进程中的每个线程中的栈 寄存器内容独立 进程都是独立的 通常的IPC 管道 共享内存都可以通讯 处于一个线程的代码觉得它拥有
  • Redis基础知识(二):事务机制

    文章目录 一 什么是事务机制 二 Redis模式下如何实现事务机制 2 1 显式开启一个事务 2 2 将命令入队列Queue 2 3 执行事务或丢弃 2 4 EXEC命令执行示例 2 5 DISCARD命令 放弃事务 2 6 因为命令错误导
  • RabbitMQ用途及问题

    转自 https blog csdn net u013871100 column info 27053 1 用途 1 解耦 系统A在代码中直接调用系统B和系统C的代码 如果将来D系统接入 系统A还需要修改代码 过于麻烦 2 异步 将消息写入
  • ROS学习笔记(五)---话题发布

    1 话题通信是什么 在ROS 机器人操作系统 中 话题通信是一种常用的通信机制 用于在不同的ROS节点之间传递消息 话题通信基于发布者 订阅者模式 其中一个节点 发布者 发布消息到一个特定的话题 而其他节点 订阅者 可以订阅该话题以接收消息
  • 一篇文章让你深入了解RGB数据格式和互转(YUV数据组成)

    我们日常看到的图片 视频由RGB或YUV数据组成 说明 1 RGB分为RGB16 RGB24 RGB32 RGB RGB16 RGB24 RGB32 一 RGB分RGB16 RGB24 RGB32 1 RGB16格式分为RGB565 RGB
  • 某市财政收入预测分析:GM模型+神经网络

    from numpy random import seed seed 1 import tensorflow tensorflow random set seed 2 import numpy as np import pandas as
  • openssl生成椭圆曲线的私钥是如何做到每次不同的?

    目录 例子 排查 随机算法 小结 例子 生成一个私钥只需要3步 1 获得指定曲线的group 如比特币的secp256k1 2 group和key绑定 3 用key来生成私钥 先上一段代码例子 key1 EC KEY new if key1
  • 2021.11.9

    把数据结构 第一章的课后写了一下 有点难哎 第二章的1 12题 对空间复杂度有了进一步的了解 假设法和最深层语句执行次数 但是涉及到 log 什么的我就不咋会了 如何去设计一个抽象数据类型 基本能将算法的功能看出来 如何设置更加高效的算法
  • 一个完整的语法分析、词法分析例子——Universal Pasrser

    需求 用户用formal notation指定语法 词法 然后可以匹配相应的文本 用法类似正则表达式 只需给出formal notation 不需要为每一种格式的文本单独写匹配器 formal notation主要是3个部分 1 BNF 列
  • 应用层---HTTP协议

    应用层 应用层是TCP IP协议分层的最顶层模型 它的作用是维持好应用程序之间的沟通 维护好特定的协议 如简单电子邮件传输 SMTP 文件传输协议 FTP 网络远程访问协议 Telnet 等 应用层协议分为两种 一种是根据客户要求自己对数据
  • 【计算机网络】章节思维导图《谢希仁第八版》

    仅作为笔记 如有错误 还请斧正 第一章 概述思维导图 第二章 物理层
  • Linux下一种运行时切换动态库的方法

    概述 假设有这样一种应用场景 有一个 lib libfoo so动态库 有两个或多个厂家各自实现了自己的版本 每个版本都不是尽善尽美 分别有自己的优势和缺点 可能app1使用v1版本的库比较合适 app2使用v2版本的库不会出bug等等 在
  • 中国图书分类法检索计算机方面的图书,《中国图书分类法》(简称《中图法》)是我国常用的分类法,要检索计算机方面的图书,需要在( )类目下查找。...

    A Q类目 B T 类目 C S类目 D 答案 中国图书分类法 简称 中图法 是我国常用的分类法 要检索计算机方面的图书 需要在 类目下查找 解析 判断题 查全率是指检索出的符合课题需要的文献与检索出的相关文献量之比 判断题 文献是记录知识
  • SQL基础语句

    SQL SQL简介 SQL Structured Query Language 结构化查询语言 是用于管理关系数据库管理系统 RDBMS SQL 的范围包括数据插入 查询 更新和删除 数据库模式创建和修改 以及数据访问控制 SQL语法 us
  • Java中BigDecimal比较大小的方法

    场景 数据中某字段 是decimal类型 现在要在业务代码中获取某个Model的此属性是否为0 即要将其与0 00比较大小 实现 java中对bigdimical比较大小一般用的是bigdemical的compareTo方法 int a b
  • python进阶-你是否真的懂函数,装饰器,闭包,一等对象

    函数的定义 函数是一段具有特定功能的 可重用的语句组 通过函数名来表示和调用 经过定义 一组语句等价于一个函数 在需要使用这组语句的地方 直接调用函数名称即可 函数的使用包括两部分 函数的定义和函数的使用 以上是函数的通用定义 无可厚非 那