Pythonic 类 和 Python 的禅宗

Pythonic Classes and the Zen of Python

Python 的禅宗说:

“应该有一种——最好只有一种——显而易见的方法。”

假设我想创建一个 class 来构建金融交易。 class 应该允许用户建立交易,然后调用 sign() 方法来签署交易,为通过 API 调用广播做好准备。

class 将具有以下参数:

sender
recipient
amount
signer (private key for signing)
metadata
signed_data

所有这些都是字符串,除了 amount 是一个 int,除了最后两个之外都是必需的:metadata 是一个可选参数,signed_data 在方法 sign() 被调用。

我们希望所有参数在签名之前都经过某种验证,这样我们就可以通过向用户提出适当的错误来拒绝格式错误的交易。

使用 classic Python class 和构造函数似乎很简单:

class Transaction:
    def __init__(self, sender, recipient, amount, signer, metadata=None):
            self.sender = sender
            self.recipient = recipient
            self.amount = amount
            self.signer = signer

            if metadata:
                self.metadata = metadata

    def is_valid(self):
        # check that all required parameters are valid and exist and return True, 
        # otherwise return false

    def sign(self):
        if self.is_valid():
            # sign transaction
            self.signed_data = "pretend signature"
        else:
            # raise InvalidTransactionError

或属性:

class Transaction:
    def __init__(self, sender, recipient, amount, signer, metadata=None):
        self._sender = sender
        self._recipient = recipient
        self._amount = amount
        self._signer = signer
        self._signed_data = None

        if metadata:
            self._metadata = metadata

    @property
    def sender(self):
        return self._sender

    @sender.setter
    def sender(self, sender):
        # validate value, raise InvalidParamError if invalid
        self._sender = sender

    @property
    def recipient(self):
        return self._recipient

    @recipient.setter
    def recipient(self, recipient):
        # validate value, raise InvalidParamError if invalid
        self._recipient = recipient

    @property
    def amount(self):
        return self._amount

    @amount.setter
    def amount(self, amount):
        # validate value, raise InvalidParamError if invalid
        self._amount = amount

    @property
    def signer(self):
        return self._signer

    @signer.setter
    def signer(self, signer):
        # validate value, raise InvalidParamError if invalid
        self._signer = signer

    @property
    def metadata(self):
        return self._metadata

    @metadata.setter
    def metadata(self, metadata):
        # validate value, raise InvalidParamError if invalid
        self._metadata = metadata

    @property
    def signed_data(self):
        return self._signed_data

    @signed_data.setter
    def signed_data(self, signed_data):
        # validate value, raise InvalidParamError if invalid
        self._signed_data = signed_data

    def is_valid(self):
        return (self.sender and self.recipient and self.amount and self.signer)

    def sign(self):
        if self.is_valid():
            # sign transaction
            self.signed_data = "pretend signature"
        else:
            # raise InvalidTransactionError
            print("Invalid Transaction!")

我们现在可以在设置时验证每个值,所以在我们去签名时我们知道我们有有效参数并且 is_valid() 方法只需要检查是否已设置所有必需的参数。对我来说,这比在单个 is_valid() 方法中进行所有验证更 Pythonic,但我不确定所有额外的样板代码是否真的值得。

有数据classes:

@dataclass
class Transaction:
    sender: str
    recipient: str
    amount: int
    signer: str
    metadata: str = None
    signed_data: str = None

    def is_valid(self):
        # check that all parameters are valid and exist and return True, 
        # otherwise return false

    def sign(self):
        if self.is_valid():
            # sign transaction
            self.signed_data = "pretend signature"
        else:
            # raise InvalidTransactionError
            print("Invalid Transaction!")

将此与方法进行比较

1,这还不错。它简洁、干净、可读,并且已经内置了 __init__()__repr__()__eq__() 方法。另一方面,与 Approach

相比

2 我们回到通过大量 is_valid() 方法验证所有输入。

我们可以尝试将属性与数据一起使用classes,但这实际上比听起来更难。根据 this blog post 可以这样做:

@dataclass
class Transaction:
    sender: str
    _sender: field(init=False, repr=False)
    recipient: str
    _recipient: field(init=False, repr=False)
   . . .
   # properties for all parameters

    def is_valid(self):
        # if all parameters exist, return True, 
        # otherwise return false

    def sign(self):
        if self.is_valid():
            # sign transaction
            self.signed_data = "pretend signature"
        else:
            # raise InvalidTransactionError
            print("Invalid Transaction!")

是否只有一种明显的方法可以做到这一点? dataclass是否推荐用于此类应用程序?

作为一般规则,并不局限于 Python,编写“fails fast”的代码是个好主意:也就是说,如果在运行时出现问题,您需要它尽早被检测到并发出信号(例如通过抛出异常)。

特别是在调试的上下文中,如果错误是设置了无效值,您希望在设置值时抛出异常,以便堆栈跟踪包括设置无效的方法价值。如果在使用该值时抛出异常,那么您无法指示代码的哪一部分导致了无效值。

在你的三个例子中,只有第二个让你遵循这个原则。它可能需要更多的样板代码,但与没有有意义的堆栈跟踪的调试相比,编写样板代码很容易并且不需要太多时间。

顺便说一句,如果您有执行验证的设置器,那么您也应该从构造函数中调用这些设置器,否则可能会创建一个初始状态无效的对象。

考虑到您的限制,我认为您的 dataclass 方法可以改进以产生一个富有表现力和惯用的解决方案,对生成的 Transaction 实例具有非常强大的运行时断言,主要是通过利用 __post_init__ 机制:

from dataclasses import dataclass, asdict, field
from typing import Optional

@dataclass(frozen=True)
class Transaction:
    sender: str
    recipient: str
    amount: int
    signer: str
    metadata: Optional[str] = None
    signed_data: str = field(init=False)

    def is_valid(self) -> bool:
        ...  # implement your validity assertion logic

    def __post_init__(self):
        if self.is_valid():
            object.__setattr__(self, "signed_data", "pretend signature")
        else:
            raise ValueError(f"Invalid transaction with parameter list "
                             f"{asdict(self)}.")

这减少了您必须维护和理解的代码量,使每一行都与您的需求中有意义的部分相关,这是 pythonic 代码的本质。

换句话说,这个 Transaction class 的实例可能会指定 metadata 但不需要也可能不会提供它们自己的 signed_data,这是可能在您的变体#3 中。属性在初始化后不能再改变(由 frozen=True 强制执行),因此有效的实例不能更改为无效状态。最重要的是,由于验证现在是构造函数的一部分,因此不可能存在无效实例。只要您能够在运行时引用 Transaction,您就可以 100% 确定它通过了有效性检查并且会再次这样做。

既然你的问题是基于 python-zen 一致性(参考 美丽胜于丑陋简单胜于复杂),我想说这个解决方案比基于 property 的解决方案更可取。