# Collections

A more detailed treatment of the properties and the manipulation of sequences will be given soon, but let's start off by giving an example of why one might need a data type that holds more than one element in it.

A sequence is an ordered set of objects. We've already seen one kind of sequence, the string, which is an ordered set of characters. Here, we will be looking at ordered sets of numbers

```#The basic representation of a sequence of numbers is
seq = [5, 3, 2, 1, 7, 20, -10]
print seq

#to get a handle on elements in this sequence, use the construction seq[i],
#remembering that the index starts at 0.
print seq  #first element in sequence
print seq  #third element in the sequence

#you can also find out how many elements are in a sequence using the
#function len()
print len(seq)

#this way you can print out the last element of the sequence
#(always remembering that the index starts at 0)
print seq[len(seq)-1]

#to get a partial list of elements in the sequence, use the construction
#seq[i, k] which will extract the elements between the ith and kth index
# not including the element at the kth index -- this is called slicing
print seq[2:5]

#if no first index is supplied, this will give all elements up to but not
#including the second index
print seq[:4]

#if no second index is supplied, this will give all elements after and
#including the first index
print seq[4:]

#you've also already seen a function that returns a sequence
print range(1, 7)

#you can also concatenate sequences together using +
seq1 = [1.1, 3, 7,1]
seq2 = [2,3.5]
print seq1+seq2

#or, if you start off with an empty list, you can add one one element at a time
seqList = []
seqList.append(4)
print seqList
seqList.append(101)
print seqList

#now revisit the divisors example so that instead of printing out all
#the divisors, instead store all divisors as a sequence
xIn = 270

if xIn > 0:
seqOut = []  #this initializes the sequence variable
for candidate in range(1, xIn):
if xIn%candidate ==0:
seqOut.append(candidate)
return seqOut
else:
print xIn, " is negative.  Please try a positive integer"

Now for a more complete treatment of lists.

```# We've now worked with lists of the same types of objects, like integers
listInt = [1, 2, 4, 6, 3, 7, 12]

# or lists of points
p1 = Point(1, 2, 0)
p2 = Point(-2, 3, 0)
p3 = Point(-1, 2, 100)
listPoints = [p1, p2, p3]

#but lists can also comprise heterogeneous objects.
listMisc = ["time", 3.15, 10, Point(1, 4, 5)]

#notice that even though a list can contain another list, the length of this list is still 4
print len(listMisc)

# Lists with consectutive or evenly-spaced integers are so common that there is a built-in
# function that does exactly this
print range(17)
print range(3, 17)
print range(0, 21, 3)

# and of course there is always the empty list which is often used to assign a list
# variable that will be filled in the course of the program
listEmpty = []```

Lists have the property that they are mutable, meaning that we can change elements within the list.

```listInt = [1, 2, 4, 6, 3, 7, 12]
# we can change any element in the list
listInt = 5
print listInt

# or change several elements using slicing
listInt[2:4] = [10, 20]
print listInt

# or change every element in the list;  for instance, scaling by a uniform scaling k
k = -2
listInt[:] = [k*x for x in listInt]
print listInt

# we can also delete an element from a list:
del listInt
print listInt

# or several using slicing
del listInt[2:5]
print listInt```

Cloning vs. Aliasing

```# Sometimes, it will be useful to modify a list but also to keep a copy of the original.
# This is called cloning
listIntClone = listInt[:]
#Now you can work with listIntClone and this won't affect listInt.
listIntClone.append(0)
print listIntClone
print listInt

# Notice that this is different if you were to just assign a list to another.
# This is called aliasing
listIntAlias = listInt
listIntAlias.append(0)
print listIntAlias
print listInt```

Lists and Loops

```# The general syntax for lists and while is given by
# i = 0
# while i < len(LIST):
#	VARIABLE = LIST[i]
#	STATEMENTS
#	i = i + 1
#
# This is equivalent to the syntax for lists and for loops (slightly more concise)
# for VARIABLE in LIST:
#	STATEMENTS
#
#Iterating through a collection of points can be done concisely as
for n, pt in enumerate(pts):
print "Point at index ",n," is", pt'```

Points along an Interval

```# Suppose we want to assign a sequence of points spaced at equal interval size along the x-axis
interval_size = 2.5

pts_take1 = []
for i in range(cnt):
x = i*interval_size
pts_take1.append(Point(x, 0))

# if we now want to draw out a subset of this list of points (pts_x), we can as
# easily do this by accessing every other index
pts_take1_subset = []
for i in range(0, cnt, 2):
pts_take1_subset.append(pts_take1[i])  #this selects every other point

# It is often more flexible to specify an interval (a,b) and then divide it
# into equal parts.  This can be achieved by the following loop
a,b = 0,10
pts_take2 = []
for i in range(cnt):
x = a + i*(b-a)/cnt
pts_take2.append(Point(x, 0))

#We can use the Interval class and list shorthands in Python to reduce this to two lines
ival = Interval(a,b)
pts_take2 = [Point(t, 0) for t in ival/cnt]```

Points along a Grid

```"""
2D grids of points
cnt_x  #number of grid points along the x-direction
cnt_y  #number of grid points along the y-direction
"""
#Let's generate a grid of points spaced along equal grid size (in each direction)
grid_size  = 1.5
pts_take1 = []
for i in range(cnt_x):
for j in range(cnt_y):
pts_take1.append(Point(i*grid_size, j*grid_size))

#this can be reduced to one line using list shorthands
pts_take1 = [Point(i*grid_size, j*grid_size) for i in range(cnt_x) for j in range(cnt_y)]

#It is often useful to start with a canvas of fixed dimensions
#and then create a grid on top of that with the desired resolution.
dim_x = 20
dim_y = 30
pts_take2 = [Point(i*dim_x/cnt_x, j*dim_y/cnt_y) for i in range(cnt_x) for j in range(cnt_y)]
#This is already succinct but we can use two intervals and write
#this equivalently as follows

ival_x = Interval(0, dim_x)
ival_y = Interval(0, dim_y)
pts_take2 = [Point(u,v) for u in ival_x/cnt_x for v in ival_y/cnt_y]

#this makes it flexible to position your canvas (not necessarily at the origin)
#eg. try ival_x = Interval(a,b) and ival_y = Interval(c,d)```

Lists as Input

```# Points along an ellipse can be done using the same syntax as drawing points
# along an interval

from math import pi, cos, sin

ival = Interval(0, 2*pi)

pts_ellipse = [Point(10*cos(t), 17*sin(t)) for t in ival.divide(cnt, True)]

# Now if we wanted to draw this ellipse at different heights, we can loop across two lists--
# one representing points along any curve drawn in plane and the other holding the
# list of heights
heights = [0,10,22,32,48,60,75,82,91]
pts_tower = [Point(pt.x,pt.y,height) for height in heights for pt in pts_ellipse]

# If we want to make this more general so that any floor shape can be used as input,
# we can encapsulate this as a function which takes the two lists as input
def tower(pts_floor, floor_heights):
pts = [Point(pt.x,pt.y,height) for height in floor_heights for pt in pts_floor]
return pts

#which can be called as follows
heights = range(0, 200, 8)
drawPointsAtHeights(pts_ellipse,heights)

# Inputting a grid of points is also often done.  In this case, heights
# are proportional to cos(theta) where theta is the angle of incidence
def height_field(source, grid):
height_lines = []
for pt_grid in grid:
v1 = Vec(pt_grid, source).normalized()
cos_theta = v1.dot(Vec(0,0,1))
height_lines.append(Line(pt_grid, Vec(0,0,10*cos_theta)))
return height_lines

pts_out = height_field(pt_source, grid_pts)```