A class is a named conglomoration of information and/or functions---that is, of other Python objects. We can define a class; we can use a class directly; and we can create any number of instances of a given class, and use them just as we would any other Python object (which is itself a class or an instance of a class).
Classes are something like modules. Remember, a module is a file with Python code in it. (More correctly, the module is the object resulting from importing the file.) That code can provide information, and it can include functions that know about, and work with, that information. The same is true of a class; but classes are more flexible, because they, and their instances, can be defined and modified as a program is executed, and there can be any number of classes defined in a given module. More importantly, there is only one copy of a given module, with its associated data, in use in a program at a given time; but there can be many different instances of a given class, each with its own specific information.
Modules and classes (and packages, for that matter) implement Python's "divide and conquer" approach to keeping programs writable, readable, and maintainable as the problems they address become increasingly complex. They break information and functionality down into independent, testable, documentable, resuable chunks, with well-defined inputs and outputs to connect the chunks.
Enough verbiage; let's make our own classes.
# define the class:
class Attribute_holder(object):
pass
# Make a couple of instances of it, and give each
# one some attributes to hold:
c1 = Attribute_holder()
c1.some_attribute = "this is a test"
c1.some_other_attribute = 5
c1.y = 20
c2 = Attribute_holder()
c2.x = "the x attribute of this Attribute_holder instance"
c2.y = 50
print("c1 is an instance of", type(c1))
print(c1.some_attribute)
print(c2.y)
print(c1.some_other_attribute * c2.y)
print("c1.y is %d but c2.y is %d" % (c1.y, c2.y))
Above we have an example of the simplest possible type of custom class, a container that is initially empty. We can make instances of it, and we can assign attributes to those instances. An attribute is simply an object in a class or instance that can be accessed using the "dot" syntax, as shown above.
Although it is not mandatory, it is advisable to define any class as inheriting from some pre-existing class. In the example above, we are inheriting from the general Python object
type--use this if you don't need specific behavior from some more specialized class. (The terms "class" and "type" are now nearly synonymous in Python from a practical standpoint. "Type" is the proper term for built-in types such as the object, the list, and the dictionary. "Class" usually refers to a user-customized type. In the example above, we are customizing the object
type--trivially, by copying it with a new name.)
Normally, a class definition includes one or more method definitions. A method is a function defined within a class, with one special characteristic: when the method is called, the instance to which the method belongs is prepended to its argument list. It is easier to see how this works with an example than with a description, so here is a minimal illustration:
class Demo_of_method(object):
def return_self(self):
return self
# make an instance:
x = Demo_of_method()
# verify that return_self is returning the instance itself:
print(x.return_self() is x)
In the example above, notice that:
self
argument.self
argument is the instance.Here is another illustration; it is a little bit more realistic because it illustrates how self
is used within a method to access an attribute of the instance; and how an attribute can be set when the instance is created:
class Named_demo(object):
def __init__(self, name):
self.name = name
# Now "name" is an attribute of the instance.
def __str__(self):
return "Named_class instance with name " + self.name
def show_name(self):
print("My name is", self.name)
a = Named_demo("Harold")
b = Named_demo("Maud")
print("Variable 'a' is printed via its __str__ method as:\n ", a)
a.show_name()
b.show_name()
Things to notice:
Method names and attributes that start and end with two underscores are special.
__init__
is the method that creates a new instance of the class. It is called automatically when the class name is called--treated like a function--and it automatically returns the new instance--no explicit return
statement is needed. __str__
is the method that is called to make a string representation of the instance when printing, or when calling the built-in str()
function.In this example, __init__
takes two arguments: self
, which is prepended automatically when it is called (just as with any other method), and name
, which we supply when we create the instance.
There are other special "double underscore" methods. One of the most useful is __call__
, which makes
an instance of the class behave like a function. Here is an example:
class Polynomial(object):
"""
Very simple class for evaluating polynomials.
"""
def __init__(self, *args):
"""
Arguments are successively higher order coefficients
of the polynomial, starting from zero.
"""
self.coefs = args
def __str__(self):
s = []
for i, a in enumerate(self.coefs):
if a == 0:
continue
if i == 0:
s.append(str(a))
elif i == 1:
s.append('%s*x' % (a,))
else:
s.append('%s*x**%d' % (a, i))
out = 'evaluates: ' + ' + '.join(s)
return out
def __call__(self, *args):
"""
Evaluate the polynomial at points given in one or more args.
With one input, a scalar will be returned; otherwise
a list will be returned.
"""
out = []
for x in args:
y = 0
for n, coef in enumerate(self.coefs):
y += coef * x ** n
out.append(y)
if len(out) == 1:
return out[0]
return out
# Make an instance: a second order Hermite polynomial
hermite2 = Polynomial(-2, 0, 4)
# (This called its __init__() method.)
print("The 'hermite2' instance", hermite2) # calls its __str__() method
# and evaluate it at multiple points
print(hermite2(-1, -0.5, 0, 0.5, 1))
# or at a single point.
print(hermite2(0.25))
Here is another example, in which we will define 3 classes. This illustrates how attributes and methods can be inherited from its parent by a subclass, or they can be overridden with customized replacements.
class AddressBase(object):
state = None
# subclass must override this "state" attribute
def __init__(self, name=None, number="", street="", city=""):
if name is None:
raise ValueError("name must be specified")
self.name = name
self.number = number
self.street = street
self.city = city
def __str__(self):
return "%s, %s %s, %s %s" % (self.name, self.number, self.street,
self.city, self.state)
def is_complete(self):
"""
Return True if the address includes all fields, or False otherwise.
"""
complete = self.number and self.street and self.city and self.state
return complete
Things to notice:
state
attribute.) It can be overridden in any given instance, or by subclassing, as we will illustrate shortly.None
object as the default value for the name
argument, and then used that as a flag to show whether a value for this optional argument was supplied.Now let's subclass AddressBase
by writing a simple class that inherits from it.
class HawaiiAddress(AddressBase):
state = "HI"
# Customize the __init__ method to provide a default city.
def __init__(self, name=None, number="", street="", city="Honolulu"):
AddressBase.__init__(self, name=name, number=number,
street=street, city=city)
# That's it--that's all the customization we need here.
# Make a list of HawaiiAddress instances:
addresses = [HawaiiAddress(name="Henry Smith", number="417a",
street="Oak St"),
HawaiiAddress("Lisa Chun", street="Kumu Hula Way"),
HawaiiAddress("Sally Sato", number="223", street="Koa St",
city="Wailuku")]
# We might want to look up an address by name, so make a dictionary:
address_dict = dict()
for a in addresses:
address_dict[a.name] = a
print(address_dict["Sally Sato"])
for a in addresses:
if a.is_complete():
print("Address for %s is complete" % a.name)
else:
print("Warning: address for %s is incomplete" % a.name)
Notice that we are inheriting almost everything, and just adding a little customization.
Now do the same thing for a different state, but with more customization: an additional method.
class CaliforniaAddress(AddressBase):
state = "CA"
def __init__(self, *args, **kwargs):
# (alternative way of handling args and kwargs)
kwargs.setdefault('city', 'Los Angeles')
AddressBase.__init__(self, *args, **kwargs)
def is_northern(self):
return self.city in ['San Francisco', 'Sacramento']
# list some CA addresses:
addresses = [CaliforniaAddress(name="Hubert Smith", number="417a",
street="Walnut St"),
CaliforniaAddress("Lisa Chun", street="Google Way",
city='San Francisco'),
CaliforniaAddress("Sally Jones", number="223",
street="Brown St", city="Sacramento")]
# Test the methods, including the one we added for California
for a in addresses:
print(a)
if a.is_complete():
print("Address for %s is complete" % a.name)
else:
print("Warning: address for %s is incomplete" % a.name)
if a.is_northern():
print(" It's in Northern California")
That's probably enough introduction to classes. Now it is time to learn about numpy, so we can work efficiently with arrays of numbers, and about matplotlib, so we can plot them.