python函数

函数是 python 中组织代码的最小单元。函数有输入(参数)和输出(返回值),函数其实是一个代码单元,把输入转换成输出。

在 python 中可以创建四种函数:

  1. 全局函数:定义在模块中;
  2. 局部函数:嵌套于其他函数中;
  3. lambda 函数:表达式;
  4. 方法:与特定数据类型关联的函数,并且只能与数据类型关联一起使用。

将程序中重复的地方抽取出来,定义成一个函数。使用函数的好处:

  • 程序可扩展性;
  • 减少程序代码;
  • 方便程序架构的更改。

定义语法:

def 函数名(参数1, 参数2):
    print(参数1 + 参数2)
    ...
    return 参数1 + 参数2 # 定义函数的返回值
复制代码

定义函数的时候,并不会执行函数体。只有当我们调用的时候,它才会执行。

函数的调用:

函数名(参数1, 参数2)
复制代码

传入的参数必须和函数定义时的参数相匹配,如果不匹配,会抛出 TypeError。参数按照定义的顺序传入,这样的传参方法叫做位置参数。

函数的参数

函数的参数分为形式参数和实际参数。

  • 在定义函数时,函数后面括号中的变量名称叫做“形式参数”,或者称为“形参”;
  • 在调用函数时,函数后面括号中的变量名称叫做“实际参数”,或者称为“实参”。
In [1]: def fun(x,y):
   ...:     print x + y
   ...:

In [2]: fun(3,4)
7

In [3]: fun('ab','c')
abc
复制代码

判断传递给脚本的参数是数字:

#!/usr/bin/env python
import sys
def isNum(s):
    for i in s:
        if i in '0123456789':
            print '%s is a number' %s
            sys.exit()
    print '%s is not a number' %s

isNum(sys.argv[1])
复制代码

位置参数传参

参数按照定义的顺序传入,这样的传参方法叫做位置参数。这是最简单、也是最容易理解的传参方式了。

>>> def add(x, y):
...     return x + y
...
>>> add(2, 3) # 2 对应 x,3 对应 y
Out[10]: 5
复制代码

默认参数

函数中的参数可以不给其赋值,它会有默认值,比如有的网站选择国籍不管选不选都会有一个默认值。

>>> def inc(base, x=1):
...     return base + x
...
>>> inc(3) # 可以传递一个参数
Out[12]: 4
>>> inc(3, 3) # 可以给其传递参数进行覆盖
Out[13]: 6
复制代码

但是默认参数必须出现在不带默认值参数之后,不然当只传递一个参数的时候,它不知道给谁。

>>> def inc(x=1, base):
...     return base + x
...
  File "<ipython-input-14-ac010ba50fd9>", line 1
    def inc(x=1, base):
           ^
SyntaxError: non-default argument follows default argument
复制代码

可变参数

有这么一种情况:

>>> def sum(lst):
...     ret = 0
...     for i in lst:
...         ret += i
...     return ret
...
>>> sum([1, 2, 3]) # 只能传递一个参数
Out[16]: 6
复制代码

这个函数只能传递一个参数给它,但是如果想要想要将列表中的元素通过参数的形式传递给它时,可以这么定义参数:

>>> def sum(*lst): # 加个星号即可
...     ret = 0
...     for i in lst:
...         ret += i
...     return ret
...
>>> sum(1, 2, 3, 4) # 可以传递任意个参数
Out[19]: 10
复制代码

可变参数分两种:

  • 位置可变参数:参数前加一个星号,表示这个参数是可变的,也就是可以接受任意多个参数。这些参数构成一个元组,参数只能通过位置参数传参。
  • 关键字可变参数:参数前加两个星号,表示这个参数是可变的,也就是可以接受任意多个参数。这些参数构成一个字典,参数只能通过关键字参数传参。
>>> def connect(**kwargs):
...     print(type(kwargs))
...     for k, v in kwargs.items():
...         print('{} -> {}'.format(k, v))
...
>>> connect(host='10.0.0.1', port=3306)
<class 'dict'>
host -> 10.0.0.1
port -> 3306
复制代码

注意事项:

  • 这两种可变参数可以一起使用,但是字典必须在后。
  • 普通参数可以和可变参数一起使用,但是传参的时候必须匹配。
  • 位置可变参数可以在普通参数之前,但是在它之后的普通参数只能通过关键字参数传递。
  • 关键字可变参数不允许在普通参数之前。

为了防止出错,或者调用方疑惑,我们应该遵循以下规则:

  1. 默认参数靠后;
  2. 可变参数靠后;
  3. 默认参数和可变参数不同时出现。

当我们连接数据库时,可以这么处理:

# 第一种方式
def connect(host='127.0.0.1', port='3306', user='root', password='', db='test', **kwargs):
    pass

# 第二种方式
def connect(**kwargs):
    host = kwargs.pop('host', '127.0.0.1')
复制代码

参数解构

调用函数时,使用 * 开头的参数,可用于将参数集合打散,从而传递任意多基于位置或关键字的参数。在这之前,我们回顾下变量解包:

>>> l1 = ['Sun', 'Mon', 'Tus']
>>> x, y, z = l1
>>> x, y, z
('Sun', 'Mon', 'Tus')
复制代码

参数解包也就是这种结果:

>>> def f1(a, b, c):
...   print a, b, c
...
>>> f1(*['Sun', 'Mon', 'Tus'])
Sun Mon Tus
复制代码

参数的个数和列表中元素的个数要一一匹配,多了少了都不行。

>>> def sum(*args):
...     ret = 0
...     for i in args:
...         ret += i
...     return ret
...
>>> sum(*range(10))
Out[34]: 45
复制代码

参数的解构有两种形式:

  • 一个星号:解构的对象是可迭代对象,解构的结果是位置参数;
  • 两个星号:解构的对象是字典,结构的结果是关键字参数。

要解构的字典的 key 必须是字符串。

关键字传参

这个是针对实参的,也就是传递给函数时使用的,函数本身的定义并没有特殊的地方。这样就避免了默认参数不能定义顺序的问题了,可以说这个可以配合默认参数使用。

先定义一个函数,然后使用关键字参数传递:

>>> def add(x, y):
...     return x + y
...
 >>> add(2, y=3)
Out[21]: 5
复制代码

当位置参数和关键字参数混合使用时,位置参数必须在前面,不然会报错。

>>> def add(x, y):
...     return x + y
...
>>> add(x=2, 3)
  File "<ipython-input-9-92c728388ce1>", line 1
    add(x=2, 3)
            ^
SyntaxError: positional argument follows keyword argument
复制代码

默认参数和关键字传参结合起来非常好用,它能让函数的调用非常简洁。

def connect(host='127.0.0.1', port='3306', user='root', password='', db='test'):
    pass

connect('10.0.0.5', password='123456')
复制代码

可变参数允许你传入 0 个或任意个参数,这些可变参数在函数调用时自动封装为一个 tuple。而关键字参数允许你传入 0 个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个 dict。请看示例:

