Python魔法方法入门


纸上得来终觉浅,绝知此事要躬行。

Python魔法方法入门


引子

Python 中所有的魔术方法均在 Python 官方文档中有相应描述,但是对于他们的描述比较混乱而且组织比较松散,我决定给 Python 中的魔术方法提供一些用平淡的语言和实例驱使的文档。

Python语言中存在一系列特殊的方法可以增强class的效果,它们在Python界中被称为**Dunder Methods,是double under methods的缩写,即双下划线的方法**。如__init____str__等,都是我们日常编程中常见的方法了。

当我们创建一个没有任何行为和动作的class类时,会发现其创建的实例是没有任何属性和方法让我们去操作和使用的。

class NoLenSupport:
    pass
>>> obj = NoLenSupport()
>>> len(obj)
TypeError: "object of type 'NoLenSupport' has no len()"

为了能够使用到len方法,我们只能实现其__len__魔术方法。之后我们在使用len方法的时候,其实就是在使用我们自己实现的__len__方法。

class LenSupport:
    def __len__(self):
        return 42
>>> obj = LenSupport()
>>> len(obj)
42

1. 对象初始化: __init__

写一个class类的时候,如果我们需要在创建实例对象的时候就给其进行传递,这时就需要使用__init__来对对象初始化了。

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 = []

之后,我们就可以通过进行实例化操作了。

>>> acc = Account('bob')
>>> acc = Account('bob', 10)

2. 对象可视化: __str____repr__

我们日常写代码的时候,通常打印出来的对象是没有任何可读性的,很不容易帮助我们识别它们的用途,所以就需要我们在写class类的时候,实现__str____repr__方法了。

其中__repr__方法,通常被称为官方字符串方法,就是其的目录是更接近于程序的可读性考虑的。而__str__方法,则为非官方的字符串方法,对人的可读性很好。

class Account:
    def __init__(self, owner, amount=0):
        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)
>>> str(acc)
'Account of bob with starting amount: 10'

>>> print(acc)
"Account of bob with starting amount: 10"

>>> repr(acc)
"Account('bob', 10)"

3. 对象可哈希: __hash__

我们在调用hash时,实际上是解释器会自行调用该对象的__hash__方法。hash方法常用在判断实例之后的对象,是否为同一个。

# 100个Student对象,如果他们的姓名和性别相同,则认为是同一个人
# 先根据hash值进行判断是否相同,之后再进行判断是否是同一个对象
# 根据set的这个特性,所以这里我们重写了__eq__和__hash__方法

class Student:
    def __init__(self, name, age, sex):
        self.name = name
        self.age = age
        self.sex = sex

    def __eq__(self, other):
        if self.name is other.name and self.sex is other.sex:
            return True
        return False

    def __hash__(self):
        return hash(self.name+self.sex)
In [1]: set_ = set()

In [2]: for i in range(100):
   ...: stu = Student('escape', 18, 'N')
   ...: set_.add(stu)
   ...:

In [3]: print(set_)

4. 对象可迭代: __len____getitem____reversed__

为了能够遍历可迭代对象,我们就需要给class类添加一些方法,是其能够支持对象的迭代。之后,我们会学到@property这个装饰器的。

class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)
>>> 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

当我们创建好实例之后,想知道该实例对象的长度、类型等,却发现无法查看。

>>> len(acc)
TypeError

>>> for t in acc:
...    print(t)
TypeError

>>> acc[1]
TypeError

当我们想知道该实例对象的长度,就需要自行实现__len__方法。之后我们就能通过len方法,来查看其长度了。

我们想使用切片操作和循环的时候,就需要自行实现__getitem__方法。之后我们创建出的对象就可以支持切片操作了,如obj[start:stop]等。

当然,我们还可以实现__reversed__方法,来给其进行逆序。所以魔法方法其实就是那些以双下划线开始和结束的特殊方法。

class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)

    def __len__(self):
        return len(self._transactions)

    def __getitem__(self, position):
        return self._transactions[position]

    def __reversed__(self):
        return self[::-1]
