How do I do the block letters part of Python 3 Course?

I have been stuck trying to do this part of the course. I don’t understand any of it to be honest with you. Can someone please help.

Question:
" Take a look at the complete alphabet and find your initials. Notice how each block letter is 7x5 and formed by the letter itself.

My initials are S and L, so my initials.py program should output:

Once you are ready, mark this task complete by checking off the box."

6 Likes

Hey there and welcome to the forums! :slightly_smiling_face:

Could you post what code you have right now? That would help to see were your at.

Essentially you just think of printing one line or letters at a time and these stack together to create an image. as an example lets say you wanted to print a smiley face and you draw it out like this:

   a    a
   a    a
a          a
  aa    aa 
    aaaa

Pardon my poor drawing :sweat_smile:

Now you print this one line at time:

print("   a    a   ")
print("   a    a   ")
print("a          a")
print("  aa    aa  ")
print("    aaaa    ")
3 Likes

It’s gets to more fun if we think like a programmer. Take for example,

alpha = {}
alpha['A'] = "  A     A A   A   A  AAAAA  A   A  A   A  A   A  "
alpha['B'] = "BBBB   B   B  B   B  BBBB   B   B  B   B  BBBB   "

for i in range(7):
  print (alpha['A'][i*7:i*7+7], alpha['B'][i*7:i*7+7])

Output

  A     BBBB   
 A A    B   B  
A   A   B   B  
AAAAA   BBBB   
A   A   B   B  
A   A   B   B  
A   A   BBBB   

We can build out the table so that eventually we can have a function that takes two letters and generates the output from the lookup table.

alpha['L'] = "L      L      L      L      L      L      LLLLL  "
alpha['S'] = " SSS   S   S  S       SSS       S  S   S   SSS   "
def print_initials(f, l):
  for i in range(7):
    print (alpha[f.upper()][i*7:i*7+7], alpha[l.upper()][i*7:i*7+7])
  
print_initials("s", "l")

Output

 SSS    L      
S   S   L      
S       L      
 SSS    L      
    S   L      
S   S   L      
 SSS    LLLLL  
3 Likes

Fortunately, since we’re stuck inside with fans and A/C so had some time to put this together.

alpha = {}
alpha['A'] = "  A     A A   A   A  AAAAA  A   A  A   A  A   A  "
alpha['B'] = "BBBB   B   B  B   B  BBBB   B   B  B   B  BBBB   "
alpha['C'] = " CCC   C   C  C      C      C      C   C   CCC   "
alpha['D'] = "DDDD   D   D  D   D  D   D  D   D  D   D  DDDD   "
alpha['E'] = "EEEEE  E      E      EEE    E      E      EEEEE  "
alpha['F'] = "FFFFF  F      F      FFF    F      F      F      "
alpha['G'] = " GGG   G   G  G      GGGGG  G   G  G   G   GGG   "
alpha['H'] = "H   H  H   H  H   H  HHHHH  H   H  H   H  H   H  "
alpha['I'] = "IIIII    I      I      I      I      I    IIIII  "
alpha['J'] = "JJJJJ    J      J      J    J J    J J     JJ    "
alpha['K'] = "K   K  K  K   K K    KK     K K    K  K   K   K  "
alpha['L'] = "L      L      L      L      L      L      LLLLL  "
alpha['M'] = "M   M  MM MM  MM MM  M M M  M   M  M   M  M   M  "
alpha['N'] = "N   N  NN  N  N N N  N  NN  N   N  N   N  N   N  "
alpha['O'] = " OOO   O   O  O   O  O   O  O   O  O   O   OOO   "
alpha['P'] = "PPPP   P   P  P   P  PPPP   P      P      P      "
alpha['Q'] = " QQQ   Q   Q  Q   Q  Q   Q  Q   Q  Q  Q    QQ Q  "
alpha['R'] = "RRRR   R   R  R   R  RRRR   R R    R  R   R   R  "
alpha['S'] = " SSS   S   S  S       SSS       S  S   S   SSS   "
alpha['T'] = "TTTTT    T      T      T      T      T      T    "
alpha['U'] = "U   U  U   U  U   U  U   U  U   U  U   U   UUU   "
alpha['V'] = "V   V  V   V  V   V  V   V  V   V   V V     V    "
alpha['W'] = "W   W  W   W  W   W  W W W  WW WW  WW WW  W   W  "
alpha['X'] = "X   X  X   X   X X     X     X X   X   X  X   X  "
alpha['Y'] = "Y   Y   Y Y     Y      Y      Y      Y      Y    "
alpha['Z'] = "ZZZZZ      Z     Z     Z     Z     Z      ZZZZZ  "

