Create a list with initial capacity in Python
Asked Answered
V

11

233

Code like this often happens:

l = []
while foo:
    # baz
    l.append(bar)
    # qux

This is really slow if you're about to append thousands of elements to your list, as the list will have to be constantly resized to fit the new elements.

In Java, you can create an ArrayList with an initial capacity. If you have some idea how big your list will be, this will be a lot more efficient.

I understand that code like this can often be refactored into a list comprehension. If the for/while loop is very complicated, though, this is unfeasible. Is there an equivalent for us Python programmers?

Venavenable answered 22/11, 2008 at 20:56 Comment(6)
As far as I know, they are similar to ArrayLists in that they double their size each time. The amortized time of this operation is constant. It isn't as big of a performance hit as you would think.Dirigible
seems like you're right!Venavenable
Perhaps pre-initialization isn't strictly needed for the OP's scenario, but sometimes it definitely is needed: I have a number of pre-indexed items that need to be inserted at a specific index, but they come out of order. I need to grow the list ahead-of-time to avoid IndexErrors. Thanks for this question.Quadrivalent
@Venavenable The accepted answer is misleading. The highest-upvoted comment under it explains why. Would you consider accepting one of the other answers?Kolva
If the for/while loop is very complicated, though, this is unfeasible -- not necessarily. Most complicated loop bodies are prime candidates for conversion to a function which can then be used in a list comprehension. This practice tends to promote good design by abstracting away complexity. For while loops with unclear or nondeterministic termination conditions, itertools and generators can rescue the logic back to list comprehension land much of the time.Docent
Pardon my ignorance, but would list comprehension really solve anything? The comprehension generator doesn't a'priori know it length in most cases, so performance-wise you would still need to resize the underlying array dynamically during construction, just like the loop in question.Fiorin
K
119

Warning: This answer is contested. See comments.

def doAppend( size=10000 ):
    result = []
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result.append(message)
    return result

def doAllocate( size=10000 ):
    result=size*[None]
    for i in range(size):
        message= "some unique object %d" % ( i, )
        result[i]= message
    return result

Results. (evaluate each function 144 times and average the duration)

simple append 0.0102
pre-allocate  0.0098

Conclusion. It barely matters.

Premature optimization is the root of all evil.

