函数是 Python 内建支持的一种封装,我们通过把大段代码拆成函数,通过一层一层的函数调用,就可以把复杂任务分解成简单的任务,这种分解可以称之为面向过程的程序设计。函数就是面向过程程序设计的基本单元。

函数式编程就是一种抽象程度很高的编程范式,纯粹的函数式编程语言编写的函数没有变量,因此,任意一个函数,只要输入是确定的,输出就是确定的,这种纯函数我们称之为没有副作用。而允许使用变量的程序设计语言,由于函数内部的变量状态不确定,同样的输入,可能得到不同的输出。因此,这种函数是有副作用的。

Python 对函数式编程提供部分支持,由于 Python 允许使用变量,因此 Python 不是纯函数式编程语言。

纯函数

纯函数指的是 没有副作用(内存或 I/O)的函数,函数会返回新值,而不会修改原来的值,函数间无共享变量。

  • 非纯函数
>>> a = 1

>>> def func():
...     global a
...     a += 1
...     return a
... 

>>> res = func()

>>> print(res)
2

>>> print(a)      # 全局变量 a 被修改
2
  • 纯函数
>>> a = 1

>>> def func():
...     global a
...     return a + 1
... 

>>> res = func()

>>> print(res)
2

>>> print(a)      # 全局变量 a 未被修改
1

匿名函数

lambda 函数又称为匿名函数,即没有名字的函数。

语法:

lambda argument_list: expression

其中 argument_list 是参数列表,它的结构与 Python 中函数的参数列表是一样的;expression 是一个关于参数的表达式,并且表达式只能是单行的。

lambda 函数特性:

  • lambda 函数是匿名的,即没有名字的函数
  • lambda 函数有输入和输出,输入是传入到参数列表 argument_list 的值,输出是根据表达式 expression 计算后得到的值
  • lambda 函数一般功能简单,单行 expression 决定了 lambda 函数不可能完成复杂的逻辑,只能完成非常简单的功能。

lambda 函数使用示例:

  • lambda 函数赋值给一个变量,通过这个变量间接调用该 lambda 函数
>>> add = lambda x, y: x + y

>>> print(add(1, 2))
3
  • lambda 函数作为其他函数的返回值,返回给调用者

函数的返回值也可以是函数,例如 return x, y: x + y 返回一个加法函数,这时,lambda 函数实际上是定义在某个函数内部的函数,称之为嵌套函数,或者内部函数。

>>> def func():
...     return lambda x, y: x + y
... 

>>> res = func()

>>> print(res)
<function func.<locals>.<lambda> at 0x10454a048>

>>> print(res(1, 2))
3
  • lambda 函数作为参数传递给其他函数
# filter 函数

>>> list(filter(lambda x: x % 3 == 0, range(10)))
[0, 3, 6, 9]

此时,lambda 函数用于指定过滤列表元素的条件。

# sorted 函数

>>> sorted(range(10), key=lambda x: abs(5 - x))
[5, 4, 6, 3, 7, 2, 8, 1, 9, 0]

此时,lambda 函数用于指定对列表中所有元素进行排序的准则,按照列表元素与 5 的距离从小到大进行排序。

# map 函数

>>> list(map(lambda x: x + 1, range(3)))
[1, 2, 3]

此时,lambda 函数用于指定对列表中每一个元素的共同操作。

返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

>>> def func1(x):
...    def func2():
...        return x
...    return func2

# 调用函数 func1 时,返回的是函数 func2 对象:
>>> func1(10)
<function func1.<locals>.func2 at 0x0000000003F8CBF8>

# 需要对返回的函数对象再次调用才能得到结果:
>>> a = func1(10)
>>> a()
10

高阶函数

函数接收的参数是一个函数名、函数的返回值是一个函数名,满足上述条件中的任意一个,都可称之为高阶函数。

  • 变量可以指向函数
>>> abs(-10)    # abs(-10) 是函数调用
10

>>> abs         # abs 是函数本身
<built-in function abs>

# 将函数本身赋值给变量
>>> f = abs
>>> f(-10)
10

结论: 函数本身也可以赋值给变量,即:变量可以指向函数名。

  • 函数名也是变量

函数名其实就是指向函数的变量,对于 abs() 这个函数,完全可以把函数名 abs 看成变量,它指向一个可以计算绝对值的函数。

# 将函数名指向其它对象,并调用

>>> abs = 100

>>> abs(-10)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
TypeError: 'int' object is not callable

此时: abs 这个变量(函数名)已经不指向求绝对值函数而是指向一个整数 100。

  • 函数作为参数
>>> def add(x, y, f):
...    return f(x) + f(y)
...

>>> add(-5, 6, abs)
11
  • 函数名作为返回值
>>> def func1(x):
...    def func2():
...        return x
...    return func2

# 调用函数 func1 时,返回的是函数名 func2
>>> func1(10)
<function func1.<locals>.func2 at 0x0000000003F8CBF8>

# 需要对返回的函数名再次调用才能得到结果
>>> a = func1(10)
>>> a()
10

map/reduce

map(function, iterable):

