Python 类的魔术方法(定制类)

今天一整个上午学习了一下Python中的双下划线方法(魔术方法)。这些方法的功能非常强大,也能使Python代码可读性更高,更加Pythonic。这篇文章包含了很多魔术方法,包括:

  • __init__
  • __str__, __repr__
  • __iter__, __getitem__, __len__
  • __eq__, __lt__

  • __add__, __radd__

  • __call__
  • __enter__, __exit__

运行环境:Python3.6 + Jupyter notebook。

下面就是 Jupyter notebook 笔记。

 

Python 类的魔术方法(定制类)

 

__init__

In [3]:
class Account:
    'A simple account class'
    
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
In [4]:
acc1 = Account('zxzhu')
acc1
Out[4]:
<__main__.Account at 0x1e4581f96a0>
 

注:构造函数使我们可以从类中创建实例。

 

__str__, __repr__

In [5]:
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
In [7]:
acc = Account('bob', 1000)
In [8]:
print(acc)
 
Account of bob with starting amount: 1000
In [9]:
acc
Out[9]:
<__main__.Account at 0x1e4581f9cf8>
 

直接显示变量调用的不是 __str__(),而是 __repr__(),前者是给用户看的,后者则是为调试服务的。

In [10]:
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
In [11]:
acc = Account('Bob', 1000)
In [12]:
acc
Out[12]:
Account('Bob', 1000)
In [13]:
print(acc)
 
Account of Bob with starting amount: 1000
 

__iter__, __getitem__, __len__

 

我们先给 Account 类实现一个交易函数和一个查看交易余额的属性。

