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