简体   繁体   English

你能在每次修改另一个字段时修改一个对象的字段吗?

[英]Can you modify an object's field every time another field is modified?

I have a dataclass that looks like this我有一个看起来像这样的数据类

from dataclasses import dataclass, field

@dataclass
class Data:
    name: str | None = None
    file_friendly_name: str | None = field(default=None, init=False)

    def __post_init__(self):
        # If name, automatically create the file_friendly_name
        if self.name:
            self.file_friendly_name = "".join(
                i for i in self.name if i not in "/:*?<>|"
            )

If user passes name on instantiation, file_friendly_name is automatically created.如果用户在实例化时传递name ,则会自动创建file_friendly_name

Is there a way to do it so that every time name is updated/changed, file_friendly_name also changes?有没有办法做到每次更新/更改name时, file_friendly_name也会更改?

eg例如

data = Data()
data.name = 'foo/bar'
print(data.file_friendly_name) # want: 'foobar'

data = Data(name='foo/bar')
data.name = 'new?name'
print(data.file_friendly_name) # want: 'newname'

Update based on answers:根据答案更新:

  1. I've tried setting _name: str and creating name using getters/setters.我试过设置_name: str并使用 getters/setters 创建name But I don't like how when you do print(Data()) it shows _name as an attribute.但我不喜欢当你执行print(Data())时它如何将_name显示为属性。 I'd like that not to happen.我不希望这种情况发生。
  2. I like setting file_friendly_name as a property.我喜欢将file_friendly_name设置为属性。 But then you can't see that as an attribute when you do print(Data()) .但是当您执行print(Data())时,您无法将其视为属性。 This is less of an issue but still not ideal.这不是什么大问题,但仍然不理想。

Can it just show name and file_friendly_name as attributes when doing print(Data()) ?它可以在执行print(Data())时仅将namefile_friendly_name显示为属性吗?

I'd suggest defining file_friendly_name as @property instead.我建议改为将file_friendly_name定义为@property

from dataclasses import dataclass, fields

@dataclass
class Data:
    name: str | None = None
    
    @property
    def file_friendly_name(self) -> str | None:
        if self.name is not None:
            return "".join(
                i for i in self.name if i not in "\/:*?<>|"
            )
        else:
            return None

    def __repr__(self):
        fields_str = [f'{field.name}={getattr(self, field.name)!r}'
                      for field in fields(self)]
        fields_str.append(f'file_friendly_name={self.file_friendly_name}')
        fields_res = ', '.join(fields_str)
        return f'{type(self).__name__}({fields_res})'

Indeed, there is a way!确实,有办法!

Code代码

from dataclasses import dataclass, field

@dataclass
class Data:
    _name: str | None = None
    file_friendly_name: str | None = field(default=None, init=False)

    def __post_init__(self):
        # If _name is not None, automatically create the file_friendly_name
        if self._name is not None:
            self.file_friendly_name = "".join(
                i for i in self._name if i not in "/:*?<>|"
            )

    @property
    def name(self) -> str | None:
        return self._name

    @name.setter
    def name(self, new_val: str | None) -> None:
        if self._name == new_val:
            return
        self._name = new_val
        if self._name is None:
            self.file_friendly_name = None
        else:
            self.file_friendly_name = "".join(
                i for i in self._name if i not in "/:*?<>|"
            )

Explanation解释

Since you asked for a way to actually update the file_friendly_name field whenever name changes, I've changed the name field into a property which reads from private attribute _name .由于您要求一种在name更改时实际更新file_friendly_name字段的方法,因此我已将name字段更改为从私有属性_name读取的属性。 Now it's _name which is assessed in __post_init__ .现在是在__post_init__中评估的_name

Then I've created a "setter" for the name property.然后我为name属性创建了一个“setter”。 This setter will be called every time name is updated.每次更新name时都会调用此设置器。 Note that there's no sort of Data.name.setattr(...) boilerplate-y nonsense, given that we're in Python-land.请注意,这里没有Data.name.setattr(...)样板式的废话,因为我们在 Python 领域。 When I say "updated", I mean whenever you do当我说“更新”时,我的意思是每当你这样做

>>> d = Data("Zev")
>>> d.name = "**Zev**"

that setter will be invoked and the name and file_friendly_name fields will be updated accordingly:该设置器将被调用,并且namefile_friendly_name字段将相应更新:

>>> d.file_friendly_name
'Zev'
>>> d.name
'**Zev**'

>>> data = Data()
>>> data.name = 'foo/bar'
>>> print(data.file_friendly_name) 
'foobar'

>>> data = Data('foo/bar')
>>> data.name = 'new?name'
>>> print(data.file_friendly_name) 
'newname'

Pretty Printing漂亮的印刷

One small drawback of this is that printing data shows our private field:这样做的一个小缺点是打印data会显示我们的私有字段:

>>> print(data)
Data(_name='new?name', file_friendly_name='newname')

However, you can work around this by defining your own __repr__ method:但是,您可以通过定义自己的__repr__方法来解决此问题:

def __repr__(self) -> str:
    return f"Data(name='{self._name}', file_friendly_name='{self.file_friendly_name}')"
>>> print(data)
Data(name='new?name', file_friendly_name='newname')

Making name work in the constructor使name在构造函数中起作用

Finally, if you'd like your name keyword argument back for constructing Data instances, you can add your own constructor to it.最后,如果您希望您的name关键字参数返回用于构造Data实例,您可以向其添加自己的构造函数。 We'll DRY up the code this requires while we're at it:我们将干掉这需要的代码:

def __init__(self, name: str | None = None):
    self._name = name
    if self._name is not None:
        self.file_friendly_name = self.make_file_friendly_name(self._name)

def __post_init__(self):
    # If _name is not None, automatically create the file_friendly_name
    if self._name is not None:
        self.file_friendly_name = self.make_file_friendly_name(self._name)

@name.setter
def name(self, new_val: str | None) -> None:
    if self._name == new_val:
        return
    self._name = new_val
    if self._name is None:
        self.file_friendly_name = None
    else:
        self.file_friendly_name = self.make_file_friendly_name(self._name) # 👈 revised
    

@staticmethod
def make_file_friendly_name(name: str) -> str:
    return "".join(
        i for i in name if i not in "\\/:*?<>|"
    )

After this, the sample code works as expected:在此之后,示例代码按预期工作:

>>> data = Data()
>>> data.name = 'foo/bar'
>>> print(data.file_friendly_name) 
'foobar'

>>> data = Data(name='foo/bar')
>>> data.name = 'new?name'
>>> print(data.file_friendly_name) 
'newname'

Similiarly to @Yevhen's suggestion, but using setter on property you can trigger a specific function when setting to an attribute.与@Yevhen 的建议类似,但在属性上使用 setter 可以在设置属性时触发特定的 function。 You can then check if class has related private attribute to tell if you are definining it right now or it already exists.然后你可以检查 class 是否有相关的私有属性来判断你是现在定义它还是已经存在。

from dataclasses import dataclass, field

def methodToTrigger():
    print("Triggered method")

@dataclass
class Data:
    name: str = None

    def __post_init__(self):
        # If name, automatically create the file_friendly_name
        if self.name:
            self.file_friendly_name = "".join(
                i for i in self.name if i not in "\/:*?<>|"
            )
    @property
    def file_friendly_name(self):
        return self._file_friendly_name

    @file_friendly_name.setter
    def file_friendly_name(self, value):
        if not hasattr(self, "_file_friendly_name"):
            methodToTrigger()
        self._file_friendly_name = value

d = Data(name = "asdf")

print(d.file_friendly_name)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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