我有一个包含张量的类,这些张量具有不可预测的属性列表。
我希望它根据实例化时给出的列表生成带有 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)
想到了两种方法。两者都很混乱,并不总是能很好地与 linters、mypy 等一起使用。基本上,复制和粘贴或仅使用您已经拥有的 get 和 set 方法可能是更干净、更清晰和更简单的解决方案。但是如果你要处理很多属性或不同的类,那么这些应该可以工作。
选项 1:使用 getattr 和 setattr 而不是属性
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)
所以我不确定我是否真的会推荐其中任何一个,但它们是选项。
有一种比上面详述的答案更简洁的方法。
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__
属性。