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
这是什么意思?
【问题讨论】:
创建一个生成器(例如,包含yield
的def
),它会在产生一个值之前导致像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中的惰性求值的主要内容,如果未能解决你的问题,请参考以下文章