Home > Mobile >  Class Inheritance Problems in Python
Class Inheritance Problems in Python

Time:02-23

I'm writing a program that allows a user to search an API for media information and then divided them into Songs and Movies. I'm using classes to do this and have a parent class (Media) and two child classes (Song, Movie). My Media class works great when I search the API and I get all the information I want. However, when I try to create Song or Movie instances, the information won't inherit from the Media class (I get an error that the subclass is missing positional arguments) and I can't figure out why. Any help would be great!

class Media:
    def __init__(self, title="No Title", author="No Author", release_year="No Release Year", url="No URL", json=None):
        if json==None:
            self.title = title
            self.author = author
            self.release_year = release_year
            self.url = url
        else:
            if 'trackName' in json.keys():
                self.title = json['trackName']
            else:
                self.title = json['collectionName']
            self.author = json['artistName']
            self.release_year = json['releaseDate'][:4]
            if 'trackViewUrl' in json.keys():
                self.url = json['trackViewUrl']
            elif 'collectionViewUrl' in json.keys():
                self.url = json['collectionViewUrl']
            else:
                self.url = None

    def info(self):
        return f"{self.title} by {self.author} ({self.release_year})"

    def length(self):
        return 0


class Song(Media):
    def __init__(self, t, a, r_y, u, json=None, album="No Album", genre="No Genre", track_length=0):
        super().__init__(t,a, r_y, u)
        if json==None:
            self.album = album
            self.genre = genre
            self.track_length = track_length
        else:
            self.album = json['collectionName']
            self.genre = json['primaryGenreName']
            self.track_length = json['trackTimeMillis']

    def info(self):
        return f"{self.title} by {self.author} ({self.release_year}) [{self.genre}]"

    def length(self):
        length_in_secs = self.track_length / 1000
        return round(length_in_secs)


class Movie(Media):
    def __init__(self, t, a, r_y, u, json=None, rating="No Rating", movie_length=0):
        super().__init__(t, a, r_y, u)
        if json==None:
            self.rating = rating
            self.movie_length = movie_length
        else:
            self.rating = json['contentAdvisoryRating']
            self.movie_length = json['trackTimeMillis']

    def info(self):
        return f"{self.title} by {self.author} ({self.release_year}) [{self.rating}]"

    def length(self):
        length_in_mins = self.movie_length / 60000
        return round(length_in_mins)

CodePudding user response:

My Media class works great when...I want.

When you create an instance of Media class say, med = Media() it will work well even without any args as all its args are kwargs (or optional args). But,

However, when I try to create Song or Movie instances...

This will not 'cause in the constructor of a subclass of Media say, Movie you did :

    def __init__(self, t, a, r_y, u, json=None, rating="No Rating", movie_length=0):
        super().__init__(t, a, r_y, u)

which takes some positional args t, a etc. So when you create an instance of this subclass say mov = Movie() it will throw a TypeError.

So the one way you can fix this is, obviously provide those positional args in each instances of Movie class.

The other way (and here I assume that you wanted to mean title by t; author by a; release_year by r_y and url by u) is make them all kwargs.

So first in your base class,

class Media:
    def __init__(self, title="No Title", author="No Author", release_year="No Release Year", url="No URL", json=None, **kwargs):
        self.json = json # Store it here.
        if self.json is None: # Better suited than '=='.
            self.title = title
            self.author = author
            self.release_year = release_year
            self.url = url
        else:
            ...

With **kwargs you are actually instructing your base class Media that it (or/and its subclasses) can have arbitrary no. of keyword-arguments.

Using this and super appropriately in any of your subclass(es) you will be able to incorporate new kwargs (apart from the defined ones in the base class, actually now you need not to mention them again).

Now in a subclass say Movie you can just define new kwargs as,

class Movie(Media):
    def __init__(self, rating="No Rating", movie_length=0, **kwargs):
        # Now call super and provide only the new ones. With an extra '**kwargs' you keep the consistency with the base class.
        super().__init__(rating="No Rating", movie_length=0, **kwargs)
        if self.json is None:
            self.rating = rating
            self.movie_length = movie_length
        else:
            ...