Kerrin answered 22/11, 2008 at 22:2 Comment(26)
fair enough! i did a similar test and found a difference of 1.00 vs 0.8 or so. I guess it doesn't matter so much.Venavenable
I agree 100% with this answer. The only thing I would add is that you may get some lift out of using generator expressions like: list(i for i in xrange(size))Socha
What if the preallocation method (size*[None]) itself is inefficient? Does the python VM actually allocate the list at once, or grow it gradually, just like the append() would?Vase
@haridsv: "What if the preallocation method (size*[None]) itself is inefficient?" What does that mean? It's equally efficient. It's not inefficient. It's the same.Kerrin
What I was wondering is that if python preallocates the list with the final size or will only grow it on demand (ie., attempt to grow "size" number of times, just like for the regular append()). Just considering all corners... it is simple enough to optimize so it is quite likely that this case is already optimized, but remains an assumption unless someone knows the internals for sure.Vase
@haridsv: (1) I can't understand what hair you're splitting. The time is the same. I don't know what other "optimization" you could be talking about. (2) The internals are trivially available -- you can read the source at any time.Kerrin
@haridsv: I made some test by putting the allocation (result=size*[None]) outside the function and can't find a significant difference. By replacing the loop body by result[i] = i, I then get 1.13 ms versus 0.62 ms with preallocation.Ansate
@S.Lott: What haridsv is saying is that if int*list is implemented by growing a list one element at a time, then it's no wonder both implementations shown above take the same time. If this were the case, then it is possible that pre-allocating might be much faster - but we haven't yet found any code to test that case.Mcbrayer
@Tartley: "haven't yet found any code to test that case". Baffling. If it can't be expressed in Python, then what are we talking about?Kerrin
Hey. It presumably can be expressed in Python, but nobody has yet posted it here. haridsv's point was that we're just assuming 'int * list' doesn't just append to the list item by item. That assumption is probably valid, but haridsv's point was that we should check that. If it wasn't valid, that would explain why the two functions you showed take almost identical times - because under the covers, they are doing exactly the same thing, hence haven't actually tested the subject of this question. Best regards!Mcbrayer
This isn't valid; you're formatting a string with each iteration, which takes forever relative to what you're trying to test. Additionally, given that 4% can still be significant depending on the situation, and it's an underestimate...Sweetener
Would it work faster if we already used an instance of what will be the final data type to prefill the list, instead of None?Gantry
The other problem with this answer is that the memory usage of both is not the same (sure, asymptotically the same, but potentially twice as much as it needs to be). The real benefit of preallocation is not speed, it's that the thing is exactly as big as it should be.Quadrivalent
As @Philip points out the conclusion here is misleading. Preallocation doesn't matter here because the string formatting operation is expensive. I tested with a cheap operation in the loop and found preallocating is almost twice as fast.Savate
This answer should be adjusted to address the comments about writing strings inside the loop. Maybe I should say something that is supposed to be witty as is in this answer... "Incorrect testing of optimizations is the most efficient way zero credibility."Bourbonism
@Keith, Theoretical computer science backs you up here. Depending on the implementation of append, it should be 2 to 3 times faster. cs.stackexchange.com/questions/9380/… .Tropo
Wrong answers with many upvotes are yet another root of all evil.Tropo
Just to be clear, despite the upvotes this answer is most definitely incorrect.Shugart
As said above, this answer is completely wrong. see https://mcmap.net/q/41440/-python-equivalent-of-vector-reserve for a correct profilingYoicks
@Philip It's less than half a millisecond in an interpreter known for being too slow for that magnitude of time to matter. You wouldn't be writing Python code if that level of performance was critical.Laywoman
Who said it was pre-mature optimization? Tell this to any game developer who can anticipate where the 80/20 pressure points will beVassell
As expected, the accepted answer is both subjectively and objectively harmful. What's unexpected is that S.Lott – a very active Pythonista here – failed to redact his or her spurious claims, preferring instead to push a baseless agenda of "Premature optimization is the root of all evil." Welcome to 2020, where even failure is institutionalized.Miscarry
Using timeit on Python 3.8.5 and removing the string formatting I got 5.68 ms (pre-allocate) vs 8.26 ms (append). Almost 50% improvement, not too shabby!Corroboration
@CecilCurry same goes for the equally active OP who seems unwilling to change the accepted answer. I smell collusion! (joke) But seriously though, guys, why keep unwitting SO users in the dark?Treacle
There is a hugely important use case for this: you receive results from worker threads in arbitrary order, but you must collect them in some logical order. It can be done by appending a record consisting of desired offset and payload, then afterwards sorting by desired offset and extracting the payload column, but that is cumbersome and incurs sorting overhead. It is better to preallocate the list and put each payload immediately at the correct offset.Northwards
Wouldn't it be better to update the answer, or delete it completely, rather than just say to read the comments? With these kinds of answers, it gets incredible hard to navigate SO for useful information.Consolidate
U
98

Python lists have no built-in pre-allocation. If you really need to make a list, and need to avoid the overhead of appending (and you should verify that you do), you can do this:

l = [None] * 1000 # Make a list of 1000 None's
for i in xrange(1000):
    # baz
    l[i] = bar
    # qux

Perhaps you could avoid the list by using a generator instead:

def my_things():
    while foo:
        #baz
        yield bar
        #qux

for thing in my_things():
    # do something with thing

This way, the list isn't every stored all in memory at all, merely generated as needed.

Ultramodern answered 22/11, 2008 at 21:7 Comment(2)
+1 Generators instead of lists. Many algorithms can be revised slightly to work with generators instead of full-materialized lists.Kerrin
generators are a good idea, true. i was wanting a general way to do it besides the setting in-place. i guess the difference is minor, thoguh.Venavenable
S
72

Short version: use

pre_allocated_list = [None] * size

to preallocate a list (that is, to be able to address 'size' elements of the list instead of gradually forming the list by appending). This operation is very fast, even on big lists. Allocating new objects that will be later assigned to list elements will take much longer and will be the bottleneck in your program, performance-wise.

Long version:

I think that initialization time should be taken into account.

Since in Python everything is a reference, it doesn't matter whether you set each element into None or some string - either way it's only a reference. Though it will take longer if you want to create a new object for each element to reference.

