Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add _apply_list_op, each, map, filter to ComplexBase #47

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

cifkao
Copy link
Contributor

@cifkao cifkao commented Jan 7, 2021

  • Adding an _apply_list_op method that recursively applies a list operation to list attributes.
  • Implementing remove_duplicate, remove_invalid and sort using this method (seems to fix remove_invalid() makes object even more invalid #49).
  • Adding each, map and filter functions.

Example for filter:

def is_in_range(obj, a, b):
    if hasattr(obj, "time"):
        return obj.time >= a and obj.time <= b
    return True

# Crop to a given time range
music.filter(functools.partial(is_in_range, a=4, b=8)).adjust_time(lambda t: t - 4)

Example for each:

def fix_pitches(obj):
    if hasattr(obj, "pitch") and obj.pitch < 0:
        obj.pitch += (-obj.pitch + 11) // 12 * 12

# Transpose and fix invalid pitches
music.transpose(-60).each(fix_pitches)

@cifkao
Copy link
Contributor Author

cifkao commented Jan 8, 2021

Related to #32.

@salu133445
Copy link
Owner

The functions map, filter and each can be quite handy, although they look pretty non-pythonic to me.

For map and each function, I think a more pythonic way is to have a for loop iterating over all the objects regardless of their types (similar to music21.score.flat). We could have a method Music.objects() that returns an iterator on all the objects. Then, we can do something as follows, which seems more pythonic to me.

for obj in music.objects():
    fix_pitches(obj)

(We cannot achieve the filter function as above though as it needs to modify the lists.)

@cifkao
Copy link
Contributor Author

cifkao commented Jan 11, 2021

map is still useful, because it replaces values with new ones (maybe you can think of a name for it that better reflects this functionality). But replacing each with a flat iterator sounds nice. objects sounds quite generic, I think flat, like in numpy or music21, is better.

@salu133445
Copy link
Owner

Oops. You are right. map does not work that way. We could have a list iterator and do as follows.

for list_ in music.lists():
    list_[:] = map(func, list_)

Similarly, for filter, we could do the following.

for list_ in music.lists():
    list_[:] = [obj for obj in list_ if is_in_range(obj, a=4, b=8)]

Neither is that pythonic though.

@salu133445
Copy link
Owner

Also, isn't map more like apply in PyTorch? Python's built-in map returns a new iterable, but the one in this pull request does modify the lists in place.

@salu133445
Copy link
Owner

Music.objects() is probably not a good one as it sounds like everything, including non-list attributes, will be included. Music.flat() sounds good to me.

@salu133445
Copy link
Owner

It seems that Music._apply_list_op() is rather powerful. Why don't we make it Music.apply()? This could perhaps simplify map and filter by using apply with built-in map and filter.

@cifkao
Copy link
Contributor Author

cifkao commented Jan 12, 2021

Also, isn't map more like apply in PyTorch? Python's built-in map returns a new iterable, but the one in this pull request does modify the lists in place.

Yes, a bit, but apply in PyTorch also doesn't replace the values. This map is like map but in-place, I'm not sure what's a good name for it. Maybe apply would work actually?

It seems that Music._apply_list_op() is rather powerful. Why don't we make it Music.apply()? This could perhaps simplify map and filter by using apply with built-in map and filter.

I was thinking about that. But the thing is that it accepts a function with 3 different parameters (of which I'm currently using only 2 actually...) and so (1) it can't be used with existing functions like built-in map and filter and (2) I thought it would be difficult to document it clearly.

@salu133445
Copy link
Owner

It does behave like a map on all the lists but does not return anything.

>>> a, b, c = [0], [1], [2]
>>> map(lambda x: x.append(-1), [a,b,c])
<map at 0x23c92c3bf88>
>>> print(a, b, c)  # has no effect as map returns an iterator
[0] [1] [2]
>>> list(map(lambda x: x.append(-1), [a,b,c]))
[None, None, None]
>>> print(a, b, c)
[0, -1] [1, -1] [2, -1]

@salu133445
Copy link
Owner

In fact, adjust_time is not really pythonic either. It would be better if we could iterate over all timestamped objects.

@salu133445
Copy link
Owner

I do think implementing remove_duplicate, remove_invalid and sort in this way is fine, and it seems to fix #49. For map, filter and each (and possibly apply), we could open another pull request and see if we can come up with a more consistent, pythonic way to implement them.

@cifkao
Copy link
Contributor Author

cifkao commented Jan 13, 2021

In fact, adjust_time is not really pythonic either. It would be better if we could iterate over all timestamped objects.

If it weren't for the intricacies of adjusting durations...

@cifkao
Copy link
Contributor Author

cifkao commented Jan 13, 2021

OK, I removed the methods.

I replaced _apply_list_op with _traverse_lists, which seems more pythonic. This also allowed me to implement flat.

Copy link
Owner

@salu133445 salu133445 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • The implementation of _traverse_lists looks good to me.
  • I am not a fan of flat in music21. We should use a more obvious and pythonic name. For now, I can only come up with lists() and lists_with_names(). There should be better options.
  • I already fixed remove_invalid() makes object even more invalid #49 by 8899576, but I am not sure which would be faster

muspy/base.py Outdated Show resolved Hide resolved
muspy/base.py Outdated
Comment on lines 746 to 757
@property
def flat(self) -> Iterable:
"""A flat representation of this object. Iterating over it
yields all items in all list attributes inside this object
(recursively). Non-list attributes are not included.
"""
return self._flat

def _flat_generator(self) -> Iterable:
value: list
for _, _, value in self._traverse_lists(attr=None, recursive=True):
yield from value
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be problematic as it will keep reusing the generator created at __init__. We probably don't want this behavior. Also, I am not a fan of having flat as a property. Could we simply make it Music.lists(), which is rather straightforward from its name except that it's recursive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I didn't realize that.

How about this? flat is now reusable, and it's always the same object, so it's not a problem to have it as a property.

If you still insist on it being a function, why not call it e.g. walk_lists?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want it to behave more like dict.items() or dict.values() rather than numpy.ndarray.flat or music21.stream.Stream.flat. It's quite common in Python to have a function that returns an iterator.

Copy link
Contributor Author

@cifkao cifkao Jan 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Is walk_lists() OK then? The name should give a hint that the return value is good only for (one) iteration.

BTW dict.items() and dict.values() also return objects that support more than just iteration (e.g. len()).

Copy link
Contributor Author

@cifkao cifkao Jan 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, lists() suggests that it would iterate over lists. But my flat iterates over list items, not lists themselves. So we need a better name. I think flat() (just changing the property to a function) or walk_lists() is OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made it list_items() and added list() to iterate over the actual lists.

yield from item._traverse_lists( # type: ignore
attr=None, recursive=recursive)

yield (attr, attr_type, getattr(self, attr))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A return signature of either the value (like dict.values) or a tuple of the key and the value(attr, value) (like dict.items) would be more straightforward. Maybe we can make them two separate methods.

But if we do this, we can no longer make remove_invalid, remove_duplicate and sort work using this, right?

Or we could keep this unchanged and have two public wrappers lists and lists_with_names.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I would definitely keep this and internal function.

Comment on lines +678 to +684
if isclass(attr_type) and issubclass(attr_type, Base):
value[:] = [item for item in value # type: ignore
if (isinstance(item, attr_type)
and cast(Base, item).is_valid())]
else:
value[:] = [item for item in value # type: ignore
if isinstance(item, attr_type)]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type checking can be simplified if we change the return signature into Iterator[Tuple[str, List]].

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there must be a bug in mypy, it was complaining about things whose type is actually known...

@cifkao
Copy link
Contributor Author

cifkao commented Jan 17, 2021

OK, I replaced flat with list_items() and also added lists().

Example:

>>> list(m.lists())
[('tempos', []),
 ('key_signatures', []),
 ('time_signatures', []),
 ('downbeats', []),
 ('lyrics', []),
 ('annotations', []),
 ('tracks',
  [Track(program=33, is_drum=False, name='BB Bass', notes=[Note(time=0, pitch=34, duration=12, velocity=59), Note(time=18, pitch=43, duration=17, velocity=74), Note(time=36, pitch=39, duration=5, velocity=62), ...])])]

>>> list(m.list_items())
[Track(program=33, is_drum=False, name='BB Bass', notes=[Note(time=0, pitch=34, duration=12, velocity=59), Note(time=18, pitch=43, duration=17, velocity=74), Note(time=36, pitch=39, duration=5, velocity=62), ...])]

>>> list(m.lists(recursive=True))
[('tempos', []),                                                                                                                                                                                            
 ('key_signatures', []),                                                                                                                                                                                    
 ('time_signatures', []),                                                                                                                                                                                   
 ('downbeats', []),                                                                                                                                                                                         
 ('lyrics', []),                                                                                                                                                                                            
 ('annotations', []),                                                                                                                                                                                       
 ('notes',                                                                                                                                                                                                  
  [Note(time=0, pitch=34, duration=12, velocity=59),                                                                                                                                                        
   Note(time=18, pitch=43, duration=17, velocity=74),                                                                                                                                                       
   Note(time=36, pitch=39, duration=5, velocity=62),                                                                                                                                                        
   ...]),
 ('chords', []),
 ('lyrics', []),
 ('annotations', []),
 ('tracks',
  [Track(program=33, is_drum=False, name='BB Bass', notes=[Note(time=0, pitch=34, duration=12, velocity=59), Note(time=18, pitch=43, duration=17, velocity=74), Note(time=36, pitch=39, duration=5, velocity=62), ...])])]

>>> list(m.list_items(recursive=True))
[Note(time=0, pitch=34, duration=12, velocity=59),                                                                                                                                                          
 Note(time=18, pitch=43, duration=17, velocity=74),                                                                                                                                                         
 Note(time=36, pitch=39, duration=5, velocity=62),
 ...,
 Track(program=33, is_drum=False, name='BB Bass', notes=[Note(time=0, pitch=34, duration=12, velocity=59), Note(time=18, pitch=43, duration=17, velocity=74), Note(time=36, pitch=39, duration=5, velocity=62), ...])]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

remove_invalid() makes object even more invalid
2 participants