简体   繁体   中英

Python typing return type of class composition's method

I have a class Human which can have a pet and a method to make it talk, either a Dog or a Cat . I initialize the Human with a Dog , and the method pet_speak has the correct return type hint Literal["Woof"] .

If I change the Human 's pet to a Cat after initialization, the method return type hint isn't updated to Literal["Meow"] .

Is there a way to change this behavior? This is a simplified version of my problem, creating two classes, such as, HumanDog and HumanCat is more complex.

class Cat:
    def speak(self): # implicit return type hint Literal["Meow"]
        return "Meow"

class Dog:
    def speak(self): # implicit return type hint Literal["Woof"]
        return "Woof"

# Class `Human` has a `pet`, and can call its `speak()` method
class Human:
    pet = Dog()

    def pet_speak(self):
        return self.pet.speak()

# Helper function to change `human`'s pet
def change_pet(human):
    human.pet = Cat()
    return human

bob = Human()
woof = bob.pet_speak() # Return type is correctly Literal["Woof"]
bob = change_pet(bob) # change attribute `pet` to `Cat`
meow = bob.pet_speak() # Return type is Literal["Woof"], but should be Literal["Meow"]

The closest question I could find was Can you type hint overload a return type for a method based on an argument passed into the constructor? , but it also adds an overhead to every Human method and subclasses.


Sorry if I dumbed the problem down too much, here it's more similar to what I'm trying to do:

from __future__ import annotations

class Api:
    def request(self):
        return "Response"

class AsyncApi:
    async def request(self):
        return "Response"

class Database:
    api: Api | AsyncApi = Api()

    def get(self):
        return self.api.request()

def Async(database):
    database.api = AsyncApi()
    return database


async def main():
    db = Database()
    db.get()
    adb = Async(db)
    await adb.get() # typing error

If you add the missing annotation run the code through Pyright (or Mypy), it will spot an error here:

def change_pet(human: Human) -> None:
    human.pet = Cat()
    # ^
    # Cannot assign member "pet" for type "Human"
    #  Expression of type "Cat" cannot be assigned to member "pet" of class "Human"
    #    "Cat" is incompatible with "Dog"
    return human

Indeed, the type of human.pet is Cat . Generally, the type of an object can't change when you're mutating it. This makes sense, for example because an object might be shared:

class CatHumans:
    def __init__(self, humans: Iterable[Human]) -> None:
        # somehow establish that all the `humans` have a cat
        self._humans = list(humans)

    def do_something_assuming_humans_have_cats(self) -> None:
        for human in self._humans:
            assert human.pet.speak() == "Meow"
            ...

alice, bob, charlie = make_humans_with_cats(3)
cat_humans = CatHumans([alice, bob, charlie])

alice.pet = Dog()
cat_humans.do_something_assuming_humans_have_cats()

It would be very difficult if not impossible for type checkers to track situations like this.


I don't know what real problem you're solving, but I have two solutions:

  1. Specify a more general type for pet :

A common protocol/base class:

class Human:
    pet: Animal = Dog()

Or a union type:

from typing import Union

class Human:
    pet: Union[Cat, Dog] = Dog()
  1. Make a generic class
from typing import Generic, TypeVar

Pet = TypeVar("Pet", bound=Animal)

class Human(Generic[Pet]):
    def __init__(self, pet: Pet) -> None:
        self.pet = pet

This still doesn't let you have the same object be Human[Dog] today and Human[Cat] tomorrow, but you can have distinct Human[Cat] and Human[Dog] objects.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM