Refactoring For-Loops in Python - python

I am currently working on an iTunes data program that's cycling constantly through a user's library to get statistics about one's library.
returns
I have a few code snippets like these:
def numArtist(self):
num = 0
for song in self.allSongs:
tempList = []
if song.artist not in tempList:
tempList.append(song.artist)
num += 1
return num
def getAlbumNames(self):
albums = []
for song in self.allSongs:
if song.album not in albums:
albums.append(song.album)
return albums
Where the main for loop body is repeated:
for song in self.allSongs: # same for-loop condition
# different for-loop body
Is there a way to refactor methods like these, where I have the same for loop conditions but with different body definitions?
I have a quite a few methods with the same for-loop, so I'd like to find a way to decrease the complexity and redundancy of my code.
Just for reference, all Song objects have attributes - artist, album (name),genre, etc - that I'm using to get my data.

Use set comprehensions and len to simplify each of them:
def numArtist(self):
return len({song.artist for song in self.allSongs})
def getAlbumNames(self):
return {song.album for song in self.allSongs}
To make it more generic, you could write a method that takes a lambda and use that to filter the property out of each song:
def uniqueProps(self, fxn):
return {fxn(song) for song in self.allSongs}
def getAlbumNames(self):
return self.uniqueProps(lambda song: song.album)

