Python 5.4 定制类

发布时间:2019-07-06 10:47:13编辑:auto阅读(1663)

    定制类


    看到类似的__slots__这种形如__xx__的变量或者函数名就要注意,这些在Python中有特殊用途。

    Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。


    __str__

    我们先定义一个Student类,打印一个实例:
    >>>class Student(object):
        def __init__(self,name):
            self.name =name

    >>>print(Student('Michael'))

    <__main__.Student object at 0x109afb190>

    打印<__main__.Student object at 0x109afb190>不好看,怎么才能好看呢?

    只需要定义好__str__()方法, 返回一个好看的字符串就可以了。

    >>>class Student(object):

        def __init__(self,name):
            self.name=name

       def __str__(self):
            return 'Student object (name: %s)' % self.name

    >>>print(Student('Michael'))

    Studnet object (name: Michael)

    但是直接敲命令不使用print,打印出来的实例还是不好看:

    >>>s =Student('Michael')

    >>>s

    <__main__.Student object at 0x109afb190>

    这是因为直接显示变量调用的不是__str__(),而是__repr__(),两者区别是__str__返回的是用户看到的字符串,__repr__返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

    解决办法是在定义一个__repr__()。但通常__str__() __repr__()内容相同。所以有个偷懒的写法:

    >>>class Student(object):

        def __init__(self,name):
            self.name=name

       def __str__(self):
            return 'Student object (name: %s)' % self.name

       __repr__ = __str__


    __iter__

    如果一个类想被用于for ...in循环,类似list或者tuple那样,就必须实现一个__iter__()方法。该方法返回一个迭代对象,然后for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

    我们以斐波那契数列为例写一个Fib,可以作用于for循环:
    class Fib(object):
        def __init__(self):
            self.a,self.b = 0,1

        def __iter__(self):
            return self

        def __next__(self):
            self.a,self.b =self.b,self.a + self.b

           if self.a >10000:

                raise StopIteration()

            return self.a

    >>>for n in Fib():
        print(n)


    __getitem__ 

    Fib实例虽然能作用于for循环,看起来和list有点像,但是把它当list还是不行的。比如,取第五个元素:
    >>>fib()[5]

    Traceback (most recent call last):

     File "<stdin>", line 1, in <module>

    TypeError: 'Fib' object does not support indexing

    要表现得像list那样按照下标读取元素,需要实现__getitem__()方法:
    class Fib(object):

        def __getitem__(self,n):
             a,b, =1,1

            for x in range(n):
                a,b =b,a + b

            return a 

    现在,就可以像list那样按下标访问数列的任一项了:
    >>>Fib()[0]

    1

    >>>Fib()[1]

    1

    >>>Fib()[2]

    2

    >>>Fib()(10)

    89

    但是list有个神奇的切片方法:
    >>>list(range(100))[5:10]

    [5,6,7,8,9]

    对于Fib却报错。原因是__getitem__()传入的参数可能是一个int,亦可能是一个切片对象slice,所以要做判断:
    class Fib(object):
        def __getitem__(self,n):

            if isinstance(n,int):

                a,b =1,1

                 for x in range(n):
                    a,b =b,a +b

                return a

             if isinstane(n,slice):
                start =n.start

               stop =n.stop

                if start is None:
                    start =0

                a,b =1,1

               L =[]

               for x in range(stop):
                  if x >=start:
                   L.append(a)
                  a,b =b,a + b

               return L

    现在试试Fib的切片。

    >>>f =Fib()

    >>>f[0:5]

    [1,1,2,3,5]

    >>>f[:10]

    [1,1,2,3,5,8,13,21,34,55]

    但是没有对step参数做处理:
    >>>f[:10:2]

    [1,1,2,3,5,8,13,21,34,55,89]

    也没有对负数做处理,所以,要实现一个__getitem__()函数有很多工作要做。

    此外,如果把对象看成dict,__getitem__()的参数也可能是一个可以作key的object如str。

    与之对应的是__setitem__()方法,把对象看作list或dict来对集合赋值。最后还有一个__delitem__()方法,用于删除某个元素。

    总之,通过上面的方法,我们自定义的类和Python自带的list、tuple、dict没什么区别。这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。


    __getattr__


    正常情况下,当我们调用类的方法或者属性时,如果不存在,就会报错。

    要避免这个错误,就写一个__getattr__()动态返回一个属性。修改如下:
    class Student(object):
        def __init__(self):
            self.name ='Michael'


        def __getattr__(self,attr):
            if attr == 'score':

                return 99

    当调用属性不存在时,比如score,Python解释器会试图调用__getattr__(self,'score')来尝试获得属性,这样,我们就有机会返回score的值:
    >>>s =Student()

    >>>s.name

    Michael

    >>>s.socre

    99

    返回函数也是可以得:

    class Student(object):
        def __getattr__(self,attr):
            if attr =='age':

                return lambda:25

    调用方式改为:
    >>>s.age()

    25

    注意,只有在没有找到属性的情况下,才会调用__getattr__,已有的属性,比如name,不会在__getattr__中查找。

    class Student(object):

       def __getattr__(self, attr):

           if attr=='age':            

               return lambda: 25

           raise AttributeError('\'Student\' object has no attribute \'%s\'' % attr)

    这实际上是把一个类的所有属性和方法全能动态处理了。不需要任何特殊手段。

    这种完全动态调用的特性有什么好处呢?可以针对完全动态的情况调用。

    举例,现在好多网站都搞REST API,调用API的URL类似:

    如果要写SDK,给每个API写一个方法,不现实。而且,API一改,SDK也需要改。

    利用完全动态__getattr__,我们可以写出一个链式调用:
    class Chain(object):
        def __init__(self,path =''):
            self.path =path

        def __getattr__(self,path):

            return Chain('%s %s' % (self.path,path))

        def __str__(self):
            return self.path

       __repr__ = __str__

    试试:
    >>>Chain().status.user.timeline.list

    '/status/user/timeline/list'

    这样,无论API怎样变,SDK都可以根据URL实现完全动态的调用,而且,不随API的增加而改变。

    还有些REST API会把参数放到URL中,比如GitHub的API:
    GET /users/:user/repos

    调用时,需要把:user替换为实际用户名。如果我们 能写出这样的链式调用:
    Chain().users('Michael').repos

    就可以非常方便的调用API了。有兴趣的同学可以试试写出来。

      

    __call__

    一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能在实例本身上调用方法呢?在Python中答案是肯定的。

    任何类,只需定义一个__call__()方法,就直接可以对实例进行调用。

    class Student(object):
        def __init__(self,name):

            self.name =name

        def __call__(self):

            print('My name is &s' % self.name)

    调用方式如下:
    >>>s =Student('Michael')

    >>>s()

    My name is Michael

    __call__()还可以定义参数。对实例进行直接调用就好比对一个函数调用一样,所以,完全可以把对象看成函数,把函数看成 对象,这两者之间本来就没什么区别。

    如果你把对象看成函数,那么函数本身其实也可以在运行期间动态创建出来,因为类的实例都是运行期间创建的,这么一来,我们就模糊了对象和函数的界限。

    >>> callable(Student())

    True

    >>> callable(max)

    True

    >>> callable([1, 2, 3])

    False

    >>> callable(None)
    False
    >>> callable('str')

    False

    通过callable()函数,我们就可以判断一个对象是否是“可调用”。


    小结:

    Python的class允许定义许多定制方法,可以让我们非常方便的生成特制类。


关键字