>>> def person(name, age, **kw):
...     print('name:', name, 'age:', age, 'other:', kw)
复制代码

函数 person 除了必选参数 name 和 age 外,还接受关键字参数 kw。在调用该函数时,可以只传入必选参数:

>>> person('Michael', 30)
name: Michael age: 30 other: {}
复制代码

也可以传入任意个数的关键字参数:

>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
复制代码

关键字参数有什么用?它可以扩展函数的功能。比如,在 person 函数里,我们保证能接收到 name 和 age 这两个参数,但是如果调用者愿意提供更多的参数,我们也能收到。

试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个 dict,然后把该 dict 转换为关键字参数传进去:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
复制代码

当然,上面复杂的调用可以用简化的写法:

>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
复制代码

**extra 表示把 extra 这个 dict 的所有 key-value 用关键字参数传入到函数的 **kw 参数,kw 将获得一个 dict,注意 kw 获得的 dict 是 extra 的一份拷贝,对 kw 的改动不会影响到函数外的 extra。

keyword-only参数

这个是 python3 新加入的。

  • 星号之后的参数只能通过关键字参数传递,叫做 keyword-only 参数。
  • 星号本身不接收任何值。
  • 可变位置参数之后的参数也是 keyword-only 参数。
  • keyword-only 参数可以有默认值;
  • keyword-only 参数可以和默认参数一起出现,不管它有没有默认值,不管默认参数是不是 keyword-only 参数。
>>> def fn(*, x): # 星号后面的参数都是 keyword-only 参数,不管星号后面的参数有多少个
...     print(x)
...
>>> fn(1) # 不能使用位置参数传递
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-15-cb5d79cf2c77> in <module>()
----> 1 fn(1)

TypeError: fn() takes 0 positional arguments but 1 was given

>>> fn(x=3) # 只能通过关键字参数传递
3
复制代码

通常的用法,keyword-only 参数都有默认值。python 的标准库中大量的使用了 keyword-only 参数。

return

函数一般都有输入输出,return 就是定义函数的输出,也就是返回值的。

def fun():
    print('hello world')

fun()
复制代码

执行这个脚本会输出 hello world,如何查看该函数的返回值呢?只要加个 print 即可:

>>> def fun():
...     print('hehe')
...
>>> print(fun())
hehe
None
复制代码

可以看到返回值为 None,函数的返回值如果没有定义,默认为 None。此时就可以通过 return 定义返回值了:

>>> def fun(x, y):
...     return x + y
...     print('hehe')
...
>>> fun(3, 5)
Out[57]: 8
复制代码

从上面可以看到一旦遇到 return 函数就终止运行了,后面的代码不会再运行。

一个函数中可以有多个 return,执行到哪个 return,就由哪个 return 返回结果并结束函数。

>>> def guess(x):
...     if x > 3:
...         return '> 3'
...     else:
...         return '<= 3'
...
>>> guess(4)
Out[59]: '> 3'
复制代码

函数的返回值可以任意定义。事实上函数中一般都是使用 return,很少使用 print。使用 return 的目的在于能够用到它返回的值,然后使用这个值做些事情。

作用域和全局变量

作用域是一个变量的可见范围。在接触函数之前,是没有作用域这个概念的,因为所有的代码都在同一个作用域中。由于函数是组织代码的最小单位,因此从函数开始就有了作用域的概念。

python 创建、改变或查找变量名都是在名称空间中进行,在代码中变量名被赋值的位置决定了其能被访问到的范围。函数定义了局部作用域,而模块定义了全局作用域。每个模块都是一个全局作用域,因此,全局作用域的范围仅限于单个程序文件。

每次对函数的调用都会创建一个新的本地作用域,赋值的变量除非声明为全局变量,否则均为变量。

所有的变量名都可以归纳为局部、全局或内置的(由 __builtin__ 模块提供)。

变量名引用分为三个作用域进行,首先是局部,接着是全局,最后是内置。也就是说引用这个变量先在本地函数(Local function)中查找,没找到就会在它的外层函数(Enclosing function locals),还没有找到就要找模块中的全局变量(Global module),还没有的话找内置变量(Built-in),如果还是找不到的话就报错了。

局部变量的作用域仅限于一个函数中,局部变量只存在于函数中,也就是说在函数中定义的变量就是局部变量;而全局变量的作用域为整个程序。局部变量的优先级高于全局变量,当局部变量和全局变量的变量名相同时,它们之间并不会相互覆盖。

>>> x = 1 # 定义在全局作用域中
>>> def inc():
...     x += 1
...
>>> inc() # 直接报错
Traceback (most recent call last):
  File "/usr/local/python/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-62-ae671e6b904f>", line 1, in <module>
    inc()
  File "<ipython-input-61-661b9217054c>", line 2, in inc
    x += 1
UnboundLocalError: local variable 'x' referenced before assignment
复制代码

每个程序都有一个全局作用域,顶行写的代码的作用域就是全局的。有全局自然也有局部,并且局部作用域会随着它层次的加深,会出现多层的局部作用域。

>>> def outer(): # 上级作用域对下级只读可见
...     x = 1
...     print(x)
...     def inner():
...         x = 2 # 赋值即定义,在下级作用域中重新定义了 x
...     inner()
...     print(x)
...
>>> outer()
1
1
复制代码

作用域的特点:

  • 变量的作用域在于定义它的位置。如果定义在全局,那么任何地方都是可见的;
  • 上级作用域对下级只读可见。

作用域的规则是可以打破的。

>>> x = 5
>>> def fun():
...     global x # 通过 global 提升变量作用域为全局变量
...     x += 1
...     return x
...
>>> fun()
Out[69]: 6
复制代码

global 只是将一个变量标记为全局变量,但是并不会定义,因此还需手动定义。

除非你清楚的知道 global 会带来什么,并且明确的知道非 global 不行,否则不要使用 global。

如果不想定义全局变量,但是又要在其他函数中使用某一个函数中的局部变量应该怎么办呢?方法很简单,只要在某个函数中使用 return 返回需要调用函数的值,然后在函数外再使用变量接收即可。

>>> def fun():
...     x = {'a': 1, 'b': 2}
...     return x
...
>>> x = fun()
>>> x
Out[72]: {'a': 1, 'b': 2}
复制代码

闭包

Python 中的最小作用域是函数,也就是说函数内的变量从外部无法访问到,但是子函数却可以访问到父函数的变量,这就是闭包。

global 的缺陷是:

>>> def outer():
...     y = 1
...     def inner():
...         global y
...         y += 1
...         return y
...     return inner
...
>>> y = 1
>>> f()
Out[83]: 2
>>> y = 100 # 某个位置定义了 y
>>> f() # 直接就影响了这个函数
Out[85]: 101
复制代码

