Classes and Object-Oriented Programming
In Python, they say "everything is an object." Let's explore this idea using lists as an example. Every list contains specific and likely different elements. But all lists have certain capabilities in common as well. You can append items to a list, retrieve individual elements by indexing, and so on. So objects have two distinct features. First, each object is an instance of some class or type, which specifies how it can behave, or, in other words, which methods and operations can be used with that object. Second, each object has its own unique data, private to that object and distinct from the other objects in the same class.
The Python language contains some built-in data types, and the interpreter has a built-in "knowledge" of how objects of a given type should behave, but of course, it has no idea which instances of which types your programs will create. So, the interpreter contains the definitions of the data types, but your program creates the individual instances of the types, each of which behaves according to its (built-in) type definition.
In our first example here, we'll explore the nature of one of Python's objects: the complex number. Type the commands as shown:
>>> c = 3+4j
>>> type(c)
<class 'complex'>
>>> dir(c)
['__abs__', '__add__', '__bool__', '__class__', '__complex__', '__delattr__', '__dir__', '__doc__',
'__eq__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__mul__', '__ne__', '__neg__',
'__new__', '__pos__', '__pow__', '__radd__', '__reduce__', '__reduce_ex__', '__repr__',
'__rmul__', '__rpow__', '__rsub__', '__rtruediv__', '__setattr__', '__sizeof__', '__str__',
'__sub__', '__subclasshook__', '__truediv__', 'conjugate', 'from_number', 'imag', 'real']
>>> c
(3+4j)
>>> c.__add__
<method-wrapper '__add__' of complex object at 0x1046261d0>
>>> c.real
3.0
>>> c.imag
4.0
>>> type(c.imag)
<class 'float'>
>>> c.imag = 2.5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
c.imag = 2.5
^^^^^^
AttributeError: readonly attribute
The interpreter reports that the type of c is <class 'complex'>. The call on dir(c) shows us that many methods have names that begin with double underscores— a lot of the names represent operators that you may want to use on a complex number (add, subtract, divide, and so on). Two of the names do not begin with double underscores: "real" and "imag." In mathematics, complex numbers have a real and an imaginary part. Each complex number in Python has two attributes called real and imag; those names are bound to floating-point numbers. You can access the value of each attribute separately, but because all numbers in Python are immutable, the interpreter won't allow you to change them.
| Modern Python | The dir() output above is from current Python and differs from the original. Notably,
__complex__, __dir__, __getstate__, __init_subclass__, and
from_number are new additions, while __divmod__, __float__,
__floordiv__, __int__, __mod__, __radd__,
__rdivmod__, __rfloordiv__, __rmod__ and a few others
have been removed or reorganised across Python versions. The exact set varies with the interpreter version.
Also, modern tracebacks include a caret line pinpointing the offending expression. |
In keeping with a long-standing tradition in the object-oriented programming world, Python lets you define your own data types called classes. In Python we tend to reserve the word "type" to mean a class that is built into the interpreter, and "class" to mean those defined by the programmer. Python uses the compound class statement to introduce a class definition. The indented suite that follows the class statement contains descriptions of the various methods that should be available, as well as any data items relating to the class as a whole. The simplest suite is a single pass statement. Let's take a look. Type the commands below as shown:
>>> class First:
... pass
...
>>> First
<class '__main__.First'>
>>> first = First()
>>> first
<__main__.First object at 0x109b58c20>
>>> dir(first)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__',
'__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__',
'__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__',
'__subclasshook__', '__weakref__']
>>> first.name = "My first object"
>>> first.location = "Here"
>>> first.__dict__
{'name': 'My first object', 'location': 'Here'}
>>> first.name
'My first object'
>>> type(first.__dict__)
<class 'dict'>
| Modern Python | In Python 3 all classes are automatically "new-style." You may see older code written as
class First(object):; in Python 3 the explicit (object) base is unnecessary and
is no longer recommended. class First: is the idiomatic modern form. |
We establish the type with a class statement. Then we create an instance of our new class named first, by calling First() as though it were a function. The interpreter identifies the instance by its name and address in hexadecimal (base 16): "<__main__.First object at 0x02699A90>". This instance of your class is equipped to behave in certain ways. The behaviors shown in the result of the dir(first) call are common to all Python objects.
Unlike instances of built-in classes, when using instances of those you create yourself, you can bind values to named attributes. These bindings work just like the binding of values to keys in a dict, because, in fact, they are dicts. Assignment to a dotted name results in that name being added as a key to a dict named __dict__ in the instance's namespace, with the associated value becoming the dict value. For most names, inst.name is equivalent to inst.__dict__["name"], though clearly much easier to read and write!
The class statement takes an indented suite as its body. When you bind a name during the execution of the class body, that class, like an instance, will have a namespace. Bindings in that class body are created within the class namespace. To fully understand that, run the following commands in an interactive session:
>>> class Second:
... one = "Depp"
... two = "Pitt"
...
>>> dir(Second)
['__class__', '__delattr__', '__dict__', '__doc__', ... ,
'__weakref__', 'one', 'two']
>>> Second.__dict__
{'__module__': '__main__', '__firstlineno__': 1, 'one': 'Depp', 'two': 'Pitt',
'__static_attributes__': (), '__dict__': <attribute '__dict__' of 'Second' objects>,
'__weakref__': <attribute '__weakref__' of 'Second' objects>, '__doc__': None}
>>> list(Second.__dict__.keys())
['__module__', '__firstlineno__', 'one', 'two', '__static_attributes__', '__dict__', '__weakref__', '__doc__']
>>> Second.__dict__["one"]
'Depp'
>>> Second.one
'Depp'
>>> second = Second()
>>> dir(second)
['__class__', '__delattr__', '__dict__', '__doc__', ...,
'__weakref__', 'one', 'two']
>>> second.__dict__
{}
>>> second.two
'Pitt'
>>> Second.two = "Clooney"
>>> second.two
'Clooney'
Don't close that interactive session; we'll be using it again shortly. Our example shows some subtle differences between classes and instances. Although each contains a __dict__, the instance was a dict already, and bindings to the instance are seen only in that dict. In the class, however, the bound names also appear as part of the class's namespace, and __dict__ is no longer a dict, but something called a dict_proxy. A dict_proxy provides a selective view of the class's namespace. These differences are significant to a Python implementer, but for now you can file this information away.
| Note | In current Python, Second.__dict__ is a mappingproxy (previously
called dict_proxy in older versions). The text above uses the original term; the behaviour is the same.
Also note that Second.__dict__.keys() now returns a dict_keys view rather than a list,
so the example wraps it in list() to show the keys directly. |
More importantly, notice that the names one and two have been bound in the class namespace, and now also appear in the instance namespace (though not in its __dict__). In addition, they have the same value in the instance namespace as they do in the class namespace. If you rebind the name in the class namespace, it also changes in the instance. Our example demonstrates that names that appear to be in the instance namespace are actually defined in the class.
We'll take a closer look at the relationship between a class and its instances later. For the moment, just be aware that you can access attributes of the class in any of its instances. If you bind the same attribute to the instance, it does not change the class at all—the binding remains local to the instance. Continuing the interactive session, type the commands below as shown:
>>> second2 = Second()
>>> second2.one
'Depp'
>>> second2.one = "Bloom"
>>> Second.one = "DiCaprio"
>>> second.one
'DiCaprio'
>>> second2.one
'Bloom'
>>> dir(second2)
['__class__', '__delattr__', '__dict__', ..., 'two', 'one']
>>> second2.__dict__
{'one': 'Bloom'}
Here we created a second Second instance, named second2, which initially showed the same value as the class for its one attribute. When we assigned "Bloom" to the second2 instance's one attribute, it overrides the class attribute, but only for that one instance. The second instance's one attribute still reflects the class's value for that attribute. When the Second class's one attribute is rebound, the second instance's one attribute also changes, but not that of the second2 instance. (Did you catch all that?)
The attributes of a class can be accessed by all instances of that class but, as we've just seen, an assignment to an instance attribute of the same name will override the class attribute. Check out the last two expressions in the last session; not only does the second2 instance have a one attribute (the one inherited from the Second class) in its namespace, it also has a one attribute in its __dict__ as a result of being bound to second2.one. The interpreter is looking in an instance's __dict__ first, and only looks in the namespace if it fails to find the attribute in the dict.
Hopefully it's starting to feel natural to you to write functions that operate on instances of classes. Now let's suppose Python didn't have complex numbers, and you had to implement them yourself. How would you create a new complex number, and how would you add two complex numbers together? You could define a class called Cplx (Python already uses complex for the existing complex data type), then write a cplx() function to create a complex number from the values of its real and imaginary parts. Then you could implement a cadd() function that takes two complex numbers and returns the sum of the two as its result. You could also write a cstr function to call from inside print() to output complex values.
The resulting code, with a couple of calls on the functions to test the code, might look like the program we'll create now. Create a new program as shown:
"""Initial implementation of complex numbers."""
class Cplx:
pass
def cplx(real, imag):
c = Cplx()
c.real = real
c.imag = imag
return c
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = cplx(0.0, 0.0)
one = cplx(1.0, 0.0)
i = cplx(0.0, 1.0)
result = cadd(zero, cadd(one, i))
print(cstr(result))
Save it as cplx.py, and run it. The result 1.0+1.0j prints. You aren't using very much of Python's class mechanism though. To do that, you need to separate the creation of the instances from their initialization. Then you'll rename the cplx() function to cinit(), and change its code so that it operates on an existing rather than a new instance, initialize it and return the instance. This initially complicates your calling code, because you now have to create the instances before initializing them, but don't worry about that now. Let's play with some code! Modify your program as shown:
"""Initial implementation of complex numbers."""
class Cplx:
pass
def cplx(real, imag):
c = Cplx()
def cinit(c, real, imag):
c.real = real
c.imag = imag
return c
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = cplx(0.0, 0.0)
one = cplx(1.0, 0.0)
i = cplx(0.0, 1.0)
zero = Cplx()
cinit(zero, 0.0, 0.0)
one = Cplx()
cinit(one, 1.0, 0.0)
i = Cplx()
cinit(i, 0.0, 1.0)
result = cadd(zero, cadd(one, i))
print(cstr(result))
Save and run it. It prints the same result as before—after all, it's really the same code.
So far, we've focused on the data attributes of classes and their instances. We know that when a class and one of its instances both have the same name, the instance attribute "wins." We can access a class's attributes via the instance, as long as the instance doesn't have its own attribute with the same name. But assignment is only one way to bind values to a class. Another way is through the def statement used to define functions.
Go ahead and edit cplx.py so that the functions become methods of the class:
| Note | To indent the function declarations, just select the block of code you want to indent and press Tab. |
"""Initial implementation of complex numbers."""
class Cplx:
def cinit(c, real, imag):
c.real = real
c.imag = imag
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = Cplx()
Cplx.cinit(zero, 0.0, 0.0)
one = Cplx()
Cplx.cinit(one, 1.0, 0.0)
i = Cplx()
Cplx.cinit(i, 0.0, 1.0)
result = Cplx.cadd(zero, Cplx.cadd(one, i))
print(Cplx.cstr(result))
Save and run it. You might see warnings on the def lines stating that the methods should have self as the first parameter, but you can ignore them for now. You'll still get this result: 1.0+1.0j.
By declaring a function as part of the class body, we bind the function name within the class namespace rather than the module namespace. This means that, to call the function, it must be preceded by the class name and a dot. Because the class body is no longer empty, you don't need the pass statement any more.
Now let's break the code! Don't worry; we'll fix it right up once you understand the details of the breakage. The Cplx class has three new attributes—cinit, cadd, and cstr. You can access class attributes (attributes bound in the class namespace) through an instance of the class. So you'd think that you could access those methods through the instance, rather than the class. But when you change the code to do that, a strange error occurs. Modify cplx.py to call the methods on the instances as shown:
"""Initial implementation of complex numbers."""
class Cplx:
def cinit(c, real, imag):
c.real = real
c.imag = imag
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = Cplx()
Cplxzero.cinit(zero, 0.0, 0.0)
one = Cplx()
Cplxone.cinit(one, 1.0, 0.0)
i = Cplx()
Cplxi.cinit(i, 0.0, 1.0)
result = Cplxzero.cadd(zero, Cplxone.cadd(one, i))
print(Cpresulxt.cstr(result))
Save and run it. You might be surprised to see a traceback and error message:
Traceback (most recent call last):
File "./cplx.py", line 18, in <module>
zero.cinit(zero, 0.0, 0.0)
~~~~~~~~~~^^^^^^^^^^^^^^^^
TypeError: Cplx.cinit() takes 3 positional arguments but 4 were given
This message may be a bit difficult to understand. It says that zero.cinit(zero, 0.0, 0.0) has four arguments, but it's clear that it provides only three. Where is the source of the fourth argument?
When the interpreter sees a reference to a class's method relative to an instance, it assumes that the method will need to know which instance it was being called upon. Consequently, it inserts the instance as the first argument automatically. Methods are being called with too many arguments because the interpreter assumes you will want a reference to the instance, and inserts it automatically. The fix for your code is to remove the explicit instance arguments. Fix cplx.py as shown:
"""Initial implementation of complex numbers."""
class Cplx:
def cinit(c, real, imag):
c.real = real
c.imag = imag
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = Cplx()
zero.cinit(zero, 0.0, 0.0)
one = Cplx()
one.cinit(one, 1.0, 0.0)
i = Cplx()
i.cinit(i, 0.0, 1.0)
result = zero.cadd(zero, one.cadd(one, i))
print(result.cstr(result))
i.cinit(0.0, 1.0)
result = zero.cadd(one.cadd(i))
result = zero.cadd(one.cadd(i))
print(result.cstr())
Save and run it. You should get 1.0+1.0j as your result again.
The code you have developed so far works, but it's a little on the ugly side. Separating the creation of objects from their initialization means that two lines of code are required to create a complex number. To remedy the ugliness we'll unleash some of Python's deeper magic–programming techniques that are less widely known. But you, my friend, are about to dip a toe into Python deep magic to cast about yourself! Let's start with a special method Python provides to beautify your code: __init__().
When you create an instance of a class by calling it, the interpreter looks to see whether the class has an __init__() method. If it finds one, it calls that method on the newly-created instance. Because it's an instance method call, the new instance is inserted as the first argument to the call. Further, if the call to the class has any arguments, they are passed to __init__() as additional arguments.
| Note | The __init__() method must not return a value. If __init__() returns something, it affects the instance creation process. This causes the interpreter to raise an exception, and your program to fail. You'll learn about instance creation in more detail later. |
By renaming the Cplx class's cinit() method to __init__(), you can shorten the code that creates and initializes the new instance to a single line. Very nice. Python users appreciate elegance and simplicity. Ugly Python code can be a sign that the language isn't being used to its full advantage. Let's try a bit more experimentation. Edit cplx.py below as shown:
"""Initial implementation of complex numbers."""
class Cplx:
def c__init__(c, real, imag):
c.real = real
c.imag = imag
def cadd(c1, c2):
c = Cplx()
c.real = c1.real+c2.real
c.imag = c1.imag+c2.imag
c = Cplx(c1.real+c2.real, c1.imag+c2.imag)
return c
def cstr(c):
return "%s+%sj" % (c.real, c.imag)
if __name__ == "__main__":
zero = Cplx()
zero.cinit(0.0, 0.0)
one = Cplx()
one.cinit(1.0, 0.0)
i = Cplx()
i.cinit(0.0, 1.0)
zero = Cplx(0.0, 0.0)
one = Cplx(1.0, 0.0)
i = Cplx(0.0, 1.0)
result = zero.cadd(one.cadd(i))
result = zero.cadd(one.cadd(i))
print(result.cstr())
print(Cplx.cstr(result))
Save and run it. You'll get 1.0+1.0j for a result yet again. Python objects tend to have a lot of those special methods with names that begin and end with double underscores. To make discussing them easier, "__init__()" is often pronounced "dunder-init"; "dunder" being an abbreviation for "double under." We'll convert the other methods of your complex class to "dunder" methods in a bit.
When printing in Python, you get lots of help from the print() function. Without going into too much detail, the function converts each argument into a string by calling the object's __str__() method. So each class in Python can determine exactly how its instances get printed by defining a __str__() method. You can rename your cstr() method to __str__() and print Cplx instances directly.
Similarly, when you write a + b in Python, the interpreter tries to execute the task in a number of ways: first it tries to compute a.__add__(b) (which requires that a has a __add__ method). If that doesn't work, Python tries to compute b.__radd__(a). So, to enable your program to add Cplx objects, rename the cadd method to __add__.
Let's take another quick peek at the first argument of your class's methods—the one that the interpreter puts in automatically when you call a method on an instance. Experienced Python programmers would be able to interpret the code in the last listing, but they would want to know why the argument was called c or c1.
There is an almost universal convention that the first argument of a method should be called self. Reading other people's programs is difficult enough, so it's important to stick to convention—not only will it make your code easier for other programmers to read, it will make it easier for you to read as well, and that's an important time saver.
So how should the code look when you make all the changes discussed in the last two sections? Edit cplx.py below as shown:
"""Initial implementation of complex numbers."""
class Cplx:
def __init__(cself, real, imag):
cself.real = real
cself.imag = imag
def c__add__(c1self, c2):
c = Cplx(c1self.real+c2.real, c1self.imag+c2.imag)
return c
def c__str__(cself):
return "%s+%sj" % (cself.real, cself.imag)
if __name__ == "__main__":
zero = Cplx(0.0, 0.0)
one = Cplx(1.0, 0.0)
i = Cplx(0.0, 1.0)
result = zero.cadd( + one.cadd( + i))
print(Cplx.cstr(result))
Save and run it. You'll still get a result of 1.0+1.0j.
| Modern Python | For data-holding classes like Cplx, Python 3.7+ offers
dataclasses as a convenient alternative. Decorating a class with
@dataclass auto-generates __init__, __repr__,
and __eq__ from annotated fields, eliminating a lot of boilerplate.
That said, when you need custom arithmetic operators like __add__ and
__str__, writing them by hand (as here) remains the right approach. |
How does it feel to be an up-and-coming Python programmer? You've really come a long way! You've learned the basics of object-oriented programming in Python. The Python interpreter offers a lot of hooks in the form of __xxx__() methods that you can use to make your own classes as convenient and natural to work with as the built-in Python types.
In future lessons, you'll do lots more object-oriented programming; I'm confident you can handle it!
