zoukankan      html  css  js  c++  java
  • python小记(7)

    运算符重载

    在Python中,如果要实现某种运算,必须要有运算符,这是毫无疑问已经很熟悉的了。但是,这些运算符之所以能够被使用,都是因为有一些特殊方法才得以实现的。以下表格中列出几种常见运算符所对应的特殊方法,供参考。

    二元运算符特殊方法
    + __add____radd__
    - __sub____rsub__
    * __mul__ , __rmul__
    / __div__ , __rdiv____truediv____rtruediv__
    // __floordiv____rfloordiv__
    % __mod____rmod__
    ** __pow____rpow__
    << __lshift____rlshift__
    >> __rshift____rrshift__
    & __and____rand__
    == __eq__
    !=,<> __ne__
    > __get__
    < __lt__
    >= __ge__
    <= __le__

    以“+”为例,不论是实现1 + 2还是'abc' + 'xyz',都是要执行1.__add__(2)或者'abc'.__add__('xyz')操作。也就是两个对象是否能进行加法运算,首先就要看相应的对象是否有__add__()方法(读者不妨在交互模式中使用dir(),看一看整数、字符串是否有__add__()方法),一旦相应的对象有__add__()方法,即使这个对象从数学上不可加,我们都可以用加法的形式,来表达obj.__add__()所定义的操作。在Python中,运算符起到简化书写的功能,但它依靠特殊方法实现。

    所以,在刚才自定义的类Fraction中,为了实现分数加法,我们重写了__add__()方法,也可以称之为运算符重载(对于Python是否支持重载,也是一个争论话题)。

    就这样,我们解决了分数相加的问题。

    但,上述加法还不是很完美,还可以有很多优化的地方,比如分数结果要化成最简分数等等。真正要做好一个分数运算的类,还有很多工作。

    不过,在Python中,其实不用你自己做了,自由高手做好。标准库中就有相应模块解决此问题。

    >>> from fractions import Fraction
    >>> m, n = Fraction(1, 3), Fraction(1, 2)
    >>> m + n
    Fraction(5, 6)
    >>> print m + n        #Python 3: print(m + n)
    5/6
    >>> a, b = Fraction(1, 3), Fraction(1, 6)
    >>> print a + b        #Python 3:  print(a + b)
    1/2
    

    Python的魅力之一,就是它强大的标准库和第三方库,让你省心省力。

    优化内存的__slots__

    首先声明,__slots__能够限制属性的定义,但是这不是它存在终极目标,它存在的终极目标更应该是一个在编程中非常重要的方面:优化内存使用。

    >>> class Spring(object):
    ...     __slots__ = ("tree", "flower")
    ... 
    >>> dir(Spring)
    ['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', 'flower', 'tree']
    

    仔细看看dir()的结果,还有__dict__属性吗?没有了,的确没有了。也就是说__slots____dict__挤出去了,它进入了类的属性。

    >>> Spring.__slots__
    ('tree', 'flower')
    

    这里可以看出,类Spring有且仅有两个属性。

    >>> t = Spring()
    >>> t.__slots__
    ('tree', 'flower')
    

    实例化之后,实例的__slots__与类的完全一样,这跟前面的__dict__大不一样了。

    >>> Spring.tree = "liushu"
    

    通过类,先赋予一个属性值。然后,检验一下实例能否修改这个属性:

    >>> t.tree = "guangyulan"
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Spring' object attribute 'tree' is read-only
    

    看来,我们的意图不能达成,报错信息中显示,tree这个属性是只读的,不能修改了。

    >>> t.tree
    'liushu'
    

    因为前面已经通过类给这个属性赋值了。不能用实例属性来修改。只能:

    >>> Spring.tree = "guangyulan"
    >>> t.tree
    'guangyulan'
    

    用类属性修改。但是对于没有用类属性赋值的,可以通过实例属性赋值。

    >>> t.flower = "haitanghua"
    >>> t.flower
    'haitanghua'
    

    但此时:

    >>> Spring.flower
    <member 'flower' of 'Spring' objects>
    

    实例属性的值并没有传回到类属性,你也可以理解为新建立了一个同名的实例属性。如果再给类属性赋值,那么就会这样了:

    >>> Spring.flower = "ziteng"
    >>> t.flower
    'ziteng'
    

    当然,此时在给t.flower重新赋值,就会爆出跟前面一样的错误了。

    >>> t.water = "green"
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Spring' object has no attribute 'water'
    

    这里试图给实例新增一个属性,也失败了。

    看来__slots__已经把实例属性牢牢地管控了起来,但更本质是的是优化了内存。诚然,这种优化会在大量的实例时候显出效果。

    书接上回,不管是实例还是类,都用__dict__来存储属性和方法,可以笼统地把属性和方法称为成员或者特性,一句话概括,就是__dict__存储对象成员。但,有时候访问的对象成员没有存在其中,就是这样:

    >>> class A(object):
    ...     pass
    ... 
    >>> a = A()
    >>> a.x
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'A' object has no attribute 'x'
    

    x不是实例的成员,用a.x访问,就出错了,并且错误提示中报告了原因:“'A' object has no attribute 'x'”

    在很多情况下,这种报错是足够的了。但是,在某种我现在还说不出的情况下,你或许不希望这样报错,或许希望能够有某种别的提示、操作等。也就是我们更希望能在成员不存在的时候有所作为,不是等着报错。

    要处理类似的问题,就要用到本节中的知识了。

    属性拦截

    有时候,访问某个类或者实例属性,它不存在,就会异常。对于异常,总是要处理的。就好像“寻隐者不遇”,却被童子“遥指杏花村”,将你“拦截”了,不至于因为“不遇”而垂头丧气。

    在Python中,有一些方法就具有这种“拦截”能力。

    • __setattr__(self, name,value):如果要给name赋值,就调用这个方法。
    • __getattr__(self, name):如果name被访问,同时它不存在的时候,此方法被调用。
    • __getattribute__(self, name):当name被访问时自动被调用(注意:这个仅能用于新式类),无论name是否存在,都要被调用。
    • __delattr__(self, name):如果要删除name,这个方法就被调用。

    用例子说明。

    >>> class A(object):        #Python 3: class A:
    ...     def __getattr__(self, name):
    ...         print "You use getattr"        #Python 3: print("You use getattr"),下同,从略
    ...     def __setattr__(self, name, value):
    ...         print "You use setattr"
    ...         self.__dict__[name] = value
    ... 
    

    A除了两个方法,没有别的了。

    >>> a = A()
    >>> a.x
    You use getattr
    

    a.x这个实例属性,本来是不存在的,但是,由于类中有了__getattr__(self, name)方法,当发现属性x不存在于对象的__dict__中的时候,就调用了__getattr__,即所谓“拦截成员”。

    >>> a.x = 7
    You use setattr
    

    给对象的属性赋值时候,调用了__setattr__(self, name, value)方法,这个方法中有一句self.__dict__[name] = value,通过这个语句,就将属性和数据保存到了对象的__dict__中,如果再调用这个属性:

    >>> a.x
    7
    

    它已经存在于对象的__dict__之中。

    在上面的类中,当然可以使用__getattribute__(self, name),并且,只要访问属性就会调用它。例如:

    >>> class B(object):
    ...     def __getattribute__(self, name):
    ...         print "you are useing getattribute"
    ...         return object.__getattribute__(self, name)
    

    为了与前面的类区分,新命名一个类名字。需要提醒注意,在这里返回的内容用的是return object.__getattribute__(self, name),而没有使用return self.__dict__[name]样式。因为如果用return self.__dict__[name]这样的方式,就是访问self.__dict__,只要访问这个属性,就要调用__getattribute__`,这样就导致了无线递归下去(死循环)。要避免之。

    >>> b = B()
    >>> b.y
    you are useing getattribute
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in __getattribute__
    AttributeError: 'B' object has no attribute 'y'
    >>> b.two
    you are useing getattribute
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in __getattribute__
    AttributeError: 'B' object has no attribute 'two'
    

    访问不存在的成员,可以看到,已经被__getattribute__拦截了,虽然最后还是要报错的。

    >>> b.y = 8
    >>> b.y
    you are useing getattribute
    8
    

    当给其赋值后,意味着已经在__dict__里面了,再调用,依然被拦截,但是由于已经在__dict__内,会把结果返回。

    当你看到这里,是不是觉得上面的方法有点魔力呢?不错,的确是“黑魔法”。但是,它有什么具体应用呢?看下面的例子,能给你带来启发。

    #!/usr/bin/env python
    # coding=utf-8
    
    """
    study __getattr__ and __setattr__
    """
    
    class Rectangle(object):        #Python 3: class Rectangle:
        """
        the width and length of Rectangle
        """
        def __init__(self):
            self.width = 0
            self.length = 0
    
        def setSize(self, size):
            self.width, self.length = size
        def getSize(self):
            return self.width, self.length
    
    if __name__ == "__main__":
        r = Rectangle()
        r.width = 3
        r.length = 4
        print r.getSize()        #Python 3: print(r.getSize())
        r.setSize( (30, 40) )
        print r.width              #Python 3: print(r.width)
        print r.length            #Python 3: print(r.length)
    

    上面代码来自《Beginning Python:From Novice to Professional,Second Edittion》(by Magnus Lie Hetland),根据本教程的需要,稍作修改。

    $ python 21301.py 
    (3, 4)
    30
    40
    

    这段代码已经可以正确运行了。但是,作为一个精益求精的程序员。总觉得那种调用方式还有可以改进的空间。比如,要给长宽赋值的时候,必须赋予一个元组,里面包含长和宽。这个能不能改进一下呢?

    #!/usr/bin/env python
    # coding=utf-8
    
    """
    study __getattr__ and __setattr__
    """
    
    class Rectangle(object):          #Python 3: class Rectangle:
        """
        the width and length of Rectangle
        """
        def __init__(self):
            self.width = 0
            self.length = 0
    
        def setSize(self, size):
            self.width, self.length = size
        def getSize(self):
            return self.width, self.length
    
        size = property(getSize, setSize)
    
    if __name__ == "__main__":
        r = Rectangle()
        r.width = 3
        r.length = 4
        print r.size
        r.size = 30, 40
        print r.width
        print r.length
    

    以上代码的运行结果同上。但是,因为加了一句size = property(getSize, setSize),使得调用方法是不是更优雅了呢?原来用r.getSize(),现在使用r.size,就好像调用一个属性一样。难道你不觉得眼熟吗?在《多态和封装》中已经用到过property函数了,虽然写法略有差别,但是作用一样。

    本来,这样就已经足够了。但是,因为本节中出来了特殊方法,所以,一定要用这些特殊方法从新演绎一下这段程序。虽然重新演绎的不一定比原来的好,主要目的是演示本节的特殊方法应用。

    #!/usr/bin/env python
    # coding=utf-8
    
    class NewRectangle(object):
        def __init__(self):
            self.width = 0
            self.length = 0
    
        def __setattr__(self, name, value):
            if name == "size":
                self.width, self.length = value
            else:
                self.__dict__[name] = value
    
        def __getattr__(self, name):
            if name == "size":
                return self.width, self.length
            else:
                raise AttributeError
    
    if __name__ == "__main__":
        r = NewRectangle()
        r.width = 3
        r.length = 4
        print r.size        #Python 3: print(r.size)
        r.size = 30, 40
        print r.width        #Python 3: print(r.width)
        print r.length       #Python 3: print(r.length)
    

    除了类的样式变化之外,调用样式没有变。结果是一样的。

    如果要对于这种黑魔法有更深的理解,可以阅读:Python Attributes and Methods,读了这篇文章,对Python的对象属性和方法会有更深入的理解。

    至此,是否注意到,我们使用了很多以双下划线开头和结尾的方法或者属性,比如__dict____init__()等。在Python中,用这种方法表示特殊的方法和属性,当然,这是一个惯例,之所以这样做,主要是确保这些特殊的名字不会跟你自己所定义的名称冲突,我们自己定义名称的时候,是绝少用双划线开头和结尾的。如果你需要重写这些方法,当然是可以的。

  • 相关阅读:
    无题
    【HNOI 2002 】营业额统计
    P1589
    【网络流24题】最长递增子序列
    【NOI2008】志愿者招募
    【NOI2015】软件包管理器
    P1347
    【BZOJ 3262三维偏序】陌上花开
    数论六·模线性方程组
    数论五·欧拉函数
  • 原文地址:https://www.cnblogs.com/liqiantu/p/5794731.html
Copyright © 2011-2022 走看看