关于python:Python函数式编程系列007惰性求值

33次阅读

共计 3826 个字符,预计需要花费 10 分钟才能阅读完成。

本系列文章一些重要的函数、办法、类我都实现的一遍,你能够在 github(点击此处)中找到代码和测试例子(如果网速过慢我也放了一份在 gitee(点击此处)上,但请勿在 gitee 上提 issue 或者留言),欢送star/fork

缘起

咱们回到介绍高阶函数的一章,咱们提到了高阶函数特地是科里化的一个益处便是「提前求值」和「推延求值」,通过这些操作,咱们能够大大优化很多代码。比方,咱们应用之前的例子:

def f(x): # x 贮存了某种咱们须要的状态
    ## 所有能够提前计算的放在这里
    z = x ** 2 + x + 1
    print('z is {}'.format(z))
    def helper(y):
        ## 所有提早计算的放在这里
        return y * z
    return helper

咱们在调用 f(1) 的时候,其实就曾经当时计算了 z 的局部,如果咱们长期保留这个值,重复调用时就能够节俭很大的工夫:

>>> g = f(1)
z is 3
>>> g(2) + g(1) # 能够看到这次就不会打印 `z is xxxx` 的输入了
9

也就是说适时的「提前求值」和「推延求值」都能够帮忙咱们大大地缩小很多运算开销。这就引入咱们这一篇要讲的「惰性求值」的概念,惰性求值的概念次要是:调用时才计算,并且只计算一次。

惰性属性与惰性值

咱们思考上面一个例子:

定义一个圆的类,通过圆心和半径来形容,然而当咱们晓得圆心和半径之后咱们能晓得很多事,比方:

  1. 周长(perimeter)
  2. 面积(area)
  3. 圆最下面坐标的地位(upper_point)
  4. 圆心到原点的间隔(distance_from_origin)

这个列表可能十分十分多,而且随着软件性能的减少,这个列表可能还会增加。咱们可能有两种办法实现。第一种就是在初始化的时候都给设定为圆的属性:

@dataclass
class CircleInitial:
    x: float
    y: float
    r: float

    def __init__(self, x, y, r):
        self.x = x
        self.y = y
        self.r = r

        self.perimeter = 2 * r
        self.area = r * r * 3.14
        self.upper_point = (x, y + r)
        self.lower_point = (x, y - r)
        self.left_point = (x - r, y)
        self.right_point = (x + r, y)
        self.distance_from_origin = (x ** 2 + y ** 2) ** (1/2)

咱们马上能够看出问题:如果这样的属性十分多,而且波及的计算也十分多的话,那么当咱们实例化一个新的对象的时候,消耗的工夫将会十分长。然而,大部分的属性,咱们可能都不会用到。

于是,就有了第二个计划,把这些实现成一个办法(咱们这里仅举例一个 area 办法):

@dataclass
class CircleMethod:
    x: float
    y: float
    r: float

    def area(self):
        print("area calculating...")
        return self.r * self.r * 3.14

当然,因为这个值是一个「常」量的概念,咱们也能够应用 property 润饰器,这样咱们就能够不必带括号地调用它了:

@dataclass
class CircleMethod:
    x: float
    y: float
    r: float

    @property
    def area(self):
        print("area calculating...")
        return self.r * self.r * 3.14

我成心在其中退出了一行打印代码,咱们能够发现,咱们每次调用 area 时,都会被计算一次:

>>> a = CircleMethod(1, 2, 3)
>>> a.area ** 2 + a.area + 1
area calculating...
area calculating...
827.8876000000001

这又是另外一种节约了,于是咱们发现,第一种计划适宜须要常常被重复调用的属性,第二个计划实现很少被调用的属性。然而,可能咱们在保护代码的时候,没法当时预判一个属性是不是常常被调用,而且这也不是一个长久之计。但咱们发现咱们须要的就是那么一个属性:

  1. 这个属性不会初始化的时候计算
  2. 这个属性只在被调用时计算
  3. 这个属性只会计算一次,前面不会调用

