Skip to content

Python Objects

This section presents python classes and objects and some common use cases. The concept of objects and classes is permeating computing and it is often referred to as an object-oriented-programming paradigm.

Definition

Classes represent aggregations of various data type values capturing some semantic concept. Classes are templates from which object instances are constructed. They are defined by using the class keyword followed by the name of the class. By convention, class names start with upper-case letters and objects with lower-case.

The special function __init__( ) is known as a constructor. The first parameter self is special because it is used for defining and accessing the data items, or member variables, stored within an object. It must appear first before any additional parameters. Additionally, it is not possible to return a value from a constructor because the result value is the new object itself by convention.

The following syntax is used for declaring a Triangle class. Geometrically we can define a triangle by three points in space or in the plane. The class' name is used as a function for constructing triangle objects. The parameters are passed to the __init__( ) method and the new object is returned.

""" Declaring a triangle class
"""
class Triangle:
    """ Constructor
    """
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

""" Constructing a triangle object
"""
triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Getting member values
"""
print( triangle.PointA   )
print( triangle.PointA.X )

""" Setting member values
"""
triangle.PointB = Point3d( 2.0, 0.0, 0.0 )
triangle.PointC.X = 3.0

Member variables can be accessed using the object-dot-name syntax. Note that member variables can be objects themselves, therefore it is not uncommon to encounter consecutive applications of the object-dot-name convention for accessing values in a hierarchical manner.

Methods

Apart from packaging values using member variables, classes may also contain functions which express various semantically relevant operations. Those are declared as functions within the class definition. Functions that belong to classes and objects are also known as member methods. Methods require their first parameter to be the keyword self which enables accessing an object's members within the function's body.

In the example below triangle objects now support translation using the Translate( ) method. The object-dot-method syntax is used for calling such functions. Note that the translation method is modifying the triangle itself or in-place instead of creating a new translated copy.

""" Declaring a triangle class
"""
class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Translate Method
    """
    def Translate( self, direction ):
        self.PointA += direction
        self.PointB += direction
        self.PointC += direction
        return self

""" Constructing a triangle object
"""
triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Calling the translation method
"""
triangle.Translate( Vector3d( 0.0, 0.0, 1.0 ) )

It is common to return the object itself return self if there is no other meaningful thing to return. This is because this idiom allows chaining operations using the dot syntax such as triangle.Translate( ).Rotate( ).Scale( ).AndSoOn( ). This is possible exactly because the result of each operation is the object itself. However, if nothing or a value other than self was returned, then we could not have used this chaining style of writing expressions.

Get and Set

Instead of modifying an object's member variables it is often preferred to provide special access methods known as getters and setters. This approach is used for keeping some form of isolation between the internal state and the outside world world, as well as for introducing computation logic associated with an object's attributes.

In the example below, the GetCentroid( ) method computes the triangle's centroid from its points. The notion a triangle's centroid computed as the mean of its points is mathematically objective and unambiguous.

The SetCentroid( ) method requires a centroid to be passed as a parameter such that the triangle is modified accordingly. This operation requires some assumptions such as the area of triangle should not change and/or it should only be transformed in a rigid manner without rotation etc.

The implementation below uses the current (computed) and desired (passed as parameter) centroids to form a vector and translate the triangle.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Centroid Getter
    """
    def GetCentroid( self ):
        return ( self.PointA +
                 self.PointB +
                 self.PointC ) / 3

    """ Centroid Setter
    """
    def SetCentroid( self, centroid ):
        return self.Translate(
            self.GetCentroid( ) - centroid )

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Getting the triangle's centroid
"""
centroid = triangle.GetCentroid( )
print( centroid )

""" Setting the triangle's centroid
"""
triangle.SetCentroid( Point3d.Origin )

Properties

Class properties are supported in python version >= 3 and Rhino >= 8. They provide a stylistic improvement over getter and setter methods, such that object properties look and feel like member variables even though they are methods.

Property getters must be annotated using the @property symbol before their definition and setters using the @PropertyName.setter, here @Centroid.setter symbol as seen below. Thereafter, the centroid property can be accessed as it was a member variable.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Centroid Property Getter
    """
    @property
    def Centroid( self ):
        return ( self.PointA +
                 self.PointB +
                 self.PointC ) / 3

    """ Centroid Property Setter
    """
    @Centroid.setter
    def Centroid( self, centroid ):
        self.Translate(
            self.GetCentroid( ) - centroid )

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Getting the triangle's centroid property
"""
centroid = triangle.Centroid
print( centroid )

""" Setting the triangle's centroid property
"""
triangle.Centroid = Point3d.Origin

Note that we don't need to use the GetSomething and SetSomething style anymore. In addition, the setter methods by convention must require a new value passed as a parameter and cannot return a value themselves.

However, it is not required to provide both getters and setters for every property. Some properties may just support only getting their value. This is conceptually equivalent to being read-only.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Read-Only Property
    """
    @property
    def Area( self ):
        return Vector3d.CrossProduct(
            self.PointB - self.PointA,
            self.PointC - self.PointA ).Length / 2

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Getting the triangle's area
"""
print( triangle.Area )

Statics

Classes can define member variables that are shared among all objects instantiated. They are known as class members or static members as opposed to object members. The are defined outside methods and for accessing them the name of the class must be used with the dot syntax instead of the object's name or the self keyword.

In the example below we tag each triangle created with a serial number. In order to do so we need to keep track for how many objects have been constructed. We can use a static variable named SerialNumbers representing the next available serial number. Every time a new triangle is created its SerialNumber is assigned by looking up what is the next available serial number. But since we used the currently available we also need to increment the static member.

''' Triangle class declared -> S/N = 0
'''
class Triangle:
    """ Shared Static Member
    """
    SerialNumbers = 0

    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

        """ Read and Update Static
        """
        self.SerialNumber = Triangle.SerialNumbers
        Triangle.SerialNumbers += 1

''' Triangle created -> S/N = 0 -> S/N += 1
'''
a = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

''' Triangle created ->  S/N = 1 -> S/N += 2
'''
b = Triangle(
    Point3d( 0.0, 1.0, 0.0 ),
    Point3d( 1.0, 0.0, 1.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

print( a.SerialNumber ) ''' 0 '''
print( b.SerialNumber ) ''' 1 '''

Static class methods are commonly used for (a) performing operations that are semantically relevant to the concept captured by the class but without creating new objects, and (b) for defining alternative methods of construction. Static methods must be annotated with the @staticmethod symbol prior to their definition.

The example below demonstrates these use cases. The AreaFromPoints( ) can be used without first constructing a triangle from three points only to just measure its area using the Area property. Additionally, the Equilateral( ) methods provides a shortcut for constructing a specialized form of a triangle by passing in only its side's length.

Note that static methods do not support the self keyword because there is no object constructed and thus cannot be referenced.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Static Utility Method
    """
    @staticmethod
    def ComputeArea( pa, pb, pc ):
        return Vector3d.CrossProduct(
            pb - pa, pc - pa ).Length / 2

    @property
    def Area( self ):
        return Triangle.ComputeArea(
            self.PointA,
            self.PointB,
            self.PointC )

    """ Static Construction Method
    """
    @staticmethod
    def Equilateral( alpha ):
        return Triangle(
            Point3d( 0.0, 0.0, 0.0 ),
            Point3d( alpha, 0.0, 0.0 ),
            Point3d( alpha / 2, math.sqrt( 3 ) * alpha / 2, 0.0 ) )

a = Point3d( 0.0, 0.0, 0.0 )
b = Point3d( 1.0, 0.0, 0.0 )
c = Point3d( 0.0, 1.0, 0.0 )

""" Computing the area
"""
area = Triangle( a, b, c ).Area
#-- equivalent without construction
area = Triangle.ComputeArea( a, b, c )

""" Constructing an equilateral triangle
"""
equilateral = Triangle.Equilateral( 100.0 )

Overloading

Operator overloading enables (re)defining how arithmetics are performed with object values. Typical mathematical operators such as addition +, subtraction -, multiplication * etc can be overloaded by implementing a special member method, namely __add__, __sub__ and __mul__, respectively. Complete list of all operators is available by python documentation.

In the example below the addition operation between a triangle and vector is associated with performing translation. Note that by convention operators should return a new value instead of modifying and existing one, hence constructing a new translated triangle.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Operator self + other -> new result
    """
    def __add__( self, direction ):
        return Triangle(
            self.PointA + direction,
            self.PointB + direction,
            self.PointC + direction )

""" Constructing a triangle
"""
a = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" A translated copy of a
"""
b = a + Vector3d( 0.0, 0.0, 1.0 )

Indexing

The __getitem__, __setitem__ and __len__ methods when defined by a class allow to make objects behave like collections of data values such as lists or dictionaries. The example below presents a scenario where a triangle's points can be read or written using the same notation as with lists, namely the square brackets notation with an index.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Operator len( self )
    """
    def __len__( self ):
        return 3

    """ Operator self[index] -> value
    """
    def __getitem__( self, index ):
        if( index == 0 ): return self.PointA
        if( index == 1 ): return self.PointB
        if( index == 2 ): return self.PointC
        raise IndexError( 'Index Out of Range' )

    """ Operator self[index] <- value
    """
    def __setitem__( self, index, point ):
        if( index == 0 ):
            self.PointA = point
        elif( index == 1 ):
            self.PointB = point
        elif( index == 2 ):
            self.PointC = point
        else:
            raise IndexError( 'Index Out of Range' )

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" The triangle works with len( )
"""
print( 'The triangle has {} points'.format( len( triangle ) ) )