I favor TNT Strong Malt, brewed in Prince George, B. C. (hint, hint).


This is where the fun begins. Looking at all those patterns the one thing they all have in common is that they are binary in nature. Space, or letter? That means we can store them as 49 bits instead of 49 bytes.

Closer examination exposes a possible weakness if we choose the wrong bit value conversion. Binary numbers do not lead off with zeroes which suggests that spaces become 1’s so we can preserve them.

Consider,

>>> a = "  A     A A   A   A  AAAAA  A   A  A   A  A   A  "
>>> b = map(lambda x: '1' if x == ' ' else '0', [*a])
>>> c = "".join(b)
>>> c
'1101111101011101110110000011011101101110110111011'
>>> d = '0b' + c
>>> e = int(d, 2)
>>> e
491188304928187
>>> f = bin(e)
>>> f
'0b1101111101011101110110000011011101101110110111011'
>>> 

Forty-nine bits is a bit (pun intended) of an exaggeration of minimum. The integer will use whatever space Python has allotted it, some number of bytes, in this case seven or eight, one would imagine. Needless, it won’t be a 49 character string that likely uses closer to 100 bytes of memory. (Both points about memory use need to be followed up.)

The point is that integers take less space than strings, by far so that makes this concept plausible and encouraging.

Only thing missing now is converting those bits back into their respective letter string. Who would like to take this one up?


Give up? Can’t blame you. This is head scratching stuff. Let the confusion happen, but keep your eyes open.

>>> g = [*map(lambda x: ' ' if x == '1' else 'A', [*f[2:]])]
>>> h = "".join(g)
>>> h
'  A     A A   A   A  AAAAA  A   A  A   A  A   A  '
>>> 
2 Likes

Would you be able to get better memory management using bits? Python isn’t my strong point here so hopefully some C is acceptable.

Given in C a string is just a group of adjacent chars with a common pointer, and a char is simply a single byte, we can already limit our size to a byte per point, 7 * 7 = 49

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LETTER_SIZE 50

int main(void) {
  
  char* foo = malloc(50);
  strncpy(foo, "  A     A A   A   A  AAAAA  A   A  A   A  A   A  ", LETTER_SIZE - 1);
  free(foo);

}

Of course we have to add an extra byte as a buffer, could probably be ignored in this context but not usually good practice.

Though by defining a structure and bit fiddling we can get it down to only 7 bytes:

typedef struct {
  char a: 7;
  char b: 7;
  char c: 7;
  char d: 7;
  char e: 7;
  char f: 7;
  char g: 7;
} Letter;

// If anyone has insight into dividing the structure into less 
//  members please feel free to mention anything

Letter gen_letter(char* map) {
  Letter result = {0, 0, 0, 0, 0, 0, 0};
  
  char i = 0;

  while (i < 49) {
    
    int bit = (map[i] != ' ');

    if (i < 7) {
      result.a = result.a | bit << i % 7; 
    } else if (i < 14) {
      result.b = result.b | bit << i % 7; 
    } else if (i < 21) {
      result.c = result.c | bit << i % 7; 
    } else if (i < 28) {
      result.d = result.d | bit << i % 7; 
    } else if (i < 35) {
      result.e = result.e | bit << i % 7; 
    } else if (i < 42) {
      result.f = result.f | bit << i % 7; 
    } else {
      result.g = result.g | bit << i % 7; 
    }

    ++i;
  }
  return result;
}

To my knowledge this is the lowest you could go since pointers have to point to full bytes, and we can’t place the extra bits anywhere else, though we could use them for other things, such as how to display the character.

As for our conversion, because we aren’t concentrating on a numerical property any more, but rather each individual bit, the conversion can be done with leading 0s.

To print out our character you can print a space or letter based on each bit.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LETTER_SIZE 50

typedef struct {
  char a: 7;
  char b: 7;
  char c: 7;
  char d: 7;
  char e: 7;
  char f: 7;
  char g: 7;
} Letter;


Letter gen_letter(char* map) {
  Letter result = {0, 0, 0, 0, 0, 0, 0};
  
  char i = 0;

  while (i < 49) {
    
    int bit = (map[i] != ' ');

    if (i < 7) {
      result.a = result.a | bit << i % 7; 
    } else if (i < 14) {
      result.b = result.b | bit << i % 7; 
    } else if (i < 21) {
      result.c = result.c | bit << i % 7; 
    } else if (i < 28) {
      result.d = result.d | bit << i % 7; 
    } else if (i < 35) {
      result.e = result.e | bit << i % 7; 
    } else if (i < 42) {
      result.f = result.f | bit << i % 7; 
    } else {
      result.g = result.g | bit << i % 7; 
    }

    ++i;
  }
  return result;
}


void print_letter(Letter letter) {
  int i = 0;

  while (i < 49) {
    if (i < 7) {
      (letter.a & 1 << i % 7) ? printf("1") : printf(" ");
    } else if (i < 14) {
      (letter.b & 1 << i % 7) ? printf("1") : printf(" ");
    } else if (i < 21) {
      (letter.c & 1 << i % 7) ? printf("1") : printf(" ");
    } else if (i < 28) {
      (letter.d & 1 << i % 7) ? printf("1") : printf(" ");
    } else if (i < 35) {
      (letter.e & 1 << i % 7) ? printf("1") : printf(" ");
    } else if (i < 42) {
      (letter.f & 1 << i % 7) ? printf("1") : printf(" ");
    } else {
      (letter.g & 1 << i % 7) ? printf("1") : printf(" ");
    }
    if (++i % 7 == 0) {
      printf("\n");
    }
  }
}


int main(void) {
  char* map; 
  if ((map = malloc(LETTER_SIZE)) == NULL) {
    fputs("Memory allocation error", stderr);
    exit(EXIT_FAILURE);
  }

  strncpy(map, "  A     A A   A   A  AAAAA  A   A  A   A  A   A  ", LETTER_SIZE-1);

  Letter a = gen_letter(map);
  free(map);

  print_letter(a);

  return 0;
}

Also I know this isn’t the most elegant code, I’ll probably work on refining it later. Please let me know if you have any insight here.

Sorry, C is not my language so we will have to wait for someone to pipe in.

As for my effort to switch to numbers, I’ve got some wrong assumptions working against me. Still scratching my head.

1 Like

I got a question, I don’t know if I completely forgot a part of a lesson, but I don’t remember learning anything in the python course about this.

I did some porting and found that python bit fiddling is still more efficient, though not quiet as good as using C’s basic types.

Running sys.getsizeof() on your binary string gives a value of 100 bytes (Worth noting this testing was done in a repl):

import sys

a = "  A     A A   A   A  AAAAA  A   A  A   A  A   A  "
b = map(lambda x: '1' if x == ' ' else '0', [*a])
c = "".join(b)
d = '0b' + c
e = int(d, 2)
f = bin(e)

print(sys.getsizeof(f))
# ouputs 100

However bit fiddling with a single integer, set to 0, I managed to wean it down to a size of 32 bytes:

import sys

char_map = "   A     A A   A   A  AAAAA  A   A  A   A  A   A "

def bit_let(char_map):
  letter = 0
  for i in range(len(char_map)):
    if char_map[i] != ' ':
      letter |= (1<<i)
    else:
      letter &= ~(1<<i)
  return letter


def print_let(letter):
  for i in range(49):
    if letter & (1<<i):
      print("1", end="")
    else:
      print(" ", end="")
    
    if (i+1) % 7 == 0:
      print("")


letter = bit_let(char_map)
print_let(letter)

print("\nSizeof string", sys.getsizeof(char_map))  # outputs 98
print("Sizeof int", sys.getsizeof(letter))         # outputs 32

Still massive in comparison to working with a pure bit object, but only 32% the size of the binary string.

EDIT: Also still not very elegant

Most of these examples are rather advanced compared to where you are at the moment, but all good to remember for the future. When you get farther ahead it’s excellent material to come look back at.