For Python 3.2:

import time
import copy

def print_timing (func):
  def wrapper (*arg):
    t1 = time.time()
    res = func (*arg)
    t2 = time.time ()
    print ("{} took {} ms".format (func.__name__, (t2 - t1) * 1000.0))
    return res

  return wrapper

@print_timing
def prealloc_array (size, init = None, cp = True, cpmethod = copy.deepcopy, cpargs = (), use_num = False):
  result = [None] * size
  if init is not None:
    if cp:
      for i in range (size):
          result[i] = init
    else:
      if use_num:
        for i in range (size):
            result[i] = cpmethod (i)
      else:
        for i in range (size):
            result[i] = cpmethod (cpargs)
  return result

@print_timing
def prealloc_array_by_appending (size):
  result = []
  for i in range (size):
    result.append (None)
  return result

@print_timing
def prealloc_array_by_extending (size):
  result = []
  none_list = [None]
  for i in range (size):
    result.extend (none_list)
  return result

def main ():
  n = 1000000
  x = prealloc_array_by_appending(n)
  y = prealloc_array_by_extending(n)
  a = prealloc_array(n, None)
  b = prealloc_array(n, "content", True)
  c = prealloc_array(n, "content", False, "some object {}".format, ("blah"), False)
  d = prealloc_array(n, "content", False, "some object {}".format, None, True)
  e = prealloc_array(n, "content", False, copy.deepcopy, "a", False)
  f = prealloc_array(n, "content", False, copy.deepcopy, (), False)
  g = prealloc_array(n, "content", False, copy.deepcopy, [], False)

  print ("x[5] = {}".format (x[5]))
  print ("y[5] = {}".format (y[5]))
  print ("a[5] = {}".format (a[5]))
  print ("b[5] = {}".format (b[5]))
  print ("c[5] = {}".format (c[5]))
  print ("d[5] = {}".format (d[5]))
  print ("e[5] = {}".format (e[5]))
  print ("f[5] = {}".format (f[5]))
  print ("g[5] = {}".format (g[5]))

if __name__ == '__main__':
  main()

Evaluation:

prealloc_array_by_appending took 118.00003051757812 ms
prealloc_array_by_extending took 102.99992561340332 ms
prealloc_array took 3.000020980834961 ms
prealloc_array took 49.00002479553223 ms
prealloc_array took 316.9999122619629 ms
prealloc_array took 473.00004959106445 ms
prealloc_array took 1677.9999732971191 ms
prealloc_array took 2729.999780654907 ms
prealloc_array took 3001.999855041504 ms
x[5] = None
y[5] = None
a[5] = None
b[5] = content
c[5] = some object blah
d[5] = some object 5
e[5] = a
f[5] = []
g[5] = ()

As you can see, just making a big list of references to the same None object takes very little time.

Prepending or extending takes longer (I didn't average anything, but after running this a few times I can tell you that extending and appending take roughly the same time).

Allocating new object for each element - that is what takes the most time. And S.Lott's answer does that - formats a new string every time. Which is not strictly required - if you want to preallocate some space, just make a list of None, then assign data to list elements at will. Either way it takes more time to generate data than to append/extend a list, whether you generate it while creating the list, or after that. But if you want a sparsely-populated list, then starting with a list of None is definitely faster.

Swash answered 4/4, 2011 at 0:48 Comment(3)
hmm interesting. so the answer mite be - it doesnt really matter if you're doing any operation to put elements in a list, but if you really just want a big list of all the same element you should use the []* approachVenavenable
As an un-fun aside, this has interesting behavior when done to lists (e.g. to preallocate a m * n matrix): x = 3 * [3 *[0]] gives [[0, 0, 0], [0, 0, 0], [0, 0, 0]], but then assignment is wonky: x[0][0] = 1 gives [[1, 0, 0], [1, 0, 0], [1, 0, 0]].Hilten
Yes, because x = 3 * [3 *[0]] only allocates two lists. See this canonical post on the issue.Docent
M
39

The Pythonic way for this is:

x = [None] * numElements

Or whatever default value you wish to prepopulate with, e.g.

bottles = [Beer()] * 99
sea = [Fish()] * many
vegetarianPizzas = [None] * peopleOrderingPizzaNotQuiche

(Caveat Emptor: The [Beer()] * 99 syntax creates one Beer and then populates an array with 99 references to the same single instance)

Python's default approach can be pretty efficient, although that efficiency decays as you increase the number of elements.

Compare

import time

class Timer(object):
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        end = time.time()
        secs = end - self.start
        msecs = secs * 1000  # Millisecs
        print('%fms' % msecs)

Elements   = 100000
Iterations = 144

print('Elements: %d, Iterations: %d' % (Elements, Iterations))


def doAppend():
    result = []
    i = 0
    while i < Elements:
        result.append(i)
        i += 1

def doAllocate():
    result = [None] * Elements
    i = 0
    while i < Elements:
        result[i] = i
        i += 1

def doGenerator():
    return list(i for i in range(Elements))


def test(name, fn):
    print("%s: " % name, end="")
    with Timer() as t:
        x = 0
        while x < Iterations:
            fn()
            x += 1


test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)