>>> len(acc)
5

>>> for t in acc:
...    print(t)
20
-10
50
-20
30

>>> acc[1]
-10

>>> list(reversed(acc))
[30, -20, 50, -10, 20]

5. 比较运算符: __eq____lt__

有时候,我们需要比较两个实例对象的大小,这时就需要对比较运算符进行重载,从而实现两个实例对象的大小比较了。

我们经常使用Python交互式界面进行大小比较,但我们有没有想过为什么它们可以比较呢?然而,我们自己的写的类创建的实例对象却不能进行比较呢?

>>> 2 > 1
True

>>> 'a' > 'b'
False
>>> 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'"

究其原因,它们都是Python解释器自行实现了对应的函数方法,所以对我们来说是无感知的,而且我们都只是使用而已。这里,我们可以通过使用内建的dir方法进行查看。

>>> dir('a')
['__add__',
...
'__eq__',    <---------------
'__format__',
'__ge__',    <---------------
'__getattribute__',
'__getitem__',
'__getnewargs__',
'__gt__',    <---------------
...]

这里,我们以__eq____lt__为例进行说明。

from functools import total_ordering

@total_ordering
class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)

    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

6. 算数运算符: __add__

上面刚说了比较运算符,我们自然会想到算数运算符,如*/+-等操作。当然在Python交互式界面进行上述操作是没有任何问题的,但是如果我们想让自己写的class类也具有这些方法呢?

>>> 1 + 2
3

>>> 'hello' + ' world'
'hello world'
>>> acc + acc2
TypeError: "unsupported operand type(s) for +: 'Account' and 'Account'"

是的,万变不离其宗,这也是因为Python解释器自行实现了对应的函数方法。如下所示,我们可以看到__add____radd__方法。

>>> dir(1)
[...
'__add__',
...
'__radd__',
...]

我们只需要实现对应的魔术方法即可,这里以__add__为例。

class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)

    def __add__(self, other):
        owner = self.owner + other.owner
        start_amount = self.balance + other.balance
        return Account(owner, start_amount)
>>> acc3 = acc2 + acc
>>> acc3
Account('tim&bob', 110)

>>> acc3.amount
110
>>> acc3.balance
240
>>> acc3._transactions
[20, 40, 20, -10, 50, -20, 30]

7. 可调用对象: __call__

如果让一个普通的对象变成可调用的话,只需要在class类中添加__call__方法,就可以实现了。

class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)

    def __call__(self):
        print('Start amount: {}'.format(self.amount))
        print('Transactions: ')
        for transaction in self:
            print(transaction)
        print('\nBalance: {}'.format(self.balance))

可以使用acc()进行调用类中的__call__方法了。我们需要知道,可调用对象常用在装饰器中。

>>> 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: 8

8. 上下文管理: __enter____exit__

Python有一个更为先进的理念,那就是上下文管理。其实上下文管理是一个简单的协议或者说是接口,可以通过添加__enter____exit__方法来实现。

当我们进入类的时候,程序自动调用__enter__中的方法;当我们出来的时候,程序自动调用__exit__中的方法,从而实现上下文的管理。

class Account:
    def __init__(self, owner, amount=0):
        self.owner = owner
        self.amount = amount
        self._transactions = []

    def add_transaction(self, amount):
        if not isinstance(amount, int):
            raise ValueError('please use int for amount')
        self._transactions.append(amount)

    @property
    def balance(self):
        return self.amount + sum(self._transactions)

    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')

实例说明__enter__方法的效果。

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!')
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

实例说明__enter__方法的效果。

acc4 = Account('sue', 10)

print('\nBalance start: {}'.format(acc4.balance))
try:
    validate_transaction(acc4, -50)
except ValueError as exc:
    print(exc)

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

总结

希望通过阅读这篇文章,能够让各位了解到**Dunder Methods**的好处。如果我们合理的使用这些魔术方法,可以让我们写的class类方法更加出色,虽然通常情况下,可以不加修改直接使用解释器自带的方法。


文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录