<jupyter_start><jupyter_text>Study session 11 - classes BIOINF 575 - Fall 2020 SOLUTION  Resources - classes and object oriented programminghttps://docs.python.org/3/tutorial/classes.html   https://www.python-course.eu/python3_properties.php   https://www.tutorialspoint.com/python3/python_classes_objects.htm   https://www.w3schools.com/python/python_classes.asp   https://www.geeksforgeeks.org/python-classes-and-objects/  https://www.datacamp.com/community/tutorials/property-getters-settersproperty ____ Below is the Pileup class to provide functionality for a pileup object that stores information regarding alignment at a specific genomic position.<jupyter_code># Pileup uses Counter so we need to import before we can create an object of that type
# The definition will run with no error bu we will not be able to use it without importing Counter

from collections import Counter
# Pileup Object
class Pileup:
    """
    Contains a counter of nulceotides aligned at a genomic position and 
    computes depth and consensus for a genomic position
    
    Attributes:
    counts (Counter):  counter of all nucleotides aligned at the genomic position
    depth (int): total number of reads aligned at the genomic position
    consensus (str): most common nucleotide
    """
    
    # method called when we initialize an object of type Pileup
    # p = Pileup() or p = Pileup(Counter("AAA")) will call this method
    def __init__(self, counts = None):
        self.counts = counts
        if self.counts == None:
            self.counts = Counter()
        self.depth = sum(self.counts.values())
        if self.depth == 0:
            self.__consensus = "" # using __ at the beginning of the attribute name makes it inaccessible outside of the class definition 
        else:
            self.__consensus = self.counts.most_common()[0][0]

    # see more examples in the last link in RESOURCES
    # property (the @property above the function makes it so) 
    # should be used without () when called for an object e.g. p.consensus
    # getter that allows us to display the value of a hidden attribute outside of the class definition
    @property
    def consensus(self):
        """
        Get the consensus nucleotide for the pileup
        """
        return self.__consensus 

    # method that implements what the string representation of a Pileup object is
    # for a Pileup object p this method is called when print(p) or str(p) are used
    def __str__(self):
        return f"Pileup({self.counts})"
 

    # method that implements what the string representation of a Pileup object is 
    # and that allows you to create another object idential to the one you are calling it for
    # it is called when you use the name of the object to see its value
    # typing 
    # it is called when print(p) or str(p) are used
    def __repr__(self):
        return f"Pileup({self.counts})"
    
    # used to implement the addition operator
    # if p1 and p2 are Pileup objects this is called when we compute p1+p2  
    # self will be assigned p1 and p will be assigned p2 in the __add__ method 
    def __add__(self, p):
        c = self.counts.copy() # make a copy otherwise we are updating p1 when we do p1+p2
        c.update(p.counts)
        return Pileup(c)
        
    #the first argument of every method in a class (self) will be assigned the value of the object we call it from
    # e.g if variable p contains a pileup object the statement p.update("AAA") will call this method below and assign p to self and "AAA" to seq
    def update(self, seq):
        """
        Update the counts depth and consensus for the pileup
        Given a sequence of nucleotides to add to the pileup
        """
        self.counts.update(seq)
        self.depth = sum(self.counts.values())
        self.__consensus = self.counts.most_common()[0][0]<jupyter_output><empty_output><jupyter_text>We copied and ran all or part of the below code for each exercise to test we did not break something while trying to implement the exercise requirement<jupyter_code>p = Pileup()
