2

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)
asked Feb 20, 2022 at 20:43
11
  • Try replace super().__init__(t,a, r_y, u) with Media.__init__(self,t,a,r_y,u) Commented Feb 20, 2022 at 20:52
  • @InhirCode There's no reason to do that. What we need is a reproducible example. Commented Feb 20, 2022 at 21:16
  • @Jeffrey in simple words, as your subclass (say Movie) suggests, you have to provide explicitly the positional args namely, t, a etc. while creating an instance of the same. Commented Feb 20, 2022 at 21:26
  • @ApuCoder is there no way to initialize the subclasses so that they use whatever values are decided on in the base class (whether it's the default values or the information pulled from a json object)? Commented Feb 20, 2022 at 22:48
  • 1
    @pippo1980 I clarify that in answer. Commented Feb 23, 2022 at 7:48

2 Answers 2

2

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
answered Feb 22, 2022 at 20:50
Sign up to request clarification or add additional context in comments.

3 Comments

Though it deals with 'kwd_only_arg' not optional/kwargs (as in the OP's post), I found it more suitable than that of mine (because of the implementation with classmethod). Upvoted.
@ApuCoder, sorry to bother, but you answed before to me. Here if I try pippo = Movie('titleeee','authorrrrrrrrr',1900, 'www.pippo.com') [No keyword] I get --> pippo error : __init__() takes 1 positional argument but 5 were given, the '1 positional argument' would be 'self' ??
Read the linked article, which presents an argument for using keyword arguments rather than positional arguments in this case.
1

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())
answered Feb 22, 2022 at 20:21

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.