text: """             Tuple as Data Structure
  We have see how we interpreted tuples as data structures
  
  The position of the object contained in the tuple gives it meaning
  
  For example, we can represent a 2D coordinate as: (10, 20)
                                                      x   y
  
  If pt is a position tuple, we can retrieve the x and    x, y = pt      or  x = pt[0]
  y coordinates using:                                                       y = py[1]
  
  For example, to calculate the distance of pt from the origin we could write:
  
  dist = math.sgrt(pt[0] ** 2 + pt[1] ** 2)
  
  Now this is not very readable, and if someone sees this code they will have ti know thatpt[0] mans the x-coordinate and pt[1] means the y-coordinate.
  
  This is not very transparent.
  
  
  
              # Using a class instead.
  At this point, in order to make things clearer for the reader (not the complier, the reader), we might want to approach this using a class method instead.
  
  """
  from calendar import month
  import symbol
  
  
  class Point2D:
      def __init__(self, x, y):           # pt = Point2D(10, 20)
          self.x = x
          self.y = y
  
  
  class Stock:
      def --init__(self, symbol, year, month, day, open, high, low, close):
          self.symbol = symbol
          self.year = year
          self.month = month
          self.day = day          # Class approach            # Tuple Approach
          self.open               # djia.symbol               # djia[0]
          self.high               # djia.open                 # djia[4]
          self.low = low          # djia.close                # djia[7]
          self. close = close
                                  # djia.high - djia.low      # djia[5] - djia[6]
  
  
  """         Extra stuff
  At the very least we shouldimpliment the __eq__ method too
      -> Point(10, 20) == Point(10, 20)  ->  True
  
  """
  
  class Point2D:
      def __init__(self, x, y):           # pt = Point2D(10, 20)
          self.x = x
          self.y = y
  
      def __repr__(self):
          return f'Point2D(x={self.x}, y={self.y}'
  
      def __eq_(self, other):
          if isinstance(other, Point2D):
              return self.x == other.x and self.y == other.y
          else:
              return False
  
  
  """         Named Tuples to the rescue
  There are other reasons to seek another approach, We cover those in the coding video
  
  Amonst other thing, Point2D objects are mutable - something we may not want!
  
  There's a lot to like using tuples to represent simple data structures
  
  The real drawback is that we have to know what the positions mean, and remember this in our code.
  
  If we ever need to change the structure of our tuple in our code (like inserting a value that we forgot) and most likely our code will break!
  
  
  
  
          Named Tuples to the rescue
  
  Named tuples give meaningful name to positions:
      They subclass tuple, and add a layer to assign property names to the positional elements
  
  Located in the collections standard library module
      from collections import nametuple
  
          named tuple is a function (not a type) which generates a new class      -> class factory
                  that new class inherits from tuple
                  but also provides named properties to access elements of the tuple
                  but an instance of that class is still a tuple
  
  
  
          Generating Named Tuple Classes
  
  We have to understand that namedtuple is a class factory
  
  namedtuple needs a few things to  generate this class:
      the class name we want to use
      a sequence of field names (strings) we want to assign, in the order of the elements in the tuple
          field names can be any valid variable name
          except they cannot start with an underscore
  
  The return value of the call to namedtuple will be a class
  
  We need to assign that class to a variable name in our code so we can use it to construct instances
  n general, we use the same name as the name of the class that was generated
  
  Point2D = namedtuple('Point2D', ['x', 'y'])
  
  We can create instances of Point2D just as we would with anny class (since it is a class)
  Pt2D = namedTuple('Point2D', ['x', 'y'])
  pt = Point2D(19, 20)
                  Variable:MyClass ---> Class: MyClass 0xFF300  # they point to the same memory object
  class MyClass:                                /
      pass                                     /
                                              /
  MyClassAlias = MyClass      Variable: MyClassAlias
  instance_1 = MyClass()          instance_2 = MyClassAlias()     # instances of the same class
  
      Similarly
  Pt2DAlias = namedtuple('Point2D', ['x', 'y'])
                                          | # Points to the same class
                                      Class:  )xFF900
      Variable: Pt2DAlias ------> Point2D
  
  There are many ways we can provide the list of field names to the namedtuple function
      a list of strings      -> in fact any sequence, just remember that order matters!
      a tuple of strings     -> in fact any sequence, just remember that order matters!
      a single string with the field names seperated by whitespace or commas
  
  namedtuple( 'Point2D', ['x', 'y'])
  
  namedtuple( 'Point2D', ('x', 'y'))
  
  namedtuple( 'Point2D', 'x, y')
  
  namedtuple( 'Point2D', 'x y')
  
  
  
          Instantiating Named Tuples
  
  After we have created a named tuple class, we can instanciate them just like an ordinary class
  
  In fact, the __new__ method of the generated class uses the field names we provided as parm names
  
      Point2D = namedtuple( 'Point2D', ' x y')
  
  Once we have created a class, I can use positional arguments:
  
      pt1 = Point2D(10, 20)       10 -> x     20 -> y
  
  And even keyword arguments:
      pt2 = Point2D(x=10, y=20)       "           "
  
  
  
          Accessing Data in a named tuple
  
  Since named tuples are  also regular tuples. we can still handle them just like any other tuple
      by index
      slicing
      iterate
  
  Point2D = namedtuple( 'Point2D', 'x y')
  
  pt1 = Point2D(10, 20)           isinstance(pt1, tuple) -> True
  
          x, y = pt1      # unpacking
  
          x = pt1[0]
  
          for e in pt1:   # iterate
              print(e)
  
  But now, in addition, we can alos access the data using the field names:
  
  Point2D = namedtuple( 'Point2D, 'x y')
  pt1 = Point2D(10, 20)
  
  
  pt1.x  -> 10
  
  pt1.y  -> 20
  
  Since nametuple generated classes inherit from tuple    class Point2D(tuple):
                                                              ...
  
  pt1 is a tuple, and is therefore immutable
  
  pt1.x = 100     will not work!
  
  
  
          The rename keyword-only argument for namedtuple
  
  Remember that field names for named tuples must be valid identifiers but cannot start with a underscore
  This will NOT work!!!   Person = namedtuple( 'Person), 'named age, _ssn')
  
  nametuple has a keywordonly argument,rename     (defaults to False)     that will automatically rename any invalid field name
  
  uses convention:    _{position in list of field names}
  
  This will not work:
  Person = namedtuple('Person', 'name age _ssn', rename=True)
  
  And the actual field names would be:        name    age     _2
  
  
  
          Introspection
  
  We can easily find out the field names in a named tuple generated class
  
  class property -> _fields
  
  person = namedtuple( 'Person', 'name age _snn', rename=True)
  
  Person._fields      -> ('name', 'age', '_2')
  
  Remember that namedtuple is a class factory, i.e. it generates a class
  We can actually see what the code for that class is, using the class property _source
  
  """
  Point2D = namedtuple('Point2D', 'x y')
  
  Point2D._source                   # lots of code omitted
                                          /
                                         /
  class Point2D(tuple):                 /
  'Point2D(x, y')'                     /
                                      /
                                     /
      def __new__(_cls, x, y):
          'Create new instance of Point2D(x, y)'
          return _tuple.__new__(_cls, (x, y))
  
      def __repr__(self):
          'Return a nicely formated representation string'
          return self.__class__.__name__
  