p
# hidden attribute only accesible in the class definition
p.__consensus
#dir(p)
# Nothing in Python is truly private; internally
p._Pileup__consensus = 3
p.consensus
p.counts
p.depth
# we have only implementerd a getter (a way to retrieve the value)
# a setter (a way to set the value) was not implemented 
# we want this to be changed only internally when the counts change
p.consensus = "T"
# we update the counts but the consensus and depth stay the same
# we are going to make this a property so we can also update the depth and consensus when we assign a value to the counts
# see the class defintion below
p.counts = Counter("ACGTTTT")
p.counts
p.depth
p.consensus
p.depth = 40
p.depth
p.update("ACGTTGGGG")
p.counts
p.consensus
p.depth<jupyter_output><empty_output><jupyter_text>Exercise - Make the Pileup attribute depth read only - Test the change<jupyter_code># Pileup Object
class Pileup:
    """
    Contains a counter of nulceotides aligned at a genomic position and 
    computes depth and consensus for a genomic position
    
    Attributes:
    counts (Counter):  counter of all nucleotides aligned at the genomic position
    depth (int): total number of reads aligned at the genomic position
    consensus (str): most common nucleotide
    """
    
    
    def __init__(self, counts = None):
        self.__counts = counts
        if self.__counts == None:
            self.__counts = Counter()
        self.__depth = sum(self.__counts.values())
        if self.__depth == 0:
            self.__consensus = ""        
        else:
            self.__consensus = self.__counts.most_common()[0][0]

    @property # getter
    def consensus(self):
        """
        Get the consensus nucleotide for the pileup
        """
        return self.__consensus
    
    @property # getter
    def depth(self):
        """
        Get the depth for the pileup - number of reads that aliged at that position
        """
        return self.__depth
    
    @property # getter
    def counts(self):
        """
        Get the counts for the pileup - frequencies of the nucleotides that aligned at that position
        """
        return self.__counts
    
    @counts.setter # property setter used when we assign a value to the property: p.counts = Counter("AACG")
    def counts(self, counts_value):
        self.__counts = counts_value
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]

    def __str__(self):
        return f"Pileup({self.__counts})"
    
    def __repr__(self):
        return f"Pileup({self.__counts})"
    
    def __add__(self, p):
        c = self.__counts.copy()
        c.update(p.counts)
        return Pileup(c)
        
        
    def update(self, seq):
        """
        Update the counts, depth and consensus nucleotide for the pileup
        given a sequence of nucleotides to add to the pileup
        """
        self.__counts.update(seq)
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]<jupyter_output><empty_output><jupyter_text>We also updated the counts to a property in the class definition above to be able to update the consensus and depth when we set up the counts<jupyter_code>p = Pileup()
p
p.consensus
p.counts
p.depth
p.consensus = "T"
p.counts = Counter("ACGTTTT")
p.counts
p.depth
p.consensus
# depth is now read only as intended
# it can only be computed using the counts and gets updated every time the counts change

p.depth = 40
p.depth
p.update("ACGTTGGGG")
p.counts
p.consensus
p.depth<jupyter_output><empty_output><jupyter_text>Exercise - Implement a dunder method so that we can apply the `len()` function to the pileup object  - Test the change<jupyter_code># we did not tell our class what to do when we apply the function len to our object
# we can do that by implementing the __len__ method see that in the class definition below
len(p)
# Pileup Object
class Pileup:
    """
    Contains a counter of nulceotides aligned at a genomic position and 
    computes depth and consensus for a genomic position
    
    Attributes:
    counts (Counter):  counter of all nucleotides aligned at the genomic position
    depth (int): total number of reads aligned at the genomic position
    consensus (str): most common nucleotide
    """
    
    
    def __init__(self, counts = None):
        self.__counts = counts
        if self.__counts == None:
            self.__counts = Counter()
        self.__depth = sum(self.__counts.values())
        if self.__depth == 0:
            self.__consensus = ""        
        else:
            self.__consensus = self.__counts.most_common()[0][0]

    @property # getter
    def consensus(self):
        """
        Get the consensus nucleotide for the pileup
        """
        return self.__consensus
    
    # @set.consensus # property setter
    # def consensus(self, cons):
    #    self.__consensus = consensus
    
    @property # getter
    def depth(self):
        """
        Get the depth for the pileup - number of reads that aliged at that position
        """
        return self.__depth
    
    @property # getter
    def counts(self):
        """
        Get the counts for the pileup - frequencies of the nucleotides that aligned at that position
        """
        return self.__counts
    
    @counts.setter # property setter
    def counts(self, counts_value):
        self.__counts = counts_value
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]

    def __str__(self):
        return f"Pileup({self.__counts})"
    
    def __repr__(self):
        return f"Pileup({self.__counts})"
    
    def __add__(self, p):
        c = self.__counts.copy()
        c.update(p.counts)
        return Pileup(c)

    # called when the len function is applied to a Pileup object
    # e.g. len(p)
    def __len__(self):
        return self.__depth
    
    # this is method to test that the first argument in the method is self
    def test(arg1, x):
        print(x)
        
        
    def update(self, seq):
        """
        Update the counts, depth and consensus nucleotide for the pileup
        given a sequence of nucleotides to add to the pileup
        """
        self.__counts.update(seq)
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]



