Class coding and usage

I’m trying to extend my Python skills and feel that I need to tackle the subject of Classes. I’ve read a few tutorials, but none of them seem to deal with the topic in a way that I can fully relate to. By pure coincidence this post popped up which seemed to me to be a project to which I could apply a ‘Class’.

Rather than hijack that thread, I thought it better to create this RFC thread.

This is a working example of what I’ve coded:

import csv


class movie:
    def __init__(self, title, director, year):
        self.title = title
        self.director = director
        self.year = year


# this would be a csv file read in fact, rather than a hard coded object
data = (
    "Scarface,Brian De Palma,1983",
    "Reservoir Dogs,Quentin Tarantino,1992",
    "Jackie Brown,Quentin Tarantino,1997",
    "Pulp Fiction,Quentin Tarantino,1994",
    "Monty Python's Life of Brian,Terry Jones,1979",
    "American Gangster,Ridley Scott,2007"
)

movies = {}

#===============<data loader>================#
for enrty in data:
    read = csv.reader(data)
    for entry in read:
        title = entry[0]
        director = entry[1]
        year = entry[2]
        key = title.replace(" ", "_")
        movies[key] = movie(title, director, year)
#=============<end of data loader>============#

Now, the details of any movie can be accessed in the usual way…

print(movies['Pulp_Fiction'].title)
print(movies['Scarface'].director)

… and so on.

The RFC is for how I’ve coded this and is it a good example of, not only how to code a Class, but also the usage of said.

This is my first attempt at coding/using a custom Class in any app, so please be fair, but also honest.

Thank you.

1 Like

Before looking at creating your own classes, it’s worth taking some time to understand the classes that already exist. I’m going to use the built-in integer class as an example here.

What can you do with integers? You can create them and print them out:

x = int("1A2B3", 16)
print(x)
# displays: 107187

They have attributes, although not particularly exciting ones here:

print(x.real, x.imag)
# prints the number itself, and zero for the imaginary part)

And they have methods, which are “function attributes” if you like:

binary = x.to_bytes(4, "big")
# is the bit string b'\x00\x01\xa2\xb3'

Integers also respond to operators, although not all classes have to:

x = 5 * 7
# x is now 35

Great! So, how can you make your own type? First off, you define a class. You then usually want to define how you create them, which you’ve done with your __init__ method. That right there is a great clue as to how Python lets you hook into different parts of the language: create a method that begins and ends with a double underscore. So, how’s your movie class looking? Pretty good so far, in fact.

The only trouble is, it’s not really much of a class. So the next real question is: what can you do with a movie? You have some attributes, but without anything else, your class is just a storage area or dump space. What methods would a movie have? Would it respond to operators? For example, you could have movies respond to comparison operators, such as:

"Luke Skywalker" in movies["Star Wars: A New Hope"]
# returns True because the character is present
"Jar Jar Binks" in movies["Star Wars: A New Hope"]
# returns False, because mistakes that bad weren't made until the prequel trilogy

Or you could have a movie.watch() method that locates the movie on a streaming service you have a subscription to, and launches your web browser to let you watch that movie. Or whatever makes sense for what you’re doing.

So, the short answer is: Your class is fine, now you just need to make it do more :slight_smile:

One small point though. It’s conventional for custom classes to be named with an uppercase first letter, so this would be class Movie: instead of class movie:. It’s a minor thing but people will expect it, so code is easier to read when that pattern is maintained. Otherwise though, looks good, carry on!

4 Likes

Hi @rob42,

Thanks for the class example.

Following are a couple of suggestions.

See Class Names in PEP 8 – Style Guide for Python Code. It suggests using the CapWords convention with class names. Accordingly, you could use Movie instead of movie.

Also see object.__repr__(self) in the Data model documentation. Based on that information, you could include this dunder method in the class Movie definition:

    def __repr__(self):
        return f'Movie("{self.title}", "{self.director}", "{self.year}")'

Then print one of the Movie instances. You should see the same code that would be used to instantiate that instance.

print(movies["Monty_Python's_Life_of_Brian"])

Output:

Movie("Monty Python's Life of Brian", "Terry Jones", "1979")

To verify that it works, copy that output and use it to create an equivalent instance.

There are of course additional features that you could add to the class Movie definition. As you learn more, just try out the ideas and enjoy the adventure.

1 Like

@Rosuav and @Quercus

My thanks to you both for the feedback: it’s good to know that I am (least ways) in the right ball park.