1 Like

Not what we’re shooting for, at this stage. We need to ideate and test our assumptions as we go. I was convinced (even though assuming) that int would be less overhead than str just because of the nature of character data. Python uses Unicode so each character is at least two bytes. I also assumed that the system has overhead added to the data, regardless of type. Just seems integers would be better managed in memory than strings in terms of resource load.

Did get around the shoddy analysis of my earlier stated assumption. All it took in the end was brute force.

Given: alpha is in session memory.

def encode(dic):
  y = []
  for k in alpha:
    v = alpha[k]
    y.append(int("0b1" + "".join(map(lambda x: '0' if x == ' ' else '1', [*v])), 2))
  return dict(zip([*"abcdefghijklmnopqrstuvwxyz".upper()], y))

def decode(y):
  z = bin(beta[y.upper()])
  return "".join([*map(lambda x: y.upper() if x == '1' else ' ', [*z[3:]])])

def user_initials(f, l):
  for i in range(7):
    print (decode(f)[i*7:i*7+7], decode(l)[i*7:i*7+7])

beta = encode(alpha)  # run once global
user_initials("c", "d")
 CCC    DDDD   
C   C   D   D  
C       D   D  
C       D   D  
C       D   D  
C   C   D   D  
 CCC    DDDD   

Did you spot the brute force?

Looks like dictionaries have an overhead of 1176 bytes. To measure the true memory consumption, we would need to sum up all the items. This brings us back to @8-bit-gaming’s analysis of 98 bytes for the strings. and 32 bytes for the integers. It also supports my assumption that Unicode takes at least two bytes per character.

As it stands, our alpha table needs 4KB and our beta table needs 2KB when we include overhead.

Now that we have an implementation that produces an encoded dictionary from the alpha lookup table, all we really need to save in our production code is the beta dictionary; and park alpha.

We would also only need the decode() function.

beta = {'A': 634711601914436, 'B': 1093070503355000, 'C': 811594335396408, 'D': 1093070394303096, 'E': 1110524159860860, 'F': 1110524159860800, 'G': 811594461291064, 'H': 864372093166148, 'I': 1108861805398140, 'J': 1108861806454832, 'K': 864512694821956, 'L': 846641268531324, 'M': 865757136233028, 'N': 865475799097924, 'O': 811595417592376, 'P': 1093070503288896, 'Q': 811595417592884, 'R': 1093070503552068, 'S': 811594317636152, 'T': 1108861805398032, 'U': 864371975725624, 'V': 864371975722000, 'W': 864372009940548, 'X': 864364350022212, 'Y': 863395834497040, 'Z': 1108447341322364}

def user_initials(first, last):
  def decode(y):
    z = bin(beta[y.upper()])
    return "".join([*map(lambda x: y.upper() if x == '1' else "\u0020", [*z[3:]])])
  f, l = decode(first), decode(last)
  for i in range(7):
    j = i * 7
    k = j + 7
    print (f[j:k], l[j:k])

user_initials("c", "d")

Note: I use IDLE for all my coding. This does not work in Codebyte. It collapses side by side space characters.

I’m also content to post raw code. If the person wants to run it, then copy it and paste it wherever the user has a session environment going. Finding the Codebyte thing a little underwhelming and taxing at the same time. Give me code, not screen candy.

https://replit.com/@mtf/Two-Place-Initials

1 Like

Like @8-bit-gaming, explained, it will come up eventually. I also agree with him that this will be a topic to bookmark and return to, even just out of interest when the time comes. Pipe in if you ever do arrive back at this page. We’d be glad to hear how you are progressing.

And, if you do have any questions about the Python code above, don’t be afraid to ask. It can help to connect the dots, and we’ll relate it to the concept you may be about to learn, or have just learned. We’re a forum. You don’t have to hold up your hand. Ask a question and who knows what answer you’ll get? We only hope it is one that propels you forward.

1 Like

Would it make sense to declare the beta object on every run through? I’m not sure (tongue in cheek). This is one kind of closure that can come at a cost. Can we give it closure using a class, and make this a one time expense? (The goal is to get the object out of global scope.)

By closure I mean in the same direct sense as the above decode() function has closure. It is only callable in its parent scope, which happens to be a function. That means it cannot be called from the outside.

