【问题标题】:Python: Is math.factorial memoized?Python:math.factorial 被记忆了吗?
【发布时间】:2011-06-25 23:51:09
【问题描述】:

我正在以三种不同的方式解决problem,其中两种是递归的,我自己记住它们。另一个不是递归的,而是使用 math.factorial。我需要知道是否需要为其添加显式记忆。

谢谢。

【问题讨论】:

标签: python algorithm caching


【解决方案1】:

在这个链接上搜索math_factorial,你会发现它在python中的实现:

http://svn.python.org/view/python/trunk/Modules/mathmodule.c?view=markup

附:这是针对python2.6的

【讨论】:

  • 谢谢 Asterisk,(你是高卢人吗)?
【解决方案2】:

Python 的 math.factorial 没有被记忆,它是一个简单的 for 循环,将值从 1 乘以你的 arg。如果你需要记忆,你需要明确地去做。

这是一种使用字典 setdefault 方法进行记忆的简单方法。

import math
cache = {}
def myfact(x):
    return cache.setdefault(x,math.factorial(x))
print myfact(10000)
print myfact(10000)

【讨论】:

  • 感谢 Senthil。我能够从源头确认这一点。
  • @Paddy3118 - 添加了一个简单的示例。它不使用装饰器,而只是使用字典的 setdefault 方法,该方法可用于此类目的。
  • 再次感谢@Senthil。我正在使用来自here 的代码的不太灵活的版本,我可以用它来装饰三个functions
  • 这不会带来任何性能优势。 setdefault 与所有 Python 方法一样,始终评估其所有参数 - 当字典中已经存在密钥时,它不会短路。相反,您需要使用 result = cache.get(x) 并测试 None 或 if x in cache: ... 我自己也犯了同样的错误。 cache.get(x, math.factorial(x)) 也好不到哪里去。
  • 我认为@Peter 一定是对的。不幸的是,我无法改变我的赞成票。 :-) 有关记忆函数的另一种方式,请参阅python-course.eu/python3_memoization.php
【解决方案3】:

Python 的 math.factorial 没有被记忆。

我将通过一些试验和错误示例来指导您了解为什么要获得一个真正记忆化且有效的阶乘函数,您必须从头重新定义它,并考虑几件事情。

另一个答案实际上是不正确的。这里,

import math
cache = {}
def myfact(x):
    return cache.setdefault(x,math.factorial(x))

线

return cache.setdefault(x,math.factorial(x))

每次都计算xmath.factorial(x),因此您不会获得任何性能提升。

你可能会想到做这样的事情:

if x not in cache:
    cache[x] = math.factorial(x)
return cache[x]

但实际上这也是错误的。是的,您避免再次计算相同x 的阶乘,但请考虑,例如,如果您要计算myfact(1000) 并在此之后不久计算myfact(999)。它们都被完全计算,因此没有利用myfact(1000)自动计算myfact(999)这一事实。

这样写是很自然的:

def memoize(f):
    """Returns a memoized version of f"""
    memory = {}
    def memoized(*args):
        if args not in memory:
            memory[args] = f(*args)
        return memory[args]
    return memoized

@memoize
def my_fact(x):
    assert x >= 0
    if x == 0:
        return 1
    return x * my_fact(x - 1)

这将起作用。不幸的是,它很快就达到了最大递归深度。

那么如何实现呢?

这是一个真正记忆阶乘的示例,它利用了阶乘的工作原理,并且不会通过递归调用消耗所有堆栈:

# The 'max' key stores the maximum number for which the factorial is stored.
fact_memory = {0: 1, 1: 1, 'max': 1}

def my_fact(num):
    # Factorial is defined only for non-negative numbers
    assert num >= 0

    if num <= fact_memory['max']:
        return fact_memory[num]

    for x in range(fact_memory['max']+1, num+1):
        fact_memory[x] = fact_memory[x-1] * x
    fact_memory['max'] = num
    return fact_memory[num]

我希望你觉得这很有用。

编辑:

请注意,实现相同优化同时具有递归的简洁和优雅的一种方法是将函数重新定义为tail-recursive 函数。

def memoize(f):
    """Returns a memoized version of f"""
    memory = {}
    def memoized(*args):
        if args not in memory:
            memory[args] = f(*args)
        return memory[args]
    return memoized

@memoize
def my_fact(x, fac=1):
    assert x >= 0
    if x < 2:
        return fac
    return my_fact(x-1,  x*fac)

实际上,尾递归函数可以被解释器/编译器识别并自动翻译/优化为迭代版本,但并非所有解释器/编译器都支持这一点。

可惜python不支持尾递归优化,所以还是得到:

RuntimeError: maximum recursion depth exceeded

当 my_fact 的输入为高时。

【讨论】:

  • 你想省略递归并使用循环。
  • 你能改写一下吗?您是在问是否可以对函数的递归定义做同样的事情?如果这是问题所在,只要您使用的解释器支持这一点,就可以通过尾递归方式重新定义函数。尾递归函数实际上可以被编译器/解释器识别并被翻译/优化为迭代算法。不幸的是,从我上次的编辑中可以看出,标准 python 解释器 2.7.3 似乎不支持这一点。
  • 有很多装饰器在 Python 中执行尾调用的伪优化,即它们可以防止调用堆栈溢出。
  • @EliKorvigo 很有趣。您能否提供更多相关信息?
  • @DomenicoDeFelice 当然,我个人更喜欢 fn 包中的 recur.tcodecorator 有几个原因,尽管它有一些限制(即,您的函数必须以特殊方式修改)。我见过许多可以使用几乎任何功能的装饰器。 This one 是一个示例(嵌套函数可能会变得棘手),cmets 中还提供了许多其他示例。
【解决方案4】:

我迟到了,但这是我在 Python 中实现高效记忆阶乘函数的 2c。这种方法更有效,因为它依赖于类似数组的结构(即list)而不是散列容器(即dict)。不涉及递归(为您节省一些 Python 函数调用开销)并且不涉及缓慢的 for 循环。它(可以说)在功能上是纯的,因为不涉及外部副作用(即它不修改全局变量)。它缓存了所有中间阶乘,因此如果您已经计算了factorial(n),那么对于任何 0 n 都需要 O(1) 来计算 factorial(m)

def inner_func(f):
    return f()


@inner_func
def factorial():
    factorials = [1]

    def calculate_factorial(n):
        assert n >= 0
        return reduce(lambda cache, num: (cache.append(cache[-1] * num) or cache),
                      xrange(len(factorials), n+1), factorials)[n]

    return calculate_factorial

【讨论】:

  • 不错的解决方案。我唯一不喜欢的是:1)or cache 依赖于append 返回None 的知识(更明确的东西会更好更清楚地理解),2)我不知道你是什么意思是“慢 for-loops”,但是,如果我们只关心性能,我不认为 reduce 更快:它在每一步都对 lambda 进行函数调用(除非解释器做了一些内联的优化lambda 代码)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-05-23
  • 1970-01-01
  • 2015-02-18
  • 2020-07-21
  • 2018-01-18
  • 1970-01-01
相关资源
最近更新 更多