I agree that my Class lacks functionality (which is one of the reasons for the RFC). As an example, I have implemented a function that takes a “search” argument and returns any movie in which the search term is found. The way I’ve done that, is via a string object, but I do feel that it’s a bit of a hack and that such functionality could and should be baked into my ‘Class’.

I will work on that concept and post back if if needs be.

Again, thank you for your help, guidance, insights and of course your time.

Peace to you both.

2 Likes

The way I understand classes is that its the combination of some data and some behaviour.

As suggested your Movie class could know how to play() the movie, access IMDB to return a list of actors, etc. Without the methods the class is missing that behaviour.

What is the purpose of the class?

As a rule-of-thumb if your answer includes the word AND you likely have a design issue.
As in Movie plays movies AND orders pizza.

1 Like

Hang on, that actually sounds like a really good EntertainmentNight class… Kappa

1 Like

In practical terms, the purpose is a ‘moving target’, as I’m going to extend the functionality of the ‘Class’, inline with what I would like it to be able to do: it’s a very basic Class right now and simply creates instances of a movie and stores said in a dictionary object.

Keep in mind that this is a branch of off the thread that I’ve linked and I don’t want to teach the OP anything that would be considered ‘bad practice’, hence the RFC here.

1 Like

That not an issue, in fact that’s almost always the case as software envolves.
You can always update the purpose as requirements change and feedback from the implementation.

With OO having as clear an idea as you can of your requirements helps a lot.

1 Like

Thank you for your feedback.

I know that one can choose ignore all OO features in Python and still get quite a bit of useful coding done, which, for the most part, I have been doing over the years.

I also know that there are times when OO features make a lot of sense and would actually reduce complexity, which I feel (rightly or wrongly) would be the case for the Project to which I’ve linked and from which this thread has spawned.

If I’m wrong, then tell me so and I’ll advise the OP that a ‘Class’ is not the way to go and I’ll simply carry on with my exploration and exclude posting anything else to the other thread that relates to creating and using a ‘Class’ for that project: my overriding concern is that I lead the OP in the wrong direction.

1 Like

Using a class is a reasonable thing to learn for the OP’s problem. A Movie class that has methods to do the show etc.

2 Likes

That’s fine. Along with Barry’s suggestion that a class is some data and
behaviour, typically a class instance refers to a specific object with a
central definition. In your case it’s a movie, and you should keep that
in mind as the focus.

So most of the methods are things you can do with a movie, or things
you can ask about a movie. You might make other classes for related
concepts such as a MovieNightEvent which might have a time/date and
references to some movies as part of its data. Etc.

And a MovieNightEvent.play(movie) method might itself ahnd off to
some method on the movie eg movie.play(). Not everything needs to
implement everything, some things are convenience/helper methods which
know how to run other methods on related objects.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

A bit late to the party movie night, but while others have done well to answer the “what” of classes (and OO), what I haven’t seen so much here—and what I really struggled with for a long time as a beginner—is the “why”. This is something I didn’t really understand for a long time when I was learning programming, at least taking a basic Java class (pun intended) that focused a lot on OO, but I rather picked up in practice after I got started helping out on a larger open source project (Spyder).

(Sidenote—I’ve found answering user questions here, as you are doing here, to be a great way to broaden and sharpen my knowledge. Conversely, I’ve found contributing to a decent-sized FOSS project to be a great way to deepen it).

To summarize, two really important and powerful features of classes—arguably, the primary motivating reasons they exist—is their inheritance and composibility.

Inheritance

Inheritance means that classes exist in a hierarchy; a child class can inherit a parent class’ attributes and methods, and either implement them differently or add some of its own. In your case, let’s tweak your Movie class to add an abstract method play(), which would play the movie:

class Movie:
    def __init__(self, title, director, year):
        self.title = title
        self.director = director
        self.year = year

    def play(self):
        # In reality you'd probably do this with `@abc.abstractmethod`; keeping it simple here
        raise NotImplementedError

The play method is abstract because it doesn’t have concrete implementation in Movie. So, what good is it? Well, let’s say you implement a child class NetflixMovie, and add the necessary logic to find and play the movie from Netflix:

class NetflixMovie(Movie):
    def play(self):
        results = netflix.search(title=self.title, year=self.year)
        netflix.stream(results[0])

And supposed you also have a child class DisneyplusMovie that implements the same for a movie on Disney+:

class DisneyplusMovie(Movie):
    def play(self):
        results = disneyplus.find(film_title=self.title, release_year=self.year)
        disneyplus.start_stream(results[0])

