Python Magic Method

Python 的 Magic Method 就是以双下划线起始和结尾的方法。
为什么要有这些方法呢?我的理解是:
Python 中万物皆对象,所以如果 Python 完全遵循面向对象的原则,那么所有的方法调用应该都是 obj.method(args) 这种形式,但事实上 Python 中有许多的内置函数和操作符,这些内置函数和操作符调用的就是这些 magic method。所以说,magic method的存在是为了让自定义对象能够使用 Python 内置函数和操作符。
我认为 Pythonic 很大一部分指的就是这个。

A Guide to Python's Magic Methods

A Pythonic Card Deck

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])

class Deck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]

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

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

>>> deck = Deck()
>>> len(deck)
52
>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]

Pythonic Vector

# -*- coding: utf8 -*-

from array import array
import reprlib
import math
import numbers
import operator
import functools
import itertools

class Vector:
    """
    >>> Vector([1, 2])
    Vector([1.0, 2.0])
    >>> Vector([2.1, 3.0, 4.2])
    Vector([2.1, 3.0, 4.2])
    >>> Vector(range(10))
    Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
    """
    typecode = 'd'

    # 要求 components 是个能迭代的对象
    def __init__(self, components):
        self._components = array(self.typecode, components)

    # 使 Vector 支持迭代
    # Vector 能迭代的特性使得下面的一些 magic method 定义起来特别方便
    def __iter__(self):
        return iter(self._components)

    def __repr__(self):
        components = reprlib.repr(self._components)
        components = components[components.find('['):-1]
        return 'Vector({})'.format(components)

    # tuple 中的参数要求能迭代
    def __str__(self):
        return str(tuple(self))

    def __bytes__(self):
        return bytes([ord(self.typecode)]) + bytes(self._components)

    @classmethod
    def frombytes(cls, octets):
        typecode = chr(octets[0])
        memv = memoryview(octets[1:]).cast(typecode)
        return cls(memv)

    def __bool__(self):
        return bool(abs(self))

    # 要使 Vector 是一个序列,必须支持 __len__, __getitem__ 方法
    def __len__(self):
        return len(self._components)

    # 如果这样实现的话,如果我们用切片,会发现返回的是 array 而不是 Vector
    # >>> v1 = Vector([3, 4, 5])
    # array('d', [4.0, 5.0])
    # def __getitem__(self, index):
    #     return self._components[index]

    # 用下面这张方法实现能返回 Vector
    # 注意切片时参数 index 是个切片对象
    # v = v = Vector(range(7))
    # >>> v[-1]
    # 6.0
    # >>> v[1:4]
    # Vector([1.0, 2.0, 3.0])
    def __getitem__(self, index):
        cls = type(self)
        if isinstance(index, slice):
            return cls(self._components[index])
        elif isinstance(index, numbers.Integral):
            return self._components[index]
        else:
            msg = "{cls.__name__} indeces must be integers"
            raise TypeError(msg.format(cls=cls))


    # 由于我们想要 Vector immutable, 所以我们不定义 __setitem__

    # 目前我们取得元素只能用 v[0] 这样的形式
    # 我们想用 v.x 这样的形式取得元素
    # >>> v = Vector(range(10))
    # >>> v.x
    # 0.0
    # >>> v.k
    # ... Vector object has no attribute k
    shortcut_names = 'xyz'

    def __getattr__(self, name):
        cls = type(self)
        if len(name) == 1:
            pos = cls.shortcut_names.find(name)
            if 0 <= pos < len(self):
                return self[pos]
        msg = "{cls.__name__} object has no attribute {name}"
        raise AttributeError(msg.format(cls=cls, name=name))


    # 由于 __getattr__ 的调用方式是:
    # 先从对象的属性(包括父类属性)中寻找,找不到时才调用 __getattr__ 方法
    # 所以如果我们动态绑定对象属性,则不会调用 __getattr__ 方法
    # >>> v = Vector(range(10))
    # >>> v.x
    # 0.0
    # >>> v.x = 10
    # >>> v.x
    # 10
    # >>> v
    # Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
    # 我们当然可以用 __slots__ 限制对象属性,
    # 但 __slots__ 的主要目的是节约内存而不是限制属性
    # 在这里我们自定义 __setattr__ 
    def __setattr__(self, name, value):
        cls = type(self)
        if len(name) == 1:
            if name in cls.shortcut_names:
                msg = "readonly attribute {name}"
            elif name.islower():
                msg= "can't set attribute 'a' to 'z' in {cls.__name__}"
            else:
                msg = ''
            if msg:
                raise AttributeError(msg.format(cls=cls, name=name))
        super().__setattr__(name, value)


    # 要使 Vector hashable,必须定义 __eq__ 和 __hash__ 方法
    # 这里 a == b 的调用顺序是:
    # 先调用 a.__eq__(b)
    # 如果返回 NotImplemented,则接着调用 b.__eq__(a) 
    # 如果仍然返回 NotImplemented,则使用两者的id比较值作为最后结果
    def __eq__(self, other):
        if isinstance(other, Vector):
            return len(self) == len(other) and \
                   all(a == b for a, b in zip(self, other))
        else:
            return NotImplemented


    def __hash__(self):
        hashes = (hash(x) for x in self)
        return functools.reduce(operator.xor, hashes, 0)


    # **Operator Overloading**

    # 定义 __neg__ 和 __pos__ 方法
    # 一般这种一元操作符要求返回一个新对象
    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))

    def __neg__(self):
        return Vector(-x for x in self)

    def __pos__(self):
        return Vector(self)

    # __add__ 和 __iadd__
    # 一般来说 __add__ 返回新对象,__iadd__ 修改本身
    # 对一些 immutable 对象,虽然没有定义 __iadd__ 方法,但仍能使用 a += b
    # 这是因为在这种情况下 a += b 是 a = a + b 的语法糖,调用的是 __add__

    # >>> v = Vector([1, 2 ,3])
    # >>> v + Vector([10, 10, 10])
    # Vector([11.0, 12.0, 13.0])
    # >>> v + (10, 10, 10)
    # Vector([11.0, 12.0, 13.0])
    def __add__(self, other):
        try:
            pairs = itertools.zip_longest(self, other, fillvalue=0.0)
            return Vector(a+b for a, b in pairs)
        else TypeError:
            return NotImplemented

    # 现在我们的 Vector 可以与其他可迭代对象相加,但反过来却不行
    # Python 中另外有个所有操作符都有个反方法,比如 __radd__
    def __radd__(self, other):
        return self + other

    def __mul__(self, scalar):
        if isinstance(scalar, numbers.Real):
            return Vector(x * scalar for x in self)
        else:
            return NotImplemented

    def __rmul__(self, scalar):
        return self * scalar

    # 注意上面 __add__ 和 __mul__ 对传入参数类型的检测:
    # __add__ 比较符合 duck typing,这种方式更加灵活
    # __mul__ 对传入参数进行限制,这种方式更加安全
    # 虽然用 isinstance 不符合 OOP,但在操作符覆盖上常常使用
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容