p = Pileup()
p
p.consensus
p.counts
p.depth
p.consensus = "T"
p.counts = Counter("ACGTTTT")
p.counts
p.depth
p.consensus
p.depth = 40
p.depth
p.update("ACGTTGGGG")
p.counts
p.consensus
p.depth
len(p)
len(5)
# from the class we will apply the test method 
    ## this is method to test that the first argument in the method is self
    #  def test(arg1, x):
    #    print(x)
# The test method only has one argument
# the first argument (arg1) is not counted 
# since it is automatically assigned the value of the variable that is being called from in this case p
    
p.test()
p.test(5)
p.test(p)
# The test method only has one argument
# the first argument (arg1) is not counted 
# since it is automatically assigned the value of the variable that is being called from

p.test(3,4)<jupyter_output><empty_output><jupyter_text>Exercise - Create a list of 10 Pileup objects - Update the pileup objects with the corresponding sequences fom the following list.<jupyter_code>seq_list = ["CCCCATTTG","CATTTAG","GGGATC","AACTGA", "GCCCTAA", "CCCCATTTG", "AAAAAC","TTTTTTG","GGGGAT", "TTTTA"]

pileup_list = [Pileup(Counter(seq_list[i])) for i in range(10)]
pileup_list
# creating a list of empty pileups
pileup_list = [Pileup() for i in range(10)]
pileup_list
# creating a list of  pileups based on the sequence in seq_list

for i in range(10):
    pileup_list[i].update(seq_list[i])
pileup_list
pileup_list = [Pileup() for i in range(10)]
pileup_list
list(map(Pileup.update, pileup_list, seq_list))
pileup_list<jupyter_output><empty_output><jupyter_text>Exercise - Implement a method that compares the pileup it is called for with another given pileup and returns a tuple with the consensus for the two pileups and the ratio of the consensus frequencies available for the consensus in the counts attribute.  - Test the change<jupyter_code># Pileup Object
class Pileup:
    """
    Contains a counter of nulceotides aligned at a genomic position and 
    computes depth and consensus for a genomic position
    
    Attributes:
    counts (Counter):  counter of all nucleotides aligned at the genomic position
    depth (int): total number of reads aligned at the genomic position
    consensus (str): most common nucleotide
    """
    
    
    def __init__(self, counts = None):
        self.__counts = counts
        if self.__counts == None:
            self.__counts = Counter()
        self.__depth = sum(self.__counts.values())
        if self.__depth == 0:
            self.__consensus = ""        
        else:
            self.__consensus = self.__counts.most_common()[0][0]

    @property # getter
    def consensus(self):
        """
        Get the consensus nucleotide for the pileup
        """
        return self.__consensus
    
    # @set.consensus # property setter
    # def consensus(self, cons):
    #    self.__consensus = consensus
    
    @property # getter
    def depth(self):
        """
        Get the depth for the pileup - number of reads that aliged at that position
        """
        return self.__depth
    
    @property # getter
    def counts(self):
        """
        Get the counts for the pileup - frequencies of the nucleotides that aligned at that position
        """
        return self.__counts
    
    @counts.setter # property setter
    def counts(self, counts_value):
        self.__counts = counts_value
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]

    def __str__(self):
        return f"Pileup({self.__counts})"
    
    def __repr__(self):
        return f"Pileup({self.__counts})"
    
    def __add__(self, p):
        c = self.__counts.copy()
        c.update(p.counts)
        return Pileup(c)
    
    def __len__(self):
        return self.__depth
        
        
    def update(self, seq):
        """
        Update the counts, depth and consensus for the pileup
        given a sequence of nucleotides to add to the pileup
        """
        self.__counts.update(seq)
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]
        
    def compare_pileup(self, p):
        """
        Creates a tuple with three elements:
        - consensus of the pileup we are calling this method for
        - consensus of the pileup p given as an argumet
        - ration of the frequencies of the consensus in the counts for each of the pileups
        """
        tuple_res = ()
        f1 = self.__counts[self.__consensus]
        f2 = p.counts[p.consensus]
        try: 
            tuple_res = (self.__consensus, p.consensus, f1/f2)
        except ZeroDivisionError:
            tuple_res = (self.__consensus, p.consensus, 0)
        return tuple_res