这个就是「惰性求值」的概念,咱们也把这种属性叫「惰性属性」。Python没有内置的惰性属性的概念,不过,咱们能够很容易从网上找到一个实现(你也能够在我的 Python-functional-programming 中的 lazy_evaluate.py 中找到):

def lazy_property(func):
    attr_name = "_lazy_" + func.__name__

    @property
    def _lazy_property(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, func(self))
        return getattr(self, attr_name)

    return _lazy_property

具体的应用,只是切换一下润饰器property

@dataclass
class Circle:
    x: float
    y: float
    r: float

    @lazy_property
    def area(self):
        print("area calculating...")
        return self.r * self.r * 3.14

咱们采纳和下面一样的调用形式,能够发现,area只计算了一次(只打印了一次):

>>> b = Circle(1, 2, 3)
>>> b.area ** 2 + b.area + 1
area calculating...
827.8876000000001

同样的理由咱们也能够实现一个惰性值的概念,不过因为 python 没有代码块的概念,咱们只能用 没有参数 的函数来实现:

class _LazyValue:

    def __setattr__(self, name, value):
        if not callable(value) or value.__code__.co_argcount > 0:
            raise NotVoidFunctionError("value is not a void function")
        super(_LazyValue, self).__setattr__(name, (value, False))      
        
    def __getattribute__(self, name: str):
        try:
            _func, _have_called = super(_LazyValue, self).__getattribute__(name)
            if _have_called:
                return _func
            else:
                res = _func()
                super(_LazyValue, self).__setattr__(name, (res, True))
                return res
        except:
            raise AttributeError("type object'Lazy'has no attribute'{}'"
                .format(name)
            )

lazy_val = _LazyValue()

具体调用办法如下,如果你要设计一个模块而这个变量不在类中,那么就能够很不便地应用它了:

def f():
    print("f compute")
    return 12

>>> lazy_val.a = f
>>> lazy_val.a
f compute
12
>>> lazy_val.a
12

惰性迭代器 / 生成器

此外,Python内置了一些惰性的构造次要就是迭代器和生成器,咱们能够很不便验证它们只计算 / 保留一次(这里只验证迭代器):

>>> a = (i for i in range(5))
>>> list(a)
[0, 1, 2, 3, 4]
>>> list(a)
[]

咱们能够设计上面两个函数:

def f(x):
    print("f")
    return x + 1

def g(x):
    print("g")
    return x + 1

而后咱们思考上面的后果:

>>> a = (g(i) for i in (f(i) for i in range(5)))
>>> next(a)

它可能有两种后果,一个它可能的计算形式是这样的:

>>> temp = [f(i) for i in range(5)]
>>> res = g(temp[0])

如果是这种后果,则它会打印出 5 个 f 而后再打印出g

另一种可能性则是:

>>> res = (g(f(i)) for i in range(5))

则,这样子便只会打印一个 f 和一个 g。如果依据惰性求值的定义,i=1 并没有被实在调用,所以它应该不必求值,所以,如果他合乎第二个打印状况,则它就是惰性的对象。事实也就真如此。

当然,这个个性曾经十分的 Fancy 了,然而咱们基于此能够联想出的一个十分微妙的援用,因为在迭代器计算中,咱们并不是在生成的时候,就计算出了迭代器中的每个值,因而,咱们能够用这个形式存储一个无穷系列。通过下面的形式计算后返回后果。一个最简略的例子是内置模块中的 itertools.repeat,咱们能够生成一个无穷的全为1 的线性构造:

from itertools import repeat

repeat_1 = repeat(1)

这样,咱们就能够用下面的列表表达式来做一些计算再通过 next 调用了。

res = (g(i) for i in (i * 3 for i in repeat_1))
next(res)

咱们也将这些线性构造称为「惰性列表」(这里的 repeat_1 则是一个「无穷惰性列表」的例子),在上面的文章中,咱们将具体地用这个形式来实现一些乏味的事件。

正文完
 0