Can I construct the corresponding type from the input parameter type

Hi, everyone!
my friend ask me a interesting question:

I want to return tuples if I accept tuples, and return lists if I accept lists. Is there a method that can handle all cases in a unified way except isinstance()

I think it’s a good question about python’s constructor, I tell him judging by isinstance() is a common and easy solution, and I give another strange func:

def get_elem(elem:[list|tuple]):
	
	pre_elem = [1, 2]    # elems you want to return 
	type_elem = type(elem)
	new_elem = type_elem.__dict__["__new__"](type_elem, pre_elem)
	
	if len(new_elem) != len(pre_elem):
		# input is not a tuple
		new_elem.extend(pre_elem)
	return new_elem

i know it’s not a good solution, but i think this question is interesting, so i want to know your ideas and reason why you mention this idea.

Thank you!

Why the limitation about not using instance()? Here’s what I think is a more readable way:

>>> def get_elem(elem):
...    pre_elem = [1, 2]
...    new_elem = list(elem)
...    new_elem.extend(pre_elem)
...    if type(new_elem) == type(elem):
...        return new_elem
...    return tuple(new_elem)
...
>>> get_elem([3])
[3, 1, 2]
>>> get_elem((3,))
(3, 1, 2)

For better readability, I should have written new_list instead of new_elem, which I took from the original post.

I konw your meaning, your solution is good, and my friend want to get a method which can handle all cases not only tuple and list. I think it’s a intersting question so that i give him a solution like factory mode to get param’s type object’s new func to new a object, and i know my solution is not good, i just want to konw more interesting function regardless of practicality

I konw your meaning, your solution is good, and my friend want to get a
method which can handle all cases not only tuple and list.

Without know where he wants to use this I’d be inclined to go something
like:

if isinstance(src, tuple):
    return result if isinstance(result, tuple) else tuple(result)
if isinstance(src, list):
    return result if isinstance(result, list) else list(result)
raise TypeError("src %s was neither tuple nor list" % type(src))

I’m just letting src be your source object on which you want to base
your return type, and result be whatever your function computed.

You could be slightly trickier and use:

return result if isinstance(result, list) else type(src)(result)

which might in turn lead to:

return (
    result if isinstance(result, type(src))
    else type(src)(result)
)

This assumes the src’s type can be initialised with the result as an
argument. If we’re dealing with lists and tuples and expect the result
to be a sequence or maybe iterable this will work, if src’s class does
not have a different style of __init__ method.

I think it’s a intersting question so that i give him a solution like
factory mode to get param’s type object’s new func to new a object,
and i know my solution is not good, i just want to konw more
interesting function regardless of practicality

I think the difficult part is initialising the return object i.e. how to
call your factory. You don’t know how to do that if src might be any
type of object. If you presume it is a tuple or list or a subclass which
does not change __init__, or a duck type which acts like a tuple or
list and has the same kind of __init__ then you’re ok.

I think the task is open ended enough that I would at least have a
nonclever version of the function which just returned a list (or a tuple
if that was natural) and a separate wrapper function which calls the
basic function then does the clever inversion:

def basefunc(src, ...):
    ... compute result, probably a list ...
    return result

def sametype_func(src, ...):
    result = basefunc(src, ...)
    ... convert result into the target src type ...
    return converted_result

and that leads into what is a common pattern:

class Thing:
    def __init__(self, ....):
        .............
    @classmethod
    def from_list(cls, list_value:[list,tuple]):
        return cls(args_to_init)

and you’d make args_to_init from whatever was in list_value. This
puts the class-specific knowledge of how to make a Thing into the
Thing class itself.

Then if you’re working with various classes, if they all provide their
own from_list factory method you can just call:

return type(src).from_list(result)

to have the class itself convert your list-or-tuple into the right
object.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

While others have provided useful and informative concrete answers, I will try to address your friend’s question from a more abstract perspective. What you seem to be describing is function overloading, which in this context can be thought of as polymorphic behavior to the caller, i.e. allowing the same function to handle multiple different data types. The difficulty here is that your friend hasn’t described what they want the function to actually do, i.e. what tuples/lists it should return—the original? Something derived from it? A precomputed list/tuple? Or something else entirely? Without that as the context, it isn’t really possible to specify a generalized solution.

However, if you want to perform an operation that both tuples and lists support (indexing, max/min/sum, etc), you can simply call the appropriate methods/functions on the object without regard for its type—classic object-oriented polymorphism. If it is a behavior you can reasonably mimic (appending, element replacement, etc) on both, you can either use isinstance(), or there’s BTAFTP (better to ask for forgiveness than permission), an approach often favored by Pythonic code, in which you use a try-except to call the desired method on the object (e.g. .append), and fall back to something else if it raises an error (perhaps +). This latter approach has the advantage of working on any object that exposes one of the methods you try, not just those that subclass list and tuple.

To note, relying on type rather than isinstance is usually a very bad idea unless you have very specific and well motivated reason, since it means that it will only work on objects of that exact type, not subclasses (or in the case of the BTAFTP approach above, any objects that implement the same interface(s)). Also, the correct PEP 484 type syntax for your function would be, assuming you’re using Python 3.9+ or Python 3.7+ with from __future__ import annotations:

def get_elem(elements: list | tuple)

Or if you want to annotate the return type as well, you need to use typing.TypeVar to express this:

T = TypeVar(T, bound=Sequence)
def get_elem(elements: T) -> T:

For simplicity and generality, I use typing.Sequence (or collections.abc.Sequence, in Python 3.9+) instead of its more specific subtypes list and tuple, because in general the concepts that apply to both will also apply to Sequances in general, in particular your example here.

1 Like