3/0
p = Pileup()
p
p.consensus
p.counts
p.depth
p.consensus = "T"
p.counts = Counter("ACGTTTT")
p.counts
p.depth
p.consensus
p.depth = 40
p.depth
p.update("ACGTTGGGG")
p.counts
p.consensus
p.depth
len(p)
len(5)
p1 = Pileup(Counter("AACTTTGAAAAA"))
p1
p.compare_pileup(p1)
p.compare_pileup(Pileup())
pileup_list = [Pileup(Counter(seq_list[i])) for i in range(10)]
pileup_list
pileup_list[0].compare_pileup(pileup_list[1])<jupyter_output><empty_output><jupyter_text>An alternative is to compare the pileups and compute the tuple (variant) in a function outside of the class<jupyter_code># Pileup Object
class Pileup:
    """
    Contains a counter of nulceotides aligned at a genomic position and 
    computes depth and consensus for a genomic position
    
    Attributes:
    counts (Counter):  counter of all nucleotides aligned at the genomic position
    depth (int): total number of reads aligned at the genomic position
    consensus (str): most common nucleotide
    """
    
    
    def __init__(self, counts = None):
        self.__counts = counts
        if self.__counts == None:
            self.__counts = Counter()
        self.__depth = sum(self.__counts.values())
        if self.__depth == 0:
            self.__consensus = ""        
        else:
            self.__consensus = self.__counts.most_common()[0][0]

    @property # getter
    def consensus(self):
        """
        Get the consensus nucleotide for the pileup
        """
        return self.__consensus
    
    # @set.consensus # property setter
    # def consensus(self, cons):
    #    self.__consensus = consensus
    
    @property # getter
    def depth(self):
        """
        Get the depth for the pileup - number of reads that aliged at that position
        """
        return self.__depth
    
    @property # getter
    def counts(self):
        """
        Get the counts for the pileup - frequencies of the nucleotides that aligned at that position
        """
        return self.__counts
    
    @counts.setter # property setter
    def counts(self, counts_value):
        self.__counts = counts_value
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]

    def __str__(self):
        return f"Pileup({self.__counts})"
    
    def __repr__(self):
        return f"Pileup({self.__counts})"
    
    def __add__(self, p):
        c = self.__counts.copy() 
        c.update(p.counts)
        return Pileup(c)
    
    def __len__(self):
        return self.__depth
    
    def test(self, x):
        print(x)
        
        
    def update(self, seq):
        """
        Update the counts, depth and consensus for the pileup
        given a sequence of nucleotides to add to the pileup
        """
        self.__counts.update(seq)
        self.__depth = sum(self.__counts.values())
        self.__consensus = self.__counts.most_common()[0][0]




def create_variant(pn,pt):
        """
        Creates a tuple with three elements:
        - consensus of the pileup we are calling this method for
        - consensus of the pileup p given as an argumet
        - ration of the frequencies of the consensus in the counts for each of the pileups
        """
        tuple_res = ()
        f1 = pn.counts[pn.consensus]
        f2 = pt.counts[pt.consensus]
        try: 
            tuple_res = (pn.consensus, pt.consensus, f1/f2)
        except ZeroDivisionError:
            tuple_res = (pn.consensus, pt.consensus, 0)
        return tuple_res    
p1 = Pileup(Counter("CCCGT"))
create_variant(p1, Pileup())
p2 = Pileup(Counter("GGGGTACGGGG"))
create_variant(p2, p1)<jupyter_output><empty_output><jupyter_text>Unrelated extra example  Passing arguments to a function by value or by reference<jupyter_code>## By reference - using the variable name for a list, set, dict ...
def test(d):
    d["A"] = 2120
d1 = {"A":4, "B":5}
d1
test(d1)
d1
## By value - unpacking the vaariable values using * or ** for a list, set, dict ...
def test(**d):
    x = d["A"]
    x = x + 5
    
    d["A"] = 4000
test(**d1)
d1<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.08566261947817101, 'fraction_numerical': 0.006406613278222681, 'mean_word_length': 2.6500094286253066, 'pattern_counts': {'":': 2, '<': 37, '<?xml version=': 0, '>': 37, 'https://': 6, 'lorem ipsum': 0, 'www.': 5, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '497279', 'license': 'BSD-3-Clause', 'n_tokens_mistral': 5855, 'n_tokens_neox': 5630, 'n_words': 2014, 'path': 'study_sessions/study_session11_classes_solution.ipynb', 'repo_name': 'mitreac/b57520', 'size': '19355'}