With this change(s) the following should work as expected,

Mov = Movie()
print(Mov.title)
print(Mov.info())

CodePudding user response:

First, let's simplify the problem. Ignore JSON, and assume all arguments are required (i.e., no dummy defaults like "No title").

class Media:
    def __init__(self, *, title, author, release_year, url, **kwargs):
        super().__init__(**kwargs)
        self.title = title
        self.author = author
        self.release_year = release_year
        self.url = url

    def info(self):
        return f"{self.title} by {self.author} ({self.release_year})"

    def length(self):
        return 0


class Song(Media):
    def __init__(self, *, album, genre, track_length, **kwargs):
        super().__init__(**kwargs)
        self.album = album
        self.genre = genre
        self.track_length = track_length

    def info(self):
        return super().info()   f" [{self.genre}]"

    def length(self):
        length_in_secs = self.track_length / 1000
        return round(length_in_secs)


class Movie(Media):
    def __init__(self, *, rating, movie_length, **kwargs):
        super().__init__(**kwargs)
        self.rating = rating
        self.movie_length = movie_length

    def info(self):
        return super().info()   f" [{self.rating}]"

    def length(self):
        length_in_mins = self.movie_length / 60000
        return round(length_in_mins)

song = Song(title="...", author="...", release_year="...", url="...", album="...", genre="...", track_length="...")

The use of keywords arguments and super are as recommended in Python's super() considered super!. Further, each info method is defined in terms of the parent's info method, in order to reduce duplicate code.


Now, how do we deal with JSON? Ideally, we'd like to define a single class method that extracts values from the dict decoded from JSON and use those values to create an instance. For example,

class Media:
    ...

    @classmethod
    def from_json(cls, json):
        if 'trackName' in json:
            title = json['trackName']
        else:
            title = json['collectionName']
        author = json['artistName']
        release_year = json['releaseDate'][:4]
        if 'trackViewUrl' in json:
            url = json['trackViewUrl']
        elif 'collectionViewUrl' in json.keys():
            url = json['collectionViewUrl']
        else:
            url = None
        return cls(title=title, author=author, release_year=release_year, url=url)

However, we'd also like to build up Song.from_json and Movie.from_json in terms of Media.from_json, but it's not obvious how to do that.

class Song:
    ...
    @classmethod
    def from_json(cls, json):
        obj = super().from_json(json)
        

Media.from_json attempts to return an instance of Song, but it doesn't know how to get the required arguments to do so.

Instead, we'll define two class methods. One takes care of processing the JSON into an appropriate dict, the other simply calls cls using that dict.

class Media:
    ...

    @classmethod
    def _gather_arguments(cls, json):
        d = {}
        if 'trackName' in json:
            d['title'] = json['trackName']
        else:
            d['title'] = json['collectionName']
        d['author'] = json['artistName']
        d['release_year'] = json['releaseDate'][:4]
        if 'trackViewUrl' in json:
            d['url'] = json['trackViewUrl']
        elif 'collectionViewUrl' in json.keys():
            d['url'] = json['collectionViewUrl']
        else:
            d['url'] = None

        return d

    @classmethod
    def from_json(cls, json):
        d = cls._gather_arguments(json)
        return cls(**d)

Now for our two subclasses, we don't need to touch from_json at all: it already does everything we need it to do. All we need to do is to override _gather_arguments to add our additional values to whatever Media._gather_arguments returns.

class Song(Media):
    ...

    @classmethod
    def _gather_arguments(cls, json):
        d = super()._gather_arguments(json)
        d['album'] = json['collectionName']
        d['genre'] = json['primaryGenreName']
        d['track_length'] = json['trackTimeMillis']
        return d

and

class Movie(Media):
    ...

    @classmethod
    def _gather_arguments(cls, json):
        d = super()._gather_arguments(json)
        d['rating'] = json['contentAdvisoryRating']
        d['movie_length'] = json['trackTimeMillis']
        return d
  • Related