inner 是无法修改 outer 中的 y 的,因为 inner 中对 y 重新进行了赋值。反过来说,如果我们不对 y 重新赋值,而是修改它自身,是不是就可以绕过这个规则呢?我们知道列表是可变的,因此可以拿列表进行测试。

>>> def outer():
...     y = [0]
...     def inner():
...         y[0] += 1
...         return y[0]
...     return inner
...
>>> f = outer()
>>> f()
Out[89]: 1
>>> f()
Out[90]: 2
>>> y = 100 # 定义全局变量
>>> f()
Out[91]: 3 # 完全不受影响
复制代码

闭包的概念是:函数已经结束,但是函数内部的部分变量的引用还存在。在上面的例子中,当 outer 函数中执行 return inner 时,这个函数就已经结束了,按理来讲,它里面的变量都会被销毁。但是我们通过其内部的函数 inner 却还是可以访问到里面的变量 y。

python 中的闭包可以通过可变的容器(比如列表)实现,这也是 python2 唯一的方式。而在 python3 中还可以使用 nonlocal 关键字。

>>> def outer():
...     y = 1
...     def inner():
...         nonlocal y
...         y += 1
...         return y
...     return inner
...
>>> f = outer()
>>> f()
Out[97]: 2
>>> f()
Out[98]: 3
>>> f()
Out[99]: 4
复制代码

nonlocal 关键字用于标记一个变量由它的上级作用域定义,通过 nonlocal 标记的变量,可读可写。而如果上级作用域没有定义此变量的话,会抛出语法错误。

默认参数作用域

python 中一切皆对象,函数也是对象,参数是函数对象的属性,所以函数参数的作用域伴随函数整个生命周期。也就是说只要函数一直存在,那么其内部的参数对其都是一直可见的。

对于定义在全局作用域里面的函数,销毁的时机为:

  • 重新定义
  • del 删除
  • 程序结束退出

局部作用域为:

  • 重新定义
  • del
  • 上级作用域被销毁

看下面的代码:

>>> def fn(x=[]):
...     x.append(1)
...     return x
...
>>> fn()
Out[101]: [1]
>>> fn()
Out[102]: [1, 1]
>>> fn()
Out[103]: [1, 1, 1]
复制代码

每执行一次都会往这个列表中添加一个元素。

函数中的默认参数保存在其内部的 __default__ 方法中:

>>> fn.__defaults__
Out[104]: ([1, 1, 1],)
>>> fn()
Out[105]: [1, 1, 1, 1]
>>> fn.__defaults__
Out[106]: ([1, 1, 1, 1],)
复制代码

在上面的例子中,默认参数是可变对象列表,那么换成非可变对象会出现这种情况吗?

>>> def fn(x=1, y=1):
...     x += 1
...     y += 1
...
>>> fn.__defaults__
Out[112]: (1, 1)
>>> fn()
>>> fn.__defaults__
Out[114]: (1, 1)
复制代码

可以看出可变对象和非可变对象的结果是不同的。为什么会出现这种情况呢?还是那就话,赋值即定义。列表直接修改自身,并没有赋值操作。

因此,当可变类型作用函数默认参数时,需要特别注意。

为了避免这种情况的发生,我们可以:

  • 不使用可变类型作为函数的默认参数;
  • 在函数中不对其做修改。

不使用列表作为参数可以这么干:

>>> def fn(lst=None):
...     if lst is None:
...         lst = []
...     # else: # 这种方式更灵活,想修改就修改,想不修改就不修改
...     #     lst = lst[:]
...     lst.append(1) # 如果传入的参数不是 None,还是会对传入的参数进行修改,因此可以加上上面的 else
...     return lst
...
>>> fn()
Out[123]: [1]
>>> fn()
Out[124]: [1]
>>> fn.__defaults__
Out[125]: (None,)
复制代码

函数内不修改可以这么干:

>>> def fn(lst=[]):
...     lst = lst[:] # 注意这是影子拷贝
...     lst.append(1) # 无论如何都不会对传入参数做修改
...     return lst
...
>>> fn()
Out[128]: [1]
>>> fn()
Out[129]: [1]
>>> fn.__defaults__
Out[130]: ([],)
复制代码

通常如果使用一个可变类型做为函数的默认参数,会使用 None 来代替。

函数执行流程

程序会在 CPU 上执行,并且从上到下。刚开始时程序的主流程首先在 CPU 上执行,遇到函数之后,主流程会从 CPU 上下来,让函数上去。此时主流程的状态,包括变量、执行到哪段代码等这样的信息会保存在栈中。当函数执行完之后,这个函数会被销毁,然后主流程又被调度到 CPU 之上,并且从栈中恢复它的状态。

当调用函数时,解释器会把当前现场压栈,然后执行被调函数执行完成,解释器弹出当前栈顶,恢复现场。

定义函数帮助文档

写在函数名下面的第一行的字符串就是帮助信息,通过 help 这个函数可以获得。

def fn():
    '''this is fn'''

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

fn()
    this is fn
(END)
复制代码

还可以通过函数本身的 __doc__ 方法:

>>> fn.__doc__
Out[3]: 'this is fn'
复制代码

匿名函数

lambda 函数也叫做匿名函数,也就是没有名字的函数。它是一种快速定义单行的最小函数,可以用在任何需要函数的地方。它与 def 不同的是,def 定义的是语句,而 lambda 却是表达式,它可以出现在任意表达式可以出现的地方。

语法为:

lambda [args]: expression
复制代码
  • args:以逗号分隔的参数列表,参数可省略;
  • expression:定义返回值。

最简单的使用方法:

>>> lambda x, y: x + y
Out[135]: <function __main__.<lambda>>
复制代码

调用它:

>>> (lambda x, y: x + y)(3, 4)
Out[134]: 7
复制代码

第一对括号用来改变优先级,第二对括号表示函数调用。由于我们不确定函数的定义和执行的哪个优先级更高,因此使用第一个括号来提升它的优先级。

常规的调用方式:

>>> f = lambda x, y: x + y
>>> f(3, 4)
Out[137]: 7
复制代码

匿名函数的特点:

  • 使用 lambda 定义;
  • 参数列表不需要小括号;
  • 冒号不是用来开启新的语句块;
  • 普通函数支持的参数的变化,匿名函数都支持;
  • 没有 return,最后一个表达式的值即返回值。

lambda 语句定义的代码必须是合法的表达式,只能写在一行。不能出现多条件语句(可使用 if 的三元表达式)和其他非表达式语句,如 for 和 while 等。lambda 的首要用途是指定短小的回调函数,它将返回一个函数而不是将函数赋值给某变量名。

lambda 也支持默认参数:

>>> (lambda x, y=4: x + y)(3)
Out[138]: 7
复制代码

可变参数也支持:

>>> (lambda *args, **kwargs: print(args, kwargs))(*range(3), **{str(x): x for x in range(3)})
(0, 1, 2) {'0': 0, '1': 1, '2': 2}
复制代码

keywork-only 同样支持:

>>> (lambda *, x: x)(x=5)
Out[141]: 5
复制代码