map 函数接收两个参数,一个是函数,另一个是 Iterable 可迭代对象;
map 将传入的函数 依次作用到序列的每个元素,并把结果作为新的 Iterator 返回。

  • 将 list 中的每个元素平方
>>> def func(x):
...     return x * x
... 

>>> l = [1, 2, 3, 4]
>>> list(map(func, l))
[1, 4, 9, 16]

# 使用 lambda 函数实现
>>> list(map(lambda x: x * x, [1, 2, 3, 4]))
[1, 4, 9, 16]
  • 获取字典 key 值
>>> list(map(str, {'A': 1, 'B': 2, 'C': 3}))
['A', 'B', 'C']

>>> list(map(int, {1: 'A', 2: 'B', 3: 'C'}))
[1, 2, 3]

filter

filter 函数用于 过滤序列,将传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃该元素。

filter(function, iterable)
  • 过滤列表中的所有奇数
>>> def is_odd(n):
...     return n % 2 == 1
... 

>>> list(filter(is_odd, [1, 2, 3, 4, 5, 6]))
[1, 3, 5]

# 使用 lambda 函数
>>> list(filter(lambda x: x % 2 == 1, range(7)))
[1, 3, 5]
  • 过滤 1 ~ 100 以内平方根是整数的数字
>>> import math

>>> list(filter(lambda x: math.sqrt(x) % 1 == 0, range(1, 101)))
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

sorted

sorted 函数对所有可迭代对象进行 排序操作sorted 函数进行排序的数据类型必须一致,如 int 类型和 str 类型无法进行排序操作。

sorted(iterable, key=None, reverse=False):

传入 Iterator 可迭代对象,返回一个新的 Iterator

  • 对列表中的元素进行排序
>>> g = [1, 4, 6, 8, 9, 3, 5]
>>> sorted(g)
[1, 3, 4, 5, 6, 8, 9]
  • 接收一个 key 函数来实现自定义的排序
>>> sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)
['about', 'bob', 'Credit', 'Zoo']

key 指定的函数将作用于 list 的每一个元素上,并根据 key 函数返回的结果进行排序。

  • 自定义函数,按学生的成绩排序
>>> def func(n):
...     return n[1]
... 

