Search code examples
pythonconstructorprotocols

In Python, how to specify that a Protocol implementer has a specific constructor signature?


Is it possible to define that a class needs a specific constructor?

class Constructible(Protocol):

  def __init__(self, i: int):  # how do I do this?
    raise NotImplementedError

  def get_value(self):
    raise NotImplementedError

def map_is(cs: Iterable[Constructible], i: int):
  mapped = tuple(C(i) for C in cs)
  values = tuple(c.get_value() for c in mapped)
  # both the constructor and a member method are used
  return mapped, values

# implementors (omitting __hash__ and __eq__ for brevity)
class X(Constructible):
  def __init__(self, i):
    self.i=i
  def get_value(self):
    return self.i

class Sq(Constructible):
  def __init__(self, i):
    self.i=i
  def get_value(self):
    return self.i * self.i

cs, values = tuple(map_is((X, Sq), 5))
assert values == (5, 25)

When specifying it like this, I get

$ mypy constr.py 
constr.py:12: error: "Constructible" not callable
Found 1 error in 1 file (checked 1 source file)

Is this even possible? Or should I revert to a factory function @classmethod def construct(i: int): Self?


Solution

  • As explained by @jonrsharpe, you do not pass an iterable of Constructible instances to map_is but an iterable of classes. That means that you should define the function that way:

    def map_is(cs: Iterable[Type[Constructible]], i: int):
      return (C(i) for C in cs)
    

    That is enough for mypy to validate the code.

    But there is an unrelated problem: you never declared any __hash__ nor __equal__ special method. That means that in assert values == (X(5), Sq(5)) the equality used is the one defined on the object class (same as is). So after the above fix, the code executes successfully but still raises an AssertionError, because the objects do have same value, yet they are distinct objects...