DEV Community

Cover image for Object Relationships - Modeling Real World Relationships in Python
Kyra HP
Kyra HP

Posted on

Object Relationships - Modeling Real World Relationships in Python

Table of Contents:

Why use Objects and Object Oriented Programming (OOP)?

If you have coding experience, you have likely come across classes and objects, whether they're ones you created or ones native to your programming language. A class in programming simply refers to a bundle of related data and functionality. 'Hello' in Python, for example, is an object (or instance) of the String class. It comes with its own methods that enhance its functionality, such as capitalize(), join(), find(), etc. Classes create abstractions of real concepts with understandable, reusable code.

Creating your own classes and instances can add a lot of functionality and conciseness to your code, but what if you want those objects to be related to each other? Say you have objects representing paintings and objects representing artists, but now you want to bring them together. Once you have an understanding of how to build object relationships, you can model those real world connections in your code.

Here is a basic set up for the Artist and Painting classes. Right now they are completely separate entities. Each only includes the __init__ and __repr__ methods to initialize object attributes and generate a prettier string when printing the object. I'm also tracking all instances of each class with the class attribute all.

class Artist:
    all = []

    def __init__(self, name):
        self.name = name
        Artist.all.append(self)

    def __repr__(self):
        return f"Artist: {self.name}"


class Painting:
    all = []

    def __init__(self, title, genre):
        self.title = title
        self.genre = genre
        Painting.all.append(self)

    def __repr__(self):
        return f"Painting: {self.title}, {self.genre}"
Enter fullscreen mode Exit fullscreen mode

One to Many Relationships

One to many relationships refer to when an object of type A is associated with multiple objects of type B, but an object of type B only belongs to one object of type A.

Implementing a One-to-Many Relationships

From the example above, artists can have many paintings but paintings only belong to one artist. In order to model this one to many relationship, the object which 'belongs' to another object should include an attribute to keep track of that relationship. Since a painting can only belong to one artist, I'll add an attribute called artist to my Painting class.

class Painting:
    all = []

    # Added artist attribute below
    def __init__(self, title, genre, artist):
        self.title = title
        self.genre = genre
        self.artist = artist
        Painting.all.append(self)

    def __repr__(self):
        return f"Painting: {self.title} by {self.artist.name}, {self.genre}"
Enter fullscreen mode Exit fullscreen mode

NOTE: If you are creating your own application, it'd be beneficial to manage these attributes with properties which perform validation checks. For example, you can assert that a painting's artist attribute is an object of type Artist before setting it. You can read more about how to do this here.

Now if I create an example of a painting and its artist, I will always have access to that artist through the painting.

vangogh = Artist("Vincent van Gogh")
starry_night = Painting("Starry Night", "post-impressionism", vangogh)

print(starry_night)
print(starry_night.artist)
# LOG: Painting: Starry Night by Vincent van Gogh, post-impressionism
#      Artist: Vincent van Gogh
Enter fullscreen mode Exit fullscreen mode

This is a good start, but this relationship is still one-sided. We need to do a little more if we also want to view all of an artist's paintings. An object of type A, which has many objects of type B, can include a method to return all of its B objects. In this example, I can use list comprehension to access all paintings and print only the ones that belong to the current artist.

class Artist:
    all = []

    def __init__(self, name):
        self.name = name
        Artist.all.append(self)

    def __repr__(self):
        return f"Artist: {self.name}"

    # New method
    def paintings(self):
        return [painting for painting in Painting.all if painting.artist == self]
Enter fullscreen mode Exit fullscreen mode
vangogh = Artist("Vincent van Gogh")
starry_night = Painting("Starry Night", "post-impressionism", vangogh)
sower = Painting("The Sower", "post-impressionism", vangogh)

print(vangogh.paintings())
# LOG: [Painting: Starry Night by Vincent van Gogh, post-impressionism, 
#       Painting: The Sower by Vincent van Gogh, post-impressionism]
Enter fullscreen mode Exit fullscreen mode

Adding another one-to-many relationship

The one to many relationship between artists and paintings is now set up! I can easily add another one that behaves in a similar way. Here's an example of an additional class, Museum, that also has a one to many relationship with Painting:

class Artist:
    all = []

    def __init__(self, name):
        self.name = name
        Artist.all.append(self)

    def __repr__(self):
        return f"Artist: {self.name}"

    def paintings(self):
        return [painting for painting in Painting.all if painting.artist == self]


class Museum:
    all = []

    def __init__(self, name):
        self.name = name
        Museum.all.append(self)

    def __repr__(self):
        return f"Museum: {self.name}"

    def paintings(self):
        return [painting for painting in Painting.all if painting.museum == self]


class Painting:
    all = []

    def __init__(self, title, genre, artist, museum):
        self.title = title
        self.genre = genre
        self.artist = artist
        self.museum = museum
        Painting.all.append(self)

    def __repr__(self):
        return f"Painting: {self.title} by {self.artist.name}, {self.genre}, located at {self.museum}"
Enter fullscreen mode Exit fullscreen mode

Many to Many Relationships

Many-to-many refers to relationships where an object of type A can be associated with multiple instances of type B and vice versa. There are many real world examples that fall under this category, such as students and teachers, employees and projects, and actors and movies.

Because artists can have multiple paintings, any of which potentially belonging to different museums, it makes sense to say that an artist may be featured at multiple museums. Similarly, museums contain multiple paintings that may belong to different artists, so we can also say that museums feature multiple artists. This relationship between Artist and Museum would be considered a many-to-many relationship.

Implementing a Many-to-Many Relationship

Many-to-many object relationships can be implemented with intermediary classes where two classes are related to each other through the intermediary class. Because artists cannot be featured at museums without a painting and vice versa, Painting will serve as our intermediary class. Other options include creating intermediary classes for the primary purpose of connecting two other classes, or storing relationships in list attributes.

NOTE: If you are not using an intermediary class, be sure to store the many-to-many relationship in just one class to maintain a single source of truth. Without the Painting class, Artist might have a property called museums which is a list containing its associated museums. The Museum class should not include an artists property. Museum can instead include an artists() method that finds all artists which contain the current museum in its museums list.

In this example, we should be able to view all of an artist's museums by finding its paintings, then accessing the museums that those paintings belong to.

We already have a paintings() method in our Artist class so we can make another short method to access the corresponding museums.

class Artist:
    all = []

    def __init__(self, name):
        self.name = name
        Artist.all.append(self)

    def __repr__(self):
        return f"Artist: {self.name}"

    def paintings(self):
        return [painting for painting in Painting.all if painting.artist == self]

    # New method
    def museums(self):
        return [painting.museum for painting in self.paintings()]
Enter fullscreen mode Exit fullscreen mode

If we look at an Artist example, we can now access all of the museums they're featured at.

vangogh = Artist("Vincent van Gogh")
met = Museum("The MET")
npg = Museum("The National Portrait Gallery")
starry_night = Painting("Starry Night", "post-impressionism", vangogh, met)
sower = Painting("The Sower", "post-impressionism", vangogh, npg)

print(vangogh.museums())
# LOG: [Museum: The MET, 
#       Museum: The National Portrait Gallery]
Enter fullscreen mode Exit fullscreen mode

To complete the relationship, we need to add a similar method in the Museum class to access its featured artists through its paintings.

class Museum:
    all = []

    def __init__(self, name):
        self.name = name
        Museum.all.append(self)

    def __repr__(self):
        return f"Museum: {self.name}"

    def paintings(self):
        return [painting for painting in Painting.all if painting.museum == self]

    def artists(self):
        return [painting.artist for painting in self.paintings()]
Enter fullscreen mode Exit fullscreen mode

Now if we have multiple artists featured at one museum, we can access them directly from the Museum object.

met = Museum("The MET")
vangogh = Artist("Vincent van Gogh")
pollock = Artist("Jackson Pollock")
starry_night = Painting("Starry Night", "post-impressionism", vangogh, met)
convergence = Painting("Convergence", "abstract expressionism", pollock, met)

print(met.artists())
# LOG: [Artist: Vincent van Gogh,
#       Artist: Jackson Pollock]
Enter fullscreen mode Exit fullscreen mode

Conclusion

As you expand your knowledge of OOP, you may use object relationships in the context of databases. While this is a simple example only involving python, your future object methods may include queries to a database to access and maintain up-to-date information. Modeling object relationships allows us to build scalable, flexible, and complex systems.

Top comments (0)