25

Is it possible to access the previous element generated in a list comprehension.

I am working on some toy encryption stuff. Given the key as an arbitrarily large integer, an initialization value, and a list of elements as the message to encrypt. I need to xor each element with the previous ciphered element and the key. The following loop would do.

previous = initialization_value
cipher = []
for element in message:
    previous = element ^ previous ^ key
    cipher.append(previous)

I feel like it should be possible to turn this into a list comprehension but I am not exactly sure how to handle both the initial value or accessing the previous value generated. Is it possible and if so what would the comprehension be?

Matt
  • 1,841
  • 2
  • 25
  • 30

9 Answers9

26

There isn't a good, Pythonic way to do this with a list comprehension. The best way to think about list comprehensions is as a replacement for map and filter. In other words, you'd use a list comprehension whenever you need to take a list and

  • Use its elements as input for some expression (e.g. squaring the elements)

  • Remove some of its elements based on some condition

What these things have in common is that they each only look at a single list element at a time. This is a good rule of thumb; even if you could theoretically write the code you showed as a list comprehension, it would be awkward and unpythonic.

Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256
  • 8
    +1: That's why we still have the for statement -- for situations exactly like this question. – S.Lott Apr 27 '09 at 19:48
  • It would be convenient to get native functionality for this. Databases and data-centric languages like SAS have a `lag` keyword. The coding in the question is unnecessarily verbose IMHO and there isn't a great pythonic solution. Interestingly I find no PEPs for this feature – prauchfuss Feb 27 '21 at 19:52
6

As a generator:

def cypher(message, key, seed):
    for element in message:
        seed = element ^ seed ^ key
        yield seed

list(cypher(message, key, initial_seed))
OldTimer
  • 71
  • 1
  • 5
3

Since Python 3.8, there is now support for an assignment expression that can be used to implement a list comprehension that remembers the last value in the iteration:

previous = initialization_value
cipher = [previous := element ^ previous ^ key for element in message]

If you prefer to write everything in one line, you can initialize previous in a singleton list, and discard it with the and operator to let the list comprehension take over:

cipher = [previous := initialization_value] and [previous := element ^ previous ^ key for element in message]
blhsing
  • 91,368
  • 6
  • 71
  • 106
  • This worked perfectly. As an example, you can replace itertools.accumulate(old) with: x=0; new = [x:=i+x for i in old]. This will take old=(1,2,3) and create new=[1,3,6] – Kurt E. Clothier May 04 '23 at 20:15
3

You could have done this using reduce(). It's not list comprehension, but it's the functional style approach:

cipher = []
def f(previous, element):
    previous = element ^ previous ^ key
    cipher.append(previous)
    return previous
reduce(f, message, initialization_value)

It isn't any prettier than the plain loop in this case though.

airportyh
  • 21,948
  • 13
  • 58
  • 72
  • 1
    Check the performance before using reduce; it can often lead to remarkably inefficient structures. – S.Lott Apr 27 '09 at 20:30
  • 2
    The 'for loop' version is *much* cleaner, so treat this answer only as a "theoretically possible to do otherwise". – Eli Bendersky Apr 28 '09 at 07:12
2

Here is an example how to access the last and next created elements in a list comprehension using enumerate.

The previous value...

Here -1 is the offset and None is the default value.

A = ['A','B','C','D','E','F','G','H','I','J']
[(A[i-1] if (i-1) >= 0 else None,x) for i,x in enumerate(A)

Output as tuple (Previous, Current)

[(None, 'A'), ('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E'), ('E', 'F'), ('F', 'G'), ('G', 'H'), ('H', 'I'), ('I', 'J')]

The next value...

A = ['A','B','C','D','E','F','G','H','I','J']
[(x, A[i+1] if (i+1) < len(A) else None) for i,x in enumerate(A)]

Output as tuple (Current, Next)

[('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E'), ('E', 'F'), ('F', 'G'), ('G', 'H'), ('H', 'I'), ('I', 'J'), ('J', None)]

This is very similar to the lag() and lead() analytical functions in sql.

paul g
  • 29
  • 1
1

I would prefer to use something more like enumerate generator

def emit_previous(iterable, initial=None):
    previous = initial
    for item in iterable:
        yield previous, item
        previous = item

cipher = []
for previous, element in emit_previous(message, initial=initialization_value):
    seed = element ^ previous ^ key
    cipher.append(seed)
rodfersou
  • 914
  • 6
  • 10
1

itertools.accumulate() exists exactly for that:

from itertools import accumulate
result = list(accumulate(message, lambda prev, element: prev^element^key), initial=initialization_value)
del result[0] # Remove the initialization value

A simple one-liner, no need for a comprehension or anything complex.

Bharel
  • 23,672
  • 5
  • 40
  • 80
1

You could use a helper object to store all the internal state while iterating over the sequence:

class Encryption:
  def __init__(self, key, init_value):
    self.key = key
    self.previous = init_value
  def next(self, element):
    self.previous = element ^ self.previous ^ self.key
    return self.previous

enc = Encryption(...)
cipher = [enc.next(e) for e in message]

That being said, adding the previously encrypted element into the xor doesn't make your algorithm any harder to break than just xor'ing every element with the key. An attacker can just xor any character in the cipher text with the previous encrypted character and so cancel out the xor that was done during encryption.

sth
  • 222,467
  • 53
  • 283
  • 367
0

So you could append to a list within the list comprehension and use the last added element as the new element for that list comprehension and then finally assigning that comprehension to the list variable.

message = [0,1,2,3,4,5,6,7]
key = 8
previous = 9

cipher = [previous]

cipher = [(cipher.append(element^cipher[-1]^key), cipher[-1])[1] for element in message]

print(cipher)
jur
  • 514
  • 5
  • 5