Wednesday, 25 February 2015

Entry #3: Recursion, Recursion, Recursion! (As Well As A Summary On OOP)

NOTE: I would like this blog entry to be graded.

Recursion is perhaps one of the more interesting parts of programming as it manages to simplify processes that would otherwise be more convoluted to code. It focuses on using properties already displayed in a function again when a certain case comes up. Recursive functions are very helpful for going over nested lists or tuples, restarting games or when a while loop is too inconvenient or complicated to be implemented. I had already messed around with a sort of simple version of recursion before, which I'll demonstrate in this first function.

def recurs(n):
    if n % 2 == 1 and n < 16:
        if n in [11, 13, 15]:
            n = n - 7
            print(n)
            recurs(n)            
        else:
            n = abs(n) * 2
            print(n)
            recurs(n)            
    elif n % 2 == 0 and n < 16:
        n = n - 11
        print(n)
        recurs(n)
    else:

        print(str(n) + ' is greater than 16')

This function has the interesting property of printing each number that goes through its designated step and returning '18 is greater than 16' for any number below 16. For example, recurs(7) produces:

14
3
6
-5
10
-1
2
-9
18

18 is greater than 16

It's easy for anyone to see that the function could very well just be a while loop, but it's fun to play around with this sort of style of repeating steps. But let's start going with functions that are more reminiscent of work we've done in class/labs.

def sumlen(L):
    """ (arbitrarily nested list of int) -> int
    
    Return the sum of the ints in the list divided by the length of the list
    """
    if isinstance(L, list):
        return int(sum([sumlen(x) for x in L]) / len(L))
    else:

        return L

The docstring clearly explains what the function does, and for the sake of analyzation, we'll look at some examples on how this function works.

>>> sumlen([1, 2, 3])

The list's sum is 1+2+3 and the length is 3. 6 / 3 = 2

>>> sumlen([10, [1, 2, 3], 3])
    
We have inside a recursive list. The whole list is length 3 and the inner list is length 3. Since we already calculated it's value, we know that what we're adding is 10+2+3 which divided by 3 would produce 5.

>>> sumlen([1, [2, 3, [4, 5], 6], 7, 9])

Now we have a recursive list of depth 2. First we'd concern ourselves with the most inner list which is int((4+5)/2) = 4. Now the 1st-depth inner list results in int((2+3+4+6)/4) = 3. This leaves with the list as [1, 3, 7, 9] which is (1+3+7+9)/4 = 5.

>>> sumlen([1, [1, 3], 2, [10, 15, 2], 3])

Again, we consider the inner lists first and then the whole list. The whole list would be [1, 2, 2, 9, 3] whose sum is 17 and divided by 5 and floored (or converted to an int) results in 3

Here we have a more complicated function.

def negative_hiccup(L):
    """ (arbitrarily nested list of int) -> int
    
    Consider a man who adds two numbers, but then subtracts the third
    number, following a pattern of (0 + a + b - c + d + e - f...). Return
    an int based on this pattern.
    
    """
    
    result = 0
    
    for i in range(len(L)):
        if i % 3 == 0 or i % 3 == 1:
            if isinstance(L[i], list):
                result = result + negative_hiccup(L[i])
            else:
                result = result + L[i]
        else:
            if isinstance(L[i], list):
                result = result - negative_hiccup(L[i])
            else:
                result = result - L[i]     
                

    return result

Technically, we could have converted this into a helper function for just a list and then used it recursively if we wanted it to consider it for a recursive list, but this is still a valid way of implementing it. Let's look at the examples.

>>> negative_hiccup([4])

This is simply 4 as the first item is added to 0.

>>> negative_hiccup([4, 1])

The pattern goes 0 + 4 + 1, so it results in 5.
    
>>> negative_hiccup([4, 1, 5])
    
As a reminder, when we have the third item (or an item in an index which % by 3 results in 2), we subtract it from the total, so 0 + 4 + 1 - 5 results in 0.

>>> negative_hiccup([4, 1, 5, 2])

Again, we know the pattern, so it'll just be 2. Where's the fun stuff?!

>>> negative_hiccup([4, 1, [5, 2]])

Ah! Now we've got something going on. See, when we hit index 2, the function considers the list as the start of another negative-hiccup calculation, adding 5 and 2 to 0. This makes the whole list [4, 1, 7]. So now when negative_hiccup looks over the whole list, it simply subtracts 7, leaving -2.

>>> negative_hiccup([4, 1, 5, 2, 3])
    
Gah, boring stuff again! It's 5! Next!

>>> negative_hiccup([4, 1, [5, 2, 3]])
   
Now, we're back to same example in which we just go through the pattern anew on the list. This time it's 0 + 5 + 2 - 3 which results in the whole list being [4, 1, 4], resulting in 1.

>>> negative_hiccup([4, 1, 5, 2, 3, 10])

Okay, back to this tripe, it's -5!

>>> negative_hiccup([4, [1, 5], 2, [3, 10]])

By now we know what to do with those inner lists, so we basically would go like this:

0 + 4 + (1 + 5) - 2 + (3 + 10) = 0 + 4 + 6 - 2 + 13 = 21

>>> negative_hiccup([4, [1, [5, 2, 3], 10]])

Oh! Now we got a deeper list to look at. By now we know we start with 4, but in the second index, we have [1, [5, 2, 3], 10]. We would have to go by the negative-hiccup calculation in the second index's inner list which result in the second index being reduced to [1, 4, 10] which equals -5. When added to 4, we get -1.

>>> negative_hiccup([45, [10, 17, [8, 9, 5], [15, [19, 32, [27, [5, 2, [3, 7, 2]], 39], 11], 10, 48], 33], 6, 24, 10, [5, 24]])

Uh...let's just say 220 and leave it at that.

So, there you have a little bit of recursive function fun!

##########################################################################

Object Oriented Programming Concept Summary:


  • Object Oriented Programming is focused on using custom classes with various special methods and properties included, some of which serve as Abstract Data Types (like lists, stacks, queues, trees).
  • Stacks follow a First In Last Out format in which objects are pushed to the top and then popped when necessary. It's easy to think of a stack because one can imagine a pile of books used for a project. You get the one on the top, use it's information and then follow with the next one. 
  • Queues follow a First In First Out format in which objects are enqueued into a queue and then dequequed when necessary. A very prominent example of a queue is a playlist. No matter what you add to it, the song you put in first plays. 
  • Trees are constructed with nodes, some which have children and others which do not (called leaves). A tree is a very useful ADT to visualize as based on the logic one applies to it, it can lead one to understand the relationship between a node and its children. Trees require an understanding of recursion as they are far more complex to implement than both stacks and queues. 

Monday, 2 February 2015

Entry #2: Creating A Lottery Ticket Subclass And Superclass

Recently we had learned about creating superclasses and subclasses and implementing them into the course. I decided that I wanted to demonstrate my understanding of this by making a lottery ticket using this approach. First I had to consider the barebones of any lottery ticket. These barebones would be essential in implementing the superclass. The idea behind a lottery ticket is that it contains information which based on certain rules will give out a prize. A ticket tends to be "empty" until it is revealed. Once it is revealed, then the prize is determined. 

This means:


  • A lottery ticket needs contents and a place to hold the prize
  • A lottery ticket starts off empty or "not revealed"
  • When a lottery ticket is revealed, the contents are set on the ticket
  • Having the contents, one can then determine the prize. 

So with that in mind, I create my lottery ticket superclass:


class LotteryTicket():
    """ A class to represent a lottery ticket """
    
    def __init__(self):
        """ (LotteryTicket) -> NoneType
        
        Initialize a lottery ticket to contain contents and prize
        
        >>> lot = LotteryTicket()
        >>> isinstance(lot, LotteryTicket)
        True
        >>> lot.contents
        ''
        >>> lot.prize
        0
        """
        
        self.contents = ''
        self.prize = 0

Here, it is noted that contents and prize are set right from the get-go, though since we are not sure what the contents will be, we leave it as a blank string. Prize is also set to 0 since we are not aware of what it will be based on the contents.
    
    def __str__(self):
        """ (LotteryTicket) -> str
        
        Return a string representation of LotteryTicket
        
        >>> lot = LotteryTicket()
        >>> print(lot)
        LotteryTicket
        Contents: 
        Prize: 0
        """
        
        return "LotteryTicket\nContents: {0}\nPrize: {1}".format(self.contents, self.prize)

I've defined the __str__ method more to give an idea of what a generic ticket should look like. 
    
    def __repr__(self):
        """ (LotteryTicket) -> str
        
        Return a shell representation of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    
    def __eq__(self, other):
        """ (LotteryTicket, LotteryTicket) -> bool
        
        Return whether two LotteryTickets are the same
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    def set_ticket(self):
        """ (LotteryTicket) -> NoneType
        
        Set the contents of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')
    
    def get_prize(self):
        """ (LotteryTicket) -> NoneType
        
        Obtain prize based on the contents of LotteryTicket
        
        """
        
        raise NotImplementedError('Subclass needed')

All of these methods have not been implemented but they are crucial to the skeleton of a lottery ticket so they've been included for the sake of reference and convenience. 

Now that I have a basic representation of a generic lottery ticket dubbed LotteryTicket, I can now start to think of making more specific LotteryTickets. There are many ways I can go about making more specific lottery tickets and I have even managed to come up with a few. But for the sake of convenience, I'm going to pool all of these ideas into one LotteryTicket subclass I'll call MegaWinningsTicket. 

from LotteryTicket import LotteryTicket

import random

class MegaWinningsTicket(LotteryTicket):
    """ A lottery ticket with Mega Winnings! 
    
    Price of MegaWinningsTicket determine which game you play!

    5 - Pick 2
    Try to get the winning two numbers!
    10 - Pick 3
    Try to get the winning three numbers!
    15 - Crossword
    Try to get the necessary letters to complete the crossword!
    20 - Golden Diamond

    Try to get the elusive Golden Diamond!
    """
    
    def __init__(self, price):
        """ (MegaWinningsTicket) -> NoneType
        
        Precondition: price must be in [5, 10, 15, 20]
            
        Initialize a MegaWinnings lottery ticket to contain contents and prize
        and to hold the value of price
            
        >>> mega = MegaWinningsTicket(5)
        >>> isinstance(mega, MegaWinningsTicket)
        True
        >>> mega.contents
        ''
        >>> mega.prize
        0
        >>> mega.price
        5
        """
            
        LotteryTicket.__init__(self)

        self.price = price

Notice that now there is an added parameter in MegaWinningsTicket dubbed price. While it may not be advisable to name this parameter price since it is so close to prize, this little bit allows me to extend the __init__ method to suit this subclass. Specifically, the price allows you to play a certain game that the MegaWinningsTicket can support. Let's go through each of them in order.


  • Pick 2
    Price: 5
    Contents: 2 numbers
    Objective: Obtain two numbers that are exactly the same to the results
    Prize: 2500 for both numbers
    Odds of winning the big prize: 1 in 100
  • Pick 3
    Price: 10
    Contents: 3 numbers
    Objective: Obtain three numbers that are exactly the same to the results
    Prize: 10000 for complete match (big prize), 5000 for two numbers that match
    Odds of winning the big prize: 1 in 1000
  • Crossword
    Price: 15
    Contents: 10 letters
    Objective: To get all the words in this crossword

      r
    c a b
      r
    b e l l
    a     a
    r     b
    e r r
    Prize: 50000 for all words removed (big prize), 100 for 5 words removed, 50 for 4 words removed, 25 for 3 words removed, 10 for 2 words removed, 2 for one removed
    Odds of winnings the big prize: 1 in 25294 (5311735 possible combinations / 210 that contain the letters c, a, b, r, e, l)
  • Golden Diamond
    Price: 20
    Contents: One of the following objects: Golden Diamond, Diamond, Gold, Zirconium, Pyrite, Gold-Painted Rock, Shiny Rock
    Objective: Obtain a golden diamond
    Prize: 10000000 for a golden diamond (big prize), 500000 for a diamond, 100000 for gold, 1000 for zirconium, 20 for pyrite
    Odds of winning the big prize:
    1 in 111111 (1 Golden Diamond + 10 Diamonds + 100 Gold + 1000 Zirconium + 10000 Pyrite + 50000 Gold-Painted Rocks + 50000 Shiny Rocks)
Each of these will be shown in more detail but one can see that there will have to be an implementation of __eq__ that is specific to this function 

def __str__(self):
        """ (MegaWinningsTicket) -> str
               
        Return a string representation of MegaWinningsTicket
               
        >>> mega = MegaWinningsTicket(5)
        >>> print(mega)
        MegaWinningsTicket
        Contents: 
        Prize: 0
        Price: 5
        """
        
        return "MegaWinningsTicket\nContents: {0}\nPrize: {1}\nPrice: {2}".format(self.contents, self.prize, self.price)

For the __str__ method, I overwrote the method so that it would also include the price in the string.
    
    def __repr__(self):
        """ (MegaWinningsTicket) -> str
           
        Return a shell representation of MegaWinningsTicket
        
        >>> mega = MegaWinningsTicket(5)
        >>> mega
        MegaWinningsTicket(, 0, 5)
        """    
        
        return "MegaWinningsTicket({0}, {1}, {2})".format(self.contents, self.prize, self.price)

I have implemented the __repr__ method more as practice in doing so since there is no purpose it serves in the code.

def __eq__(self, other):
        """ (MegaWinningsTicket, MegaWinningsTicket) -> bool
        
        Return whether two MegaWinningsTickets are the same
        
        >>> mega = MegaWinningsTicket(5)
        >>> winnings = MegaWinningsTicket(5)
        >>> ticket = MegaWinningsTicket(10)
        >>> mega == winnings
        True
        >>> mega == ticket
        False
        >>> mega.contents = '3'
        >>> mega == winnings
        False
        >>> winnings.contents = '3'
        >>> winnings.prize = 20
        >>> mega == winnings
        True
        """
        
        return self.contents == other.contents and self.price == other.price

Here, I've made it clear that contents have to be the same as well as price (which isn't necessary in the following code, but asserts that the tickets belong to the same game). This brings up a question as to why I didn't have this implemented in the LotteryTicket superclass:

return self.contents == other.contents

Well that is because the way that certain tickets and games conduct themselves may rely on checking for equality in different ways, which may be a matter of parameters that the LotteryTicket subclass contains or what is being checked in contents.

 def set_ticket(self):
        """ (MegaWinningsTicket) -> NoneType
        
        Set the contents of MegaWinningsTicket based on price
        
        """
        
        number_list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        
        if self.price == 5:  # Pick 2
            self.contents = [random.choice(number_list), random.choice(number_list)]
            
        if self.price == 10: # Pick 3
            self.contents = [random.choice(number_list), random.choice(number_list), random.choice(number_list)]
            
        if self.price == 15: # Crossword
            alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
            words = ['cab', 'bell', 'rare', 'err', 'bare', 'lab']
            print('The words are: {}'.format(words))
            letters = []
            
            for i in range(10):
                letter = random.choice(alphabet)
                letters.append(letter)
                alphabet.remove(letter)
                
            print('Your letters are: {}'.format(letters))
            new_words = []
            
            for word in words:
                for letter in letters:
                    word = word.replace(letter, '')
                new_words.append(word)
                
            print(new_words)
            self.contents = new_words.count('')
            
        if self.price == 20: #Golden Diamond
            
            gem_list = ['Golden Diamond'] + ['Diamond'] * 10 + ['Gold'] * 100 + ['Zirconium'] * 1000 + ['Pyrite'] * 10000 + ['Gold-Painted Rock'] * 50000 + ['Shiny Rock'] * 50000
            self.contents = random.choice(gem_list)

Now this is where we get to the implementation of an important part of the LotteryTicket subclass, that being the set_ticket method which "reveals" the ticket by setting the contents depending on the price. For Pick 2, a list of two numbers is set. For Pick 3, a list of three numbers is set. For Crossword, the amount of words removed is set. And lastly, for Golden Diamond, a string is set. Having contents set will then help us to figure out how to get the prize.
            
    def get_prize(self):
        """ (MegaWinningsTicket) -> NoneType
        
        Precondition: self.price == 15 or self.price == 20 and MegaWinningsTicket
        has been given contents by the set_ticket method
        
        Obtain prize based on the contents of MegaWinningsTicket
        
        """
        
        if self.price == 15:
            possible_winnings = [0, 2, 10, 25, 50, 100, 50000]
            self.prize = possible_winnings[self.contents]
        
        if self.price == 20:
            possible_winnings = {'Gold Diamond': 10000000, 'Diamond': 500000, 'Gold': 100000, 'Zirconium': 1000, 'Pyrite': 20, 'Gold-Painted Rock': 0, 'Shiny Rock': 0}
            self.prize = possible_winnings[self.contents]

Notice how in the docstring, it specifically indicates that set_ticket must have been run on the MegaWinningsTicket for get_prize to properly work. For Crossword, the contents is used as an index on the list possible_winnings. For example, if 2 words were removed, 2 is the contents. And if 2 is the contents, the prize is possible_winnings[2] which equals 10, thus getting the prize and setting it. Golden Diamond works similarly with the contents serving as a key in the dict possible_winnings. These approaches do not rely on another ticket as the results, so therefore Pick 2 and Pick 3 need their own method
    
    def pick_prize(self, results):
        """ (MegaWinningsTicket, MegaWinningsTicket) -> NoneType
        
        Precondition: self.price == 5 or self.price == 10 and both MegaWinningsTickets
        has been given contents by the set_ticket method
        
        Obtain prize based on how well the ticket compares with results
        
        >>> mega = MegaWinningsTicket(5)
        >>> results = MegaWinningsTicket(5)
        >>> mega.contents = [4, 2]
        >>> results.contents = [4, 2]
        >>> mega.pick_prize(results)
        >>> mega.prize
        2500
        >>> mega.contents = [0, 2]
        >>> mega.pick_prize(results)
        >>> mega.prize
        0
        >>> mega = MegaWinningsTicket(10)
        >>> results = MegaWinningsTicket(10)
        >>> mega.contents = [1, 2, 3]
        >>> results.contents = [1, 2, 3]
        >>> mega.pick_prize(results)
        >>> mega.prize
        10000
        >>> mega.contents = [1, 2, 4]
        >>> mega.pick_prize(results)
        >>> mega.prize
        5000
        >>> mega.contents = [1, 5, 4]
        >>> mega.pick_prize(results)
        >>> mega.prize
        0
        """
        
        if self.price == 5:
            if self == results:
                self.prize = 2500
            else:
                self.prize = 0
        
        if self.price == 10:
            numbers = list(results.contents)
            for num in self.contents:
                if num in numbers:
                    numbers.remove(num)
            if len(numbers) == 0:
                self.prize = 10000
            if len(numbers) == 1:
                self.prize = 5000
            if len(numbers) >= 2:
                self.prize = 0

Pick 2 was easy to implement since it just relies on using what we wrote on with __eq__. Pick 3 needed to check if at the very least 2 numbers were the same, so we couldn't have simply used the __eq__ approach. Though we could have if the conditions were equally as harsh.
                    
    def is_mega_winner(self):
        """ (MegaWinningsTicket) -> str
        
        Return a string indicating if MegaWinningsTicket is a winner 
        
        >>> mega = MegaWinningsTicket(5)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 1000
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 2500
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(10)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 10000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(15)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 50000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega = MegaWinningsTicket(20)
        >>> mega.is_mega_winner()
        'Sorry, try again'
        >>> mega.prize = 100000
        >>> mega.is_mega_winner()
        'MEGA WINNER!'
        >>> mega.prize = 10000000
        >>> mega.is_mega_winner()
        'ULTIMATE MEGA WINNER! CONGRATULATIONS'
        """
        
        if self.prize == 10000000:
            return 'ULTIMATE MEGA WINNER! CONGRATULATIONS'
        elif 10000000 > self.prize >= 2500:
            return 'MEGA WINNER!'
        else:
            return 'Sorry, try again'

Last and not least is a simple method that tells us whether or not the prize we have is worth of a mega win. A mega win is dictated by if we get a prize who is larger than 2500. Though it we were to get the Golden Diamond in Golden Diamond, we would receive a more congratulatory string. 

There's so many other subclasses of LotteryTicket we can create, each having their own implementations of the methods necessary for the class, some which may overwrite the code, others which may extend it. Though one thing is for sure...trying to win these games might not be as simple. 

Monday, 19 January 2015

Entry #1: Why Geeks Should Write (And Getting Back Into The Programming Groove)

It might be a very tricky thing for me to answer the question of why geeks should write. I wouldn't consider myself a geek in the sense that it's being used in this context. I know more about writing than I do programming and computer science (hence why I'm in U of T and not working at Microsoft, Apple, Google or any other big tech company). I could certainly say why it's essential to write in a general sense, but to specify it to geeks would have to require understanding what it means to be a geek. Geek tends to be a word that either has a softer or similar sting to the dreaded "nerd", and nerd is often associated to those heavily fixated on an academic subject. Nowadays it has expanded to being a more condescending way of saying "an expert on a given subject". So by that, perhaps I could have the label of a writing geek (or nerd if you're feeling extra mean) on me. 

But why do I bring up the negative connotation on geek and nerd? Shouldn't I just say why they should write? Well it's because the reason that there is that aggressive tone towards the geek comes from their lack of being able to write properly. I'm sure that a geek is able to impress any academic with their verbose prose but to the everyday citizen, nothing truly resonates. And since it doesn't resonate, there is no relevance to them. But a geek is wrapped up in their subject and is desperately trying to inform others about what they know. By doing so, either one or the other starts to become aggressive. The geek for not getting through to the citizen and the citizen being annoyed with this supposed whack-job bombarding them with nonsense. 

Geeks need to build the bridge between their knowledge and others, and in order to do that, they need to communicate properly. They have to find connections between their audience and their subject so that the audience becomes aware of what they have done and can appreciate all their effort. It may be easier for a geek to speak on the level of one versed in their jargon but not everyone who knows their same language will be able to give them the opportunities to become better. So they have to practice a multitude of ways of saying the same thing for whomever wants to know about what they're working on. That, in turn, removes the foreign and obnoxious nature that geek often invokes. 

########################################################

To change the subject, I've decided that for the first two weeks to create a class to practice my programming skills, as well as attempt to explain the idea behind said class.

I've decided that I want to create a simple representation of a file cabinet containing files and safe files. A file is any item that Python can contain in a list or tuple. A safe file is an object that is a tuple that contains 2 items, with the first item (index 0) containing a password (which must be a string) and the second item (index 1) containing the file. First, let's initialize it.

class Cabinet:
    """ An object of the bureaucracy """


    def __init__(self):
        """ (Cabinet) -> NoneType
        
        Initialize new cabinet self to contain an empty list files
        
        >>> c = Cabinet()
        >>> c.files
        []
        """
        
        self.files = []

I've made it so that when one creates a Cabinet, it will begin empty. This is so that it acts like a file cabinet as they usually do not contain any items inside them (unless one steals it from someone else). So in order to add items to Cabinet, I create a method dubbed file_away.

def file_away(self, file):
        """ (Cabinet, file) -> NoneType
        
        Store a file in self
        
        >>> c = Cabinet()
        >>> c.file_away('f')
        >>> c.files
        ['f']
        """
        
        self.files.append(file)

So now we can add files to the cabinet. But what about a safe file? That has a different set of parameters which you can see with the safe_file method.

def safe_file(self, file, code):
        """ (Cabinet, file, str) -> NoneType
        
        Precondition: code must be unique for every file
        
        Store a safe file in self
        >>> c = Cabinet()
        >>> c.safe_file('f', '059401')
        >>> c.files
        [('059401', 'f')]
        """
        if isinstance(code, str):
            self.files.append((code, file))

Notice how safe_file converts the code and file into a tuple if and only if the code is a string. One thing I haven't included is an error exception so that if the user does not put an item that isn't a string, a message comes up. Though since the docstring already indicates that the code parameter has to be string, checking to see if code is a string is somewhat pointless. 

Now you might be wondering what makes a safe_file special beyond its format. Well for that, I need to bring up the __str__ and __repr__ methods.


def __str__(self):
        """ (Cabinet) -> str
        
        Return a string version of self
        
        >>> c = Cabinet()
        >>> c.file_away(5001)
        >>> c.file_away(['1', '6'])
        >>> c.safe_file('f', '059401')
        >>> print(c)
        Cabinet: 
        [5001, ['1', '6']]
        1 safe file(s)
        """
        
        files = []
        safe_files = 0
        
        for i in self.files:
            if isinstance(i, tuple) and len(i) == 2:
                safe_files = safe_files + 1
            else:
                files.append(i)
                
        return 'Cabinet: \n{0}\n{1} safe file(s)'.format(files, safe_files)
    
    def __repr__(self):
        """ (Cabinet) -> str
        
        Return a shell representation of self
        
        >>> c = Cabinet()
        >>> c.file_away(5001)
        >>> c.file_away(['1', '6'])
        >>> c.safe_file('f', '059401')
        >>> c
        Cabinet([5001, ['1', '6']])
        """
        files = []
        safe_files = 0
        for i in self.files:
            if not (isinstance(i, tuple) and len(i) == 2):
                files.append(i)
        
        return 'Cabinet({})'.format(files)


Notice how the safe files are omitted from the representation and are only shown in amount when Cabinet is turned into a string. That is to ensure their safety from others who wish to access the file. It creates a semblance of privacy. Naturally, there is a way to get around that by seeing all of the files with self.files, which reveals both the file and the code to access the file, but you'll see soon why code has a purpose. 

Creating a Cabinet, we can now create a method, file_search, to see whether or not we can find a file.

def file_search(self, file):
        """ (Cabinet, file) -> index/str
        
        Precondition: file must not be a tuple of len 2
        
        Return the index that the file is in, provided it exists in self

        >>> c = Cabinet()
        >>> c.file_away(1)
        >>> c.file_away(2)
        >>> c.file_away(3)
        >>> c.file_search(2)
        1
        >>> c.file_search(4)
        'That file does not exist'
        """
        for i in range(len(self.files)):
            if self.files[i] == file:
                return i
        
        return 'That file does not exist'

To the astute reader, one could tell that file_search uses linear search, which while serviceable, may be slower for when Cabinet contains various files. I have also created a message for if a file cannot be located within the Cabinet. Be aware that in file_search's docstring, there is a precondition which is "forbidding" the search of a safe file. One could look for it since I have not written any code that explicitly prohibits you from accessing it through file_search, but to humor me, why don't we try looking at this next method, reveal_safe_file?

def reveal_safe_file(self, code):
        """ (Cabinet, str) -> file/str
        
        Return file by using code as an attempt
        
        >>> c = Cabinet()
        >>> c.safe_file('shh...', '1-1-3-1')
        >>> c.safe_file(42, 'meaning_of_life')
        >>> c.reveal_safe_file('meaning_of_life')
        42
        >>> c.reveal_safe_file('qwertyasdfzxcv')
        'No file with that code exists'
        """
        safe_files = []
        for i in self.files:
            if isinstance(i, tuple) and len(i) == 2:
                safe_files.append(i)
        
        for file in safe_files:
            if file[0] == code:
                return file[1]
        
        return 'No file with that code exists'

This is a slightly more complicated method to follow. First, I append all safe files to a list called safe_files. Then within that list, I search through the first indexes of each of those safe_files to see if one matches with code. If they do, I return the file which is in the second index. But if not, I simply return that there is no file with that code in the Cabinet. This is where code comes into use and would be the ideal way a user could access a safe file. 

This function is of course very rudimentary. There are still a variety of concepts I have missed such as:

  • __eq__ method to check if two Cabinets are the same
  • Creating methods like steal_files and copy_files to move/copy files from one Cabinet to another.
  • Creating methods like shift_files and switch_files to move files within a Cabinet
  • Creating restrictions in file_search so that searching for safe files would be prohibited
  • Being able to remove a file from Cabinet
  • Being able to sort Cabinet by a variety of ways that suit me
  • Checking if there are two of the same file
  • Ensuring that each code is unique to a designated file regardless of what the file contains
And probably various others. Still, I feel it's good practice for now and perhaps I may build on it in the future. For now, here is the completed class:


'''
A file is any item that Python can contain in a list or tuple

A safe file is an item that is a tuple with len 2, with index 0 containing
a password (code) and index 1 containing the file. Safe files can only
be accessed using reveal_safe_file to open the file and self.files for
docstring/moderator purposes
'''

class Cabinet:
    """ An object of the bureaucracy """
    
    def __init__(self):
        """ (Cabinet) -> NoneType
        
        Initialize new cabinet self to contain an empty list files
        
        >>> c = Cabinet()
        >>> c.files
        []
        """
        
        self.files = []
        
    def file_away(self, file):
        """ (Cabinet, file) -> NoneType
        
        Store a file in self
        
        >>> c = Cabinet()
        >>> c.file_away('f')
        >>> c.files
        ['f']
        """
        
        self.files.append(file)
        
    def safe_file(self, file, code):
        """ (Cabinet, file, str) -> NoneType
        
        Precondition: code must be unique for every file
        
        Store a safe file in self
        >>> c = Cabinet()
        >>> c.safe_file('f', '059401')
        >>> c.files
        [('059401', 'f')]
        """
        if isinstance(code, str):
            self.files.append((code, file))    
    
    def __str__(self):
        """ (Cabinet) -> str
        
        Return a string version of self
        
        >>> c = Cabinet()
        >>> c.file_away(5001)
        >>> c.file_away(['1', '6'])
        >>> c.safe_file('f', '059401')
        >>> print(c)
        Cabinet: 
        [5001, ['1', '6']]
        1 safe file(s)
        """
        
        files = []
        safe_files = 0
        
        for i in self.files:
            if isinstance(i, tuple) and len(i) == 2:
                safe_files = safe_files + 1
            else:
                files.append(i)
                
        return 'Cabinet: \n{0}\n{1} safe file(s)'.format(files, safe_files)
    
    def __repr__(self):
        """ (Cabinet) -> str
        
        Return a shell representation of self
        
        >>> c = Cabinet()
        >>> c.file_away(5001)
        >>> c.file_away(['1', '6'])
        >>> c.safe_file('f', '059401')
        >>> c
        Cabinet([5001, ['1', '6']])
        """
        files = []
        safe_files = 0
        for i in self.files:
            if not (isinstance(i, tuple) and len(i) == 2):
                files.append(i)
        
        return 'Cabinet({})'.format(files)
    
    def file_search(self, file):
        """ (Cabinet, file) -> index/str
        
        Precondition: file must not be a tuple of len 2
        
        Return the index that the file is in, provided it exists in self
        
        >>> c = Cabinet()
        >>> c.file_away(1)
        >>> c.file_away(2)
        >>> c.file_away(3)
        >>> c.file_search(2)
        1
        >>> c.file_search(4)
        'That file does not exist'
        """
        for i in range(len(self.files)):
            if self.files[i] == file:
                return i
        
        return 'That file does not exist'
    
    def reveal_safe_file(self, code):
        """ (Cabinet, str) -> file/str
        
        Return file by using code as an attempt
        
        >>> c = Cabinet()
        >>> c.safe_file('shh...', '1-1-3-1')
        >>> c.safe_file(42, 'meaning_of_life')
        >>> c.reveal_safe_file('meaning_of_life')
        42
        >>> c.reveal_safe_file('qwertyasdfzxcv')
        'No file with that code exists'
        """
        safe_files = []
        for i in self.files:
            if isinstance(i, tuple) and len(i) == 2:
                safe_files.append(i)
        
        for file in safe_files:
            if file[0] == code:
                return file[1]
        
        return 'No file with that code exists'

if __name__  ==  '__main__':
    import doctest
    doctest.testmod()