>>> sorted([('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)], key=func)
[('Bart', 66), ('Bob', 75), ('Lisa', 88), ('Adam', 92)]
  • 自定义函数,按学生的姓名排序
>>> sorted([('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)], key=lambda x: x[0])
[('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)]

嵌套函数

Python 允许创建嵌套函数,也就是可以在函数内部再定义一个函数,这些函数都遵循各自的作用域和生命周期准则。

>>> def outer():
...     print("outer...")
...     def inner():
...         print("inner...")
...     inner()
... 

>>> outer()
outer...
inner...

嵌套函数中的局部变量:

>>> def outer(name):
...     print("from the outer")
...     def inner():
...         print("from the inner")
...     print(locals())
... 

>>> outer("Python")
from the outer
{'inner': <function outer.<locals>.inner at 0x10454c048>, 'name': 'Python'}

嵌套函数的返回值与调用过程:

  • 例1: 显示调用内函数
>>> def foo():
...     def bar():
...         print("bar() called.")
...     print("foo() called.")
...     bar()
... 

>>> foo()
foo() called.
bar() called.
  • 例2: 返回内函数的调用
>>> def foo():
...     def bar():
...         print("bar() called.")
...     print("foo() called.")
...     return bar()
... 

>>> foo()
foo() called.
bar() called.
  • 例3: 返回内函数的引用(函数名)
>>> def foo():
...     def bar():
...         print("bar() called.")
...     print("foo() called.")
...     return bar
... 

>>> f = foo()
foo() called.

>>> f()
bar() called.

闭包

在一个外函数中嵌套定义了另一个函数(内函数), 内函数引用了外函数中的变量,并且外函数的返回值是内函数的函数名。

一般情况下,如果一个函数结束,函数内部所有东西都会释放掉还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内函数中用到,就会把这个临时变量绑定给内函数,然后再自己结束。

闭包函数一定有 __closure__ 方法。

一个简单的闭包:

>>> def outer(x):           # 外函数,x 和 y 为外函数的临时变量
...     y = 10
...     def inner():        # 内函数
...         print(x + y)
...     return inner        # 外函数返回内函数的函数名
... 

>>> demo = outer(5)         # 变量 demo 指向内函数
>>> demo()
15

>>> demo2 = outer(7)
>>> demo2()
17

满足闭包的条件

  • 必须要有嵌套函数
  • 内函数必须引用外函数中的变量
  • 外函数必须返回内函数的函数名

__closure__ 属性

>>> def funx():
...     x=5
...     def funy():
...         nonlocal x   # 如果要修改外部变量,需要用 nonlocal 
...         x += 1
...         return x
...     return funy
...
  • funy 就是闭包
  • funy 内部引用的变量 x 即闭包变量
>>> a = funx()
>>> a.__closure__
(<cell at 0x00000000037A2F18: int object at 0x000000005CF86E60>,)

>>> a.__closure__[0].cell_contents
5

>>> print(funx.__closure__)
None
  • 闭包函数有非空的 __closure__ 属性,而普通函数的 __closure__ 属性是 None
  • __closure__ 用于存储闭包变量的值

注意事项

  • 闭包中不能修改外部作用域(外函数)中的局部变量
>>> def foo():
...     a = 1
...     def bar():
...         a += 1
...         return a
...     return bar
...

>>> c = foo()

>>> c()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 4, in bar
UnboundLocalError: local variable 'a' referenced before assignment
  • Python 循环中不包含域的概念
>>> for i in range(3):
...     print(i)
... 
0
1
2

在程序里面经常会出现这类的循环语句,Python 的问题就在于,当循环结束以后,循环体中的临时变量 i 不会销毁,而是继续存在于执行环境中。还有一个 Python 的现象是,Python 的函数只有在执行时,才会去找函数体里的变量的值。

>>> flist = []

>>> for i in range(3):
...     def func(x):
...         return x * i
...     flist.append(func)
... 

>>> print(flist)
[<function func at 0x10454c158>, <function func at 0x10454c1e0>, <function func at 0x10454c268>]

>>> print(flist[0](2))
4
>>> flist = []

>>> for i in range(3):
...     def func(x):
...         return x * i
...     flist.append(func)
... 

>>> for f in flist:
...     print(f(2))
... 
4
4
4

按照正常的理解,应该输出的是 0, 2, 4,但实际输出的结果是:4, 4, 4。原因是什么呢? 这是因为循环在 Python 中是没有域的概念的,flist 在向列表中添加 func 的时候,并没有保存循环变量 i 的值,而是当执行 f(2) 的时候才去取,这时候循环已经结束,循环变量 i 这时的值是 2,所以结果都是 4。

通过在 函数func 外面再定义一个 makefunc 函数,让 func 形成闭包且循环变量 i 变成闭包变量,结果就正确了:

>>> flist = []

>>> for i in range(3):
...     def makefunc(i):
...         def func(x):
...             return x * i
...         return func
...     flist.append(makefunc(i))
... 

>>> for f in flist:
...     print(f(2))
... 
0
2
4

闭包的作用

  • 当闭包执行完后,仍然能够保持住当前的运行环境

如果你希望函数的每次执行结果,都是基于这个函数上次的运行结果。以一个类似棋盘游戏的例子来说明。假设棋盘大小为 50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标

origin = [0, 0]
legal_x = [0, 50]
legal_y = [0, 50]

def create(pos=origin):
    def player(direction, step):
        """
        这里应该首先判断参数 direction, step 的合法性
        比如 direction 不能斜着走,step 不能为负等
        然后还要对新生成的 x, y 坐标的合法性进行判断处理
        这里主要是想介绍闭包,就不详细写了。
        """
        new_x = pos[0] + direction[0] * step
        new_y = pos[1] + direction[1] * step
        pos[0] = new_x
        pos[1] = new_y
        # 注意,此处不能写成 pos = [new_x, new_y],因为参数变量不能被修改,而 pos[] 是容器类的解决方法
        return pos

    return player

player = create()    # 创建棋子 player,起点为原点

print(player([1, 0], 10))   # 向 x 轴正方向移动10步
print(player([0, 1], 20))   # 向 y 轴正方向移动20步
print(player([-1, 0], 10))  # 向 x 轴负方向移动10步
[10, 0]
[10, 20]
[0, 20]
  • 保存函数的状态信息,使函数的局部变量信息依然可以保存下来
>>> def maker(step):
...     num = 1
...     def func():
...         nonlocal num
...         num += step
...         print(num)
...     return func
... 

>>> f = maker(3)

>>> for i in range(3):
...     f()
... 
4
7
10

从上面的例子可以看出,外函数的局部变量 num = 1、以及调用外函数 maker(3) 时候传入的参数 step = 3 都被记忆了下来,所以才有 1 + 3 = 4、4 + 3 = 7、7 + 3 = 10。

  • 闭包可以根据外部作用域的局部变量来得到不同的结果

这有点像一种类似配置功能的作用,我们可以修改外部的变量,闭包根据这个变量展现出不同的功能。比如有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行:

>>> def make_filter(keep): 
...     def the_filter(file_name):
...         file = open(file_name)
...         lines = file.readlines() 
...         file.close()
...         filter_doc = [i for i in lines if keep in i]   
...         return filter_doc
...     return the_filter
... 

如果我们需要取得文件 "result.txt" 中含有 "pass" 关键字的行,则可以这样使用上述中的程序:

>>> filter = make_filter("pass") 

>>> filter_result = filter("result.txt")

>>> print(filter_result)
  • 闭包实现快速给不同项目记录日志
import logging

def log_header(logger_name):

    logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s] %(levelname)s  %(message)s',
                        datefmt='%Y-%m-%d %H:%M:%S')
    logger = logging.getLogger(logger_name)

    def _logging(something, level):
        if level == 'debug':
            logger.debug(something)
        elif level == 'warning':
            logger.warning(something)
        elif level == 'error':
            logger.error(something)
        else:
            raise Exception("I dont know what you want to do?")

    return _logging


project_1_logging = log_header('project_1')

project_2_logging = log_header('project_2')


def project_1():
    # do something
    project_1_logging('this is a debug info', 'debug')
    # do something
    project_1_logging('this is a warning info', 'warning')
    # do something
    project_1_logging('this is a error info', 'error')


def project_2():
    # do something
    project_2_logging('this is a debug info', 'debug')
    # do something
    project_2_logging('this is a warning info', 'warning')
    # do something
    project_2_logging('this is a critical info', 'error')


project_1()
project_2()
2019-02-19 23:44:18 [project_1] DEBUG  this is a debug info
2019-02-19 23:44:18 [project_1] WARNING  this is a warning info
2019-02-19 23:44:18 [project_1] ERROR  this is a error info
2019-02-19 23:44:18 [project_2] DEBUG  this is a debug info
2019-02-19 23:44:18 [project_2] WARNING  this is a warning info
2019-02-19 23:44:18 [project_2] ERROR  this is a critical info

装饰器

装饰器本质就是函数,它可以在不改变一个函数代码和调用方式的情况下给函数添加新功能。
原则:

  • 不修改被装饰函数的源代码
  • 为被装饰函数添加新功能后,不修改被修饰函数的调用方式

装饰器通用框架

def decorator(func):
    def wrapper(*args, **kwargs):
        print("Arguments were: %s, %s" % (args, kwargs))
        return func(*args, **kwargs)
    return wrapper

函数 wrapper() 可接受任意数量、类型的参数,并将它们传递给被包装的函数,这使得我们能用这个装饰器来装饰任何函数。

使用装饰器通用框架:

@decorator
def func1(x, y=1):
    return x * y


@decorator
def func2(x, y):
    return x * y


@decorator
def func3(x):
    return x


func1(5, 3)
print('\r')
func2(['Google', 'Baidu'], 3)
print('\r')
func3(x='Google') 

输出结果:

Arguments were: (5, 3), {}

Arguments were: (['Google', 'Baidu'], 3), {}

Arguments were: (), {'x': 'Google'}

分析上述中函数 func1 的执行步骤:

序号 执行步骤
1 首先,程序执行后,从上至下加载,当加载到 @decorator 时, 装饰器函数会先执行
2 @decorator 装饰器函数会先装饰函数 func1,相当于执行 func1 = decorator(func1),其返回值返回的是函数名,所以此时的 func1 已经指向了函数 wrapper
3 @decorator装饰器函数执行完后,接下来进行函数调用,即 func1(5, 3),这时就相当于调用了 wrapper(5, 3)
4 由于 wrapper 函数的返回值是函数调用,这时候才会真正执行 func1(5, 3) 函数

带参数的装饰器

装饰器本身也支持参数。达到这个目的就需要多一层嵌套函数。装饰器传参就相当于三层函数嵌套,在闭包的外面再包裹一层函数用于处理传入的参数。

实例 1: 通过装饰器的参数实现是否使用计时功能。

import time


def timer(bool=True):
    if bool:
        def _timer(func):
            def wrapper(*args, **kwargs):
                starttime = time.time()
                func(*args, **kwargs)
                endtime = time.time()
                res = (endtime - starttime) * 1000
                print('--> elapsed time: %f ms.' %(res))
            return wrapper
    else:
        def _timer(func):
            return func

    return _timer


@timer(False)
def myFunc():
    print('start myFunc.')
    time.sleep(1)
    print('end myFunc.')


@timer(True)
def add(a, b):
    print('start add.')
    time.sleep(1)
    print('result is %d.' % (a+b))
    print('end add.')


print('FuncName is %s.' % myFunc.__name__)
myFunc()

print('add is %s.' % add.__name__)
add(1, 2)

运行结果:

FuncName is myFunc.
start myFunc.
end myFunc.
add is wrapper.
start add.
result is 3.
end add.
--> elapsed time: 1005.218983 ms.

基于类实现的装饰器

装饰器函数其实是这样一个接口约束: 它必须接受一个 callable 对象作为参数,然后返回一个 callable 对象。Python 中一般 callable 对象都是函数,但也有例外。只要某个对象重载了 __call__() 方法,那么这个对象就是 callable 对象。

>>> class Test():
...     def __call__(self):
...         print("call me!")
... 

>>> t = Test()

>>> t()
call me!

如同上述代码中 __call__ 前后带 2 个下划线的方法在 Python 中称为内置方法(魔法方法)。重载这些魔法方法一般会改变对象的内部行为,上述示例作用是: 让一个对象拥有被调用的行为

装饰器要求接受一个 callable 对象,并返回一个 callable 对象,也就说:用类来实现装饰器也是 OK 的。即让类的构造函数 __init__() 接受一个函数(需要装饰的函数),然后重载 __call__() 魔法方法并返回一个函数,以达到装饰器函数的效果。

class Logging:
    def __init__(self, func):  # 构造函数接收需要装饰的函数
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[DEBUG]: enter function %s()." % self.func.__name__)
        return self.func(*args, **kwargs)

@Logging
def say(something):
    print("Say %s!" % something)


say("hello")

运行结果:

[DEBUG]: enter function say().
Say hello!

装饰器执行顺序

装饰器 由下至上 依次执行,之后调用的函数已经是被装饰后的函数了,此时是 由上至下 依次调用,整个过程有点像先上楼梯(装饰过程),再下楼梯(调用函数)。

  • 仅进行函数定义
def decorator_a(func):
    print("Get in decorator_a")
    def inner_a(*args, **kwargs):
        print("Get in inner_a")
        return func(*args, **kwargs)
    return inner_a

def decorator_b(func):
    print("Get in decorator_b")
    def inner_b(*args, **kwargs):
        print("Get in inner_b")
        return func(*args, **kwargs)
    return inner_b


@decorator_b
@decorator_a
def f(x):
    print("Get in f")
    return x * 2

输出结果:

Get in decorator_a
Get in decorator_b

上述代码仅进行了函数定义,但装饰器函数在被装饰函数定义好后会立即执行,且执行顺序为 由下至上 依次执行,所以输出结果为:

Get in decorator_a
Get in decorator_b
  • 进行函数调用
def decorator_a(func):
    print("Get in decorator_a")
    def inner_a(*args, **kwargs):
        print("Get in inner_a")
        return func(*args, **kwargs)
    return inner_a

def decorator_b(func):
    print("Get in decorator_b")
    def inner_b(*args, **kwargs):
        print("Get in inner_b")
        return func(*args, **kwargs)
    return inner_b


@decorator_b
@decorator_a
def f(x):
    print("Get in f")
    return x * 2

f(1)

输出结果:

Get in decorator_a
Get in decorator_b
Get in inner_b
Get in inner_a
Get in f

整个过程可以理解为:

序号 执行步骤
1 首先,程序执行后,从上至下加载,当加载到 @decorator_b 和 @decorator_a 时, 装饰器函数会 由下至上 依次执行
2 @decorator_a 装饰器函数会先装饰函数 f,相当于执行 f = decorator_a(f),所以先打印了 Get in decorator_a
3 @decorator_a 装饰完后,@decorator_b 再对函数 f 进行装饰,并且此时的 f 已经指向了函数 inner_a,所以 @decorator_b 对它进行装饰相当于 f=decorator_b(inner_a) 这个时候就会执行并打印 Get in decorator_b
4 此时的 f 就指向了 inner_b,就是两个装饰器都装饰完毕了,最后一行调用了 f(1),所以就相当于调用了 inner_b(1),所以会打印 Get in inner_b
5 打印完成之后,执行到 inner_b 里面的 return func(),此时的 func 是 inner_a,再执行 inner_a 打印 Get in inner_a
6 而 inner_a 里面还有 return func(),此时的 func 才是最初的 f,最后打印 Get in f
  • 装饰器函数在被装饰函数定义好后立即执行
def decorator_a(func):
    print("Get in decorator_a")
    def inner_a(*args, **kwargs):
        print("Get in inner_a")
        return func(*args, **kwargs)
    return inner_a


@decorator_a
def f(x):
    print("Get in f")
    return x * 2

输出结果:

Get in decorator_a

装饰器 @decorator_a 实现的效果和下面的代码一样:

def f(x):
    print("Get in f")
    return x * 2

f = decorator_a(f)  

当解释器执行这段代码时,decorator_a 已经被调用了,它以函数 f 为参数,返回 inner_a,所以当以后调用 f 时,实际上相当于调用 inner_a

内置装饰器

序号 装饰器 描述
1 @staticmethod 类静态方法
2 @classmethod 类方法
3 @property 属性方法
4 @abstractmethod 抽象方法

@staticmethod

@staticmethod 返回一个 staticmethod 类对象,其跟成员方法的区别是没有 self 参数(可以不传递任何参数),并且可以在类不进行实例化的情况下调用,也可以通过类名直接引用到达将函数功能与实例解绑的效果。

class Person(object):

    def __init__(self, name):
        self.name = name

    def go(self):
        print("go")

    @staticmethod
    def eat(name, food):   # 不再传递 self
        print("%s is eating %s" %(name, food))


# 通过实例调用方法
p = Person('Scott')
p.eat('Scott', 'Apple')

# 通过类调用方法
Person.eat('Jim', 'Dumplings')

输出结果:

Scott is eating Apple
Jim is eating Dumplings

@classmethod

@classmethod 返回一个 classmethod 类对象,与成员方法的区别在于 其必须使用类对象作为第一个参数,是将类本身作为操作的方法。类方法被哪个类调用,就传入哪个类作为第一个参数进行操作。其作用是 用于指定一个类的方法为类方法,而没有它时指定的类为实例方法。

定义一个时间类:

class Date():

    year, month, day = 0, 0, 0

    def __init__(self, year=0, month=0, day=0):
        self.year  = year
        self.month = month
        self.day   = day

    def print_date(self):
        print("Year: %s, Month: %s, Day: %s" %(self.year, self.month, self.day))


d = Date(2019, 3, 12)
d.print_date()


# 输出结果
Year: 2019, Month: 3, Day: 12

如果用户输入的是 "2019-03-12" 这样的字符格式,那么就需要在调用 Date 类前做一下处理: 先把 "2019-03-12" 分解成 year, month,day 三个变量,然后转成 int,并通过传递这些值实例化 Date

string_date = '2019-03-12'
year, month, day = map(int,string_date.split('-'))
d = Date(year, month, day)
d.print_date()


# 输出结果
Year: 2019, Month: 3, Day: 12

使用 @classmethod,将字符串处理的函数放入 Date 类中:

class Date():

    year, month, day = 0, 0, 0

    def __init__(self, year=0, month=0, day=0):
        self.year  = year
        self.month = month
        self.day   = day

    @classmethod
    def parse_date(cls, string_date):  # 这里第一个参数是 cls,表示调用当前的类名
        year, month, day = map(int, string_date.split('-'))
        date = cls(year, month, day)
        return date       # 返回一个初始化后的类

    def print_date(self):
        print("Year: %s, Month: %s, Day: %s" %(self.year, self.month, self.day))


date = Date.parse_date('2019-03-12')
date.print_date()


# 输出结果
Year: 2019, Month: 3, Day: 12

@property

property,译作属性,在 Python 中,表示可以通过类实例直接访问的信息,使调用类中的方法像引用类中的字段属性一样。被修饰的特性方法,内部可以实现处理逻辑,但对外提供统一的调用方式,遵循了统一访问的原则。

属性有3个装饰器: getter、setter、deleter,都是基于 property() 进行的封装,因为 setter、deleter 是 property() 的第二、三个参数,因此不能直接套用 @ 语法。getter 装饰器与不带 getter 的属性装饰器效果是一样的。经过 @property 装饰过的函数返回的不再是一个函数,而是一个 property 对象。

示例一: 定义一个屏幕分辨率对象(宽、高),在未接触到 @property 前,是这么编写的:

>>> class Screen():
...     def __init__(self):
...         self.width = 1024
...         self.height = 768
... 

>>> s = Screen()

>>> print(s.width, s.height)
1024 768

但在实际编码中可能会产生一个严重的问题,__init__() 中定义的属性是可变的,其他开发人员在知道属性名的情况下,可进行随意更改,这就可能造成严重后果:

>>> class Screen():
...     def __init__(self):
...         self.width = 1024
...         self.height = 768
... 

>>> s = Screen()

>>> s.height = 7.68

>>> print(s.width, s.height)
1024 7.68

上述代码打印的 s.height 结果将是 7.68。可能难以排查到此错误。

解决方案:将 width、height 属性都设为私有,其他人不可随意更改。@property 就起到此作用:

class Screen():
    @property
    def width(self):
        return self._width  # 变量名(不跟方法名相同,改为_width)

    @property
    def height(self):
        return self._height


s = Screen()
s.width = 1024  # 与方法名一致
s.height = 768
print(s.width, s.height)

输出结果:

Traceback (most recent call last):
  File "/Users/allen/Documents/Python/Python fullstack/Notes/函数式编程/property.py", line 12, in <module>
    s.width = 1024  
AttributeError: can't set attribute

@property 使方法像属性一样调用,如同一种特殊的属性。而此时,若要在外部给 width 或 height 重新直接赋值将报错 AtributeError,这样保证了属性的安全性。即 read only(只读属性)

若真想对属性进行操作,则提供了封装方法的方式进行属性的修改:

class Screen():
    @property
    def width(self):
        return self._width # 变量名 不跟方法名相同,改为_width

    @width.setter
    def width(self, value):
        self._width = value

    @width.deleter
    def width(self):
        del self._width

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        self._height = value

    @height.deleter
    def height(self):
        del self._height


s = Screen()
s.width = 1920  # 与方法名一致
s.height = 1200
print(s.width, s.height)

输出结果:

1920 1200

上述代码中,就可以对属性进行 赋值操作、删除属性 操作。

场景2:在定义数据库字段类时,可能需要对其中的类属性做一些限制(约束),一般用 get、set 方法来写,那么在 Python 中该如何以尽量少的代码优雅地实现想要的限制、减少错误的发生呢?下面以学生成绩表为例:

>>> class Student():
...     def get_score(self):
...         return self._score
...     def set_score(self, value):
...         if not isinstance(value, int):
...             raise ValueError('score must be an integer!')
...         if value < 0 or value > 100:
...             raise ValueError('score must between 0~100!')
...         self._score = value
... 

一般情况下,是这样调用的:

>>> s = Student()

>>> s.set_score(60)

>>> s.get_score()
60

>>> s.set_score(200)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in set_score
ValueError: score must between 0~100!

如果我们为了方便、节省时间,不想写 s.set_score(200),而是直接写 s.score = 200,则可以使用 @property 来修改代码:

>>> class Student():
...     @property
...     def score(self):
...         return self._score
...     @score.setter
...     def score(self, value):
...         if not isinstance(value, int):
...             raise ValueError('score must be an integer!')
...         if value < 0 or value > 100:
...             raise ValueError('score must between 0~100!')
...         self._score = value
... 

根据上述代码,将 get 方法变为属性只需加上 @property 装饰器即可,此时,@property 本身又会创建另外一个装饰器(@score.setter),它负责将 set 方法变成给属性赋值,这个操作后,调用将变得可控、方便:

>>> s = Student()

>>> s.score = 60    # 实际转化为 s.set_score(60)

>>> s.score         # 实际转化为 s.get_score()
60

>>> s.score = 200
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 10, in score
ValueError: score must between 0~100!

综上,@property 提供了可读、可写、可删除操作;若要实现只读属性,只需定义 @property 即可,不定义代表禁止其他操作。@property 就是负责将一个方法变成属性调用的。

@abstractmethod

抽象方法是父类的一个方法, 父类没有实现这个方法,且父类是不可以实例化的;子类继承父类,子类必须实现父类定义的抽象方法,子类才可以被实例化。

>>> from abc import ABC, abstractmethod

>>> class Foo(ABC):
...     @abstractmethod
...     def fun():
...         pass
... 
>>> f = Foo()       # 实例化报错
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Foo with abstract methods fun

子类继承父类该方法但并不实现该方法:

>>> class SubFoo(Foo):
...     def bar(self):
...         print("This is SubFoo.")
... 

>>> s = SubFoo()    # 子类并没有实现 fun 方法,实例化子类同样报错
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class SubFoo with abstract methods fun

子类继承父类该方法同时实现该方法:

>>> class SubFoo(Foo):
...     def fun(self):
...         print("From SubFoo.")
...     def bar(self):
...         print("This is SubFoo.")
... 

>>> s = SubFoo()

>>> s.fun()
From SubFoo.

需要注意的是,如果这时你实例化父类 Foo,仍然是报错的,因为 抽象基类只能继承而不能实例化

为了理解抽象基类,你可以这样想,我们有水果这个抽象基类,有苹果、香蕉、桃子这些子类,你永远只能吃得到苹果、桃子这些,而不能吃到所谓的水果。

装饰器存在的问题

至此,看到的都是讲述装饰器的优点:让代码更加优雅、减少重复。 但天下没有完美之物,使用不当,也会带来一些问题。

  • 位置错误的代码
def html_tags(tag_name):
    print('begin outer function.')
    def wrapper_(func):
        print("begin of inner wrapper function.")
        def wrapper(*args, **kwargs):
            content = func(*args, **kwargs)
            print("<{tag}>{content}</{tag}>".format(tag=tag_name, content=content))
        print('end of inner wrapper function.')
        return wrapper
    print('end of outer function')
    return wrapper_

@html_tags('b')
def hello(name='Scott'):
    return 'Hello {}!'.format(name)


hello()
hello()

在装饰器中我在各个可能的位置都加上了 print 语句,用于记录被调用的情况。你知道他们最后打印出来的顺序吗?如果你心里没底,那么最好不要在装饰器函数之外添加逻辑功能,否则这个装饰器就不受你控制了。以下是输出结果:

begin outer function.
end of outer function
begin of inner wrapper function.
end of inner wrapper function.
<b>Hello Scott!</b>
<b>Hello Scott!</b>
  • 错误的函数签名和文档

函数签名对象,表示调用函数的方式,即定义了函数的输入和输出。在 Python 中,可以使用标准库 inspect 的一些方法或类,来操作或创建函数签名。

装饰器装饰过的函数看上去表面名字没变,其实已经变了。

from datetime import datetime


def logging(func):
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print('[DEBUG] %s: enter %s().' %(datetime.now(), func.__name__))
        return func(*args, **kwargs)
    return wrapper


@logging
def say(something):
    """say something"""
    print('say %s!' %(something))

print(say.__name__)
print(say.__doc__)

输出结果:

wrapper
print log before a function.

为何上述打印的是 wrapper?只要你想想装饰器的语法糖 @ 代替的东西就明白了,@ 等同于这样的写法:

say = logging(say)

logging 其实返回的函数名字刚好是 wrapper,那么上面的这个语句刚好就是把这个结果赋值给 saysay__name__ 自然也就是 wrapper 了,不仅仅是 name,其他属性也都是来自 wrapper,比如 docsource 等等。

解决方案:使用标准库中的 functools.wraps

from functools import wraps

from datetime import datetime


def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """print log before a function."""
        print('[DEBUG] %s: enter %s().' % (datetime.now(), func.__name__))
        return func(*args, **kwargs)
    return wrapper


@logging
def say(something):
    """say something"""
    print('say %s!' %(something))


print(say.__name__)
print(say.__doc__)

输出结果:

say
say something
  • 不能装饰 @staticmethod 或 @classmethod

如若将装饰器用在一个静态方法或类方法中,则将报错:

from functools import wraps
from datetime import datetime


def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('[DEBUG] %s: enter %s().' % (datetime.now(), func.__name__))
        return func(*args, **kwargs)
    return wrapper


class Car(object):

    def __init__(self, model):
        self.model = model

    @logging    # 装饰实例方法, OK
    def run(self):
        print("%s is running!" %(self.model))

    @logging    # 装饰静态方法, Failed
    @staticmethod
    def check_type(obj):    # 不再传递 self
        tp = type(obj)
        if isinstance(obj, Car):  
            print("%s is a %s type!" %(obj.model, tp))
        else:
            print("%s is a %s type!" %(obj, tp))


Car.check_type('Benz')

运行将报: AttributeError: 'staticmethod' object has no attribute '__name__' 错误

@staticmethod 装饰器返回的是一个 staticmethod 对象,而不是 callable 对象,自然而然不可在其上方添加其他装饰器。

解决方案:将 @staticmethod 置前,然后再加上 @staticmethod 就没问题了。

from functools import wraps
from datetime import datetime


def logging(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('[DEBUG] %s: enter %s().' % (datetime.now(), func.__name__))
        return func(*args, **kwargs)
    return wrapper


class Car(object):

    def __init__(self, model):
        self.model = model

    @logging
    def run(self):
        print("%s is running!" %(self.model))

    @staticmethod
    @logging
    def check_type(obj):
        tp = type(obj)
        if isinstance(obj, Car):  # 判断一个对象是否是一个已知的类型
            print("%s is a %s type!" %(obj.model, tp))
        else:
            print("%s is a %s type!" %(obj, tp))


Car.check_type('Benz')

输出结果:

[DEBUG] 2019-02-21 23:32:28.299285: enter check_type().
Benz is a <class 'str'> type!

优化装饰器

嵌套的装饰函数不太直观,我们可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。

  • decorator.py

decorator.py 是一个非常简单的装饰器加强包,你可以很直观的先定义包装函数 wrapper(),再使用 decorate(func, wrapper) 方法就可以完成一个装饰器。

from decorator import decorate

from datetime import datetime


def wrapper(func, *args, **kwargs):
    """print log before a function."""
    print("[DEBUG] {}: enter {}()".format(datetime.now(), func.__name__))
    return func(*args, **kwargs)


def logging(func):
    return decorate(func, wrapper())    # 用 wrapper 装饰 func
  • wrapt

wrapt 是一个功能非常完善的包,用于实现各种你想到或者你没想到的装饰器。使用 wrapt 实现的装饰器你不需要担心之前 inspect 中遇到的所有问题,因为它都帮你处理了,甚至 inspect.getsource(func) 也准确无误。

import wrapt

# without argument in decorator
@wrapt.decorator
def logging(wrapped, instance, args, kwargs):  # instance is must
    print("[DEBUG]: enter {}()".format(wrapped.__name__))
    return wrapped(*args, **kwargs)

@logging
def say(something):
    pass

使用 wrapt 你只需要定义一个装饰器函数,但是函数签名是固定的,必须是 (wrapped, instance, args, kwargs),注意第二个参数 instance 是必须的,就算你不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据 instance 的值你能够更加灵活的调整你的装饰器。另外,argskwargs 也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。

偏函数

在调用函数时,如果我们已经知道了其中的一个参数,就可以通过这个参数,重新绑定一个函数,然后去调用即可;
对于有很多可调用对象,并且许多调用都反复使用相同参数的情况,使用偏函数比较合适。

int() 函数提供一个额外的 base 参数,默认值为 10,如果传入 base 参数,就可以做 N 进制的转换:

>>> int('512', base=8)    # 8 进制 => 10进制
330

>>> int('512', base=16)   # 16 进制 => 10进制
1298

假设要转换大量的二进制字符串,每次都传入 int(x, base=2) 非常麻烦,于是,我们想到,可以定义一个 int2() 的函数,默认把 base=2 传进去:

>>> def int2(x, base=2):
...     return int(x, base)
... 

这样,我们转换二进制字符串就非常方便了:

>>> int('1001', base=2)   # 2 进制 => 10进制
9

>>> int('0011', base=2)
3

functools.partial 就是帮助我们创建一个偏函数的,不需要我们自己定义 int2(),可以直接使用下面的代码创建一个新的函数 int2

>>> import functools
>>> int2 = functools.partial(int, base=2)

>>> int2('1001')
9
>>> int2('0011')
3

所以,简单总结 functools.partial 的作用就是,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。
上面的新的 int2 函数,仅仅是把 base 参数重新设定默认值为 2,但也可以在函数调用时传入其他值:

>>> int2('512', base=16)   # 16 进制 => 10进制
1298

柯里化

一个函数中有多个参数,如果想固定其中某个或几个参数的值,可以通过柯里化演变成新的函数。

  • 将加法函数柯里化过程
# 定义一个加法函数,接收三个参数
>>> def add(x, y, z):
...     return x + y + z
... 

# 通过 functools 模块中的 partial 函数实现柯里化(固定后两个参数)
>>> from functools import partial
>>> add_currying = partial(add, y=7, z=5)

# 调用加法函数
>>> add(1, 2, 3)
6

# 调用柯里化后函数
>>> add_currying(10)   # 10 + 7 + 5
22

参考文档

https://www.cnblogs.com/Lin-Yi/p/7305364.html
https://blog.csdn.net/weixin_38256474/article/details/83349809

原创文章,转载请注明出处:http://www.opcoder.cn/article/25/