如何使用 Python 生成在类实例化时设置和获取属性的方法?

问题描述 投票:0回答:2

我有一个包含张量的类,这些张量具有不可预测的属性列表。

我希望它根据实例化时给出的列表生成带有 getter 和 setter 的属性列表。

你知道怎么做吗?

谢谢

class Items:
     
    def __init___(
        self,
        names: List[str]
    ):
     #: Property names to be generated 
     self.names: List[str] = names
     #: Tensors in use
     self.tensors: Dict[str,Tensor] = {}
     
     def get_tensor(self, name:str) -> Tensor | None:
         if name in self.tensors.keys():
             return self.tensors[name]
         return None
         
     def set_tensor(self, name:str, tensor: Tensor):
         self.tensors[name] = tensor

     # The exact same setter and getter functions have to be generated for several properties:
     # entries, classes, scores, target classes, target scores, embeddings, heatmaps, losses...
     # The list is provided by self.names at instantiation 
     
     @property
     def entries(self) -> Tensor | None:
         return self.get_tensor("entries")
         
      @entries.setter
      def entries(self, tensor: Tensor):
           self.set_tensor("entries", tensor)
python class pytorch instance-variables python-class
2个回答
1
投票

想到了两种方法。两者都很混乱,并不总是能很好地与 linters、mypy 等一起使用。基本上,复制和粘贴或仅使用您已经拥有的 get 和 set 方法可能是更干净、更清晰和更简单的解决方案。但是如果你要处理很多属性或不同的类,那么这些应该可以工作。

选项 1:使用 getattrsetattr 而不是属性

from typing import Dict, Any


class Tensor:  # just a test class for debugging
    def __init__(self, name: str) -> None:
        self.name = name

    def __repr__(self) -> str:
        return self.name
    
    
class Items:
    def __init__(self, *names: str) -> None:
        self.names = set(names)
        self.tensors: Dict[str, Tensor] = {}

    def __getattr__(self, name: str) -> Any:
        if (names := self.__dict__.get('names')) and name in names:
            return self.tensors.get(name)
        try:
            return self.__dict__[name]
        except KeyError:
            raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{name}'")

    def __setattr__(self, name: str, value: Tensor) -> None:
        if (names := self.__dict__.get('names')) and name in names:
            self.tensors[name] = value
        super().__setattr__(name, value)


items = Items('entries', 'classes', 'scores', 'target_classes',
              'target_scores', 'embeddings', 'heatmaps', 'losses')
items.entries = Tensor('X')
print(items.tensors)
print(items.entries)
print(items.classes)

选项2:使用元类在创建类时创建您想要的所有属性。然后,您可以为每组属性名称创建一个不同的类(或继承一个新类)。

from __future__ import annotations

from typing import List, Dict, Any, Tuple, Callable, Optional, Type


class Tensor:  # just a test class for debugging
    def __init__(self, name: str) -> None:
        self.name = name

    def __repr__(self) -> str:
        return self.name


class ItemsMetaClass(type):
    def __new__(mcs: type, name: str, bases: Tuple[type, ...], 
                attributes: Dict[str, Any], tensor_names: List[str]) -> Type[ItemsMetaClass]:
        for tensor_name in tensor_names:
            attributes[tensor_name] = property(
                create_tensor_fget(tensor_name),
                create_tensor_fset(tensor_name))
        return super().__new__(mcs, name, bases, attributes)


def create_tensor_fget(tensor_name: str) -> Callable[[Items], Optional[Tensor]]:
    def fget(self: Items) -> Optional[Tensor]:
        if tensor_name in self.tensors:
            return self.tensors[tensor_name]
        return None
    return fget


def create_tensor_fset(tensor_name: str) -> Callable[[Items, Tensor], None]:
    def fset(self: Items, tensor: Tensor) -> None:
        self.tensors[tensor_name] = tensor
    return fset


class Items(metaclass=ItemsMetaClass, tensor_names=[
            'entries', 'classes', 'scores', 'target_classes',
            'target_scores', 'embeddings', 'heatmaps', 'losses']):
    def __init__(self) -> None:
        self.tensors: Dict[str, Tensor] = {}


items = Items()
items.entries = Tensor('X')
print(items.tensors)
print(items.entries)
print(items.classes)

所以我不确定我是否真的会推荐其中任何一个,但它们是选项。


0
投票

有一种比上面详述的答案更简洁的方法。

class Items:
    def __init__(self, names):
        self.names = names
        self.tensors = {}

        old_type = type(self)
        new_type = type(old_type.__name__, old_type.__bases__, dict(old_type.__dict__))

        for name in names:
            getter = lambda self, name=name: self.get_tensor(name)
            setter = lambda self, tensor, name=name: self.set_tensor(name, tensor)
            setattr(new_type, name, property(getter,setter))

        self.__class__ = new_type

    def get_tensor(self, name):
        return self.tensors.get(name)

    def set_tensor(self, name, tensor):
        self.tensors[name] = tensor

关于这段代码,只有几件事需要注意。

首先,如果

Items
将来被重构为通过元类获取关键字参数,您将需要更改第 7 行以使用
copy.deepcopy
new_type
创建
old_type

另一个问题是

name=name
部分。如果
name
没有以这种方式绑定到
<lambda>
范围,它将引用
names
的最终元素,因为绑定到
Items.__init__
的范围。

最后,您在

get_tensor
中有一些冗余代码,这只是
dict.get
的重新实现。我用后者代替了这个。

它是如何工作的?

Python 在获取或设置属性的过程中查找属性时,会检查

type(object)
对应的属性是否实现了描述符协议。
property
实现了 descriptor 协议,所以当 Python 查找属性时,我们可以使用 getter 和 setter。

那么

type
是如何工作的呢?在单参数
type(object)
的情况下,它所做的只是检查该对象的
__class__
属性。
__class__
本身只是对对象类的引用。因此,如果我们将
__class__
属性更改为我们自己创建的类,我们就可以创建仅特定于实例的属性。

最后,

type
的三参数版本可用于创建新类,传递名称、基数和其他参数。如果您将来更改此代码以使用元类,请将此行替换为
new_type = copy.deepcopy(old_type)

从那里开始,代码变得相对简单。我们使用

type
为我们的实例创建一个新类,遍历名称并创建属性,然后最终将我们的新类分配给
__class__
属性。

© www.soinside.com 2019 - 2024. All rights reserved.