【问题标题】:Lazy evaluation in PythonPython中的惰性求值
【发布时间】: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)的工作原理是这样的:直到被询问才完成计算。这就是“惰性求值”的意思。

标签: python python-3.x lazy-evaluation


【解决方案1】:

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

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

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

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

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

(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 patternswikipedia 的 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 是正确的。
    猜你喜欢
    • 2011-07-14
    • 2023-03-29
    • 1970-01-01
    • 2011-02-23
    • 2020-05-19
    • 2016-10-11
    • 2010-09-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多