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
的解决方案更可取。
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
的解决方案更可取。