可以看到普通函数能够支持的各种参数,匿名函数都支持。

匿名函数多用于高阶函数。

>>> User = namedtuple('Uers', ['name', 'age'])
>>> users = [User('tom', 18), User('jerry', 15), User('sam', 44)]
>>> sorted(users, key=lambda x: x.age) # 按年龄排序
Out[165]:
[Uers(name='jerry', age=15),
 Uers(name='tom', age=18),
 Uers(name='sam', age=44)]
复制代码

lambda 可以起到函数速写的作用,一些简单函数,又不想定义一个函数时,可以使用 lambda。

>>> reduce(lambda x,y:x+y,range(1,101))
5050
复制代码

求阶乘:

>>> reduce(lambda x,y:x*y,range(1,6))
120
复制代码

lambda 因为是表达式,因为可用于列表中:

>>> l1 = [(lambda x:x*2),(lambda y:y*3)]

# 通过for循环为其赋值:
>>> for i in l1: # 它会把这个lambda函数,也就是每一个元素赋值给i
...   print i(3)
...
6
9
复制代码

lambda 可以用在 map 函数中:

>>> a=range(10)
>>> a
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> map(lambda x:x**2,a) # 把a传递给x
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
复制代码

来个复杂点的:

>>> b=range(10)
>>> map(lambda x,y:x**y,a,b)
[1, 1, 4, 27, 256, 3125, 46656, 823543, 16777216, 387420489] # 最后的结果为9的9次方
复制代码

高阶函数

返回函数或者参数是函数的函数就是高阶函数。

高阶函数有什么用呢?比如定义一个排序函数:

def sort(it, r=False):
    ret = []
    def cmp(a, b):
        if r:
            return a < b
        else:
            return a > b

    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
复制代码

这个排序函数用到了一个内部的函数 cmp,用来控制是顺序排还是逆序排。但是我们可以将 cmp 函数拿出来,由用户定义。

def cmp(a, b):
    return a > b # 只要改成小于就是逆序

def sort(it, cmp):
    ret = []
    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
复制代码

还可以继续完善:

def sort(it, cmp=lambda a, b: a<b): # 给函数定义一个默认值
    ret = []
    for x in it:
        for i, e in enumerate(ret):
            if cmp(x, e):
                ret.insert(i, x)
                break
        else:
            ret.append(x)
    return ret
复制代码

以上是函数作为参数的场景,它同样用于大多数逻辑固定,少部分逻辑不固定的场景。而函数作为返回值通常用于闭包的场景,需要封装一些变量。由于类可以封装,因此使用函数作为返回值的场景并不多。

函数作为参数,返回值也是函数:同样用于作为参数函数执行前后需要一些额外操作,最经典的应用就是装饰器。

比如:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

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

>>> f = logger(add)
>>> f.__name__
Out[25]: 'wrap'
>>> f(3, 5)
call add took 0:00:00.000041
Out[26]: 8
复制代码

logger 函数就是用来对执行 add 这个函数之后和之后做一些操作。执行 add 之前记录当前时间,执行之后记录当前时间,然后可以计算执行 add 花了多少时间。

上面的两个函数可以写成这样:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

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

print(add(3, 5))
复制代码

其实意义和上面是一样的,这也就是装饰器的语法。从上面我们可以得出,通过 @函数名 装饰一个函数,就是将这个函数作为参数传递到装饰器中。

装饰器

前面已经提到了,装饰器就是通过一个函数让在执行另一个函数之前和之后做一些额外的操作。作为装饰器的函数本身是一个高阶函数。

参数是一个函数,返回值是一个函数的函数,就可以作为装饰器。

装饰器很好的体现了 AOP 的编程思想,它针对一类问题做处理而与具体的业务逻辑无关。常见的使用场景有:

  • 监控
  • 缓存
  • 路由
  • 权限
  • 审计

装饰器会有一个问题,就拿前面定义的装饰器来举例:

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

@logger
def add(x, y):
    return x + y
复制代码

此时执行 add.__name__,返回的并不是 add 这个函数的名称:

>>> add.__name__
Out[8]: 'wrap'
复制代码

多数时候并没有什么影响,但是在依赖函数名的场景中肯定会出现问题。其实解决起来也很简单。

import datetime
def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    wrap.__name__ = fn.__name__ # 重新赋值即可
    wrap.__doc__ = fn.__doc__ # 顺便也改下 doc 属性
    return wrap

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

>>> add.__name__ # 解决
Out[10]: 'add'
复制代码

还可以将赋值语句抽出来定义成一个函数:

import datetime

# 最开始是这种函数
# def copy_property(src, dst):
#     dst.__name__ = src.__name__
#     dst.__doc__ = src.__doc__

