如何检查 Python 通用类型是否具有特定属性?

How to check if a Python Generic type has a specific attribute?

先吐槽,再写代码!

现在,我正在尝试为我可以在未来的任何项目中使用的通用商店系统创建一个命令行界面模块。我正在使用 MyPy,因为坦率地说,我不相信自己能保持类型的正确性等等,所以由于这个项目的通用性尝试导致我使用了一些我以前从未使用过的东西。我的目标是我可以拥有一家商店并出售我想要的任何商品,只要它们有 .name.

无论如何,在编写通过检查输入的名称从用户购物车中删除商品的函数时(例如,如果我的购物车中有一个名称为“sword”的商品,我会在其中输入“remove sword”输入),我意识到我无法缩小输入类型的范围,因此我可以确定 ItemClass(我的 TypeVar 的名称)有一个 .name。由于我没有缩小范围,尝试访问 .name 会导致 MyPy 出错。我知道类型检查器驱动的开发是一个坏习惯,但我真的不想在那里打一个丑陋的“Any”。

出于测试目的,我为 ItemClass 使用的 class 只是一个具有 .name 属性的可哈希数据 class,但理想情况下它可以是任何可哈希的 class 带有 .name 属性。

这是有问题的函数。请注意,它位于 Shop class 内,它具有 self.cart 属性,该属性将 ItemClass 对象存储为键,将元组中的一些关联元数据存储为值:

def remove_from_cart(self, item_name):
        match: list[ItemClass] = [
            item.name for item in self.cart.keys() if item.name == item_name
        ]
        if match:
            self.cart.pop(match[0])

MyPy 在列表理解的中间行抛出一个适合,因为我没有确保用于 ItemClass 的类型具有 .name。理想情况下,如果我尝试使用 class 没有所需属性的类型检查器,我会想这样做,这样类型检查器就会抛出错误。如果可能的话,我想尽我最大的努力避免在运行时进行这种检查,只是因为我更希望能够让检查员说“嘿,你试图出售的这种类型 -它没有 .name!”在我按下 F5 按钮之前。

提前致谢, 霍比特杰克

编辑 1: 我的店铺 class:

class Shop(Generic[ItemClass, PlayerClass]):
    def __init__(self, items: dict[ItemClass, tuple[Decimal, int]]):
        self.inventory: dict[ItemClass, tuple[Decimal, int]] = items
        self.cart: dict[ItemClass, tuple[Decimal, int]] = {}

    def inventory_print(self):
        for item, metadata in self.inventory.items():
            print(f"{item}: {metadata}")

    def add_to_cart(self, item: ItemClass, price: Decimal, amount: int):
        self.cart[item] = (price, amount)

    def remove_from_cart(self, item_name):
        match: list[ItemClass] = [
            item.name for item in self.cart if item.name == item_name
        ]
        if match:
            self.cart.pop(match[0])

    def print_inventory(self):
        for item, metadata in self.inventory.items():
            print(f"{item}: {metadata[1]} * ${metadata[0]:.2f}")

    def cart_print(self):
        total_price = Decimal(0)
        for item, metadata in self.inventory.items():
            total_price += metadata[0]
            print(f"{item}: {metadata[1]} * ${metadata[0]:.2f}")
        print(f"Total price: {total_price:.2f}")

和我的测试项目class:

@dataclass(frozen=True)
class Item:
    name: str
    durability: float

带有 MyPy 错误消息: "ItemClass" has no attribute "name"

编辑 2: 这里尝试使用我上面的简单版本: 在模块 1 simple_test.py 中,我们有一个简单的数据 class:

from dataclasses import dataclass


@dataclass
class MyDataclass:
    my_hello_world: str

在模块 2 中,我们尝试使用 TypeVar 打印 MyDataclass 对象的 .my_hello_world

from typing import TypeVar
import simple_test

MyTypeVar = TypeVar("MyTypeVar")


def hello_print(string_container: MyTypeVar):
    print(string_container.my_hello_world)


hello_print(simple_test.MyDataclass("Hello, World!"))

MyPy 在 hello_print() 的打印行上抱怨错误消息 "MyTypeVar" has no attribute "my_hello_world".

我的目标是能够将传递给 hello_print 的类型缩小为 肯定 具有 .my_hello_world 的类型,以便它停止抱怨。

您可以将 ItemClass 类型变量绑定到定义所需属性的协议。它将与 任何 名称为字符串的对象兼容。

from dataclasses import dataclass
from typing import Protocol, TypeVar

class HasName(Protocol):
    name: str

ItemClass = TypeVar('ItemClass', bound=HasName)

@dataclass
class MyDataclass:
    name: str

def hello_print(string_container: ItemClass):
    print(string_container.name)

hello_print(MyDataclass("Hello, World!"))

现在 hello_print 只知道 string_containername 属性,它是一个字符串。当您将 MyDataClass 传递给它时,mypy 会检查它是否确实具有字符串 name 属性。如果是,则类型检查成功。如果你试图通过

@dataclass
class AnotherDataclass:
    foo: str

hello_print(AnotherDataclass('Foo'))

mypy会抱怨AnotherDataclassItemClass不兼容。您可以在 playground.

中尝试这个