with

#include <vector>
typedef std::vector<unsigned int> Vec;

static const unsigned int Elements = 100000;
static const unsigned int Iterations = 144;

void doAppend()
{
    Vec v;
    for (unsigned int i = 0; i < Elements; ++i) {
        v.push_back(i);
    }
}

void doReserve()
{
    Vec v;
    v.reserve(Elements);
    for (unsigned int i = 0; i < Elements; ++i) {
        v.push_back(i);
    }
}

void doAllocate()
{
    Vec v;
    v.resize(Elements);
    for (unsigned int i = 0; i < Elements; ++i) {
        v[i] = i;
    }
}

#include <iostream>
#include <chrono>
using namespace std;

void test(const char* name, void(*fn)(void))
{
    cout << name << ": ";

    auto start = chrono::high_resolution_clock::now();
    for (unsigned int i = 0; i < Iterations; ++i) {
        fn();
    }
    auto end = chrono::high_resolution_clock::now();

    auto elapsed = end - start;
    cout << chrono::duration<double, milli>(elapsed).count() << "ms\n";
}

int main()
{
    cout << "Elements: " << Elements << ", Iterations: " << Iterations << '\n';

    test("doAppend", doAppend);
    test("doReserve", doReserve);
    test("doAllocate", doAllocate);
}

On my Windows 7 Core i7, 64-bit Python gives

Elements: 100000, Iterations: 144
doAppend: 3587.204933ms
doAllocate: 2701.154947ms
doGenerator: 1721.098185ms

While C++ gives (built with Microsoft Visual C++, 64-bit, optimizations enabled)

Elements: 100000, Iterations: 144
doAppend: 74.0042ms
doReserve: 27.0015ms
doAllocate: 5.0003ms

C++ debug build produces:

Elements: 100000, Iterations: 144
doAppend: 2166.12ms
doReserve: 2082.12ms
doAllocate: 273.016ms

The point here is that with Python you can achieve a 7-8% performance improvement, and if you think you're writing a high-performance application (or if you're writing something that is used in a web service or something) then that isn't to be sniffed at, but you may need to rethink your choice of language.

Also, the Python code here isn't really Python code. Switching to truly Pythonesque code here gives better performance:

import time

class Timer(object):
    def __enter__(self):
        self.start = time.time()
        return self

    def __exit__(self, *args):
        end = time.time()
        secs = end - self.start
        msecs = secs * 1000  # millisecs
        print('%fms' % msecs)

Elements   = 100000
Iterations = 144

print('Elements: %d, Iterations: %d' % (Elements, Iterations))


def doAppend():
    for x in range(Iterations):
        result = []
        for i in range(Elements):
            result.append(i)

def doAllocate():
    for x in range(Iterations):
        result = [None] * Elements
        for i in range(Elements):
            result[i] = i

def doGenerator():
    for x in range(Iterations):
        result = list(i for i in range(Elements))


def test(name, fn):
    print("%s: " % name, end="")
    with Timer() as t:
        fn()


test('doAppend', doAppend)
test('doAllocate', doAllocate)
test('doGenerator', doGenerator)

Which gives

Elements: 100000, Iterations: 144
doAppend: 2153.122902ms
doAllocate: 1346.076965ms
doGenerator: 1614.092112ms

(in 32-bit, doGenerator does better than doAllocate).

Here the gap between doAppend and doAllocate is significantly larger.

