# Changes in this version of the code:
# - We implemented __repr__.
# - I fixed the problems in __repr__ that made
#   it fail the doctest.  There were two:
#   (a) There were two blanks at the end of
#       our expected output for one of the doctests.
#       Yes, doctest is that picky!
#   (b) The doctest that checked whether p1 == p2
#       failed because we had not defined __eq__.
#       When there is no __eq__ defined in a class,
#       the inherited version is used, and all it
#       does is compare memory addresses.  That of
#       course failed, because p1 and p2 are not the
#       very same Point, they are merely equivalent.
# - I have added an implementation of __eq__.
#   It uses several features of Python which 
#   are probably to you.  See below for details. 
# - We made coords a property, and used that to 
#   prevent client code from ever changing the
#   value of coords once it has been set.  In 
#   other words, we made Points immutable.
#   I have changed the class docstring to reflect
#   this. 

# Exercises:
# - Run point.py and observe that it passes
#   all the doctests.
# - Run client.py with and without the code
#   that tries to change an existing Point.
#   Get comfortable with how property stops
#   the client from changing coords, yet still
#   lets it look at coords.
# - Read about the cool Python features below.
#   Try them out in the shell to get familiar
#   with them.
# - Figure out how __eq__ works.
# - Use a list comprehension to shrink 
#   method distance_from_origin down to 
#   a single line.

# Cool Python features:
# (1) List comprehensions.  
# A list comprehension looks like this:
#       [<expression> for <variable> in <iterable>]
# Examples:
# >>> [x + 10 for x in range(5)]
#     [10, 11, 12, 13, 14]
# >>> [name.capitalize() for name in ['william', 'catherine', 'peter']]
#     ['William', 'Catherine', 'Peter']
#
# (2) Function all(iterable) -> bool
# which returns True if every value in the iterable
# is True.
#
# (3) class zip.  
# Here's an example of how it can be used:
# >>> Z = zip(["a", "b", "c"], [1, 2, 3])
# >>> for item in Z:
# >>>     print(item)
# >>> ('a', 1)
# >>> ('b', 2)
# >>> ('c', 3)

from math import sqrt

class Point:
    """
    An immutable n-dimensional point.
    coords is a list of numbers containing the coordinates
    of this Point.
    """

    def __init__(self, coords):
        """
        (Point, list of number) -> NoneType
        
        Initialize this point with these coords.
        We will return to this docstring to add an example
        showing that p is as it should be.
        
        >>> p = Point([3, 4.0, 5.32, 6, 7])
        >>> p.coords
        [3, 4.0, 5.32, 6, 7]
        """
    
        self.coords = coords
    
    
    def distance_from_origin(self):
        """
        (Point) -> float
    
        Return the distance from this Point to the
        origin.
        
        >>> p = Point([3, 4])
        >>> p.distance_from_origin()
        5.0
        """
        
        sum_of_squares = 0
        for coord in self.coords:
            sum_of_squares += coord**2
        return sqrt(sum_of_squares)
    
    def __str__(self):
        """
        (Point) -> str
        
        Return a convenient str representation of this Point.
        
        >>> p = Point([3.0, 4.0])
        >>> print(p)
        (3.0, 4.0)
        >>> p.__str__()
        '(3.0, 4.0)'
        >>> str(p)
        '(3.0, 4.0)'
        """
        
        return str(tuple(self.coords))
    
    def __repr__(self):
        """
        (Point) -> str
        
        Return a str representing self that produces an equivalent
        Point when evaluated in Python.
        
        >>> p1 = Point([6, 1, 2])
        >>> p2 = eval(repr(p1))
        >>> p2 == p1
        True
        >>> p1.__repr__()
        'Point([6, 1, 2])'
        """
        
        # Our next job: Write a body that passes the doctest.
        return 'Point({})'.format(repr(self.coords))

    def __eq__(self, other):
        """
        (Point, Point) -> bool
        
        Return True iff this Point is equivalent to other.

        >>> p1 = Point([3, 4, 5])
        >>> p2 = Point([3.0, 4.0, 5.0])
        >>> p1 == p2
        True
        """
        
        return (
            isinstance(other, Point) and 
            len(self.coords) == len(other.coords) and 
            all(
                [a == b for a, b in zip(self.coords, other.coords)]
            )
        )
    
    def sing(self, punctuation):
        """
        (Point, str) -> NoneType
        
        A silly method used in explaining the parameter "self".
        """
        
        for c in self.coords:
            print("{}{}".format(c, punctuation))  
            
    def set_coordinates(self, coords):
        """
        (Point, list of float) --> NoneType
        
        Set this Point to have these coordinates.  Since Points are 
        immutable, this method can only be called once.
        """
        
        if '_coords' in dir(self):
            raise Exception('Cannot reset coordinates.')
        else:
            self._coords = coords

    def get_coordinates(self):
        """
        (Point) -> list of float
        
        Return the coordinates for this Point.
        """
        
        return self._coords
        
    # Access to coordinates is delegated to property, so that 
    # get_coordinates and set_coordinates are called instead.
    coords = property(get_coordinates, set_coordinates, None, None)
    
    
if __name__ == '__main__':
    import doctest
    doctest.testmod()