#!/usr/bin/env python
# coding: utf-8

# ### Named Tuples - Modifying and Extending
from collections import namedtuple

Point2D = namedtuple('Point2D', 'x y')


# The objects generated by `namedtuple` generated classes are **immutable**.
# In other words the following will not work:
origin = Point2D(10,0)
origin.x = 0


# However, we may want to "change" the value of one of the coordinates of our `origin` variable.
# This is just like strings, we have to create a new version of the tuple, and assign it to the same label.
# Suppose we want to change the x-coordinate of our `origin` to something else, but retain whatever the y-coordinate was.
# We could do it as follows:
origin = Point2D(0, origin.y)
origin


# Of course this could become quite unwieldy when we have a larger number of properties and we only need to change a single item:
Stock = namedtuple('Stock', 'symbol year month day open high low close')
djia = Stock('DJIA', 2018, 1, 25, 26_313, 26_458, 26_260, 26_393)


# To update the `close` property for example, we could write:
djia = Stock(djia.symbol, djia.year, djia.month, djia.day,
                  djia.open, djia.high, djia.low, 26_394)


# Now that was quite painful!
# We can be a bit more clever about this and use tuple unpacking and argument unpacking as follows:
*values, _ = djia


# We didn't care about the `close` price since we are replacing it, hence the underscore variable name.
# And we now have everything else in a list:
values


# And now we are going to use the `*` again, but this time to unpack the list into separate arguments when we call the `Stock` initializer:
djia = Stock(*values, 26_393)
djia


# This is much better than our first attempt!
# But this approach does not always work, what happens if we want to change a values somewhere in the middle? Or two values?
# We cannot do:
# `*first, month, *last = djia`
# That would make no sense whatsoever! (and Python will tell you so!)
# Maybe slicing and unpacking can work here...
djia


# We could try **slicing**:
djia[:3]
djia[:3] + (26,) + djia[4:]


# So now we could use this to create a new StockPrice instance:
djia2 = Stock(*(djia[:3] + (26,) + djia[4:]))
djia2


# This works, but that's quite cumbersome...
# And it gets worse - suppose we want to modify the year and day using this approach:
djia
values = djia[0:1] + (2019,) + djia[2:3] + (26,) + djia[4:]
values
djia3 = Stock(*values)
djia3


# Or, if you want to avoid unpacking the `values` into the multiple positional arguments required by the `Stock` constructor, we can make us of the `_make` class method that can use an iterable:
djia4 = Stock._make(values)
djia4


# This is really getting too complex.
# Fortunately there's a better way!
# The namedtuple implementation also provides another instance method called `_replace` which takes keyword-only arguments. That method will make a copy of the current tuple and substitute property values based on the keyword-only arguments passed in.
djia
id(djia)
djia5 = djia._replace(year=2019, day=26)
djia5
djia
id(djia5)
# Much better!!

# #### Extending Named Tuples

# Sometimes we may want to add one or more properties to an existing class without modifying the code for the custom class itself.
# Using inheritance is one way to go about it so you may be tempted to do this with named tuples as well, but it's not easy, and there's a cleaner way to do this if all you're after is additional data fields.
# Let's say we have a Point class that is for 2D problems:
Point2D = namedtuple('Point2D', 'x y')


# We could easily create a 3D point class as follows:
Point3D = namedtuple('Point3D', 'x y z')


# But if our named tuple has many fields, such as our `Stock` named tuple that's a little more difficult:
djia


# Suppose we want to create a new class, say `StockExt`, it would take some effort:
StockExt = namedtuple('StockExt',
                      '''symbol year month day open high low
                      close previous_close''')


# Instead we can leverage that `_fields` property:
Stock._fields


# Remember that the `namedtuple` initializer can handle a list or tuple containing the field names. For example, the one we just retrieved from `_fields`.
# Now all we need to do is create a new tuple that contains those fields along with whatever extras we want:
new_fields = Stock._fields + ('previous_close',)
new_fields


# And now we can create our new named tuple this way:
StockExt = namedtuple('StockExt', Stock._fields + ('previous_close',))
StockExt._fields


# If you did not want to use tuple concatenation for some reason, you could also do it using strings:
' '.join(Stock._fields) + ' previous_close'
StockExt = namedtuple('StockExt',
                      ' '.join(Stock._fields) + ' previous_close')
StockExt._fields


# Now, with this newly extended class, we may want to take one of the "old" named tuple instance (`djia`) and create the extended version of it using the `StockExt` class.
# This is also quite simple to do, since named tuples are tuples, and can therefore be unpacked in the arguments of a function call.
djia
djia_ext = StockExt(*djia, 25_000)
djia_ext


# or, we can use the `_make` method:
djia_ext = StockExt._make(djia + (25_000, ))
djia_ext