You can use set comprehensions for both snippets, if that counts as a valid "For-Loop refactoring":
artist_count = len({song.artist for song in self.allSongs})
album_names = set({song.album for song in self.allSongs})
Generic version using getattr
get_values = lambda objs, attr: {getattr(obj, attr) for obj in objs
attributes = 'artist', 'album'
values = [get_values(self.allSongs, name) for name in attributes]
artists, albums = values
artist_count = len(artists)
Generic version using lambda
get_artist = lambda song: song.artist
get_album = lambda song: song.album
getters = get_artist, get_album
values = [
{func(song) for song in self.allSongs}
for getter in getters
]
artists, albums = values
artist_count = len(artists)
Generic version using property
# If `song` is an instance of the `Song` class and both `artist` and
# `album` are properties defined on the class, it's also possible to
# directly use the property getter (`property.fget`) to avoid defining
# the lambdas manually:
get_artist = Song.artist.fget
get_album = Song.album.fget
... # <same as above>

If the contents of your allSongs list are immutable - which I suspect they are - you can convert your lists to sets and back to lists again - or use set comprehension - to get rid of duplicates. Then your functions can be greatly simplified like so:
def numArtist(self):
return len({song.artist for sing in self.allSongs})
def getAlbumNames(self):
return list({song.album for song in self.allSongs})
If you're not sure if the song objects are mutable or not, try this out anyway. If they're mutable objects you'll get an exception like:
TypeError: unhashable type: ...

You could try to create the generators, that produces the value of song attributes. Let me give you an example:
def gen_attr(songs, attr_name):
for song in songs:
yield getattr(song, attr_name)
class Song(object):
def __init__(self, name, artist):
self.name = name
self.artist = artist
class Album(object):
def __init__(self, songs_list):
self.songs_list = songs_list
def allSongs(self):
return self.songs_list
s = Song('Ahoy', 'Pirate')
s1 = Song('Bye', 'My Son')
s2 = Song('Ahoy', 'Captain')
a = Album([s, s1])
Now if you want to get all of the song names, u can use:
song_names = list(gen_attr(a.allSongs(), 'name'))
print(song_names) # ['Ahoy', 'Bye', 'Ahoy']
For non-repeated song names you would use:
song_names = list(set(gen_attr(a.allSongs(), 'name')))
print(song_names) # ['Ahoy', 'Bye']
To count the non-repeated artists names, you would use:
artists = len(set(gen_attr(a.allSongs(), 'artist')))
And to create the list of artists, just go for:
artists = list(gen_attr(a.allSongs(), 'artist'))
print(artists) # ['Pirate', 'My Son', 'Captain']

Related

please how do i use a method to store a collection of class objects using a dictionary for to store the items with appropriate keys and values

class: Define a Python class(MovieList) with methods to do the following:
Define a constructor to create an object of the MovieList class.
A method to store a collection of movie objects that are created using the Movie class above. You should use a dictionary
for to store the items with appropriate keys and values.
A method to search through the collection and find a movie by one or more of the following movie attributes: title, genre
or release date.
A method to remove a movie from the collection based on the title of the movie.
A method to calculate and return the total number of movies stored in the collection.
You should include appropriate error checking and detailed comments in our code.
class MovieList:
def __init__(self, name, maxMovieList):
self.name = name
self.maxMovieList = maxMovieList
self.MovieCollection = []
def add_movie(self, movie):
if len(self.MovieCollection) < self.maxMovieList +1 :
self.MovieCollection.append(movie)
return True #if successfully added
return False #if not successfully added
def find_movie(self, title):
if self.title in MovieCollection:
return True
return False
As the instructions say, you should be storing the movies in a dictionary, not a list. Use the movie title as the key.
Instruction 3 says that the search method can take more than just the title. You can use keyword parameters, and then test if all the attributes match a movie. Use the getattr() method to retrieve an attribute dynamically.
class MovieList:
def __init__(self, name, maxMovieList):
self.name = name
self.maxMovieList = maxMovieList
self.MovieCollection = {}
def add_movie(self, movie):
if len(self.MovieCollection) < self.maxMovieList +1 :
self.MovieCollection[movie.title] = movie
return True #if successfully added
return False #if not successfully added
def find_movie(self, **attributes):
for movie in self.MovieCollection.values():
if all(getattr(movie, key) == value for key, value in attributes):
return movie
return False

how to print a list that is in a dictionary, as a string in python? And how to edit the list's elements?

I have a class that has a dictionary attribute. The dictionary has song titles as its key and a list containing artist, genre and playCount like this:
class library:
def __init__(self,library):
self.library={}
def addSong(self,title,artist,genre,playCount):
self.library[title]=[artist,genre,playCount]
The playCount is an integer. How do I add a 1 to the playCount element without changing any of the other elements. Do I make a new function for it or can I do it without making a function?. Also how can I make a function to print the keys and values of the dictionary as a string like this:
artist, title (genre), playCount
IIUC, you just want to increment playCount every time you pass an existing title to addSong, right?
You can put in an if-else condition in addSong to check if title is in self.library or not and if it exists, then just increment the last element of value of key title by playCount.
Also, to print, it's just a matter of assigning items in their correct positions:
class library:
def __init__(self):
self.library = {}
def addSong(self, title, artist, genre, playCount=1):
if title in self.library:
self.library[title][-1] += playCount
else:
self.library[title] = [artist, genre, playCount]
def get_song_data(self, title):
if title in self.library:
x = self.library[title] + [title]
return "{0}, {3} ({1}), {2}".format(*x)
lib = library()
Output:
lib.addSong('Easy on Me','Adele','ballad',10)
print(lib.get_song_data('Easy on Me')) # Adele, Easy on Me (ballad), 10
lib.addSong('Easy on Me','Adele','ballad',2)
print(lib.get_song_data('Easy on Me')) # Adele, Easy on Me (ballad), 12
lib.addSong('Easy on Me','Adele','ballad')
print(lib.get_song_data('Easy on Me')) # Adele, Easy on Me (ballad), 13

Is it possible to return a dictionary's values (list of objects) and call their __str__ function instead of printing their memory address?

Hey the question title might be a bit confusing. So basically I want to print out a dictionary with the keys and it's values, so I know to call dictionary.items() but I want it to print out using the object's str function instead of printing the memory address.
My Player object
class Player():
def __init__(self, id, name, position):
self.player_id = id
self.name = name
def __str__(self):
return self.name
So I have a dictionary -
depth_chart = {}
That I insert the player objects into as a list.
def addPlayer(player, position):
# Function to add player to depth chart.
if position not in depth_chart:
depth_chart[position] = []
depth_chart[position].append(player)
else:
depth_chart[position].append(player)
So I eventually might have something like this -
depth_chart = {
'shooting_guard': [PlayerObject1, PlayerObject2],
'center': [PlayerObject3],
'point_guard': [PlayerObject4]
}
Now here's my problem if I call depth_chart.items() I want it to return me something like
[('shooting_guard', ['John', 'Joseph'] ), ('center', ['Alex']), ('point_guard': ['Sean'] ]
But right now it just returns me the memory address of the Player objects. Is there a way to call the str function or another method to print out the player object names? I do have a function I can do to get it to look like how I want but I'm not sure if there's an easier way? How that function looks -
def getFullDepthChart():
# Function to print out all the positions in the depth chart
all_players = []
for items in depth_chart.items():
temp = []
position = items[0]
player_array = items[1]
for players in player_array:
temp.append(players.name)
all_players.append((position, temp))
return all_players
You can implement __repr__ to return the player's name:
class Player:
def __repr__(self):
return self.name
def getFullDepthChart():
# Function to return all positions in the depth chart
all_players = [(position,
[player.name for player in players])
for pos, players in depth_chart.items()]
return all_players

Calling Object inside of List

class MySong:
_songTitle = "Song Title"
_artistName = "Artist Name"
_likeIndicator = -1
def setTitleAndArtist(self, songTitle, artistName):
self._songTitle = songTitle
self._artistName = artistName
def setLike(self, likeIndicator):
self._likeIndicator = likeIndicator
def undoSetLike(self, songTitle):
Null
def getTitle(self):
return self._songTitle
def getArtist(self):
return self._artistName
def getLikeIndicator(self):
return self._likeIndicator
class MyPlaylist:
_mySongs = []
def add(self, song):
self._mySongs.append(song)
def showTitles(self):
index = 0
titlesList = []
while index != len(self._mySongs):
titlesList.append(self._mySongs[index].getTitle())
index = index + 1
return titlesList
def remove(self):
remindex = 0
while remindex != len(self._mySongs):
if (self._mySongs[index].getTitle()) == remChoice :
return("Song FOUND debug!")
self._mySongs.remove(index)
else:
remindex = remindex + 1
return("Song NOT FOUND debug!")
def getMySong(self):
Null
There is a list of song objects inside of _mySongs = []. I'm trying to remove one, based on the title variable of that object.
In a separate (unshown) part of the program, the user is asked to enter the title of the song they want removed as a string. This is saved as remChoice.
I'm not entirely sure how to remove the song based on the title.
I've tried for a while to get it going, obviously we find the index of the song in the list by matching it to the title (by calling the getTitle method), then removing that index when it's found.
This isn't working. Where am I going wrong?
If you want to delete an item from a list knowing it's index use:
del xs[i]
Where i is the index. (e.g: Your song's index based on your search).
list.remove() is used for removing a matching element form the list not the "ith" item.
You might also find that a list is not a suitable data structure here? Perhaps you could try storing key/value pairs in a dict. e.g:
my_songs = {}
my_aongs["My Song Title"] = MySong(title, description, length)
You can later delete songs via their keys:
del my_songs["My Song Title"]
where titles are your keys. This saves you from doing O(n) searching.
Update:
Your .remove() method should look more like the following:
def remove(self, title):
for i, song in enumerate(self._mySongs):
if song.getTitle() == title:
del self._mySongs[i]
return
print("Song not found!")
Here we're using list's iteration protocol by using a for x in xs: rather than using a while loop and doing manual bookkeeping. The builtin function enumerate() is also used to give us an index into the list we're iterating over (i.e: it's position in the sequence).
try
self._mySongs.remove(title)
That should work.
(Or from another object: replace self by whatever your object name is)

Python: referencing class object list of lists

I am fairly new to python. I have tried to define a class, I then want to create an instance from a file, then refer to specific pieces of it, but cannot seem to. This is Python 3.3.0
Here's the class....
class Teams():
def __init__(self, ID = None, Team = None, R = None, W = None, L = None):
self._items = [ [] for i in range(5) ]
self.Count = 0
def addTeam(self, ID, Team, R=None, W = 0, L = 0):
self._items[0].append(ID)
self._items[1].append(Team)
self._items[2].append(R)
self._items[3].append(W)
self._items[4].append(L)
self.Count += 1
def addTeamsFromFile(self, filename):
inputFile = open(filename, 'r')
for line in inputFile:
words = line.split(',')
self.addTeam(words[0], words[1], words[2], words[3], words[4])
def __len__(self):
return self.Count
Here's the code in Main
startFileName = 'file_test.txt'
filename = startFileName
###########
myTestData = Teams()
myTestData.addTeamsFromFile(startFileName)
sample data in file
100,AAAA,106,5,0
200,BBBB,88,3,2
300,CCCC,45,1,4
400,DDDD,67,3,2
500,EEEE,90,4,1
I think I am good to here (not 100% sure), but now how do I reference this data to see... am i not creating the class correctly? How do I see if one instance is larger than another...
ie, myTestData[2][2] > myTestData[3][2] <----- this is where I get confused, as this doesn't work
Why don't you create a Team class like this :
class Team():
def __init__(self, ID, Team, R=None, W = 0, L = 0)
# set up fields here
Then in Teams
class Teams():
def __init__(self):
self._teams = []
def addTeam (self, ID, Team, R=None, W = 0, L = 0)
team = Team (ID, Team, R=None, W = 0, L = 0)
self._teams.append (team)
Now If i got it right you want to overwrite the > operator's behaviour.
To do that overload __gt__(self, other) [link]
So it will be
class Team ():
# init code from above for Team
def __gt__ (self, otherTeam):
return self.ID > otherTeam.ID # for example
Also be sure to convert those strings to numbers because you compare strings not numbers. Use int function for that.
The immediate problem you're running into is that your code to access the team data doesn't account for your myTestData value being an object rather than a list. You can fix it by doing:
myTestData._items[2][2] > myTestData._items[3][2]
Though, if you plan on doing that much, I'd suggest renaming _items to something that's obviously supposed to be public. You might also want to make the addTeamsFromFile method convert some of the values it reads to integers (rather than leaving them as strings) before passing them to the addTeam method.
An alternative would be to make your Teams class support direct member access. You can do that by creating a method named __getitem__ (and __setitem__ if you want to be able to assign values directly). Something like:
def __getitem__(self, index):
return self._items[index]
#Aleksandar's answer about making a class for the team data items is also a good one. In fact, it might be more useful to have a class for the individual teams than it is to have a class containing several. You could replace the Teams class with a list of Team instances. It depends on what you're going to be doing with it I guess.

Categories