• 流畅的Python-Python数据模型


    Python模型其实就是对Python框架的描述,它规范了这门语言自身构建模型的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器

    Python解释器碰到特殊的语法时,会使用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下划线开题,以两个下划线结尾(例如__getitem__)

    比如obj[key]的背后就是__getitem__方法,为了求得my_collection[key]的值,解释器实际上会调用my_collection.__getitem__(key)方法。 len(obj)的背后就是__len__方法,为了求得my_collection的长度,解释器实际上会调用my_collection.__len__()方法。

    这些特殊方法名能让对象的实现和支持以下的语法构架,并与之交互:

    • 迭代
    • 集合类
    • 属性访问
    • 运算符重载
    • 函数和方法调用
    • 对象的创建和销毁
    • 字符串表示形式和格式化
    • 管理上下文(即with块)

    一摞Python风格的纸牌

    示例: 展示如何实现__getitem__和__len__这两个特殊方法

    import collections
    
    Card = collections.namedtuple('Card', ['rank', 'suit'])
    
    
    class FrenchDeck(object):
        # 牌面
        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, item):
            return self._cards[item]

    说明: 使用collections.nametuple构建一个简单的类来表示一张纸牌,collections.nametuple()方法可以构建只有少数属性但没有方法的对象。

    # Card类的使用
    beer_card = Card('7', 'diamonds')
    print(beer_card)  

    使用len()方法查看一叠牌有多少张

    deck = FrenchDeck()
    print(len(deck))

    从一叠牌中抽取特定的一张牌, 比如说第一张和最后一张

    # 抽取第一张牌
    print(deck[0])
    # 抽取最后一张牌
    print(deck[-1])

    因为__getitem__方法把[]操作交给了self._cards列表,所以deck类自动支持切片操作。

    print(deck[0:3])

    因为类实现了__getitem__方法,所以这一叠牌变成了可迭代对象,

    # 正向迭代
    for card in deck:
        print(card)
    
    # 反向迭代
    for card in reversed(deck):
        print(card)

    如果一个类没有实现__contains__方法,那么in运算符就会按顺序做一次迭代搜索,所以,in运算符可以用在FrenchDeck类上。

    print(Card('Q', 'hearts') in deck)  # True
    print(Card('7', 'bearts') in deck)  # False

    对纸牌类对象进行排序

    suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
    print(suit_values)  # {'spades': 3, 'hearts': 2, 'diamonds': 1, 'clubs': 0}
    
    
    def spades_high(card):
        rank_value = FrenchDeck.ranks.index(card.rank)
        return rank_value * len(suit_values) + suit_values[card.suit]
    
    for card in sorted(deck, key=spades_high):
        print(card)

    说明: 排序主要是使用sorted内置方法,参数key接收排序的函数,suit_values表示花色的权重,排序的方法spades_high接收一张纸牌,然后通过获取纸牌的牌面和花色返回牌面的权值,

    权值的计算公式为: 牌面 * 4 + 花色对应的value

    如何使用特殊方法

    • 特殊方法的存在是为了被Python解释器调用的,程序员并不需要手动调用他们。也就是说没有my_object.__len__()这种写法,而应该使用len(my_object)。在执行len(my_object)的时候,如果my_object是一个自定义类的对象,那么Python会自己调用其中由程序员实现的__len__方法。
    • 然后如果是Python内置的类型,比如列表(list)、字符串(str)、字节序列(bytes)等,那么Cpython会抄个近路,__len__实际上会直接返回PyVarObject里的ob_size属性,PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值会比调用一个方法快得多。
    • 很多时候特殊方法的调用是隐式的,比如for i in x: 这个语句,背后其实用的是iter(x),而这个函数的背后则是x.__iter__()方法,前提是这个方法在x中被实现了。
    • 通常程序无需直接使用特殊方法,除非有大量的元编程存在,直接调用特殊方法的频率应该远远低于去实现它的次数。唯一的例外可能是__init__方法,程序中会经常使用到它,目的是在自己的子类的__init__方法找调用超类的构造器
    • 通过内置的函数(如len、iter、str等等)来使用特殊方法是最好的选择。他们的速度很快。
    • 不要自己想当然的添加特殊方法,因为虽然这个名字没有被Python内部使用,以后就不一定了。

    示例: 实现一个二维向量(Vector)类

    from math import hypot
    
    
    class Vector(object):
    
        def __init__(self, x=0, y=0):
            self.x = x
            self.y = y
    
        def __repr__(self):
            return 'Vector(%r, %r)' % (self.x, self.y)
    
        def __abs__(self):
            return hypot(self.x, self.y)
    
        def __bool__(self):
            return bool(abs(self))
    
        def __add__(self, other):
            x = self.x + other.x
            y = self.y + other.y
            return Vector(x, y)
    
        def __mul__(self, other):
            return Vector(self.x * other, self.y * other)

    向量的加法

    v1 = Vector(2, 4)
    v2 = Vector(2, 1)
    print(v1 + v2)
    
    说明: 两个向量相加,得到的结果也是一个向量,并且"+"运算符调用的其实是Vector.__add__()方法

    向量的模

    v = Vector(3, 4)
    print(abs(v))

    abs是一个内置函数, 如果输入的是整数或者浮点数,它返回的是输入值的绝对值,如果输入的是复数,那么返回这个复数的模

    向量标量乘法

    print(v * 3)
    print(abs(v*3))

     各个特殊方法的说明:

    1. __repr__

    • Python有一个内置的函数叫做repr, 它能把一个对象用字符串的形式表示出来,repr就是通过__repr__这个特殊方法来得到一个对象的字符串表示形式。如果没有实现__repr__, 当我们在控制台打印一个向量的实例时,得到的字符串可能会是对象形式<__main__.Vector object at 0x103bd09d0>
    • 交互式控制台和调试程序用repr函数来获取字符串表示形式,在老的使用%符号的字符串格式中,这个函数返回的结果用来代替%r所代表的对象。(字符串的格式化还可以使用str.format)
    • 在__repr__的实现中,使用%r来获取对象各个属性的标准字符串表示形式,因为Vector('1', '2')和Vector(1, 2)是不一样的
    • __repr__所返回的字符串应该准确、无歧义,并且竟可能表达出如何用代码创建出这个被打印的对象
    • __repr__和__str__的区别在于, 后者是在str()函数被使用,或是在用print()函数打印一个对象时才被调用的,并且他返回的字符串对终端用户更友好。
    • 如果只是想实现这两个特殊放中的一个,__repr__是更好的选择,因为如果一个对象没有__str__函数,而Python又需要调用它的时候,解释器会用__repr__作为代替。

    2. __add__和__mul__

    • 通过__add__和__mul__可以实现+和*这两个算术运算符。这两个方法的返回值都是新创建的对象,被操作的对象还是原封不动的,代码里只是读取他们的结果而已。中缀运算符的基本原则就是不改变操作对象,而是产生一个新值

    3. __bool__

    • 任何对象都可以用于需要布尔值的上下文中(比如if或while预计,或者and、or和not运算符)。为了判断一个值x为真还是为假,Python会调用bool(x),这个函数只能返回True或者False
    • 默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对__bool__或者__len__函数有自己的实现。bool(x)的背后是调用x.__bool__()的结果;如果不存在__bool__方法,那么bool(x)会尝试调用x.__len__()。若返回0, 则bool会返回False, 否则返回True

    特殊方法一览

    跟运算符无关的特殊方法

    类别 方法名
    字符串/字节序列表现形式 __repr__、__str__、__format__、__bytes__
    数值转换 __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__
    集合模拟 __len__、__getitem__、__setitem__、__delitem__、__contains__
    迭代枚举 __iter__、__reversed__、__next__
    可调用模拟 __call__
    上下文管理 __enter__、__exit__
    实例创建和销毁 __new__、__init__、__del__
    属性管理 __getattr__、__getattribute__、__setattr__、__delattr__、__dir__
    属性描述符 __get__、__set__、 __delete__
    跟类相关的访问 __prepare__、__instancecheck__、__subclasscheck__

    跟运算符相关的特殊方法

    类别 方法名和对应的运算符
    一元运算符 __neg__ - 、 __pos__ + 、 __abs__ abs()
    众元比较运算符 __lt__ <、 __le__ <=、 __eq__ == 、 __ne__ !=、 __gt__ > 、__ge__ >=
    算术运算符 __add__ + 、 __sub__ -、 __mull__ *、 __truediv__ / 、 __floordiv__ // 、 __mod__ %、__divmod__ divmode()、__pow__ **或pow()、__round__ round()
    反向算术运算符 __radd__、 __rsub__、 __rmul__、 __rtruediv__、 __rfloordiv__、 __rmod__、__rdivmode__、 __rpow__
    增量赋值算术运算符 __iadd__、__isub__、__imul__、__itruediv__、 __ifloordiv__、 __imod__、__ipow__
    位运算符 __invert__ ~、 __lshift__ << 、 __rshift__ >> 、__and__ & 、 __or__ | 、 __xor__ ^
    反向位运算符 __rlshift__ 、 __rrshift__、 __rand__、 __rxor__、 __ror__
    增量赋值位运算符 __ilshift__、 __irshift__、__iand__、__ixor__、__ior__

    为什么len不是普通方法

    如果x是一个内置类型的实例,那么len(x)的速度会非常快。背后原因是CPython解释器会直接从一个C结构体里读取对象的长度,完全不会调用任何方法。换句话说: len之所以不是一个普通方法,是为了让Python自带的数据结构可以走后门! sbs可是同理,但是多亏了他是特殊方法,我们也可以把len用于自定义数据类型。

    总结:

    • 通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码--或者说,更具Python风格的代码
    • Python对象的一个基本要求就是它得有合理的字符串表示形式,我们可以通过__repr__和__str__来满足这个要求。前者方便我们调试和记录日志。后者则是给终端用户看的。
    • Python通过运算符重载这一模式提供了丰富的数值类型,除了内置的那些以外,还有decimal.Decimal和fractions.Fraction。这些数据类型都支持中缀算术运算符。
  • 相关阅读:
    创建live usb
    gnome2.x面板(panel)或应用程序菜单误删后恢复
    grub & grub2
    linux(CentOS6)下的wifi热点安装配置------hostapd-2.0
    linux(Ubuntu)下的wifi热点安装配置------hostapd-2.0
    BZOJ3884 上帝与集合的正确用法(欧拉函数)
    Luogu4897 【模板】最小割树
    Contest 6
    BZOJ3811 玛里苟斯(线性基+概率期望)
    Contest 5
  • 原文地址:https://www.cnblogs.com/featherwit/p/12960862.html
Copyright © 2020-2023  润新知