Obviously, the differences here really only apply if you are doing this more than a handful of times or if you are doing this on a heavily loaded system where those numbers are going to get scaled out by orders of magnitude, or if you are dealing with considerably larger lists.

The point here: Do it the Pythonic way for the best performance.

But if you are worrying about general, high-level performance, Python is the wrong language. The most fundamental problem being that Python function calls has traditionally been up to 300x slower than other languages due to Python features like decorators, etc. (PythonSpeed/PerformanceTips, Data Aggregation).

Mena answered 11/6, 2014 at 22:21 Comment(5)
@NilsvonBarth C++ doesn't have timeitMena
Python has timeit, which you should use when timing your Python code; I'm not talking about C++, obviously.Addicted
This isn't correct answer. bottles = [Beer()] * 99 does not create 99 Beer objects. Instead, creates one Beer object with 99 references to it. If you'd mutate it, all elements on the list will be mutated, cause (bottles[i] is bootles[j]) == True for every i != j. 0<= i, j <= 99.Greenleaf
@Greenleaf You judged the answer as not correct, because the author used references as an example to fill a list? First, no one is requiring to create 99 Beer objects (as versus one object and 99 references). In the case of prepopulation (what he talked about), faster is better, as the value will be replaced later. Second, the answer is not about references or mutation at all. You are missing the big picture.Extravagance
@YongweiWu You're right actually right. This example doesn't make whole answer incorrect, it might be just misleading and it's simply worth to mention.Greenleaf
H
11

As others have mentioned, the simplest way to preseed a list is with NoneType objects.

That being said, you should understand the way Python lists actually work before deciding this is necessary.

In the CPython implementation of a list, the underlying array is always created with overhead room, in progressively larger sizes ( 4, 8, 16, 25, 35, 46, 58, 72, 88, 106, 126, 148, 173, 201, 233, 269, 309, 354, 405, 462, 526, 598, 679, 771, 874, 990, 1120, etc), so that resizing the list does not happen nearly so often.

Because of this behavior, most list.append() functions are O(1) complexity for appends, only having increased complexity when crossing one of these boundaries, at which point the complexity will be O(n). This behavior is what leads to the minimal increase in execution time in S.Lott's answer.

Source: Python list implementation

Haslett answered 12/1, 2017 at 18:20 Comment(0)
B
6

Concerns about preallocation in Python arise if you're working with NumPy, which has more C-like arrays. In this instance, preallocation concerns are about the shape of the data and the default value.

Consider NumPy if you're doing numerical computation on massive lists and want performance.

Biyearly answered 30/7, 2014 at 2:22 Comment(0)
I
5

I ran S.Lott's code and produced the same 10% performance increase by preallocating. I tried Ned Batchelder's idea using a generator and was able to see the performance of the generator better than that of the doAllocate. For my project the 10% improvement matters, so thanks to everyone as this helps a bunch.

def doAppend(size=10000):
    result = []
    for i in range(size):
        message = "some unique object %d" % ( i, )
        result.append(message)
    return result

def doAllocate(size=10000):
    result = size*[None]
    for i in range(size):
        message = "some unique object %d" % ( i, )
        result[i] = message
    return result

def doGen(size=10000):
    return list("some unique object %d" % ( i, ) for i in xrange(size))

size = 1000
@print_timing
def testAppend():
    for i in xrange(size):
        doAppend()

@print_timing
def testAlloc():
    for i in xrange(size):
        doAllocate()

@print_timing
def testGen():
    for i in xrange(size):
        doGen()


testAppend()
testAlloc()
testGen()

Output

testAppend took 14440.000ms
testAlloc took 13580.000ms
testGen took 13430.000ms
Incomer answered 21/10, 2009 at 19:9 Comment(3)
"For my proj the 10% improvement matters"? Really? You can prove that list allocation is the bottleneck? I'd like to see more on that. Do you have a blog where you could explain how this actually helped?Kerrin
@Kerrin try bumping the size up by an order of magnitude; performance drops by 3 orders of magnitude (compared to C++ where performance drops by slightly more than a single order of magnitude).Mena
This could be the case because as an array grows, it might have to be moved around in memory. (Think of how objects are stored there one after the other.)Cacoepy
G
2