""" Getting corner point by index
"""
point = triangle[0]

""" Setting corner point by index
"""
triangle[0] = Point3d( 1.0, 1.0, 1.0 )

""" Indexed enumeration
"""
for index in range( len( triangle ) ):
    print( triangle[index] )

Enumerating

We can make object behave like enumerable collections using generator methods. This allows using them in standard for-loops without the range( len( ) ) syntax. Note that generators are supported in python version >= 3 and Rhino >= 8.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Enumerator Property
    """
    @property
    def Points( self ):
        yield self.PointA
        yield self.PointB
        yield self.PointC

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Using the Points property in a loop
"""
for point in triangle.Points:
    print( point )

Dumping

Classes may define a special method named __repr__ which is used for expressing an object's data in a text string representation. This is useful for development purposes because such objects can be dumped into the terminal with the builtin print( ) function, or in general converted to strings.

class Triangle:
    def __init__( self, pa, pb, pc ):
        self.PointA = pa
        self.PointB = pb
        self.PointC = pc

    """ Operator self -> str
    """
    def __repr__( self ):
        return 'Triangle A:[{}] B:[{}] C:[{}]'.format(
            self.PointA,
            self.PointB,
            self.PointC )

triangle = Triangle(
    Point3d( 0.0, 0.0, 0.0 ),
    Point3d( 1.0, 0.0, 0.0 ),
    Point3d( 0.0, 1.0, 0.0 ) )

""" Automatically calls __repr__
"""
print( triangle )

Inheriting

Classes are often used for creating hierarchies via the concept of inheritance. The idea is that concepts can be extended by reusing some existing, instead of rewriting, code. To create a derived class, the type of the base class must be provided parenthesised next to the class' name. Additionally, the constructor typically calls the base class' constructor and initializes any additional member variables. A derived class automatically inherits all of the base class' member variables and methods. Those can be extended or redefined.

In the example below a triangular prism extends the base triangle class providing semantics for an extruded solid object with a triangular profile. In this scenario apart from the three points that define the profile we also need an extrusion height, therefore the constructor requires four parameters. Inside the constructor the base class is first initialized using only the points and then the member Height is stored. As an example of how the derived class can use its parent's methods, the Volume property is defined, whereby the result is computed as the product of the triangle's area property times its extrusion height.

class ExtrudedTriangle( Triangle ):
    def __init__( self, pa, pb, pc, height ):
        """ Construct parent class
        """
        Triangle.__init__( self, pa, pb, pc )

        """ Store additional members
        """
        self.Height = height

    """ Use parent class methods
    """
    @property
    def Volume( self ):
        return self.Area * height

Interfaces

Another very useful and commonly used feature of classes is known as interface inheritance or polymorphism or dynamic dispatch. The idea is to establish a certain set of rules or behaviors that derived classes must comply with, as in they must implement, such that we can use them without caring about the internals or having to deal with differences between their types. Therefore, interfaces describe the common features between classes.

In the example below the Shape interface is defined. It establishes the requirement whereby all derived classes must define a method CenterOfMass( ) that returns the center of mass. Next we can derive specific shapes, such as lines and circles, and implement CenterOfMass( ) appropriately. Then we can process collections of shapes knowing that they all support this feature.

""" Shape requires CenterOfMass( )
"""
class Shape:
    def CenterOfMass( self ):
        pass

""" Line extends Shape
"""
class Line( Shape ):
    def __init__( self, source, target ):
        self.Source = source
        self.Target = target

    """ Line implements Shape
    """
    def CenterOfMass( self ):
        return ( self.Source + self.Target ) / 2

""" Circle extends Shape
"""
class Circle( Shape ):
    def __init__( self, center, radius ):
        self.Center = center
        self.Radius = radius

    """ Circle implements Shape
    """
    def CenterOfMass( self ):
        return self.Center

""" Create a list containing different shapes
"""
shapes = [  Line( Point3d( 0.0, 0.0, 0.0 ),
                  Point3d( 1.0, 0.0, 0.0 ) ),
          Circle( Point3d( 1.0, 1.0, 0.0 ), 1.0 )]

""" Since all shapes implement the Shape interface
    we can call their CenterOfMass method safely
"""
for shape in shapes:
    """ Dynamic Dispatch
    """
    point = shape.CenterOfMass( )
    print( point )