The built in class methods do chew up a lot of memory even if they aren’t being used. Perhaps a custom class could be in order?

You’ll have to forgive me, it took me long enough just to figure out what all you have going on, much less spot the brute force.

I’m hesitant on this. Given the work going into this, I feel like any practical use would require frequent use of the map, meaning a lot of wasted time doing the conversion. To get the object out of global scope are there any drawbacks to making it one of the functions attributes? This would mean it only have to be done once in the program regardless of how often the function is called? For my current code I implemented this in my printing function, but I’m very curious if there any problems this could cause later.

On the subject of memory management a list has a good bit (that word just keeps popping up) less over head than a dictionary does, and storing the string keys about triples the data required. Running getsizeof() on your beta and all its contents gave me a value of 3308 bytes:

print(getsizeof(beta) + len(beta) * (32+50))
#                                     \  \
#                                      \  sizeof single character string
#                                       sizeof integer

I hope I’m doing the math right, to my knowledge container classes only store pointers to their actual data so this has to be factored in when calculating.

However creating a list of the values instead results in only 1168 bytes for storage:

from sys import getsizeof as sizeof

def bit_let(char_map):
  letter = 0
  for i in range(len(char_map)):
    if char_map[i] != ' ':
      letter |= (1<<i)
    else:
      letter &= ~(1<<i)
  return letter

def encode(letters):
  return [bit_let(letters[i]) for i in letters]

foo = encode(alpha)
print("Sizeof map:", sizeof(foo) + len(foo*32))

This again assumes you have alpha already in memory.
Of course this makes finding indexes for inputs slightly harder but not by much. Since you can use ord() to get the integer value of a character you can take this and subtract A's value to get the appropriate list index in order to output.

Resulting in:

from sys import getsizeof as sizeof

def bit_let(char_map):
  letter = 0
  for i in range(len(char_map)):
    if char_map[i] != ' ':
      letter |= (1<<i)
    else:
      letter &= ~(1<<i)
  return letter

def encode(letters):
  return [bit_let(letters[i]) for i in letters]


def initials(a, b):
  if not hasattr(initials, "encoded"):
    global alpha
    initials.encoded = encode(alpha)
  
  a_map = initials.encoded[ord(a.upper()) - ord('A')]
  b_map = initials.encoded[ord(b.upper()) - ord('A')]

  for i in range(7):
    for j in range(14):
      add = i * 7
      if j < 7:
        if a_map & (1<<j+add):
          print(a, end="")
        else:
          print(" ", end="")

      else:
        num = j - 7
        if b_map & (1<<num+add):
          print(b, end="")
        else:
          print(" ", end="")
    print()

initials('s', 'c')

Could probably use some try/catch blocks but I guess that’s not our intent at the moment.

I may try to build some custom list class to shave off some more over head. Though I wonder if it’s possible to remove list methods from a single list object. I feel like that may cause some problems, but I’d imagine there’s no harm in testing it.

EDIT:
I noticed a typo in my memory calculation:

print("Sizeof map:", sizeof(foo) + len(foo*32))
# might should be
print("Sizeof map:", sizeof(foo) + len(foo)*32)

However the amount of data is still the same.

1 Like

All stored values have a 1 added to the front to insulate the empty spaces. When mapping, the slice excludes the first three characters from the iterable. We’re now able to give space a zero, and the letter a one.