Python's list doesn't support preallocation. Numpy allows you to preallocate memory, but in practice it doesn't seem to be worth it if your goal is to speed up the program.

This test simply writes an integer into the list, but in a real application you'd likely do more complicated things per iteration, which further reduces the importance of the memory allocation.

import timeit
import numpy as np

def list_append(size=1_000_000):
    result = []
    for i in range(size):
        result.append(i)
    return result

def list_prealloc(size=1_000_000):
    result = [None] * size
    for i in range(size):
        result[i] = i
    return result

def numpy_prealloc(size=1_000_000):
    result = np.empty(size, np.int32)
    for i in range(size):
        result[i] = i
    return result

setup = 'from __main__ import list_append, list_prealloc, numpy_prealloc'
print(timeit.timeit('list_append()', setup=setup, number=10))     # 0.79
print(timeit.timeit('list_prealloc()', setup=setup, number=10))   # 0.62
print(timeit.timeit('numpy_prealloc()', setup=setup, number=10))  # 0.73
Giveaway answered 2/9, 2022 at 14:7 Comment(0)
S
0

For some applications, a dictionary may be what you are looking for. For example, in the find_totient method, I found it more convenient to use a dictionary since I didn't have a zero index.

def totient(n):
    totient = 0

    if n == 1:
        totient = 1
    else:
        for i in range(1, n):
            if math.gcd(i, n) == 1:
                totient += 1
    return totient

def find_totients(max):
    totients = dict()
    for i in range(1,max+1):
        totients[i] = totient(i)

    print('Totients:')
    for i in range(1,max+1):
        print(i,totients[i])

This problem could also be solved with a preallocated list:

def find_totients(max):
    totients = None*(max+1)
    for i in range(1,max+1):
        totients[i] = totient(i)

    print('Totients:')
    for i in range(1,max+1):
        print(i,totients[i])

I feel that this is not as elegant and prone to bugs because I'm storing None which could throw an exception if I accidentally use them wrong, and because I need to think about edge cases that the map lets me avoid.

It's true the dictionary won't be as efficient, but as others have commented, small differences in speed are not always worth significant maintenance hazards.

Sontag answered 27/10, 2016 at 16:33 Comment(0)
K
0

Fastest Way - use * like list1 = [False] * 1_000_000

Comparing all the common methods (list appending vs preallocation vs for vs while), I found that using * gives the most efficient execution time.

import time

large_int = 10_000_000
start_time = time.time()

# Test 1: List comprehension
l1 = [False for _ in range(large_int)]
end_time_1 = time.time()

# Test 2: Using *
l2 = [False] * large_int
end_time_2 = time.time()

# Test 3: Using append with for loop & range
l3 = []
for _ in range(large_int):
    l3.append(False)
end_time_3 = time.time()

# Test 4: Using append with while loop
l4, i = [], 0
while i < large_int:
    l4.append(False)
    i += 1
end_time_4 = time.time()

# Results
diff_1 = end_time_1 - start_time
diff_2 = end_time_2 - end_time_1
diff_3 = end_time_3 - end_time_2
diff_4 = end_time_4 - end_time_3
print(f"Test 1. {diff_1:.4f} seconds")
print(f"Test 2. {diff_2:.4f} seconds")
print(f"Test 3. {diff_3:.4f} seconds")
print(f"Test 4. {diff_4:.4f} seconds")

print("\nTest 2 is faster than - ")
print(f"            Test 1 by - {(diff_1 / diff_2 * 100 - 1):,.0f}%")
print(f"            Test 3 by - {(diff_3 / diff_2 * 100 - 1):,.0f}%")
print(f"            Test 4 by - {(diff_4 / diff_2 * 100 - 1):,.0f}%")

python list initialization

Kilar answered 31/12, 2022 at 16:32 Comment(0)
P
-3

From what I understand, Python lists are already quite similar to ArrayLists. But if you want to tweak those parameters I found this post on the Internet that may be interesting (basically, just create your own ScalableList extension):

http://mail.python.org/pipermail/python-list/2000-May/035082.html

Plant answered 22/11, 2008 at 21:7 Comment(1)
The link is broken: "Not Found. The requested URL /pipermail/python-list/2000-May/035082.html was not found on this server."Indistinct

© 2022 - 2024 — McMap. All rights reserved.