Define a class

In python everything is an object. An object is characterize by attributes and methods.

Let's define a Square:

In [1]:
class Square:  # in python2 class Square(object)
    """
    Define a Square object
    """
    def __init__(self, side):  # define a method that initialize the instance
        """Instantiate a Square object. ::

        >>> square = Square(2)
        >>> square.side
        2
        """
        self.side = side  # set an attribute of the instance

Instantiate a class

We can instantiate the class with:

In [2]:
square = Square(2)
In [3]:
square.side
Out[3]:
2

side is an attribute of the instance Square.

In [4]:
Square?
Help on class Square in module __main__:

class Square(builtins.object)
 |  Define a Square object
 |  
 |  Methods defined here:
 |  
 |  __init__(self, side)
 |      Instantiate a Square object. ::
 |      
 |      >>> square = Square(2)
 |      >>> square.side
 |      2
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  __dict__
 |      dictionary for instance variables (if defined)
 |  
 |  __weakref__
 |      list of weak references to the object (if defined)

Add a new method to the class

Now we can create a method that return the area:

In [4]:
class Square:
    """
    Define a Square object
    """
    def __init__(self, side):
        """Instantiate a Square object. ::

        >>> square = Square(2)
        >>> square.side
        2
        """
        self.side = side

    def area(self):
        """Return the area. ::

        >>> square = Square(2)
        >>> square.area()
        4
        """
        return self.side * self.side
In [5]:
square = Square(2)
In [6]:
square.area()
Out[6]:
4

Exercise: complete the Rectangle class

Open the file: "geo_object.py". Inside you can find a class Rectangle:

In [9]:
class Rectangle(Square):
    def __init__(self, side, height):
        """Instantiate a Rectangular object. ::

        >>> rect = Rectangle(2, 4)
        >>> rect.side
        2
        >>> rect.height
        4
        """
        super(Rectangle, self).__init__(side)
        self.height = height

    def area(self):
        """Return the area. ::

        >>> rect = Rectangle(2, 4)
        >>> rect.area()
        8
        """
        pass

    def perimeter(self):
        """Return the perimeter. ::

        >>> rect = Rectangle(2, 4)
        >>> rect.perimeter()
        8
        """
        pass

Before to start

Below at the end of the of the python file we can find:

In [ ]:
if __name__ == "__main__":
    import doctest
    doctest.testmod()

This code, check if the file is execute as main program or as python module/library. If is execute as main program the import the doctest library and execute the testmod function.

Import a class from a module

In [10]:
from geo_objects import Square
In [11]:
imp_square = Square(3)
In [12]:
imp_square.area()
Out[12]:
9
In [13]:
imp_square.perimeter()
Out[13]:
12

With the above examples we read the file as a python module.

Execute the module

In [2]:
%%sh 
python3 geo_objects.py
**********************************************************************
File "geo_objects.py", line 58, in __main__.Rectangle.area
Failed example:
    rect.area()
Expected:
    8
Got nothing
**********************************************************************
File "geo_objects.py", line 67, in __main__.Rectangle.perimeter
Failed example:
    rect.perimeter()
Expected:
    8
Got nothing
**********************************************************************
2 items had failures:
   1 of   2 in __main__.Rectangle.area
   1 of   2 in __main__.Rectangle.perimeter
***Test Failed*** 2 failures.

or we can explicity execute all the doctest if we run the program with:

In [ ]:
%run python -m doctest geo_objects.py

What happened?

The module is called as "main" program, therefore the if condition that we have define at the end of the file is: True

In [ ]:
if __name__ == "__main__":
    import doctest
    doctest.testmod()

The testmod function read all the docstring inside the code and check if the class is working as described in the documentation

Time for coding!

Complete the programm fixing all the TODO in the code, start writing the docstring/doctest and then implementing the code!

Use hg to keep track of your modifications! :-D

Actually, the Rectangle is more general than a Square class, so change the code defining the Rectangle and then inherit from the Rectangle and define the Square.

Special methods

Each object has several special methods that we can overwrite, for example:

In [4]:
dir(square)  # dir is a function that return a list with the methods of a class
Out[4]:
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'area',
 'side']

When these methods are used?

In [10]:
square
Out[10]:
<__main__.Square at 0x7fbc2e147e10>
In [21]:
square.__repr__()
Out[21]:
'<__main__.Square object at 0x7fbc2e147e10>'

So, if we want to change the default rapresentation of an instance to something more meaningful/beatiful?

Overwrite the method!

In [10]:
class Square:
    """
    Define a Square object
    """
    def __init__(self, side):
        """Instantiate a Square object. ::

        >>> square = Square(2)
        >>> square.side
        2
        """
        self.side = side
        
    def __repr__(self):
        return "Square(side=%g)" % self.side