Now, if you have a dict movies as above, assuming movie is some concrete subclass of Movie (e.g. DisneyplusMovie or NetflixMovie) calling movie.play() will play the movie from the streaming service its on (otherwise, if it is just an abstract Movie, it will raise a NotImplementedError):

movies['Scarface'].play()

You might (rightly) ask—Why do this, instead of just writing a function play_movie and calling it if movie is a Netflix movie? For example, instead of the subclasses, what if you did this (with Movie having an extra type attribute with the streaming service its on):

def play_netflix_movie(movie):
    results = netflix.search(title=self.title, year=self.year)
    netflix.stream(results[0])

def play_disneyplus_movie(movie):
     results = disneyplus.find(film_title=self.title, release_year=self.year)
     disneyplus.start_stream(results[0])

which could then be called as

if movie.type == "netflix":
    play_netflix_movie(movie)
elif movie.type == "disneyplus"
    play_disneyplus_movie(movie)
else:
    raise Exception("Cannot play this type of movie.")

Two reasons:

  • First, on the caller side (the place play() is actually called), you don’t need to worry about manually dispatching to the right function for the streaming service the movie is on (Netflix, Disney+, Amazon, etc)—that’s all done for you based on what class movie is,. Furthermore, all that logic is contained in the movie subclass, rather than in separate free functions floating around, helping keep your code more logical, better organized and easier to refactor and avoid bugs—this is, in effect,separation of concerns.

  • Second, to your point about future flexibility, if you want to add a new type of movie—say, AmazonMovie, you simply add the implementation code in that subclass, and the calling and dispatch code never needs to change—it will work handle the new subclass seamlessly, because it will work for all Movie subclasses (that implement play())—it doesn’t have to care about any of the internal implementation details.

Composition

On the second major point, composition, I’ll be more brief, but it can potentially be even more powerful. With this, you can compose multiple objects inside one another to implement more complex functionality. For example, if you had a director class:

class Director:
    def __init__(self, name, birth_year, country):
        self.name = name
        self.birth_year = birth_year
        self.country = country

and you had some dictionary directors of Director instances keyed by name, you modify Movie like so

class Movie:
    def __init__(self, title, director, year):
        self.title = title
        self.director = directors[director]
        self.year = year

Now, you can get the director’s country like so:

movie['Scarface'].director.country

This would be much more complex and tedious to implement as a series of functions.

Other advantages

One other benefit of a class is you get all the attributes of the thing you’re working with neatly bundled up into a single object to pass around to functions, even if you don’t actually define them as methods. For example, instead of passing around title, director and year to search_movie:

def is_on_netflix(title, director, year):
    netflix.do_lookup_by_title(title=title)
    ...

you just pass the instance of Movie instead, and search_movie can access what it needs:

def search_movie(movie):
    netflix.do_lookup_by_title(title=movie.title)
    ...

Also, it means you can do checks that title, director and year and valid and the right type all in one place (the Movie constructor, __init__) rather than having to check validity in each function or risking something unexpected going wrong.

Also, versus just storing these as keys in a dictionary:

movie_dict = {"title": "Scarface", "director": "Brian De Palma", "year": 1983}

you can statically guarantee that title, director and year exist and are the expected types (which using a dataclass, as below, makes especially easy). This becomes especially powerful if you use a static type checker like Mypy, or even are just checking your code manually, as it can help you spot all sorts of bugs that you never could with an arbitrary dictionary.

Dataclasses

On a final note, for classes that purely represent some data (attributes) without callable functionality (methods), consider a dataclass instead:

from dataclasses import dataclass

@dataclass
class Movie:
    self.title: str
    self.director: str
    self.year: int

This is much simpler and cleaner to write, with less overhead (at least source code wise) than a regular class, and it gives you a bunch of useful dunder methods (__init__, __eq__, __str__, __repr__, etc) defined “for free”, while also allowing you to write your own if desired.

1 Like

Thank you very much for the very detailed explainer and for your time to put that together.

You’re correct: “why” is very much the question that I have and as I learn more about this, I’m finding the answer to that question. Out of all the Python topics that I’ve studied, ‘Classes’ is by far and away the most challenging one (so far).

I’ve found a very good read on this subject. It’s not the only reading I’ve been doing, but it is by far the most detailed.

I’ll have to take some time to digest what you’ve posted and I’m sure that I’ll learn even more from that.

Again, thank you.