In encode, `‘Ob1’ + …

In decode, [*z[3:]]

You’ll have observed that this is symbolic form of, list(z[3:]). In other words, a spread.

It occurred to me that the dictionary has string keys, so my memory estimates are off. However, since this is a fixed size object, the extra memory hit is worth it when we consider how easy it is to use the lookup table without the added logic. Though, it’s not a big ask just to retrieve the ordinal of a letter we already know and subtract 65 from it.


Found on SO

… the other answers use function attributes as a replacement for global variables; however, they do NOT get rid of global state, which is exactly the problem with global variables.

This is why I thought maybe a class would be good since then the dataset can be a class attribute, and would have closure that way.


z = bin(beta[y.upper()])

becomes,

z = theta[ord(y.upper()) - 65]

where theta is a list of integers.

The encode function would omit the step of casting a dictionary from a zip() object.

See the repl for the implementation of a list.

list size, 264 + 26*32 => 1096

1 Like

Still trying to wrap my head around this logic but I think I’m getting there. Is it due to the bin() function that the leading one is a necessity? So far I haven’t had to deal with this, but I’m guessing this is because I’m bit fiddling instead of using the built in function.

I guess I don’t understand closure as well as I thought, worth more research. If it being a function attribute means it is still part of the global scope, how will an object remove it? A python function is an object so it seems like that be almost the same. Unless you mean generate the object with in the function, but then I’d think it still require being defined at every call.

I got more research and testing ahead of me it seems. Saddening we can’t use single bytes in Python, I’ve gotten my C implementation down to 190 bytes for the whole map.

This was something interesting to see. A hard coded list takes up less data than one created in another way, even if their content is the same:

from sys import getsizeof as sizeof

a = [i for i in range(3)]
b = [0, 1, 2]

print(a, b)

print(sizeof(a), sizeof(b))

# a = 88 bytes overhead
# b = 80 bytes overhead

I’d guess due to the way it handles pointers.

We can also shave of more memory by converting the list to tuple, which for outputting purposes seems to work just as well as a list. This also negates any difference between hardcoding and softcoding, and still gives a smaller overhead:

a = [i for i in range (3)]
b = [1, 2, 3]
c = tuple(a)
d = tuple(b)

print(sizeof(c), sizeof(d))

Using tuples I’ve gotten the map down to 1080 bytes

from sys import getsizeof as sizeof

def bit_let(char_map):
  letter = 0
  for i in range(len(char_map)):
    if char_map[i] != ' ':
      letter |= (1<<i)
    else:
      letter &= ~(1<<i)
  return letter

encode = lambda map: tuple([bit_let(map[i]) for i in map])

def initials(a, b):
  if not hasattr(initials, "encoded"):
    global alpha
    initials.encoded = encode(alpha)
  a_map = initials.encoded[ord(a.upper()) - ord('A')]
  b_map = initials.encoded[ord(b.upper()) - ord('A')]
  for i in range(7):
    for j in range(14):
      add = i * 7
      sub = j - 7
      print((a if a_map & (1<<j+add) else " ") if j < 7 else (b if b_map & (1<<sub+add) else " "), end="")
    print()


beta = encode(alpha)
print("Size of map:", sizeof(beta) + 26*32)
print("\n")
initials('N', 'C') # Nicolaus Copernicus
initials('I', 'N') # Isaic Newton 
initials('A', 'E') # Albert Einstein 
1 Like

In order to be able to cast a binary to a decimal, the binary must be a Python str object with the prefix, 0b. Some of the maps have a space in the top left corner which if translated to a zero, would end up being a leading zero in the binary representation. Binary numbers do not have leading zeros so we end up with less than the intended 49 bits. By inserting a leading one on all strings, ('0b1') we solve the issue that surfaced on our first attempt. Now the stored integers have 50 bits, which once we remove the leading (highest order) bit, we get the correct 49 bit map.

In Python, the simplest data structure is a tuple. Only thing is, a tuple is not mutable so we cannot write a ‘tuple comprehension’ or the simplified form, the starred expression (what I called a spread, earlier) since that requires mutability. The star (or, splat) is Python’s unpacking tool. In this case we used it in two ways.

  • unpack a string, similar to list(string)
  • consume and unpack the map object.

They still require comprehension syntax so the unpacked values have a containing structure, hence, [*object].

Closure, in simple terms, is a protected state, non-global. There was a topic on this a couple of weeks ago in which I seem to remember raising factory functions as one example of closure. Can’t find the topic, just now but I believe it went something like this…

>>> from math import log
>>> def log_factory(base):
    return lambda x: log(x) / log(base)

>>> log2 = log_factory(2)
>>> log2(64)
6.0
>>> log8 = log_factory(8)
>>> log8(64)
2.0
>>> log16 = log_factory(16)
>>> log16(256)
2.0
>>> 

The base that we passed into our factory has closure in the returned function.


P. S. Please put your code in a repl so we can see it run. Thanks. Haven’t had a lot of practice ‘fiddling with bits’ though I do follow what is happening. Would like to witness it, first hand.

1 Like

Ah that makes sense, also explains why our maps end up with different integer values.

Here’s the main repl I’ve been working on:

https://replit.com/@ShaylinTRK/Block-Letters-1#main.py

And a second one I’ve been experimenting with today using a custom object.

https://replit.com/@ShaylinTRK/Block-letters-2#main.py

I’ve gotten the memory down to 880 bytes though I can’t say I’m certain what I’m doing. I’ve been using setattr() and getattr() to set makeshift indexes, though I don’t know that this is best practice.


Experimenting with classes I’m wondering how accurate our memory measurement is. For example:

class Foo():
  pass

a = Foo()
print(sizeof(a)) # 48 bytes

a.att = 5
print(sizeof(a)) # still 48 bytes

Assuming a single pointer is used for all attributes than memory shouldn’t be missed, but if a new pointer is added to the object, or perhaps to a separate list of attributes, this memory isn’t found by the getsizeof() function.
In short I feel like the second size should have had, at the very least, an extra 8 bytes for a new pointer, but hoping they are simply referred to by a single pointer.

1 Like

Good thing we’re only experimenting and don’t need to concern over best practice. The take away from this is the learning and flexing of wings. Call it a rabbit hole, or a path less taken.

Given that I’ve got an implementation that is very succinct and probably as quick as needs be, it is unlikely I would steer to the bit fiddling version, though it is very novel, and interesting in the learning sense. It takes time and practice to master that art and your time has been well spent, in my humble view. I don’t lament the time spent on this side, either so we both gained.

A production version of this would only require the integer tuple (yeah, I changed it to a tuple so it would immutable). As it turns out, most of the integers are sixteen digits with the odd one being fifteen. As strings they would end up taking 32 bytes of memory, or 30 on those instances that are only 15 digits, so strings would shave off a few bytes in terms of storage, but would require logic to cast as integers for the binary conversion. Toss the logic and store the integers.

My suspicion is that the actual addresses of the literal values in the code is what gets inserted into the namespace. so they technically don’t take any running memory, only their pointers do, which is about 208 bytes (8 * 26). The overhead of the tuple totals 248 bytes.

This is a fixed object that shaving a few more bytes off is not going to make a whole lot of difference. The point is that we have shaved it down to 32@ from 100@. I’ll be scratching my head how you shaved off another 200 from the 1080, but only until my attention span wanes.

As for the code, since space complexity is covered, there is just time complexity. I’ll bet anything that the more complex bit fiddling version is much more dependent on resources and will take considerably more time. Of course, I’ve been wrong before. What method of runtime measurement do you suggest?

My old A-10 has a 3 Gig clock so we should arrive at nearly the same results.

My thought process was to remove the majority of the lists built in methods that aren’t needed. Which since I only needed to append new values to it, and retrieve the values, I only needed three methods and 1 attribute. Since I have no pointers to work with though, I’m storing every index as a new attribute.

I’d actually guess the bit fiddling to be faster since your interacting directly with the memory, meaning you skip any built in function steps. However I’d guess my outputting process to be much slower than yours since I’m calling print once for every bit or 98 times plus newlines.

I’ll be away from the computer for most of the day but will probably experiment with runtime later today or tomorrow. Though I don’t have much experience measuring runtime, so this may be more up your alley.

I’ve been testing runtime speeds on the repls and as I expected my output was MUCH slower than yours due to the print calls. It was running between .0005 and .0007 seconds. Now I’ve gotten it down to only 7 print calls and running between .0001 and .00025 seconds. I’m not convinced this is the best way to measure the run time though, as I keep hitting an odd number every so often that is either ridiculously small or ridiculously large. And I’m unsure if factors like network speed are going to effect it since it’s a repl?

The time for the encoding has been between .0003 and .0006 seconds.

As for the actual code I’ve been running it similar to this:

import time

start_time = time.process_time()
initials('N', 'C') # Nicolaus Copernicus
end_time = time.process_time()
print("%s seconds" % (end_time - start_time))

The first call to initials() takes longer since it has encoded the map yet, but after that it has the map as an attribute and runs quicker.

Again I don’t have much experience measuring runtime, so if there’s a more efficient way I’m all ears.

1 Like