In [11]:
square = Square(3)
In [12]:
square
Out[12]:
Square(side=3)

Let's do something more useful!

Instantiate three different objects:

In [13]:
square0 = Square(3)
square1 = Square(4)
square2 = Square(3)

What is the result if we try to compare these different instances of the same class?

In [14]:
print(square0 == square1)
print(square0 == square2)
False
False

How can we teach to our object how to compare with other instances?

In [ ]:
Simple, define a new method: `__eq__`!
In [15]:
class Square:
    """
    Define a Square object
    """
    def __init__(self, side):
        """Instantiate a Square object. ::

        >>> square = Square(2)
        >>> square.side
        2
        """
        self.side = side
        
    def __eq__(self, square):
        try:
            return self.side == square.side
        except:
            return False

Time for coding!

Add a new magic method to compare different instances to each other.

In [16]:
square0 = Square(3)
square1 = Square(4)
square2 = Square(3)
In [17]:
print(square0 == square1)
print(square0 == square2)
False
True
In [19]:
square0.__eq__(square1)
Out[19]:
False
In [18]:
square0 == 2
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-18-9f6c58fb936d> in <module>()
----> 1 square0 == 2

<ipython-input-15-4ed91ce04367> in __eq__(self, square)
     13 
     14     def __eq__(self, square):
---> 15         return self.side == square.side

AttributeError: 'int' object has no attribute 'side'

Define other magic methods

Add others magic methods like:

  • __gt__ for >;
  • __ge__ for >=;
  • __lt__ for <;
  • __le__ for <=;

Maybe we can define a new general class Geo, defining these methods using the area instead of size of the side, and inherit the new class. Did you use hg? :-)

Special attributes

Python has three different types of attributes:

  • self.name => that is the standard;
  • self._name => that mean that the attribute should not be used;
  • self.__name => that is an "hide" attribute of the class

So, let see some more examples

Single underscore

Names, in a class, with a leading underscore are simply to indicate to other programmers that the attribute or method is intended to be private. However, nothing special is done with the name itself.

To quote PEP-8:

_single_leading_underscore: weak "internal use" indicator. E.g. from M import * does not import objects whose name starts with an underscore.

Double Underscore (Name Mangling)

From the Python docs:

Any identifier of the form __spam (at least two leading underscores, at most one trailing underscore) is textually replaced with _classname__spam, where classname is the current class name with leading underscore(s) stripped. This mangling is done without regard to the syntactic position of the identifier, so it can be used to define class-private instance and class variables, methods, variables stored in globals, and even variables stored in instances. private to this class on instances of other classes.

And a warning from the same page:

Name mangling is intended to give classes an easy way to define “private” instance variables and methods, without having to worry about instance variables defined by derived classes, or mucking with instance variables by code outside the class. Note that the mangling rules are designed mostly to avoid accidents; it still is possible for a determined soul to access or modify a variable that is considered private.

In [15]:
class TestAttrs:
    def __init__(self):
        self._semiprivate = "I'm still visible"
        self.__superprivate = "What are you doing?"
        
In [2]:
test_attrs = TestAttrs()
In [3]:
test_attrs._semiprivate
Out[3]:
"I'm still visible"
In [4]:
test_attrs.__superprivate
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-4-1e0d2d5b30e5> in <module>()
----> 1 test_attrs.__superprivate

AttributeError: 'TestAttrs' object has no attribute '__superprivate'
In [6]:
dir(test_attrs)
Out[6]:
['_TestAttrs__superprivate',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_semiprivate']
In [7]:
test_attrs._TestAttrs__superprivate
Out[7]:
'What are you doing?'

class attribute

Another type of attributes that some time can be useful are the class attributes. The class attribute are attribute that are not of the instance but of the class, so they are shared with all the instances.

Let say that we want to add a counter of the number of geometries.

class Geom(object):
    geometries = 0

    def __init__(self, xshift=0, yshift=0):
        self.__class__.geometries += 1

All the classes that will inherit from Geo will have an attribute geometries whit the number of geometries that have been created so far.

Unexpected behaviours?

Try to instantiate a Square using a character instead of a number.

In [17]:
square = Square('a')
In [18]:
square.side
Out[18]:
'a'
In [19]:
square.perimeter()
Out[19]:
'aaaa'
In [20]:
square.area()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-20-5da6bf4bd3b8> in <module>()
----> 1 square.area()

/home/pietro/docdat/phd/pubblicazioni/eurac/corso/geo_objects.py in area(self)
     27         4
     28         """
---> 29         return self.side * self.side
     30 
     31     def perimeter(self):

TypeError: can't multiply sequence by non-int of type 'str'

Is it a problem?

No, in general is a good rule to use the try/except syntax to manage this kind of errors.

In [ ]:
try:
    square = Square("a")
    square.area()
except TypeError:
    print("Wrong type, you must use a number.")
    raise TypeError

Today we want to see others interesting features of the language, so suppose that we want to prevent this kind of errors, before the error and not after.

Define a function to check if the parameter is a number

In [2]:
def check(x, addend=0):
    """Chek if x is a number.
    
    >>> check(1)
    1
    >>> check(1.0)
    1.0
    >>> check('a')  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
        ...
    TypeError: 'Wrong value, the value must be a number'
    """
    try:
        addend + x
        return x
    except TypeError:
        raise TypeError('Wrong value, the value must be a number')

Note: we add a comment: # doctest: +IGNORE_EXCEPTION_DETAIL to consider the test correct if raise an exception. there are several doctest option available to manage other particular cases, the most used are:

>>> [i for i in range(0, 10)]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [i for i in range(0, 10)]  # doctest: +ELLIPSIS
[0, ..., 9]
>>> print("""a long
... long,
... long message""") # doctest: +NORMALIZE_WHITESPACE
a long long, long message
>>> print("a very long string in one row") # doctest: +NORMALIZE_WHITESPACE
a very
long string
in one row

For a complete list of Option Flags see the python documentation.

What happend if we change the default parameter addend=0 to addend='a' or addent=[0, ]

In [3]:
check(1, addend='a')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-a7f7b342b97e> in <module>()
----> 1 check(1, addend='a')

<ipython-input-2-c3720fa6541c> in check(x, addend)
     15         return x
     16     except TypeError:
---> 17         raise TypeError('Wrong value, the value must be a number')

TypeError: Wrong value, the value must be a number
In [4]:
check('b', addend='a')
Out[4]:
'b'
In [5]:
check([1, 2, 'a'], addend=[0, ])
Out[5]:
[1, 2, 'a']
In [5]:
[1, 2, 3] + [0, ]
Out[5]:
[1, 2, 3, 0]
In [6]:
class Square:
    def __init__(self, side):
        self.side = check(side)
In [7]:
square = Square('a')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-8a148eeff86e> in <module>()
----> 1 square = Square('a')

<ipython-input-6-4d21b283c084> in __init__(self, side)
      1 class Square:
      2     def __init__(self, side):
----> 3         self.side = check(side)

<ipython-input-1-b26cce9d45e8> in check(x, addend)
     14         return x + addend
     15     except TypeError:
---> 16         raise TypeError('Wrong value, the value must be a number')

TypeError: Wrong value, the value must be a number

If we don't raise an exception in the function check, we will have:

In [9]:
def check(x, addend=0):
    """Chek if x is a number.
    
    >>> check(1)
    1
    >>> check(1.0)
    1.0
    >>> check('a')
    Wrong value, the value must be a number
    """
    try:
        addend + x
        return x
    except TypeError:
        print('Wrong value, the value must be a number')
In [10]:
class Square:
    def __init__(self, side):
        self.side = check(side)
In [11]:
square = Square('a')
Wrong value, the value must be a number
In [12]:
square.side = 'a'

Therefore we put the check in the wrong place!

Getters/Setters and property

We can define two "private" methods that will be call to get or to set the attribute.

In [36]:
class Square:
    def _get_side(self):
        return self._side

    def _set_side(self, side):
        self._side = check(side)

    side = property(fget=_get_side, fset=_set_side)

    def __init__(self, side):
        self._side = None
        self.side = side
In [37]:
square = Square('a')
Wrong value, the value must be a number
In [13]:
square.side = 'a'
Wrong value, the value must be: float

Using decorators

In [38]:
class Square:
    @property
    def side(self):
        return self._side
    
    @side.setter
    def side(self, side):
        self._side = check(side)
        
    @side.deleter
    def side(self):
        del self._side
        
    @property
    def read_only(self):
        return 5

    def __init__(self, side):
        self._side = None
        self.side = side
In [39]:
square = Square('a')
Wrong value, the value must be a number
In [40]:
square.side = 'a'
Wrong value, the value must be a number
In [41]:
square.read_only
Out[41]:
5
In [42]:
square.read_only = 'b'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-42-ff83bf6a1969> in <module>()
----> 1 square.read_only = 'b'

AttributeError: can't set attribute

Time for coding!

Apply the same check to the classes that you have define in the file geo_objects.py

Unit Test

So far we have seen that we can use the docstring in our objects to create the main documentation, but we can use doctest, to check that our documentation is update and it is doing what it sais. Most importantly the doctest help us to focus on how we want to interact with our classes.

To test a complex software often the doctest are not enough.

Often before to test a method/function we need to create some data, run the test, check if it worked or not and then clean everything before to start the next test. For this kind of problem doctest are too verbose and repetitive.

In the standard library you can find the unittest framework.

Previously in the method area of the Square class we wrote:

In [ ]:
"""Return the area. ::

    >>> square = Square(2)
    >>> square.area()
    4
"""

We can do the same test with the unittest with:

In [8]:
%%file test_geo.py
import unittest
from geo_objects import Square


class TestSquare(unittest.TestCase):

    def setUp(self):
        """Set up the 'environment' for each test, it is called before each test"""
        self.square = Square(2)
        
    def tearDown(self):
        """Clean the 'environment' afetr each test."""
        pass

    def test_area(self):
        """Is the area compute correctly?"""
        self.assertEqual(self.square.area(), 4)
        
    def test_perimeter(self):
        """Is the perimeter compute correctly?"""
        self.assertEqual(self.square.perimeter(), 4)
        
if __name__ == '__main__':
    unittest.main()
Writing test_geo.py
In [9]:
%run test_geo.py
.F
An exception has occurred, use %tb to see the full traceback.

SystemExit: True
======================================================================
FAIL: test_perimeter (__main__.TestSquare)
Is the perimeter compute correctly?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/pietro/docdat/phd/pubblicazioni/eurac/corso/test_geo.py", line 16, in test_perimeter
    self.assertEqual(self.square.perimeter(), 4)
AssertionError: 8 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

As before with the doctest we can also execute all the test with:

In [10]:
%%sh
python -m unittest test_geo
python -m unittest test_geo.TestSquare
python -m unittest test_geo.TestSquare.test_area
.F
======================================================================
FAIL: test_perimeter (test_geo.TestSquare)
Is the perimeter compute correctly?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./test_geo.py", line 16, in test_perimeter
    self.assertEqual(self.square.perimeter(), 4)
AssertionError: 8 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)
.F
======================================================================
FAIL: test_perimeter (test_geo.TestSquare)
Is the perimeter compute correctly?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "./test_geo.py", line 16, in test_perimeter
    self.assertEqual(self.square.perimeter(), 4)
AssertionError: 8 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

If we run python only with mode unittest without any path to file or directory, python start to looking for test.

In [12]:
%%sh
python -m unittest
.F
======================================================================
FAIL: test_perimeter (test_geo.TestSquare)
Is the perimeter compute correctly?
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/pietro/docdat/phd/pubblicazioni/eurac/corso/test_geo.py", line 16, in test_perimeter
    self.assertEqual(self.square.perimeter(), 4)
AssertionError: 8 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)

Unittest supports simple test discovery. In order to be compatible with test discovery, all of the test files must be modules or packages importable from the top-level directory of the project (this means that their filenames must be valid identifiers).

The valid pattern identifier is: test*.py

So the unittest will try to import and execute everything that start with testsomething.

But it is also possible to specify a patter, with:

python -m unittest -p *_test.py

In the previous example we used a method self.assertEqual to check if the result of method is correct or not, the unittest provide several methods to check different things, like:

  • assertEqual(a, b) => a == b
  • assertNotEqual(a, b) => a != b
  • assertTrue(x) => bool(x) is True
  • assertFalse(x) => bool(x) is False

Etcetera, please have a look at the Test Case section in the main documentation.

In [6]:
%%file test_skip.py
import unittest
import sys

class MyTestCase(unittest.TestCase):

    @unittest.skip("demonstrating skipping")
    def test_nothing(self):
        self.fail("shouldn't happen")

    @unittest.skipIf(sys.version_info.major > 2 ,
                     "not supported in this library version")
    def test_format(self):
        # Tests that work for only a certain version of the library.
        pass

    @unittest.skipUnless(sys.platform.startswith("win"), "requires Windows")
    def test_windows_support(self):
        # windows specific testing code
        pass
    
    
if __name__ == '__main__':
    unittest.main()
Overwriting test_skip.py
In [9]:
%%sh
python -m unittest test_skip --verbose
test_format (test_skip.MyTestCase) ... skipped 'not supported in this library version'
test_nothing (test_skip.MyTestCase) ... skipped 'demonstrating skipping'
test_windows_support (test_skip.MyTestCase) ... skipped 'requires Windows'

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK (skipped=3)

Time for coding!

Add tests to your geo_objects library.

Summary

What we have seen:

  • How to define a class;
  • How to define an attribute;
  • How to define a method;
  • How to define a magic method;
  • How to inherit from another class;
  • How to work with get/set and property;
  • How to define a class attribute.
  • How to write tests using doctest and unittest.