Does python have immutable lists?
Suppose I wish to have the functionality of an ordered collection of elements, but which I want to guarantee will not change, how can this be implemented? Lists are ordered but they can be mutated.
Yes. It's called a tuple.
So, instead of [1,2] which is a list and which can be mutated, (1,2) is a tuple and cannot.
Further Information:
A one-element tuple cannot be instantiated by writing (1), instead, you need to write (1,). This is because the interpreter has various other uses for parentheses.
You can also do away with parentheses altogether: 1,2 is the same as (1,2)
Note that a tuple is not exactly an immutable list. Click here to read more about the differences between lists and tuples
Here is an ImmutableList implementation. The underlying list is not exposed in any direct data member. Still, it can be accessed using the closure property of the member function. If we follow the convention of not modifying the contents of closure using the above property, this implementation will serve the purpose. Instance of this ImmutableList class can be used anywhere a normal python list is expected.
from functools import reduce
__author__ = 'hareesh'
class ImmutableList:
"""
An unmodifiable List class which uses a closure to wrap the original list.
Since nothing is truly private in python, even closures can be accessed and
modified using the __closure__ member of a function. As, long as this is
not done by the client, this can be considered as an unmodifiable list.
This is a wrapper around the python list class
which is passed in the constructor while creating an instance of this class.
The second optional argument to the constructor 'copy_input_list' specifies
whether to make a copy of the input list and use it to create the immutable
list. To make the list truly immutable, this has to be set to True. The
default value is False, which makes this a mere wrapper around the input
list. In scenarios where the input list handle is not available to other
pieces of code, for modification, this approach is fine. (E.g., scenarios
where the input list is created as a local variable within a function OR
it is a part of a library for which there is no public API to get a handle
to the list).
The instance of this class can be used in almost all scenarios where a
normal python list can be used. For eg:
01. It can be used in a for loop
02. It can be used to access elements by index i.e. immList[i]
03. It can be clubbed with other python lists and immutable lists. If
lst is a python list and imm is an immutable list, the following can be
performed to get a clubbed list:
ret_list = lst + imm
ret_list = imm + lst
ret_list = imm + imm
04. It can be multiplied by an integer to increase the size
(imm * 4 or 4 * imm)
05. It can be used in the slicing operator to extract sub lists (imm[3:4] or
imm[:3] or imm[4:])
06. The len method can be used to get the length of the immutable list.
07. It can be compared with other immutable and python lists using the
>, <, ==, <=, >= and != operators.
08. Existence of an element can be checked with 'in' clause as in the case
of normal python lists. (e.g. '2' in imm)
09. The copy, count and index methods behave in the same manner as python
lists.
10. The str() method can be used to print a string representation of the
list similar to the python list.
"""
#staticmethod
def _list_append(lst, val):
"""
Private utility method used to append a value to an existing list and
return the list itself (so that it can be used in funcutils.reduce
method for chained invocations.
#param lst: List to which value is to be appended
#param val: The value to append to the list
#return: The input list with an extra element added at the end.
"""
lst.append(val)
return lst
#staticmethod
def _methods_impl(lst, func_id, *args):
"""
This static private method is where all the delegate methods are
implemented. This function should be invoked with reference to the
input list, the function id and other arguments required to
invoke the function
#param list: The list that the Immutable list wraps.
#param func_id: should be the key of one of the functions listed in the
'functions' dictionary, within the method.
#param args: Arguments required to execute the function. Can be empty
#return: The execution result of the function specified by the func_id
"""
# returns iterator of the wrapped list, so that for loop and other
# functions relying on the iterable interface can work.
_il_iter = lambda: lst.__iter__()
_il_get_item = lambda: lst[args[0]] # index access method.
_il_len = lambda: len(lst) # length of the list
_il_str = lambda: lst.__str__() # string function
# Following represent the >, < , >=, <=, ==, != operators.
_il_gt = lambda: lst.__gt__(args[0])
_il_lt = lambda: lst.__lt__(args[0])
_il_ge = lambda: lst.__ge__(args[0])
_il_le = lambda: lst.__le__(args[0])
_il_eq = lambda: lst.__eq__(args[0])
_il_ne = lambda: lst.__ne__(args[0])
# The following is to check for existence of an element with the
# in clause.
_il_contains = lambda: lst.__contains__(args[0])
# * operator with an integer to multiply the list size.
_il_mul = lambda: lst.__mul__(args[0])
# + operator to merge with another list and return a new merged
# python list.
_il_add = lambda: reduce(
lambda x, y: ImmutableList._list_append(x, y), args[0], list(lst))
# Reverse + operator, to have python list as the first operand of the
# + operator.
_il_radd = lambda: reduce(
lambda x, y: ImmutableList._list_append(x, y), lst, list(args[0]))
# Reverse * operator. (same as the * operator)
_il_rmul = lambda: lst.__mul__(args[0])
# Copy, count and index methods.
_il_copy = lambda: lst.copy()
_il_count = lambda: lst.count(args[0])
_il_index = lambda: lst.index(
args[0], args[1], args[2] if args[2] else len(lst))
functions = {0: _il_iter, 1: _il_get_item, 2: _il_len, 3: _il_str,
4: _il_gt, 5: _il_lt, 6: _il_ge, 7: _il_le, 8: _il_eq,
9: _il_ne, 10: _il_contains, 11: _il_add, 12: _il_mul,
13: _il_radd, 14: _il_rmul, 15: _il_copy, 16: _il_count,
17: _il_index}
return functions[func_id]()
def __init__(self, input_lst, copy_input_list=False):
"""
Constructor of the Immutable list. Creates a dynamic function/closure
that wraps the input list, which can be later passed to the
_methods_impl static method defined above. This is
required to avoid maintaining the input list as a data member, to
prevent the caller from accessing and modifying it.
#param input_lst: The input list to be wrapped by the Immutable list.
#param copy_input_list: specifies whether to clone the input list and
use the clone in the instance. See class documentation for more
details.
#return:
"""
assert(isinstance(input_lst, list))
lst = list(input_lst) if copy_input_list else input_lst
self._delegate_fn = lambda func_id, *args: \
ImmutableList._methods_impl(lst, func_id, *args)
# All overridden methods.
def __iter__(self): return self._delegate_fn(0)
def __getitem__(self, index): return self._delegate_fn(1, index)
def __len__(self): return self._delegate_fn(2)
def __str__(self): return self._delegate_fn(3)
def __gt__(self, other): return self._delegate_fn(4, other)
def __lt__(self, other): return self._delegate_fn(5, other)
def __ge__(self, other): return self._delegate_fn(6, other)
def __le__(self, other): return self._delegate_fn(7, other)
def __eq__(self, other): return self._delegate_fn(8, other)
def __ne__(self, other): return self._delegate_fn(9, other)
def __contains__(self, item): return self._delegate_fn(10, item)
def __add__(self, other): return self._delegate_fn(11, other)
def __mul__(self, other): return self._delegate_fn(12, other)
def __radd__(self, other): return self._delegate_fn(13, other)
def __rmul__(self, other): return self._delegate_fn(14, other)
def copy(self): return self._delegate_fn(15)
def count(self, value): return self._delegate_fn(16, value)
def index(self, value, start=0, stop=0):
return self._delegate_fn(17, value, start, stop)
def main():
lst1 = ['a', 'b', 'c']
lst2 = ['p', 'q', 'r', 's']
imm1 = ImmutableList(lst1)
imm2 = ImmutableList(lst2)
print('Imm1 = ' + str(imm1))
print('Imm2 = ' + str(imm2))
add_lst1 = lst1 + imm1
print('Liist + Immutable List: ' + str(add_lst1))
add_lst2 = imm1 + lst2
print('Immutable List + List: ' + str(add_lst2))
add_lst3 = imm1 + imm2
print('Immutable Liist + Immutable List: ' + str(add_lst3))
is_in_list = 'a' in lst1
print("Is 'a' in lst1 ? " + str(is_in_list))
slice1 = imm1[2:]
slice2 = imm2[2:4]
slice3 = imm2[:3]
print('Slice 1: ' + str(slice1))
print('Slice 2: ' + str(slice2))
print('Slice 3: ' + str(slice3))
imm1_times_3 = imm1 * 3
print('Imm1 Times 3 = ' + str(imm1_times_3))
three_times_imm2 = 3 * imm2
print('3 Times Imm2 = ' + str(three_times_imm2))
# For loop
print('Imm1 in For Loop: ', end=' ')
for x in imm1:
print(x, end=' ')
print()
print("3rd Element in Imm1: '" + imm1[2] + "'")
# Compare lst1 and imm1
lst1_eq_imm1 = lst1 == imm1
print("Are lst1 and imm1 equal? " + str(lst1_eq_imm1))
imm2_eq_lst1 = imm2 == lst1
print("Are imm2 and lst1 equal? " + str(imm2_eq_lst1))
imm2_not_eq_lst1 = imm2 != lst1
print("Are imm2 and lst1 different? " + str(imm2_not_eq_lst1))
# Finally print the immutable lists again.
print("Imm1 = " + str(imm1))
print("Imm2 = " + str(imm2))
# The following statemetns will give errors.
# imm1[3] = 'h'
# print(imm1)
# imm1.append('d')
# print(imm1)
if __name__ == '__main__':
main()
You can simulate a Lisp-style immutable singly-linked list using two-element tuples (note: this is different than the any-element tuple answer, which creates a tuple that's much less flexible):
nil = ()
cons = lambda ele, l: (ele, l)
e.g. for the list [1, 2, 3], you would have the following:
l = cons(1, cons(2, cons(3, nil))) # (1, (2, (3, ())))
Your standard car and cdr functions are straightforward:
car = lambda l: l[0]
cdr = lambda l: l[1]
Since this list is singly linked, appending to the front is O(1). Since this list is immutable, if the underlying elements in the list are also immutable, then you can safely share any sublist to be reused in another list.
This question deserves a modern answer, now that type annotations and type checking via mypy are getting more popular.
Replacing a List[T] by a tuple may not be the ideal solution when using type annotations. Conceptually a list has a generic arity of 1, i.e., they have a single generic argument T (of course, this argument can be a Union[A, B, C, ...] to account for heterogeneously typed lists). In contrast tuples are inherently variadic generics Tuple[A, B, C, ...]. This makes tuples an awkward list replacement.
In fact, type checking offers another possibility: It is possible to annotate variables as immutable lists by using typing.Sequence, which corresponds to the type of the immutable interface collections.abc.Sequence. For example:
from typing import Sequence
def f(immutable_list: Sequence[str]) -> None:
# We want to prevent mutations like:
immutable_list.append("something")
mutable_list = ["a", "b", "c"]
f(mutable_list)
print(mutable_list)
Of course, in terms of runtime behavior this isn't immutable, i.e., the Python interpreter will happily mutate immutable_list, and the output would be ["a", "b", "c", "something"].
However, if your project uses a type checker like mypy, it will reject the code with:
immutable_lists_1.py:6: error: "Sequence[str]" has no attribute "append"
Found 1 error in 1 file (checked 1 source file)
So under the hood you can continue to use regular lists, but the type checker can effectively prevent any mutations at type-check time.
Similarly you could prevent modifications of list members e.g. in immutable dataclasses (note that field assignment on a frozen dataclass in fact is prevent at runtime):
#dataclass(frozen=True)
class ImmutableData:
immutable_list: Sequence[str]
def f(immutable_data: ImmutableData) -> None:
# mypy will prevent mutations here as well:
immutable_data.immutable_list.append("something")
The same principle can be used for dicts via typing.Mapping.
But if there is a tuple of arrays and tuples, then the array inside a tuple can be modified.
>>> a
([1, 2, 3], (4, 5, 6))
>>> a[0][0] = 'one'
>>> a
(['one', 2, 3], (4, 5, 6))
List and Tuple have a difference in their working style.
In LIST we can make changes after its creation but if you want an ordered sequence in which no changes can be applied in the future you can use TUPLE.
further information::
1) the LIST is mutable that means you can make changes in it after its creation
2) In Tuple, we can not make changes once it created
3) the List syntax is
abcd=[1,'avn',3,2.0]
4) the syntax for Tuple is
abcd=(1,'avn',3,2.0)
or abcd= 1,'avn',3,2.0 it is also correct
Instead of tuple, you can use frozenset. frozenset creates an immutable set. you can use list as member of frozenset and access every element of list inside frozenset using single for loop.
Related
In python I have an iterator returning an infinite string of indices in a fixed range [0, N] called Sampler. Actually I have a list of those and all they do is return indices in the range [0, N_0], [N_0, N_1], ..., [N_{n-1}, N_n].
What I now want to do is first select one of these iterators based on the length of their range, so I have a weights list [N_0, N_1 - N_0, ...] and I select one of these with:
iterator_idx = random.choices(range(len(weights)), weights=weights/weights.sum())[0]
Next, what I want to do is create an iterator which randomly selects one of the iterators and selects a batch of M samples.
class BatchSampler:
def __init__(self, M):
self.M = M
self.weights = [weight_list]
self.samplers = [list_of_iterators]
]
self._batch_samplers = [
self.batch_sampler(sampler) for sampler in self.samplers
]
def batch_sampler(self, sampler):
batch = []
for batch_idx in sampler:
batch.append(batch_idx)
if len(batch) == self.M:
yield batch
if len(batch) > 0:
yield batch
def __iter__(self):
# First select one of the datasets.
iterator_idx = random.choices(
range(len(self.weights)), weights=self.weights / self.weights.sum()
)[0]
return self._batch_samplers[iterator_idx]
The issue with this is that iter() only seems to be called once, so only the first time iterator_idx is selected. Obviously this is wrong... What is the way around this?
This is a possible case when you would have multiple datasets in pytorch, but you want to sample only batches from one of the datasets.
Seems to me that you want to define your own container type.
I'll try to provide examples of a few standard ways to do so
(hopefully without missing too many details);
you should be able to reuse one of these simple examples,
into your own class.
Using just __getitem__ (support indexing & looping):
object.__getitem__
Called to implement evaluation of self[key].
class MyContainer:
def __init__(self, sequence):
self.elements = sequence # Just something to work with.
def __getitem__(self, key):
# If we're delegating to sequences like built-in list,
# invalid indices are handled automatically by them
# (throwing IndexError, as per the documentation).
return self.elements[key]
t = (1, 2, 'a', 'b')
c = MyContainer(t)
elems = [e for e in c]
assert elems == [1, 2, 'a', 'b']
assert c[1:-1] == t[1:-1] == (2, 'a')
Using the iterator protocol:
object.__iter__
object.__iter__(self)
This method is called when an iterator is required for a container. This method should return a new iterator object that can iterate over all the objects in the container. For mappings, it should iterate over the keys of the container.
Iterator objects also need to implement this method; they are required to return themselves. For more information on iterator objects, see Iterator Types.
Iterator Types
container.__iter__()
Return an iterator object. The object is required to support the iterator protocol described below.
The iterator objects themselves are required to support the following two methods, which together form the iterator protocol:
iterator.__iter__()
Return the iterator object itself. This is required to allow both containers and iterators to be used with the for and in statements.
iterator.__next__()
Return the next item from the container. If there are no further items, raise the StopIteration exception.
Once an iterator's __next__() method raises StopIteration, it must continue to do so on subsequent calls.
class MyContainer:
class Iter:
def __init__(self, container):
self.cont = container
self.pos = 0
self.len = len(container.elements)
def __iter__(self): return self
def __next__(self):
if self.pos == self.len: raise StopIteration
curElem = self.cont.elements[self.pos]
self.pos += 1
return curElem
def __init__(self, sequence):
self.elements = sequence # Just something to work with.
def __iter__(self):
return MyContainer.Iter(self)
t = (1, 2, 'a', 'b')
c = MyContainer(t)
elems = [e for e in c]
assert elems == [1, 2, 'a', 'b']
Using a generator:
Generator Types
Python's generators provide a convenient way to implement the iterator protocol. If a container object's iter() method is implemented as a generator, it will automatically return an iterator object (technically, a generator object) supplying the iter() and next() methods.
generator
A function which returns a generator iterator. It looks like a normal function except that it contains yield expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with the next() function.
Usually refers to a generator function, but may refer to a generator iterator in some contexts.
generator iterator
An object created by a generator function.
6.2.9. Yield expressions
Using a yield expression in a function's body causes that function to be a generator
class MyContainer:
def __init__(self, sequence):
self.elements = sequence # Just something to work with.
def __iter__(self):
for e in self.elements: yield e
t = (1, 2, 'a', 'b')
c = MyContainer(t)
elems = [e for e in c]
assert elems == [1, 2, 'a', 'b']
I have a list of instances of a certain class. This list contains `duplicates', in the sense that duplicates share the exact same attributes. I want to remove the duplicates from this list.
I can check whether two instances share the same attributes by using
class MyClass:
def __eq__(self, other) :
return self.__dict__ == other.__dict__
I could of course iterate through the whole list of instances and compare them element by element to remove duplicates, but I was wondering if there is a more pythonic way to do this, preferably using the in operator + list comprehension.
sets (no order)
A set cannot contain duplicate elements. list(set(content)) will deduplicate a list. This is not too inefficient and is probably one of the better ways to do it :P You will need to define a __hash__ function for your class though, which must be the same for equal elements and different for unequal elements for this to work. Note that the hash value must obey the aforementioned rule but otherwise it may change between runs without causing issues.
index function (stable order)
You could do lambda l: [l[index] for index in range(len(l)) if index == l.index(l[index])]. This only keeps elements that are the first in the list.
in operator (stable order)
def uniquify(content):
result = []
for element in content:
if element not in result:
result.append(element)
return result
This will keep appending elements to the output list unless they are already in the output list.
A little more on the set approach. You can safely implement a hash by delegating to a tuple's hash - just hash a tuple of all the attributes you want to look at. You will also need to define an __eq__ that behaves properly.
class MyClass:
def __init__(self, a, b, c):
self.a = a
self.b = b
self.c = c
def __eq__(self, other):
return (self.a, self.b, self.c) == (other.a, other.b, other.c)
def __hash__(self):
return hash((self.a, self.b, self.c))
def __repr__(self):
return "MyClass({!r}, {!r}, {!r})".format(self.a, self.b, self.c)
As you're doing so much tuple construction, you could just make your class iterable:
def __iter__(self):
return iter((self.a, self.b, self.c))
This enables you to call tuple on self instead of laboriously doing .a, .b, .c etc.
You can then do something like this:
def unordered_elim(l):
return list(set(l))
If you want to preserve ordering, you can use an OrderedDict instead:
from collections import OrderedDict
def ordered_elim(l):
return list(OrderedDict.fromkeys(l).keys())
This should be faster than using in or index, while still preserving ordering. You can test it something like this:
data = [MyClass("this", "is a", "duplicate"),
MyClass("first", "unique", "datum"),
MyClass("this", "is a", "duplicate"),
MyClass("second", "unique", "datum")]
print(unordered_elim(data))
print(ordered_elim(data))
With this output:
[MyClass('first', 'unique', 'datum'), MyClass('second', 'unique', 'datum'), MyClass('this', 'is a', 'duplicate')]
[MyClass('this', 'is a', 'duplicate'), MyClass('first', 'unique', 'datum'), MyClass('second', 'unique', 'datum')]
NB if any of your attributes aren't hashable, this won't work, and you'll either need to work around it (change a list to a tuple) or use a slow, n ^ 2 approach like in.
How do you override the result of unpacking syntax *obj and **obj?
For example, can you somehow create an object thing which behaves like this:
>>> [*thing]
['a', 'b', 'c']
>>> [x for x in thing]
['d', 'e', 'f']
>>> {**thing}
{'hello world': 'I am a potato!!'}
Note: the iteration via __iter__ ("for x in thing") returns different elements from the *splat unpack.
I had a look inoperator.mul and operator.pow, but those functions only concern usages with two operands, like a*b and a**b, and seem unrelated to splat operations.
* iterates over an object and uses its elements as arguments. ** iterates over an object's keys and uses __getitem__ (equivalent to bracket notation) to fetch key-value pairs. To customize *, simply make your object iterable, and to customize **, make your object a mapping:
class MyIterable(object):
def __iter__(self):
return iter([1, 2, 3])
class MyMapping(collections.Mapping):
def __iter__(self):
return iter('123')
def __getitem__(self, item):
return int(item)
def __len__(self):
return 3
If you want * and ** to do something besides what's described above, you can't. I don't have a documentation reference for that statement (since it's easier to find documentation for "you can do this" than "you can't do this"), but I have a source quote. The bytecode interpreter loop in PyEval_EvalFrameEx calls ext_do_call to implement function calls with * or ** arguments. ext_do_call contains the following code:
if (!PyDict_Check(kwdict)) {
PyObject *d;
d = PyDict_New();
if (d == NULL)
goto ext_call_fail;
if (PyDict_Update(d, kwdict) != 0) {
which, if the ** argument is not a dict, creates a dict and performs an ordinary update to initialize it from the keyword arguments (except that PyDict_Update won't accept a list of key-value pairs). Thus, you can't customize ** separately from implementing the mapping protocol.
Similarly, for * arguments, ext_do_call performs
if (!PyTuple_Check(stararg)) {
PyObject *t = NULL;
t = PySequence_Tuple(stararg);
which is equivalent to tuple(args). Thus, you can't customize * separately from ordinary iteration.
It'd be horribly confusing if f(*thing) and f(*iter(thing)) did different things. In any case, * and ** are part of the function call syntax, not separate operators, so customizing them (if possible) would be the callable's job, not the argument's. I suppose there could be use cases for allowing the callable to customize them, perhaps to pass dict subclasses like defaultdict through...
I did succeed in making an object that behaves how I described in my question, but I really had to cheat. So just posting this here for fun, really -
class Thing:
def __init__(self):
self.mode = 'abc'
def __iter__(self):
if self.mode == 'abc':
yield 'a'
yield 'b'
yield 'c'
self.mode = 'def'
else:
yield 'd'
yield 'e'
yield 'f'
self.mode = 'abc'
def __getitem__(self, item):
return 'I am a potato!!'
def keys(self):
return ['hello world']
The iterator protocol is satisfied by a generator object returned from __iter__ (note that a Thing() instance itself is not an iterator, though it is iterable). The mapping protocol is satisfied by the presence of keys() and __getitem__. Yet, in case it wasn't already obvious, you can't call *thing twice in a row and have it unpack a,b,c twice in a row - so it's not really overriding splat like it pretends to be doing.
I'm trying to implement a class with a method that calls another method with an object that's part of the class where the lowest method mutates the object. My implementation is a little more complicated, so I'll post just some dummy code so you can see what I'm talking about:
class test:
def __init__(self,list):
self.obj = list
def mult(self, x, n):
x = x*n
def numtimes(self, n):
self.mult(self.obj, n)
Now, if I create an object of this type and run the numtimes method, it won't update self.obj:
m = test([1,2,3,4])
m.numtimes(3)
m.obj #returns [1,2,3,4]
Whereas I'd like it to give me [1,2,3,4,1,2,3,4,1,2,3,4]
Basically, I need to pass self.obj to the mult method and have it mutate self.obj so that when I call m.obj, I'll get [1,2,3,4,1,2,3,4,1,2,3,4] instead of [1,2,3,4].
I feel like this is just a matter of understanding how python passes objects as arguments to methods (like it's making a copy of the object, and instead I need to use a pointer), but maybe not. I'm new to python and could really use some help here.
Thanks in advance!!
Allow me to take on the bigger subject of mutability.
Lists are mutable objects, and support both mutable operations, and immutable operations. That means, operations that change the list in-place, and operations that return a new list. Tuples, for contrast, only are only immutable.
So, to multiply a list, you can choose two methods:
a *= b
This is a mutable operation, that will change 'a' in-place.
a = a * b
This is an immutable operation. It will evaluate 'a*b', create a new list with the correct value, and assign 'a' to that new list.
Here, already, lies a solution to your problem. But, I suggest you read on a bit. When you pass around lists (and other objects) as parameters, you are only passing a new reference, or "pointer" to that same list. So running mutable operations on that list will also change the one that you passed. The result might be a very subtle bug, when you write:
>>> my_list = [1,2,3]
>>> t = test(my_list)
>>> t.numtimes(2)
>>> my_list
[1,2,3,1,2,3] # Not what you intended, probably!
So here's my final recommendation. You can choose to use mutable operations, that's fine. But then create a new copy from your arguments, as such:
def __init__(self,l):
self.obj = list(l)
OR use immutable operations, and reassign them to self:
def mult(self, x, n):
self.x = x*n
Or do both, there's no harm in being extra safe :)
The multiplication x * n creates a new instance and does not alter the existing list. See here:
a = [1]
print (id (a) )
a = a * 2
print (id (a) )
This should work:
class test:
def __init__(self,list):
self.obj = list
def mult(_, x, n):
x *= n
def numtimes(self, n):
self.mult(self.obj, n)
Is it possible to have a list be evaluated lazily in Python?
For example
a = 1
list = [a]
print list
#[1]
a = 2
print list
#[1]
If the list was set to evaluate lazily then the final line would be [2]
The concept of "lazy" evaluation normally comes with functional languages -- but in those you could not reassign two different values to the same identifier, so, not even there could your example be reproduced.
The point is not about laziness at all -- it is that using an identifier is guaranteed to be identical to getting a reference to the same value that identifier is referencing, and re-assigning an identifier, a bare name, to a different value, is guaranteed to make the identifier refer to a different value from them on. The reference to the first value (object) is not lost.
Consider a similar example where re-assignment to a bare name is not in play, but rather any other kind of mutation (for a mutable object, of course -- numbers and strings are immutable), including an assignment to something else than a bare name:
>>> a = [1]
>>> list = [a]
>>> print list
[[1]]
>>> a[:] = [2]
>>> print list
[[2]]
Since there is no a - ... that reassigns the bare name a, but rather an a[:] = ... that reassigns a's contents, it's trivially easy to make Python as "lazy" as you wish (and indeed it would take some effort to make it "eager"!-)... if laziness vs eagerness had anything to do with either of these cases (which it doesn't;-).
Just be aware of the perfectly simple semantics of "assigning to a bare name" (vs assigning to anything else, which can be variously tweaked and controlled by using your own types appropriately), and the optical illusion of "lazy vs eager" might hopefully vanish;-)
Came across this post when looking for a genuine lazy list implementation, but it sounded like a fun thing to try and work out.
The following implementation does basically what was originally asked for:
from collections import Sequence
class LazyClosureSequence(Sequence):
def __init__(self, get_items):
self._get_items = get_items
def __getitem__(self, i):
return self._get_items()[i]
def __len__(self):
return len(self._get_items())
def __repr__(self):
return repr(self._get_items())
You use it like this:
>>> a = 1
>>> l = LazyClosureSequence(lambda: [a])
>>> print l
[1]
>>> a = 2
>>> print l
[2]
This is obviously horrible.
Python is not really very lazy in general.
You can use generators to emulate lazy data structures (like infinite lists, et cetera), but as far as things like using normal list syntax, et cetera, you're not going to have laziness.
That is a read-only lazy list where it only needs a pre-defined length and a cache-update function:
import copy
import operations
from collections.abc import Sequence
from functools import partialmethod
from typing import Dict, Union
def _cmp_list(a: list, b: list, op, if_eq: bool, if_long_a: bool) -> bool:
"""utility to implement gt|ge|lt|le class operators"""
if a is b:
return if_eq
for ia, ib in zip(a, b):
if ia == ib:
continue
return op(ia, ib)
la, lb = len(a), len(b)
if la == lb:
return if_eq
if la > lb:
return if_long_a
return not if_long_a
class LazyListView(Sequence):
def __init__(self, length):
self._range = range(length)
self._cache: Dict[int, Value] = {}
def __len__(self) -> int:
return len(self._range)
def __getitem__(self, ix: Union[int, slice]) -> Value:
length = len(self)
if isinstance(ix, slice):
clone = copy.copy(self)
clone._range = self._range[slice(*ix.indices(length))] # slicing
return clone
else:
if ix < 0:
ix += len(self) # negative indices count from the end
if not (0 <= ix < length):
raise IndexError(f"list index {ix} out of range [0, {length})")
if ix not in self._cache:
... # update cache
return self._cache[ix]
def __iter__(self) -> dict:
for i, _row_ix in enumerate(self._range):
yield self[i]
__eq__ = _eq_list
__gt__ = partialmethod(_cmp_list, op=operator.gt, if_eq=False, if_long_a=True)
__ge__ = partialmethod(_cmp_list, op=operator.ge, if_eq=True, if_long_a=True)
__le__ = partialmethod(_cmp_list, op=operator.le, if_eq=True, if_long_a=False)
__lt__ = partialmethod(_cmp_list, op=operator.lt, if_eq=False, if_long_a=False)
def __add__(self, other):
"""BREAKS laziness and returns a plain-list"""
return list(self) + other
def __mul__(self, factor):
"""BREAKS laziness and returns a plain-list"""
return list(self) * factor
__radd__ = __add__
__rmul__ = __mul__
Note that this class is discussed also in this SO.