In [88]:
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
In [89]:
acc = Account('Bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
In [90]:
acc.balance
Out[90]:
80
 

@property 是一个 decorator,具体细节查看 @property

 

接下来,我们要实现以下功能:

  • 查看交易次数
  • 查看每次交易的金额
In [91]:
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
In [92]:
acc = Account('Bob', 10)
acc.add_transaction(20)
acc.add_transaction(-10)
acc.add_transaction(50)
acc.add_transaction(-20)
acc.add_transaction(30)
In [93]:
for i in acc:
    print(i)
 
20
-10
50
-20
30
In [94]:
len(acc)
Out[94]:
5
In [95]:
acc[::-1]
Out[95]:
[30, -20, 50, -10, 20]
In [96]:
acc[::2]
Out[96]:
[20, 50, 30]
 

以上,我们就可以利用len() 函数来查看交易次数,利用切片或者迭代来查看每次交易金额。

 

__eq__, __lt__

 

接下来我们进行运算符重载,使不同的 Account 类相互之间可以进行比较,我们比较账户余额。

 

为了方便,我们实现 __eq__ 和 __lt__方法,然后利用 functools.total_ordering 这个 decorator 来完善其他的比较运算。

In [97]:
from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
        
    def __eq__(self,other):
        return self.balance == other.balance
    
    def __lt__(self,other):
        return self.balance < other.balance
In [98]:
acc1 = Account('bob')
acc2 = Account('tim', 100)
print(acc1.balance)
print(acc2.balance)
 
0
100
In [99]:
acc1 > acc2
Out[99]:
False
In [100]:
acc1 < acc2
Out[100]:
True
 

__add__, __radd__

 

下面我们实现账户合并,具体实现:

In [101]:
from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
        
    def __eq__(self,other):
        return self.balance == other.balance
    
    def __lt__(self,other):
        return self.balance < other.balance
    
    def __add__(self,other):    # 合并账户
        owner = self.owner + '&' + other.owner
        amount = self.amount + other.amount
        new_acc = Account(owner,amount)
        for transaction in self._transactions + other._transactions:
            new_acc.add_transaction(transaction)
        return new_acc
In [102]:
acc1 = Account('bob', 0)
acc2 = Account('tim', 100)
acc3 = Account('james', 200)
In [103]:
acc1 + acc2
Out[103]:
Account('bob&tim', 100)
In [104]:
acc2 + acc3
Out[104]:
Account('tim&james', 300)
 

acc1 + acc2 相当于 acc1.__add__(acc2)

 

我们尝试一下:

In [105]:
sum([acc1, acc2, acc3])
 
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-105-0fad3caba539> in <module>()
----> 1sum([acc1, acc2, acc3])

TypeError: unsupported operand type(s) for +: 'int' and 'Account'
 

报错:'int' 和 'Account' 两种不同类型不能相加。这是由于 sum() 从0开始执行:

0.__add__(acc1)

0的 __add__方法当然不可能和 acc1 相加,因此,Python会尝试调用:

acc1.__radd__(0)

所以,我们接下来要实现这个方法。

In [106]:
from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
        
    def __eq__(self,other):
        return self.balance == other.balance
    
    def __lt__(self,other):
        return self.balance < other.balance
    
    def __add__(self,other):    # 合并账户
        owner = self.owner + '&' + other.owner
        amount = self.amount + other.amount
        new_acc = Account(owner,amount)
        for transaction in self._transactions + other._transactions:
            new_acc.add_transaction(transaction)
        return new_acc
    
    def __radd__(self, other):
        if other == 0:
            return self
        else:
            return self.__add__(other)
In [107]:
acc1 = Account('bob', 0)
acc2 = Account('tim', 100)
acc3 = Account('james', 200)
In [108]:
sum([acc1, acc2, acc3])
Out[108]:
Account('bob&tim&james', 300)
 

关于 __add__ 和 __radd__ 具体使用可以看看这里

 

__call__

 

接下来我们实现实例本身的调用,即像调用函数一样调用实例。

In [111]:
from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
        
    def __eq__(self,other):
        return self.balance == other.balance
    
    def __lt__(self,other):
        return self.balance < other.balance
    
    def __add__(self,other):    # 合并账户
        owner = self.owner + '&' + other.owner
        amount = self.amount + other.amount
        new_acc = Account(owner,amount)
        for transaction in self._transactions + other._transactions:
            new_acc.add_transaction(transaction)
        return new_acc
    
    def __radd__(self, other):
        if other == 0:
            return self
        else:
            return self.__add__(other)
        
    def __call__(self):
        print('Start amount: {}'.format(self.amount))
        print('Transactions: ')
        for transaction in self:
            print(transaction,end=' ')
        print('\nBalance: {}'.format(self.balance))
In [112]:
acc1 = Account('bob', 10)
acc1.add_transaction(20)
acc1.add_transaction(-10)
acc1.add_transaction(50)
acc1.add_transaction(-20)
acc1.add_transaction(30)
acc1()
 
Start amount: 10
Transactions: 
20 -10 50 -20 30 
Balance: 80
 

__enter__, __exit__

 

最后我们实现上下文管理器。

写代码时,我们希望把一些操作放到一个代码块中,这样在代码块中执行时就可以保持在某种运行状态,而当离开该代码块时就执行另一个操作,结束当前状态;所以,简单来说,上下文管理器的目的就是规定对象的使用范围,如果超出范围就采取“处理”。这一功能是在Python2.5之后引进的,它的优势在于可以使得你的代码更具可读性,且不容易出错。

详细内容可以参考:

Python学习笔记(五)-- 上下文管理器(Context Manager)

what is a “context manager” in Python?

Python __exit__,__enter__函数with语句的组合应用

In [113]:
from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):        
        self.owner = owner
        self.amount = amount
        self._transactions = []
        self.__i = -1    #迭代索引
    
    def __str__(self):
        return '{} of {} with starting amount: {}'.format(self.__class__.__name__,
            self.owner, self.amount)
    
    def __repr__(self):
        return 'Account({!r}, {!r})'.format(self.owner, self.amount)
    
    def add_transaction(self, value):
        if not isinstance(value,int):
            raise ValueError('please use int for amount')
        self._transactions.append(value)
    
    @property
    def balance(self):
        return self.amount + sum(self._transactions)
    
    def __len__(self):
        return len(self._transactions)
    
    def __iter__(self):    
        return self    #实现迭代实例自身
    
    def __next__(self):
        self.__i += 1
        if self.__i >= len(self._transactions):
            raise StopIteration    #迭代结束
        return self._transactions[self.__i]
    
    def __getitem__(self,n):
        if isinstance(n, int):    #传入索引
            return self._transactions[n]
        if isinstance(n, slice):    # 传入切片对象
            start = n.start
            end = n.stop
            step = n.step
            return self._transactions[start:end:step]
        
    def __eq__(self,other):
        return self.balance == other.balance
    
    def __lt__(self,other):
        return self.balance < other.balance
    
    def __add__(self,other):    # 合并账户
        owner = self.owner + '&' + other.owner
        amount = self.amount + other.amount
        new_acc = Account(owner,amount)
        for transaction in self._transactions + other._transactions:
            new_acc.add_transaction(transaction)
        return new_acc
    
    def __radd__(self, other):
        if other == 0:
            return self
        else:
            return self.__add__(other)
    
    def __call__(self):
        print('Start amount: {}'.format(self.amount))
        print('Transactions: ')
        for transaction in self:
            print(transaction,end=' ')
        print('\nBalance: {}'.format(self.balance))
    
    def __enter__(self):    #进入上下文管理器
        print('ENTER WITH: making backup of transactions for rollback')
        self._copy_transactions = self._transactions.copy()    # 备份交易
        return self    #返回实例自身给 with 后的对象
    
    def __exit__(self, exc_type, exc_value, exc_traceback):    #退出上下文管理器
        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_value))    #给出异常信息
        else:
            print('transaction ok')    #代码块未抛出异常,交易成功
In [114]:
acc4 = Account('sue', 10)
amount_to_add = 20    #进账20
print('\nBalance start: {}'.format(acc4.balance))
with acc4 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 cnnot go in debt!')

print('\nBlance 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

Blance end: 30
 

上面是交易成功的情况,下面展示余额不足交易失败的情况:

In [115]:
acc4 = Account('sue', 10)
amount_to_add = -40    #支出40
print('\nBalance start: {}'.format(acc4.balance))
try:
    with acc4 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!')
except ValueError:
    pass
print('\nBlance end: {}'.format(acc4.balance))    #交易结束
 
Balance start: 10
ENTER WITH: making backup of transactions for rollback
adding -40 to account
new balance would be -30
EXIT WITH: rolling back to previous transactions
transaction resulted in ValueError (sorry cannot go in debt!)

Blance end: 10
 

余额不足时抛出异常,异常传入__exit__,我们就可以恢复到交易前状态。

posted @ 2018-02-22 15:09  orange1002  阅读(503)  评论(1编辑  收藏  举报