I like @msw's answer for readability and solid structure: the generator of primes should live in its own function (and be memoized by itself) since that stream of primes can be used for whatever purpose is appropriate.
Without losing readability, factor
can also be explicitly memoized -- I would not use a opaque decorator for the purpose (maybe leading to very inefficient recursion to try and exploit the memoization, as the OP mentions in a comment!), but one at a lower layer of abstraction, and therefore more under my control. E.g.... (note: "richly commented" version later if you find the comment-less version below hard to read - rather than, as I do, finding it easier to read thanks to its relative conciseness):
def factor(n, _memo={1: []}):
"""returns a list of the prime factors of integer n"""
if n <= 0: raise ValueError("Can't factor %r" % n)
localmemo = {}
orgn = n
p = primes.generator() # yields an infinite iterable
for x in p:
if n in _memo:
for k in localmemo:
localmemo[k].extend(_memo[n])
_memo.update(localmemo)
return _memo[orgn]
localmemo[n] = []
if n % x == 0:
n = n/x
for k in localmemo:
localmemo[k].append(x)
p.send(x) # get `x` again next time
Edit: original version didn't work correctly for numbers with a given prime factor occurring more than once -- here I'm assuming that the primes generator accepts a send
which works as a push-back in order to yield that prime again at the next next
(otherwise you'd have to nest another loop within the for
to use that x
factor as many times as necessary).
It's trickier to keep memos updated when going "top down" (as iteration naturally does here) than when going "bottom up" (as recursion will make things go), so I might have made some error in this -- and of course the readability won't be as good as @msw's clean solution -- but such optimizations (in different real-world cases, probably -- for actual factorization in the real world I'd use gmpy
of course;-) may be worth some small sacrifice in readability and effort at perfecting the optimized (and thus less-clear) logic.
Edit: here's a longer version with more comments, for readers who prefer that style:
def factor(n, _memo={1: []}):
"""returns a list of the prime factors of integer n
n must be > 0 (otherwise, raises ValueError).
uses a stream of primes in increasing order generated
by `primes.generator()` (q.v.).
do **not** pass argument _memo, it's used for memoization
(holding the list of factors for all ints that have already
been factorized by this function in this process's past).
"""
# get error cases out of the way first
if n <= 0: raise ValueError("Can't factor %r" % n)
# localmemo records all numbers which are being factorized
# for the first time in this specific call to `factor`, each
# with a list of corresponding factors found so far
localmemo = {}
# keep a copy of the original n since in the loop below n
# gets decreased
orgn = n
p = primes.generator() # yields an infinite iterable
# look at each prime (the .send call below may cause a prime
# to be looked at more than once since it's assumed to work
# as a "push back" for this specific generator)
for x in p:
if n in _memo: # we've factorized n already in the past
# (or n is 1, which is always a key in _memo!)
# so we're all done, mop up
# every list of factors in localmemo gets all n's factors
for k in localmemo:
localmemo[k].extend(_memo[n])
# add every localmemo list to _memo for future calls
_memo.update(localmemo)
# now orgn is in _memo (as it was in localmemo if it had
# not already been in _memo it's been added) so we can just
# index to get the corresponding list of factors
return _memo[orgn]
# start with an empty list since we don't know n's factors yet
localmemo[n] = []
if n % x == 0: # x is a factor of n, so of everything we're factoring
n = n/x
for k in localmemo:
localmemo[k].append(x) # ...so add it to every entry in localmemo
p.send(x) # get `x` again next time (it might be a multiple factor!)
Edit: to add single-level pushback functionality to a generator that doesn't have it...:
def withpushback(aniterator):
pushback = None
while True:
if pushback is not None: # last client action was `send`
to_yield = pushback
while pushback is not None:
pushback = yield to_yield # iterate until a `next`!
else: # last client action was `next`
try: to_yield = next(aniterator)
except StopIteration: break
pushback = yield to_yield
# an example use...:
w = withpushback(iter(range(7)))
repetitions = {2: 3, 5: 2}
for p in w:
print p,
if repetitions.get(p):
w.send(p)
repetitions[p] -= 1
print
This shows 0 1 2 2 2 2 3 4 5 5 5 6
-- i.e., three reps of 2
and two of 5
, as the repetitions
dict specified. (The assumed pattern of accesses is alternating next
and send
, though with an attempt to survive multiple send
s in a row;-).