Pythonic使用 Dunder场景应用丰富您的 Python
什么是 Dunder 方法?
在 Python 中,特殊方法是一组可用于丰富类的预定义方法。它们很容易识别,因为它们以双下划线开头和结尾,例如__init__or str。
因为很快就让人厌烦了,因为 Pythonistas 采用了“dunder methods”这个术语,这是“double under”的缩写形式。
Python 中的这些“dunders”或“特殊方法”有时也称为“魔术方法”。但是使用这个术语会使它们看起来比实际更复杂——归根结底,它们并没有什么“神奇”之处。您应该将这些方法视为普通语言功能。
Dunder 方法让您模拟内置类型的行为。例如,要获取字符串的长度,您可以调用len('string'). 但是一个空的类定义不支持这种开箱即用的行为:
Dunder 方法让您模拟内置类型的行为。例如,要获取字符串的长度,您可以调用len('string'). 但是一个空的类定义不支持这种开箱即用的行为:
NoLenSupport类: 通过
>>> obj = NoLenSupport ()
>>> len ( obj )
TypeError : “'NoLenSupport' 类型的对象没有 len()”
为了解决这个问题,你可以len在你的类中添加一个 dunder 方法:
class LenSupport :
def __len__ ( self ):
return 42
>>> obj = LenSupport ()
>>> len ( obj ) 42
另一个例子是切片。您可以实现一个getitem允许您使用 Python 的列表切片语法的方法:obj[start:stop].
特殊方法和 Python 数据模型
这种优雅的设计被称为Python 数据模型,让开发人员可以利用丰富的语言特性,如序列、迭代、运算符重载、属性访问等。
您可以将 Python 的数据模型视为一个强大的 API,您可以通过实现一个或多个 dunder 方法与之交互。如果您想编写更多 Pythonic 代码,了解如何以及何时使用 dunder 方法是重要的一步。
不过,对于初学者来说,起初这可能会有点压倒性。Account不用担心,在本文中,我将以一个简单的类为例,指导您使用 dunder 方法。
丰富一个简单的帐户类
在整篇文章中,我将使用各种 dunder 方法丰富一个简单的 Python 类,以解锁以下语言特性:
新对象的初始化
对象表示
启用迭代
运算符重载(比较)
运算符重载(加法)
方法调用
上下文管理器支持(with声明)
您可以在此处找到最终的代码示例。我还整理了一个Jupyter 笔记本,以便您可以更轻松地使用示例。
对象初始化:init
刚开始上课,我就已经需要一种特殊的方法。要从Account类中构造帐户对象,我需要一个构造函数,在 Python 中是用with语句来使用一个账户对象。当我做一笔交易来增加一个正数时,一切都很好。
让我们在帐户类上实现这两个方法。
"""一个简单的帐户类"""
class Account:
def __init__(self, owner, amount=0):
self.owner = owner
self.amount = amount
self._transactions = []
""" 这是让我们从这个类生成实例 """
acc = Account('bob') # 默认金额 = 0
acc = Account('bob', 10)
acc4 = Account('sue', 10)
对象表示法: str, repr
在 Python 中,为你的类的消费者提供你的对象的字符串表示是一种常见的做法 (有点像 API 文档。) 有两种方法可以使用 dunder 方法来做到这一点。
repr:一个对象的 "官方 "字符串表示。这就是你如何制造一个类的对象。repr 的目的是为了不含糊。
str:一个对象的 "非正式 "或可打印的字符串表示。这是给终端用户的。
让我们在帐户类上实现这两个方法。
class Account:
"""A simple account class"""
def __init__(self, owner, amount=0):
"""
This is the constructor that lets us create
objects from this class
"""
self.owner = owner
self.amount = amount
self._transactions = []
def __repr__(self):
return 'Account({!r}, {!r})'.format(self.owner, self.amount)
def __str__(self):
return 'Account of {} with starting amount: {}'.format(
self.owner, self.amount)
我还定义了一个属性来计算帐户余额,这样我就可以方便地使用account.balance.
此方法采用起始金额并添加所有交易的总和:
str(acc)
'Account of bob with starting amount: 10'
print(acc)
"Account of bob with starting amount: 10"
repr(acc)
"Account('bob', 10)"
如果你不想硬编码 "Account "作为类的名字,你也可以使用self.class.name来以编程方式访问它。
如果你想在一个 Python 类上只实现这些 to-string 方法中的一个,确保它是 repr。
现在我可以用各种方式查询这个对象,并且总是得到一个漂亮的字符串表示。
def add_transaction(self, amount):
if not isinstance(amount, int):
raise ValueError('please use int for amount')
self._transactions.append(amount)
迭代。 len, getitem, reversed
为了对我们的账户对象进行迭代,我需要添加一些事务。所以首先,我将定义一个简单的方法来添加交易。我将保持它的简单,因为这只是解释dunder方法的设置代码,而不是一个可供生产的会计系统。
def add_transaction(self, amount):
if not isinstance(amount, int):
raise ValueError('please use int for amount')
self._transactions.append(amount)
我还定义了一个属性来计算账户的余额,这样我就可以方便地用 account.balance 来访问它。这个方法获取起始金额并加上所有交易的总和。
@property
def balance (self):
return self.amount + sum(self._transactions)
让我们在帐户上进行一些存款和取款:
执行上面的Python片段会产生以下打印结果。
>>> acc = Account('bob', 10)
>>> acc.add_transaction(20)
>>> acc.add_transaction(-10)
>>> acc.add_transaction(50)
>>> acc.add_transaction(-20)
>>> acc.add_transaction(30)
>>> acc.balance
80
现在我有一些数据,我想知道。
有多少个交易?
对账户对象进行索引以获得交易号码 ...
在交易中进行循环
在我所拥有的类定义中,目前这是不可能的。以下所有的语句都会引发TypeError异常。
>>> len(acc)
TypeError
>>> for t in acc:
... print(t)
TypeError
>>> acc[1]
TypeError
Dunder 来拯救! 只需要一点点的代码就可以让这个类变得可迭代。
class Account:
"""A simple account class"""
def __init__(self, owner, amount=0):
"""
This is the constructor that lets us create
objects from this class
"""
self.owner = owner
self.amount = amount
self._transactions = []
def __len__(self):
return len(self._transactions)
def __getitem__(self, position):
return self._transactions[position]
以上操作变得可行
>>> len(acc)
5
>>> for t in acc:
... print(t)
20
-10
50
-20
30
>>> acc[1]
-10
为了以相反的顺序遍历交易事务,你可以实现reversed特殊方法。
def __reversed__(self):
return self[::-1]
>>> list(reversed(acc))
[30, -20, 50, -10, 20]
为了反转交易的列表,我使用了 Python 的反转列表切片语法。我还不得不把 reversed(acc) 的结果包装在一个 list() 调用中,因为 reversed() 返回的是一个反向迭代器,而不是一个我们可以在 REPL 中很好地打印的 list 对象。如果你想详细了解这种方法的工作原理,请查看这个关于 Python 中迭代器的教程。
总而言之,这个账户类现在对我来说已经开始变得很Pythonic了。
用于比较账户的运算符重载 eq, lt
我们每天都会写几十条语句来比较 Python 对象。
>>> 2 > 1
True
>>> 'a' > 'b'
False
这感觉是完全自然的,但实际上,这里的幕后发生的事情是相当惊人的。为什么>在整数、字符串和其他对象(只要它们是相同的类型)上同样有效?这种多态的行为是可能的,因为这些对象实现了一个或多个比较dunder方法。
验证这一点的一个简单方法是使用dir()内置程序。
dir('a')
['__add__',
...
'__eq__', <---------------
'__format__',
'__ge__', <---------------
'__getattribute__',
'__getitem__',
'__getnewargs__',
'__gt__', <---------------
...]
让我们建立第二个账户对象,并将其与第一个账户对象进行比较(我正在添加几个交易供以后使用)
>>> acc2 = Account('tim', 100)
>>> acc2.add_transaction(20)
>>> acc2.add_transaction(40)
>>> acc2.balance
160
>>> acc2 > acc
TypeError:
"'>' not supported between instances of 'Account' and 'Account'"
这里发生了什么?我们得到了一个TypeError,因为我没有实现任何比较器,也没有从父类继承它们。
让我们来添加它们。为了不需要实现所有的比较器方法,我使用了 functools.total_ordering 装饰器,它允许我走捷径,只实现 eq 和 lt。
from functools import total_ordering
@total_ordering
class Account:
# ... (see above)
def __eq__(self, other):
return self.balance == other.balance
def __lt__(self, other):
return self.balance < other.balance
而现在我可以毫无问题地比较账户实例。
>>> acc2 > acc
True
>>> acc2 < acc
False
>>> acc == acc2
False
合并账户的操作符重载。 加号(add)
在Python中,所有东西都是一个对象。我们完全可以用+ (plus) 操作符添加两个整数或两个字符串,它的行为是预期的。
>>> 1 + 2
3
>>> 'hello' + ' world'
'hello world'
我们再次看到多态性在发挥作用。你注意到 "+"是如何根据对象的类型表现出不同的行为吗?对于整数来说,它是求和的,对于字符串来说,它是连接的。再一次对对象做一个快速的dir(),揭示了数据模型中相应的 "dunder "接口。
dir(1)
[...
'__add__',
...
'__radd__',
...]
我们的账户对象还不支持加法,所以当你试图添加两个实例时,会出现TypeError。
acc + acc2
TypeError: "unsupported operand type(s) for +: 'Account' and 'Account'"
让我们实现add,以便能够合并两个账户。预期的行为是将所有的属性合并在一起:所有者的名字,以及起始金额和交易。要做到这一点,我们可以受益于我们先前实现的迭代支持。
def __add__(self, other):
owner = '{}&{}'.format(self.owner, other.owner)
start_amount = self.amount + other.amount
acc = Account(owner, start_amount)
for t in list(self) + list(other):
acc.add_transaction(t)
return acc
是的,它比到目前为止的其他dunder的实现要多一些。但它应该告诉你,你是在司机的座位上。你可以随心所欲地实现加法。如果我们想忽略历史交易--很好,你也可以这样实现它。
def __add__(self, other):
owner = self.owner + other.owner
start_amount = self.balance + other.balance
return Account(owner, start_amount)
但我认为前一种实现方式更现实,因为这一类的消费者会期望发生什么。
现在我们有一个新的合并账户,起始金额为110美元(10+100),余额为240美元(80+160)。
>>> acc3 = acc2 + acc
>>> acc3
Account('tim&bob', 110)
>>> acc3.amount
110
>>> acc3.balance
240
>>> acc3._transactions
[20, 40, 20, -10, 50, -20, 30]
注意这在两个方向上都是有效的,因为我们在添加相同类型的对象。一般来说,如果你将你的对象添加到一个内建函数 (int, str, ...) 中,内建函数的 add 方法不会知道你的对象的任何信息。在这种情况下,你需要同时实现反向添加方法 (radd)。你可以在这里看到一个例子。
可调用的 Python 对象。 call
你可以通过添加 call dunder 方法使一个对象像普通函数一样可调用。对于我们的账户类,我们可以打印一份构成其余额的所有交易的漂亮报告。
class Account:
# ... (see above)
def __call__(self):
print('Start amount: {}'.format(self.amount))
print('Transactions: ')
for transaction in self:
print(transaction)
print('\nBalance: {}'.format(self.balance))
现在,当我用双括号 acc() 语法调用该对象时,我得到了一个漂亮的账户报表,其中有所有交易和当前余额的概览。
>>> acc = Account('bob', 10)
>>> acc.add_transaction(20)
>>> acc.add_transaction(-10)
>>> acc.add_transaction(50)
>>> acc.add_transaction(-20)
>>> acc.add_transaction(30)
>>> acc()
Start amount: 10
Transactions:
20
-10
50
-20
30
Balance: 80
请记住,这只是一个玩具的例子。当你在它的一个实例上使用函数调用语法时,一个 "真正的 "账户类可能不会打印到控制台。一般来说,在你的对象上有一个 call 方法的缺点是很难看到调用对象的目的是什么。
因此,大多数情况下,最好在类中添加一个明确的方法。在这个例子中,如果有一个单独的Account.print_statement()方法,可能会更加透明。
上下文管理器支持和With语句。 enter, __exit
本教程的最后一个例子是关于 Python 中一个稍微高级的概念。上下文管理器和增加对 With 语句的支持。
现在,什么是 Python 中的 "上下文管理器"?这里有一个快速的概述。
上下文管理器是一个简单的 "协议"(或接口),你的对象需要遵循这个协议,这样它就可以和 with 语句一起使用。基本上你需要做的就是给一个对象添加 enter 和 exit 方法,如果你想让它作为一个上下文管理器发挥作用。
让我们使用上下文管理器的支持来给我们的帐户类添加一个回滚机制。如果余额在添加另一个交易时变成了负数,我们就回滚到之前的状态。
我们可以通过增加两个dunder方法来利用Pythonic with语句。我还添加了一些打印调用,以便在我们演示时使例子更加清晰。
class Account:
# ... (see above)
def __enter__(self):
print('ENTER WITH: Making backup of transactions for rollback')
self._copy_transactions = list(self._transactions)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print('EXIT WITH:', end=' ')
if exc_type:
self._transactions = self._copy_transactions
print('Rolling back to previous transactions')
print('Transaction resulted in {} ({})'.format(
exc_type.__name__, exc_val))
else:
print('Transaction OK')
由于必须提出一个异常来触发回滚,我定义了一个快速帮助方法来验证一个账户中的交易。
def validate_transaction(acc, amount_to_add):
with acc as a:
print('Adding {} to account'.format(amount_to_add))
a.add_transaction(amount_to_add)
print('New balance would be: {}'.format(a.balance))
if a.balance < 0:
raise ValueError('sorry cannot go in debt!')
现在我可以用with语句来使用一个账户对象。当我做一笔交易来增加一个正数时,一切都很好。
acc4 = Account('sue', 10)
print('\nBalance start: {}'.format(acc4.balance))
validate_transaction(acc4, 20)
print('\nBalance end: {}'.format(acc4.balance))
Balance start: 10
ENTER WITH: Making backup of transactions for rollback
Adding 20 to account
New balance would be: 30
EXIT WITH: Transaction OK
Balance end: 30
余额开始是 10
为交易回滚做备份
账户增加20
新的账户余额: 30
退出交易
账户余额: 30
然而,当我试图提取太多的钱时,exit中的代码就会启动并回滚交易。
acc4 = Account('sue', 10)
print('\nBalance start: {}'.format(acc4.balance))
try:
validate_transaction(acc4, -50)
except ValueError as exc:
print(ex)
print('\nBalance end: {}'.format(acc4.balance))
Balance start: 10
ENTER WITH: Making backup of transactions for rollback
Adding -50 to account
New balance would be: -40
EXIT WITH: Rolling back to previous transactions
ValueError: sorry cannot go in debt!
Balance end: 10
在这种情况下,我们得到一个不同的结果。
余额开始是10
ENTER WITH: 为回滚做交易备份 给账户增加-50
新的余额将是:-40
EXIT WITH: 回滚的上一条交易
ValueError: 出错提示,无法计入提取金额
最终账户余额 Balance end: 10
总结
我希望你在读完这篇文章后,对dunder方法不再感到恐惧。对它们的战略性使用会使你的类更像Pythonic,因为它们用类似Python的行为来模拟内置类型。
就像任何功能一样,请不要过度使用它。例如,操作符重载可以变得非常晦涩难懂。用+bob或tim << 3给一个人对象添加 "karma",使用dunders肯定是可行的,但可能不是使用这些特殊方法的最明显或最合适的方式。然而,对于像比较和加法这样的普通操作,它们可以成为一种优雅的方法。
展示每一个dunder方法会使教程变得很长。如果你想了解更多关于dunder方法和Python数据模型的信息,我建议你去看看Python参考文档。
另外,一定要看看我们的dunder方法编码挑战,在那里你可以进行实验,把你新发现的 "dunder技能 "付诸实践。
这里是指 "双下划线(下划线)"。这些都是常用的运算符重载。一些神奇方法的例子有
init, add, len, repr等等。
当一个类的实例被创建时,用于初始化的init方法不需要任何调用,就像某些其他
编程语言如C++, Java, C#, PHP等中的构造函数一样。这些方法是我们可以用'+'运算符
添加两个字符串而不需要任何明确的类型转换的原因。
下面是一个简单的实现。
# declare our own string class
class String:
# magic method to initiate object
def __init__(self, string):
self.string = string
# Driver Code
if __name__ == '__main__':
# object creation
string1 = String('Hello')
# print object location
print(string1)
下面两个字符串拼接是不可行的
class String:
# magic method to initiate object
def __init__(self, string):
self.string = string
# print our string object
def __repr__(self):
return 'Object: {}'.format(self.string)
# Driver Code
if __name__ == '__main__':
# object creation
string1 = String('Hello')
# concatenate String object and a string
#print(string1 + ' world')
TypeError: unsupported operand type(s) for +: 'String' and 'str'
declare our own string class
class String:
# magic method to initiate object
def __init__(self, string):
self.string = string
# print our string object
def __repr__(self):
return 'Object: {}'.format(self.string)
def __add__(self, other):
return self.string + other
# Driver Code
if __name__ == '__main__':
# 生成一个对象实例
string1 = String('Hello')
# 拼接string1对象实例和字符串'Geek'
print(string1 + ' Geeks')
Hello Geeks