# 后来换成了这个函数
def copy_property(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
    return _copy

def logger(fn):
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    copy_property(fn)(wrap)
    return wrap

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

print(add.__name__)
复制代码

之所以换成下面的函数是为了柯里化,也许这样看不明白,因为确实没有高明到哪里去,那么接着往下看:

import datetime

def copy_property(src):
    def _copy(dst):
        dst.__name__ = src.__name__
        dst.__doc__ = src.__doc__
        return dst # 加一行,_copy 就变成装饰器了,并且是带参数的
    return _copy

def logger(fn):
    @copy_property(fn) # 通过装饰器修改
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

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

print(add.__name__)
复制代码

而 copy_property 这个函数的功能完全可以由 functools.wraps 进行代替,它的作用就是将 wrap 的一些属性改成 fn 的,至于修改哪些属性通过 help 可以看到:

import datetime
import functools

def logger(fn):
    @functools.wraps(fn) # 直接替换
    def wrap(*args, **kwargs):
        start = datetime.datetime.now()
        ret = fn(*args, **kwargs)
        end = datetime.datetime.now()
        print('call {} took {}'.format(fn.__name__, end-start))
        return ret
    return wrap

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

print(add.__name__)
复制代码

带参数

一个函数,返回一个不带参数的装饰器,那么这个函数就是带参数的装饰器。因为作为装饰器的函数只能直接一个函数作为参数,而不能接受其他参数了。因此只能在装饰器外面再包一层函数,通过这个这个函数传递参数的方式将参数传递到装饰器内部。

下面定义一个函数,这个函数接受一个参数,然后返回一个装饰器。这个装饰器用来计算函数执行的时间,超过多少秒后输出信息。

import time
import datetime
import functools

def logger(s):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                print('call {} took {}'.format(fn.__name__, end-start))
            return ret
        return wrap
    return _logger

@logger(2)
def sleep(x):
    time.sleep(x)

sleep(1)
复制代码

为了帮助理解,上面的代码如果不用装饰器是这么执行的:

import time
import datetime
import functools

def logger(s):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                print('call {} took {}'.format(fn.__name__, end-start))
            return ret
        return wrap
    return _logger

# @logger(2)
def sleep(x):
    time.sleep(x)

_logger = logger(2)
sleep = _logger(sleep)
sleep(3)
复制代码

甚至可以给它传递参数,让其成为高阶函数:

import time
import datetime
import functools

def logger(s, p=lambda name, t: print('call {} took {}'.format(name, t))):
    def _logger(fn):
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = fn(*args, **kwargs)
            end = datetime.datetime.now()
            if (end - start).total_seconds() > s:
                p(fn.__name__, end-start)
            return ret
        return wrap
    return _logger

@logger(2)
def sleep(x):
    time.sleep(x)

sleep(1)
复制代码

这样一来,我们不仅可以监控函数(比如慢查询)的执行时间,还可以传递一个告警的函数给它,一旦函数执行时间超过我们定义的阀值就发送告警。

三层

带参数的装饰器是两层,装饰器外面包装了一层,如果再包一层呢?

import time
import datetime
import functools

def logger(s):
    def _logger(p=lambda name, t: print('call {} took {}'.format(name, t))):
        def __logger(fn):
            @functools.wraps(fn)
            def wrap(*args, **kwargs):
                start = datetime.datetime.now()
                ret = fn(*args, **kwargs)
                end = datetime.datetime.now()
                if (end - start).total_seconds() > s:
                    p(fn.__name__, end-start)
                return ret
            return wrap
        return __logger
    return _logger()

@logger(2)()
def sleep(x):
    time.sleep(x)

# 直接报错,不支持这么干
File "<ipython-input-23-c89f301d8b2d>", line 18
    @logger(2)()
              ^
SyntaxError: invalid syntax
复制代码

但是如果非要这么搞也是可以的,如:

f = logger(2) # 先定义一个变量即可,然后通过这个变量进行装饰
@f
def sleep(x):
    time.sleep(x)

sleep(1)
复制代码

多个装饰器装饰同一个函数

一个函数可以应用多个装饰器,第一个装饰器用来增强第二个装饰器中函数的功能,而第二个装饰器用来增强原函数的功能。

def outer0(fun):
    # 用来增强outer1中wrapper函数的功能
    def wrapper(*args, **kwargs):
        print('start')
        ret = fun(*args, **kwargs)
        return ret
    return wrapper

def outer1(fun):
    # 用来增强原函数的功能
    def wrapper(*args, **kwargs):
        print('123')
        ret = fun(*args, **kwargs)
        return ret
    return wrapper

@outer0
@outer1
def Func1(a1, a2):
    print('func1')

Func1(1, 2)
复制代码

想要理解两个装饰器的运行原理,就要将它先展开。

如上图所示,执行 Func1 这个函数时,会执行 outer0 中的 wrapper 函数。执行到这个函数中的 fun 函数时,就相当于执行 outer1 中的 wrapper 函数。执行到这个函数中的 fun 函数时,就相当于执行 Func1 这个函数。Func1 这个函数执行完毕后,将返回值 None 返回给了 outer1 中的 ret,紧接着又将 ret 返回给 outer0 中的 wrapper 函数,最终将 None 返回,也就得到了最终的结果 None。

递归

递归就是函数内部自己调用自己,通常用它计算阶乘。递归函数要给一个退出条件,不然就会陷入无止尽的递归中。递归需要边界条件、递归前进和递归返回段。

为了保护解释器,python 对递归的最大深度有限制,这个深度可以在 python 中看到:

>>> import sys
>>> sys.getrecursionlimit()
Out[132]: 2000
复制代码

通过下面的方法修改其上限:

sys.setrecursionlimit()
复制代码

python 递归很慢,特别是调用递归函数过多的时候。Python 需要为每次递归维护一个内部状态,因此,要尽量避免使用递归。

直接定义成递归,也就是函数内自己调用自己,python 自身可以检测出来它是递归,会对其深度进行限制。但是下面这样的语句其实就是递归,但是 python 却检测不出来,遇到这样的情况,解释器会当场死。

def f():
    g()

def g():
    k()

def k()
    f()
复制代码

将它们写在一起当然很容易发现,但是当这些函数分别在不同的文件中时,就不好定位了。特别是当其中一个函数在一个很小的功能中,平时都不会用上,但是一旦用上就会触发无限的重复调用,一执行就死。所以会出现这样的情况:程序跑了几个月没事,但是突然程序就挂了,然后启动之后好好的,过了一段时间又挂了。不光是 python,其他语言都会遇到这样的问题,由于这种情况下,一瞬间程序就挂了,服务器的负载什么的都是正常状态,因此这种问题排查起来非常麻烦。所以写程序一定要小心,要避免这样的情况发生。

阶乘的实现:

def fact(n):
  if n <= 1: return 1
  else: return n*fact(n-1)
复制代码

return返回一个返回值,这个返回值包含了对自己函数本身的调用。通过 n-1 的方式给出递归的退出条件。假如 n 为 3,那么:

fact(3)
  return: 3*fact(2)
    return: 2*fact(1)
      return: 1
复制代码

结果就是 321,也就是 6。它会先将所有生成的结果保存在内存中,等到最终返回1时,再从下到上进行相乘。这就是递归,一个函数内部包含了对自己的调用。

返回斐波那契数列的第 10 个数:

def f1(a1, a2, n):
    if n == 10:
        return a1
    else:
        a3 = a1 + a2
        # 一定要使用一个变量接收f1这个函数的返回值,这样才能把结果传递给上一个调用的函数,直到把值给第一个函数。
        a = f1(a2, a3, n+1)
        # 传递的参数变成了a2, a3了
        return a

print(f1(0, 1, 1))
复制代码

这个函数肯定会不断的递归嵌套,当 n=10,if 条件满足时,会返回值为 34 的 a1。这个 34 正好被前一个函数,也就是 n=9 时的函数中的 a 接收,a 也就获得 34 这个值,然后执行 return 语句,将 34 返回给前面一个函数,也就是 n=8 时的函数中的 a。以此类推,最终回到第一个函数中的 a,它获得了前面函数返回而来的 34,于是返回最终结果 34。

也就是说递归会不断嵌套,不断往里一层套一层。但是当给它一个某个时间退出的条件时,它就会不断往外一层一层的解开。因此一个要有一个参数来接收后一个函数的返回值。下面就是一个错误的案例,没有接收前一个函数的返回值:

def f1(a1, a2, n):
    if n == 10:
        return a1
    else:
        a3 = a1 + a2
        # 下面并没有接收
        f1(a2, a3, n+1)
        return a1

print(f1(0, 1, 1))
复制代码

虽然返回了 a1 也就是 34,但是前一个函数并没有接收这个值(只是执行函数),因此最终结果是最初传递给它的 0。

类型提示

Python 是动态类型的语言。也就是说一个变量的类型,是在运行时决定的,而一个变量的类型在应用的生命周期中是可变的。

当我们一定一个函数时,别人并不知道要给它传递的参数应该是什么类型,通常常用的用法是在文档中写明。比如:

def add(x, y):
    '''
    :param x: int
    :param y: int
    :return: int
    '''
    return x + y
复制代码

问题在于并不是所有人都会写文档,并且文档并不一定会随着代码一起更新。还有就是文档是自然语言,不方便机器操作。因此,Python3 开始支持类型提示,也就是有那么一个标准让我们知道参数以及返回值类型是啥。

def add(x: int, y: int) -> int:
    return x + y
复制代码

需要注意的是,它只是一个注解,Python 并不会做任何的检查。它用来提供给第三方工具(如 IDE,静态分析工具),或者在运行时获取信息。

Python 将这个信息保存在这里:

>>> add.__annotations__
Out[25]: {'return': int, 'x': int, 'y': int}
复制代码

3.5 和 之前的版本,类型提示只能用在函数的参数和返回值上。3.6 中可以这么用:

i: int = 1
复制代码

为了支持类型注解,Python 还提供了 typing 库。下面表示列表中的元素是整型。

import typing

def add(lst: typing.List[int]) -> int:
    ret = 0
    for i in lst:
        ret += i
    return ret

>>> add.__annotations__
Out[28]: {'lst': typing.List[int], 'return': int}
复制代码

以下是一个示例,用于强制输入的类型和定义的类型相符合,不然抛出异常:

import inspect
import functools

def typed(fn):
    @functools.wraps(fn)
    # 这里要判断关键字传参和非关键字传参
    def wrap(*args, **kwargs):
        # 通过 inspect 模块获取参数指定的类型
        params = inspect.signature(fn).parameters
        # 判断关键字传参
        for k, v in kwargs.items():
            if not isinstance(v, params[k].annotation):
                raise TypeError('parameter {} required {}, but {}'.format(k, params[k], type(v)))
        # 非关键字传参
        for i, arg in enumerate(args):
            param = list(params.values())[i]
            if not isinstance(arg, param.annotation):
                raise TypeError('parameter {} required {}, but {}'.format(param.name, param.annotation, type(arg)))
        return fn(*args, **kwargs)
    return wrap

@typed
# 必须要给参数指定类型,不然判断不了
def add(x: int, y: int) -> int:
    return x + y
复制代码

练习

扁平化字典(递归法):

def flatten(d):
    def _flatten(src, dst, prefix=''):
        for k, v in src.items():
            key = k if prefix is '' else '{}.{}'.format(prefix, k)
            if isinstance(v, dict):
                _flatten(v, dst, key)
            else:
                dst[key] = v
    result = {}
    _flatten(d, result)
    return result

>>> flatten({'a': 2, 'b': {'c': 4, 'g': 9}, 'd': {'e': {'f': 6}}})
Out[30]: {'a': 2, 'b.c': 4, 'b.g': 9, 'd.e.f': 6}
复制代码

虽然递归效率低,但是当字典深度不大时,使用递归更方便,只不过看起来有些难懂。

实现base64编码

base64 编码实现了将二进制转换成字符串,它的特点:

  • 有一个 table:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
  • 输入按三字节(24位)分组,不足三字节补 0;
  • 按 6 位分组,转化为整数
  • 整数作为 table 的索引
  • 补 0 的字节用 = 表示
def b64encode(data: bytes) -> str:
    table = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    encoded = bytearray()
    c = 0
    for x in range(3, len(data)+1, 3):
        i = int.from_bytes(data[c: x], 'big')
        for j in range(1, 5):
            encoded.append(table[i >> (24 - j*6) & 0x3f])
        c += 3
    r = len(data) - c
    if r > 0:
        i = int.from_bytes(data[c:], 'big') << (3-r) * 8
        for j in range(1, 5-(3-r)):
            encoded.append(table[i >> (24 - j*6) & 0x3f])
        for _ in range(3-r):
            encoded.append(int.from_bytes(b'=', 'big'))
    return encoded.decode()

print(b64encode(b'abcdefg'))
复制代码

解码:

def b64decode(data: str) -> bytes:
    table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
    decoded = bytearray()
    s = 0
    for e in range(4, len(data)+1, 4):
        tmp = 0
        for i, c in enumerate(data[s:e]):
            if c != '=':
                tmp += table.index(c) << 24 - (i+1) * 6
            else:
                tmp += 0 << 24 - (i+1) * 6
        decoded.extend(tmp.to_bytes(3, 'big'))
        s += 4
    return  bytes(decoded.rstrip(b'\x00'))
复制代码

缓存装饰器

类似于 functools.lru_cache,但是能设置超时。

难点在于:

  1. 要将 *args, **kwargs 作为 key,但是列表和字典很明显不能作为字典的 key 的,因此要对列表和字典进行解析,实现最终的 key = 'args=1&args=2&args=3&kwargs=99' 这样类似的格式。
  2. 由于没有换出策略,因此只要访问时判断如果当前的时间戳减去当时的时间戳大于超时时间就直接返回结果。
import time
import inspect
import functools
from datetime import datetime

def cache(expire=0):
    def _cache(fn):
        # 将它定义在内部针对每一个函数生效,因此函数名没有必要保存
        cache_dic = {}
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            lst = []
            s = set()
            # 这个会保存所有函数的参数,包括默认参数
            param = inspect.signature(fn).parameters
            if 'args' in param.keys():
                for arg in args:
                    lst.append(('args', arg))
                    s.add('args')
            else:
                for i, arg in enumerate(args):
                    para = list(param.keys())[i]
                    name = (para, arg)
                    lst.append(name)
                    s.add(para)
            lst.extend(list(kwargs.items()))
            s.update(kwargs.keys())
            if 'kwargs' not in param.keys():
                for k, v in param.items():
                    if k not in s:
                        lst.append((k, v.default))
                        s.add(k)
            lst.sort(key=lambda x: x[0])
            # 最终的 key 就是这个样子
            key = '&'.join(['{}={}'.format(k, v) for k, v in lst])
            now = datetime.now().timestamp()
            if key in cache_dic:
                ret, timestamp = cache_dic[key]
                if expire == 0 or now - timestamp < expire:
                    print("cache hit")
                    return ret
            ret = fn(*args, **kwargs)
            cache_dic[key] = (ret, now)
            print('cache miss')
        return wrap
    return _cache
复制代码

接下来实现设置 lru 换出。

注意事项:

  • 每一个 key 对应的值有三个:函数执行结果、用于比较过期的时间、用于记录最近一次访问的时间。
  • 如果超时时间没到,或者没设超时代码没什么变化。
  • 当缓存条数小于最大上限之前,不做检查。
  • 当缓存条数大于等于上限时,先做超时的换出,方法为遍历整个字典。注意,遍历的过程中字典可能会被修改,因此最好使用 list(cache_dic.items()),这样遍历的就不是原字典了。
  • 如果超时换出之后缓存条数依然大于等于上限,就要对字典的用于记录最近一次访问的时间进行排序,然后取字典的第一项进行删除。

上面的换出算法很慢,效率很低。我们可以通过队列完成,所有的 key 都放在队列中(也不用保存最后一个时间了)。每次命中之后,就将 key 放在队列首部,因此换出的时候只需删除队列尾部即可。这个队列可以通过列表来实现,列表的 insert 到第 0 的位置,虽然对导致其后所有的元素都往后移一位,但是性能不算太差。最大的问题在于列表的 remove,因为 remove 要遍历字典,时间复杂度为 O(n)。

因此最好的方式是写一个双向链表,插入的时候放在 head,删除的时候删掉 tail。为了解决 remove 时要遍历整个链表的方式找到对应的项,因为将数据都存到链表中,并在字典中存一个对应链表数据的引用,通过这个引用就能保存查找的速度为 O(1)。

from collections import namedtuple

Item = namedtuple('Item', ['key', 'value', 'timestamp'])

def linked_list():
    _head = None
    _tail = None

    def put(item):
        nonlocal _head
        nonlocal _tail
        if _head is None:
            _head = {'data': item, 'prev': None, 'next': None}
        else:
            node = {'data': item, 'prev': None, 'next': _head}
            _head['prev'] = node
            _head = node
        if _tail is None:
            _tail = _head
        print(id(_head), id(_tail))
        return _head

    def pop():
        nonlocal _tail
        nonlocal _head
        if _tail is None:
            _head = None
            return None
        node = _tail
        _tail = node['prev']
        return node

    def remove(node):
        nonlocal _head
        nonlocal _tail
        if node is _head:
            _head = node['next']
        if node is _tail:
            pop()
            return
        node['prev']['next'] = node['next']
        node['next']['prev'] = node['prev']

    return put, pop, remove

put, pop, remove = linked_list()


import inspect
import datetime
import functools

def cache(maxsize=128, expire=0):
    def make_key(fn, args, kwargs):
        ret = []
        names = set()
        params = inspect.signature(fn).parameters
        keys = list(params.keys())
        for i, arg in enumerate(args):
            ret.append((keys[i], arg))
            names.add(keys[i])
        ret.extend(kwargs.items())
        names.update(kwargs.items())
        for k, v in params.items():
            if k not in names:
                ret.append((k, v.default))
        ret.sort(key=lambda x: x[0])
        return '&'.join(['{}={}'.format(name, arg) for name, arg in ret])

    def _cache(fn):
        data = {}
        put, pop, remove = linked_list()
        @functools.wraps(fn)
        def wrap(*args, **kwargs):
            key = make_key(fn, args, kwargs)
            now = datetime.datetime.now().timestamp()
            if key in data.keys():
                node = data[key]
                item = node['data']
                remove(node)
                if expire == 0 or now - item.timestamp >= expire:
                    data[key] = put(item)
                    return item
                else:
                    data.pop(key)
            value = fn(*args, **kwargs)
            if len(data) >= maxsize:
                if expire != 0:
                    expires = set()
                    for k, node in data.items():
                        if now - node['data'].timestamp >= expire:
                            pop(node)
                            expires.add(k)
                    for k in expires:
                        data.pop(k)
            if len(data) >= maxsize:
                node = pop()
                data.pop(node['data'].key)
            node = put(Item(key, value, now))
            data[key] = node
            return value
        return wrap
    return _cache
复制代码

在通过 put 方法将数据加入到链表中之后,头结点就是当前插入的内容,因此将这个头结点保存在缓存字典中,下次就可以通过这个节点能以 O(1) 的速度在链表中找到并删除。

命令分发器

通过输入对应的命令来执行相应的函数。

def command():
    commands = {}

    def register(command):
        def _register(fn):
            if command in commands:
                raise Exception('command {} exist'.format(command))
            commands[command] = fn
            return fn
        return _register

    def default_fn():
        print('unknown command')

    def run():
        while True:
            cmd = input('>> ')
            if cmd.strip() == 'quit':
                return
            commands.get(cmd.strip(), default_fn)()

    return register, run

register, run = command()

@register('abc')
def papa():
    print('papa')

run()
复制代码

也就是说在函数上使用装饰器,那么这个函数就和传递给装饰器的参数建立了联系。那么下次输入对应的参数就能执行装饰的函数。

下面是带参数的版本。所谓的带参数就是用户输入的时候是可以带参数的:

import inspect
from collections import namedtuple


def dispatcher(default_handler=None):
    Handler = namedtuple('Handler', ['fn', 'params'])
    # 这个列表保存了被装饰的函数的函数名和参数列表,也就是上面的 Handler
    commands = {}

    if default_handler is None:
        default_handler = lambda *args, **kwargs: print('not found')

    def register(command):
        def _register(fn):
            # 这是由参数组成的对象的字典
            params = inspect.signature(fn).parameters
            # 将函数名和参数保存在字典中
            commands[command] = Handler(fn, params)
            return fn
        return _register

    def run():
        while True:
            command, _, params = input('>> ').partition(':')
            # 假如用户输入add:x,y,z=1
            if command.strip() == 'quit':
                return
            handler = commands.get(command.strip(), Handler(default_handler, {}))
            # 接下来解析用户输入的参数,包括使用冒号分隔的函数名和参数列表
            args = []
            kwargs = {}
            param_values = list(handler.params.values())
            for i, param in enumerate(params.split(',')):
                if '=' in param:
                    name, _, value = param.partition('=')
                    # 获取被装饰函数的参数列表中参数注解或者默认参数的值
                    # 比如默认参数的 <Parameter "y='abc'">
                    p = handler.params.get(name.strip())
                    # 事实上如果是默认参数,可以将默认参数的值通过default取出来并判断它的类型
                    if p is not None and p.annotation != inspect.Parameter.empty:
                        kwargs[name.strip()] = p.annotation(value)
                    else:
                        kwargs[name.strip()] = value
                else:
                    if len(param_values) > i and param_values[i].annotation != inspect.Parameter.empty:
                        args.append(param_values[i].annotation(param.strip()))
                    else:
                        args.append(param.strip())
            ret = handler.fn(*args, **kwargs)
            if ret is not None:
                print(ret)
    return register, run

reg, run = dispatcher()
@reg('abc')
def abc(x: int, y: int):
    print(x+y)

run()
复制代码

参数需要进行处理,因为如果函数使用的 int 类型,但是 input 输入的类型却是字符串。因此为了区分参数的类型,我们需要用到类型注解,通过这个进行判断。

当参数是列表或者字典时,这个脚本无法胜任。

解析httpd日志

import datetime
from collections import namedtuple

line = '66.256.46.124 - - [10/Aug/2016:06:05:06 +0800] "GET /robots.txt HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible: Googlebot/2.1: +http://www.google.com/bot.html)"'

Request = namedtuple('Request', ['method', 'url', 'version'])
MapItem = namedtuple('MapItem', ['name', 'convert'])
mapping = [
    MapItem('remote', lambda x: x),
    MapItem('', None),
    MapItem('', None),
    MapItem('time', lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')),
    MapItem('request', lambda x: Request(*x.split())),
    MapItem('status', int),
    MapItem('length', int),
    MapItem('', None),
    MapItem('ua', lambda x: x)
]

def strptime(src: str) -> datetime.datetime:
    return datetime.datetime.strptime(src, '%d/%b/%Y:%H:%M:%S %z')

def extract(line):
    tmp = []
    ret = []
    split = True
    for c in line:
        if c == '[':
            split = False
            continue
        if c == ']':
            split = True
            continue
        if c == '"':
            split = not split
            continue
        if c == ' ' and split:
            ret.append(''.join(tmp))
            tmp.clear()
        else:
            tmp.append(c)
    ret.append(''.join(tmp))
    result = {}
    for i, item in enumerate(mapping):
        if item.name:
            result[item.name] = item.convert(ret[i])
    return result

# 从日志文件中加载
def load(path):
    with open(path) as f:
        try:
            yield extract(f.readline())
        except:
            pass
复制代码

正则表达式版:

import re
import datetime
from collections import namedtuple

Request = namedtuple('Request', ['method', 'url', 'version'])
line = '66.256.46.124 - - [10/Aug/2016:06:05:06 +0800] "GET /robots.txt HTTP/1.1" 404 162 "-" "Mozilla/5.0 (compatible: Googlebot/2.1: +http://www.google.com/bot.html)"'

mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

def strptime(src: str) -> datetime.datetime:
    return datetime.datetime.strptime(src, '%d/%b/%Y:%H:%M:%S %z')

def extract(line):
    regexp = r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)'
    m = re.match(regexp, line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

print(extract(line))
复制代码

对于日志或者监控数据这样的时序数据做分析,通常进行时间进行相关的分析,因此需要一个滑动窗口。在时序分析中,滑动窗口是非常重要的,所谓的滑动窗口就是设置一个窗口在一堆以时间排列的数据中截取一段数据,分析完成之后,窗口往前滑动,截取下一段。滑动窗口有两个参数非常重要,一个是 width(窗口的宽度),另一个是 interval(间隔)。意思是每 interval 秒分析前 width 秒的数据,因此 interval 必须要大于等于 width。

整合起来:

import re
import time
import queue
import datetime
import threading
from collections import namedtuple

matcher = re.compile(r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)')
Request = namedtuple('Request', ['method', 'url', 'version'])
mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

def extract(line):
    m = matcher.match(line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

def read(f):
    for _ in f:
        try:
            yield extract(f.readline())
        except:
            pass

def load(path):
    with open(path) as f:
        while True:
            read(f)
            time.sleep(0.1)

def window(source, handler, interval: int, width: int):
    store = []
    start = datetime.datetime.now()
    while True:
        data = next(source)
        current = datetime.datetime.now()
        if data:
            store.append(data)
            current = data['time']
        if (current - start).total_seconds() >= interval:
            start = current
            handler(store)
            dt = current - datetime.timedelta(seconds=width)
            store = [x for x in store if x['time'] > dt]

def dispatcher(source):
    analyers = []
    queues = []

    def _source(q):
        while True:
            yield q.get()

    def register(handler, interval, width):
        q = queue.Queue()
        queues.append(q)
        t = threading.Thread(target=window, args=(_source(q), handler, interval, width))
        analyers.append(t)

    def start():
        for t in analyers:
            t.start()
        for item in source:
            for q in queues:
                q.put(item)

    return register, start

# 以下是业务逻辑,需要分析什么就写在下面
def null_handler(items):
    pass

def status_handler(items):
    status = {}
    for x in items:
        if x['status'] not in status.keys():
            status[x['status']] = 0
        status[x['status']] += 1
    total = sum(x for x in status.values())
    for k, v in status.items():
        print('{} -> {}%'.format(k, v/total * 100))

if __name__ == '__main__':
    import sys
    register, start = dispatcher(load(sys.argv[1]))
    register(status_handler, 5, 10)
    start()
复制代码

上面的 handler 函数由我们自己定义,它就是要分析的内容,比如访问的响应状态码等等。

import re
import time
import queue
import datetime
import threading
from collections import namedtuple
from watchdog.events import FileSystemEventHandler

matcher = re.compile(r'(?P<remote>\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) - - \[(?P<time>.*)\] "(?P<request>.*)" (?P<status>\d+) (?P<length>\d+) ".*" "(?P<ua>.*)')
Request = namedtuple('Request', ['method', 'url', 'version'])
mapping = {
    'length': int,
    'request': lambda x: Request(*x.split()),
    'status': int,
    'time': lambda x: datetime.datetime.strptime(x, '%d/%b/%Y:%H:%M:%S %z')
}

class Loader(FileSystemEventHandler):
    def __init__(self, path):
        self.path = path
        self.q = queue.Queue()
        self.f = open(path)

    def on_modified(self, event):
        if event.src_path == self.path:
            for item in read(self.f):
                self.q.put(item)

    def source(self):
        while True:
            yield self.q.get()

def extract(line):
    m = matcher.match(line)
    if m:
        ret = m.groupdict()
        return {k: mapping.get(k, lambda x:x)(v) for k, v in ret.items()}
    raise Exception(line)

def read(f):
    for line in f:
        try:
            yield extract(line)
        except:
            pass

def load(path):
    with open(path) as f:
        while True:
            yield from read(f)
            time.sleep(0.1)

def window(source, handler, interval: int, width: int):
    store = []
    start = None
    while True:
        data = next(source)
        store.append(data)
        current = data['time']
        if start is None:
            start = current
        if (current - start).total_seconds() >= interval:
            start = current
            try:
                handler(store)
            except:
                pass
            dt = current - datetime.timedelta(seconds=width)
            store = [x for x in store if x['time'] > dt]

def dispatcher(source):
    analyers = []
    queues = []

    def _source(q):
        while True:
            yield q.get()

    def register(handler, interval, width):
        q = queue.Queue()
        queues.append(q)
        t = threading.Thread(target=window, args=(_source(q), handler, interval, width))
        analyers.append(t)

    def start():
        for t in analyers:
            t.start()
        for item in source:
            for q in queues:
                q.put(item)

    return register, start

# 以下是业务逻辑,需要分析什么就写在下面
def null_handler(items):
    pass

def status_handler(items):
    status = {}
    for x in items:
        if x['status'] not in status.keys():
            status[x['status']] = 0
        status[x['status']] += 1
    total = sum(x for x in status.values())
    for k, v in status.items():
        print('\t{} -> {:.2f}%'.format(k, v/total * 100))

if __name__ == '__main__':
    import os
    import sys
    from watchdog.observers import Observer
    handler = Loader(sys.argv[1])
    observer = Observer()
    observer.schedule(handler, os.path.dirname(sys.argv[1]), recursive=False)
    register, start = dispatcher(handler.source())
    register(null_handler, 5, 10)
    observer.start()
    start()
复制代码
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值