Python中的惰性求值

Posted

技术标签:

【中文标题】Python中的惰性求值【英文标题】:Lazy evaluation in Python 【发布时间】:2013-12-30 09:10:52 【问题描述】:

什么是 Python 中的惰性求值?

一个网站说:

在 Python 3.x 中,range() 函数返回一个特殊的范围对象,该对象根据需要计算列表的元素(惰性或延迟评估):

>>> r = range(10)
>>> print(r)
range(0, 10)
>>> print(r[3])
3

这是什么意思?

【问题讨论】:

创建一个生成器(例如,包含yielddef),它会在产生一个值之前导致像print 这样的副作用。然后在每次迭代时以一秒的延迟循环生成器。什么时候出现指纹? Python 3 的 range(很像 Python 2 中的 xrange)的工作原理是这样的:直到被询问才完成计算。这就是“惰性求值”的意思。 【参考方案1】:

range()(或 Python2.x 中的xrange())返回的对象称为惰性迭代。

生成器不是将整个范围[0,1,2,..,9] 存储在内存中,而是存储(i=0; i<10; i+=1) 的定义并仅在需要时计算下一个值(也称为惰性求值)。

本质上,生成器允许您返回类似结构的列表,但这里有一些区别:

    列表在创建时存储所有元素。生成器会在需要时生成下一个元素。 列表可以根据需要进行多次迭代,生成器只能恰好迭代一次。 列表可以通过索引获取元素,而生成器不能——它只生成一次值,从开始到结束。

可以通过两种方式创建生成器:

(1) 非常类似于列表推导:

# this is a list, create all 5000000 x/2 values immediately, uses []
lis = [x/2 for x in range(5000000)]

# this is a generator, creates each x/2 value only when it is needed, uses ()
gen = (x/2 for x in range(5000000)) 

(2) 作为函数,使用yield返回下一个值:

# this is also a generator, it will run until a yield occurs, and return that result.
# on the next call it picks up where it left off and continues until a yield occurs...
def divby2(n):
    num = 0
    while num < n:
        yield num/2
        num += 1

# same as (x/2 for x in range(5000000))
print divby2(5000000)

注意:尽管range(5000000) 是 Python3.x 中的生成器,[x/2 for x in range(5000000)] 仍然是一个列表。 range(...) 完成它的工作并一次生成一个 x,但在创建此列表时将计算整个 x/2 值列表。

【讨论】:

实际上,range(或 2.x 中的 xrange返回生成器。生成器是一个迭代器——对于任何生成器g,您都可以调用next(g)range 对象实际上是一个可迭代对象。您可以在其上调用iter 以获取迭代器,但它本身不是迭代器(您不能对其调用next)。除此之外,这意味着您可以多次迭代单个范围对象。 "一个生成器只能迭代一次。"不是真的。您可以通过使用 next()、跳出 for 循环或根本不访问它来迭代少于一次。也许删除“完全”一词或将其更改为“最多”。 @LaurenceGonsalves 我编辑了答案以纠正问题。【参考方案2】:

简而言之,惰性求值意味着对象在需要时进行求值,而不是在创建时求值。

在 Python 2 中,范围会返回一个列表——这意味着如果你给它一个很大的数字,它会计算范围并在创建时返回:

>>> i = range(100)
>>> type(i)
<type 'list'>

在 Python 3 中,您会得到一个特殊的范围对象:

>>> i = range(100)
>>> type(i)
<class 'range'>

只有当你消费它时,它才会被实际评估——换句话说,它只会在你真正需要它们时返回范围内的数字。

【讨论】:

【参考方案3】:

一个名为 python patterns 和 wikipedia 的 github 存储库告诉我们什么是惰性评估。

延迟 expr 的 eval 直到需要它的值并避免重复 eval。

python3 中的range 不是完全惰性求值,因为它不能避免重复求值。

惰性求值的一个更经典的例子是cached_property

import functools

class cached_property(object):
    def __init__(self, function):
        self.function = function
        functools.update_wrapper(self, function)

    def __get__(self, obj, type_):
        if obj is None:
            return self
        val = self.function(obj)
        obj.__dict__[self.function.__name__] = val
        return val

cached_property(a.k.alazy_property) 是一个装饰器,它将一个函数转换为一个惰性求值属性。第一次访问属性时,调用 func 以获取结果,然后在下次访问该属性时使用该值。

例如:

class LogHandler:
    def __init__(self, file_path):
        self.file_path = file_path

    @cached_property
    def load_log_file(self):
        with open(self.file_path) as f:
            # the file is to big that I have to cost 2s to read all file
            return f.read()

log_handler = LogHandler('./sys.log')
# only the first time call will cost 2s.
print(log_handler.load_log_file)
# return value is cached to the log_handler obj.
print(log_handler.load_log_file)

用一个恰当的词来说,像 range 这样的 Python 生成器对象更像是通过 call_by_need 模式设计的,而不是 惰性求值

【讨论】:

截至 2018 年,***文章将“按需调用”和“惰性评估”视为同义词。因为range() 的 Python 3 版本确实不会创建完整的数字列表,而是会生成数字,直到我们停止请求一个数字(例如通过打破 for 循环),它确实(原文如此)“延迟 expr 的 eval 直到需要它的值”。我会说您将它们视为两个不同的概念是错误的。 执行 type(i) 时需要 i,而不是执行 i=range(100) 时。我会说 Vi.Ci 是正确的。 模式仓库的链接移至github.com/faif/python-patterns/blob/master/patterns/creational/…

以上是关于Python中的惰性求值的主要内容,如果未能解决你的问题,请参考以下文章

Python:any() / all() 中的惰性函数求值

从属属性中的 MATLAB 惰性求值

C#函数式编程中的惰性求值详解

理解 pyspark 中的惰性求值行为

奇怪的 jags.parallel 错误/避免函数调用中的惰性求值

python 短路求值或惰性求值