Executes methods and attribute calls on a list of objects of the same type

    Bundles a list of object of the same type into a single object.
    The new object contains the same functions as each original object
    but applies them to each element of the list independantly when called.

    CommandLine:
        python -m utool.util_dev instancelist

    Example:
        >>> # ENABLE_DOCTEST
        >>> from utool.util_dev import *  # NOQA
        >>> import utool as ut
        >>> obj_list = ['hi', 'bye', 'foo']
        >>> self = ut.instancelist(obj_list, check=False)
        >>> print(self)
        >>> print(self.upper())
        >>> print(self.isalpha())

def instancelist(obj_list, check=False, shared_attrs=None):
    """
    Executes methods and attribute calls on a list of objects of the same type

    Bundles a list of object of the same type into a single object.
    The new object contains the same functions as each original object
    but applies them to each element of the list independantly when called.

    CommandLine:
        python -m utool.util_dev instancelist

    Example:
        >>> # ENABLE_DOCTEST
        >>> from utool.util_dev import *  # NOQA
        >>> import utool as ut
        >>> obj_list = ['hi', 'bye', 'foo']
        >>> self = ut.instancelist(obj_list, check=False)
        >>> print(self)
        >>> print(self.upper())
        >>> print(self.isalpha())
    """
    class InstanceList_(object):
        def __init__(self, obj_list, shared_attrs=None):
            self._obj_list = []
            self._shared_public_attrs = []
            self._example_type = None

            if len(obj_list) > 0:
                import utool as ut
                self._obj_list = obj_list

                example_obj = obj_list[0]
                example_type = type(example_obj)
                self._example_type = example_type

                if shared_attrs is None:
                    if check:
                        attrsgen = [set(dir(obj)) for obj in obj_list]
                        shared_attrs = list(reduce(set.intersection, attrsgen))
                    else:
                        shared_attrs = dir(example_obj)

                #allowed = ['__getitem__']  # TODO, put in metaclass
                allowed = []
                self._shared_public_attrs = [
                    a for a in shared_attrs
                    if a in allowed or not a.startswith('_')
                ]

                for attrname in self._shared_public_attrs:
                    attrtype = getattr(example_type, attrname, None)
                    if attrtype is not None and isinstance(attrtype, property):
                        # need to do this as metaclass
                        setattr(InstanceList_, attrname,
                                property(self._define_prop(attrname)))
                    else:
                        func = self._define_func(attrname)
                        ut.inject_func_as_method(self, func, attrname)

        def __nice__(self):
            if self._example_type is None:
                typename = 'object'
            else:
                typename = self._example_type.__name__
            return 'of %d %s(s)' % (len(self._obj_list), typename)

        def __repr__(self):
            classname = self.__class__.__name__
            devnice = self.__nice__()
            return '<%s(%s) at %s>' % (classname, devnice, hex(id(self)))

        def __str__(self):
            classname = self.__class__.__name__
            devnice = self.__nice__()
            return '<%s(%s)>' % (classname, devnice)

        def __getitem__(self, key):
            # TODO, put in metaclass
            return self._map_method('__getitem__', key)

        def _define_func(self, attrname):
            import utool as ut
            def _wrapper(self, *args, **kwargs):
                return self._map_method(attrname, *args, **kwargs)
            ut.set_funcname(_wrapper, attrname)
            return _wrapper

        def _map_method(self, attrname, *args, **kwargs):
            mapped_vals = [getattr(obj, attrname)(*args, **kwargs)
                           for obj in self._obj_list]
            return mapped_vals

        def _define_prop(self, attrname):
            import utool as ut
            def _getter(self):
                return self._map_property(attrname)
            ut.set_funcname(_getter, 'get_' + attrname)
            return _getter

        def _map_property(self, attrname):
            mapped_vals = [getattr(obj, attrname) for obj in self._obj_list]
            return mapped_vals
    return InstanceList_(obj_list, shared_attrs)