美文网首页
Python中面向对象你应该知道的事

Python中面向对象你应该知道的事

作者: 阳仔_1f0c | 来源:发表于2019-08-21 09:47 被阅读0次

    关于我
    一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。
    Github:https://github.com/hylinux1024
    微信公众号:终身开发者(angrycode)

    0x00 is==

    ==运算符是比较两个对象的内容是否相等,默认情况是调用对象的__eq__方法进行比较;而is是比较两个对象是否一样,它比较的两个对象的id,即它们的内存地址是否相同。

    >>> a = [1,2,3]
    >>> b = [1,2,3]
    >>> a == b
    True
    # a和b是否是同一个对象
    >>> a is b
    False
    # a和b的地址其实是不一样的
    >>> id(a)
    4498717128
    >>> id(b)
    4446861832
    

    在比较时但也有例外。Python对一些常用的值进行缓存优化,例如在区间[-5,256]的整数,它们在创建时,无论创建多少个对象,它们的id是一样的,即它们在底层中只保存一份内存。

    >>> a = -5
    >>> b = -5
    >>> a == b
    True
    >>> a is b
    True
    >>> a = -6
    >>> b = -6
    >>> a == b
    True
    >>> a is b
    False
    

    对一些短的字符串也是如此,因此并不是所有字符串都会创建新的实例

    >>> a='123'
    >>> b='123'
    >>> a==b
    True
    >>> a is b
    True
    >>> id(a)
    4446903800
    >>> id(b)
    4446903800
    >>> x = 'long char'
    >>> y = 'long char'
    >>> x == y
    True
    >>> x is y
    False
    

    0x01 __repr____str__

    每个类都应该提供一个__repr__方法。__repr__方法和__str__方法有什么不一样呢?

    简单的说,__repr__可以反映一个对象的类型以及包含的内容,而__str__主要是用于打印一个对象的内容。例如看一下Python中的日期类datetime

    import datetime
    >>> today = datetime.date.today()
    >>> today
    datetime.date(2019, 7, 7)
    >>> print(today)
    2019-07-07
    >>> str(today)
    '2019-07-07'
    >>> repr(today)
    'datetime.date(2019, 7, 7)'
    

    __str__在字符串连接,打印等操作会用到,而__repr__主要是面向开发者,它能反馈的信息比较多,例如在交互环境下输入today这个变量会打印出datetime.date(2019, 7, 7),不仅可以看出today代表的是今天的日期信息,还可以看出它的类型信息。更重要的是你可以直接复制这段打印出来的信息,直接构造一个“相同”的对象出来。
    例如

    >>> now = datetime.date(2019, 7, 7)
    >>> now
    datetime.date(2019, 7, 7)
    

    0x02 对象复制

    对象的复制或说对象拷贝可以分为浅拷贝和深拷贝

    浅拷贝与深拷贝

    我们通过代码来说明,就很好理解

    如果要拷贝的对象是基本数据类型,那么深拷贝和浅拷贝的区别不是很大。

    >>> a = [1,2,3]
    >>> b = list(a)
    >>> a[1]=200
    >>> a
    [1, 200, 3]
    >>> b
    [1, 2, 3]
    

    修改a中的元素并不会影响到b

    但如果要拷贝的对象包含了另一个对象,那么就要考虑深拷贝和浅拷贝的问题了。

    >>> a = [[1,2,3],[4,5,6],[7,8,9]]
    >>> b = list(a)
    >>> a == b
    True
    >>> a is b
    False
    

    这里有一个列表a,里面有三个子列表,即列表里包含的是对象。
    我们使用list工厂方法创建了一个a的拷贝b,这个b就是a的浅拷贝,为什么呢?

    >>> a[1][2]='x'
    >>> a
    [[1, 2, 3], [4, 5, 'x'], [7, 8, 9]]
    >>> b
    [[1, 2, 3], [4, 5, 'x'], [7, 8, 9]]
    

    a[1][2]的元素修改成了x,这时候b列表中也响应了相同的修改。所以这是浅拷贝,因为没有把子对象进行拷贝,只是拷贝了指向子对象的引用

    知道浅拷贝,那么深拷贝就很好理解了。执行拷贝之后,拷贝对象和原对象是完全独立的,修改任何一个对象都不会影响到另一个对象

    如何深拷贝一个对象

    这时候就需要copy模块了,该模块有两个重要的方法deepcopycopy。不错,就是分别代表深拷贝和浅拷贝。

    >>> import copy
    >>> a = [[1,2,3],[4,5,6],[7,8,9]]
    >>> b = copy.deepcopy(a)
    >>> a
    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    >>> b
    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    >>> a[1][2]='change'
    >>> a
    [[1, 2, 3], [4, 5, 'change'], [7, 8, 9]]
    >>> b
    [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    

    执行深拷贝之后,对a的修改并不会影响到b

    0x03 Abstract Base Classes(ABC)

    抽象基类的使用

    为了说明为什么要使用ABC,我们先看下不使用ABC的情况

    # 定义了基类Base
    >>> class Base:
        def foo(self):
            raise NotImplemented
        def bar(self):
            raise NotImplemented
    
    # 定义实现类
    >>> class Concrete(Base):
        def foo(self):
            print('called foo')
        
        # 实现类并没有实现bar方法
        
    >>> c = Concrete()
    >>> c.foo()
    called foo
    >>> c.bar()
    Traceback (most recent call last):
      File "<pyshell#391>", line 1, in <module>
        c.bar()
      File "<pyshell#384>", line 5, in bar
        raise NotImplemented
    TypeError: exceptions must derive from BaseException
    

    可以看到没有实现基类bar方法的Concrete类,依然可以使用,并没有在第一时间抛出错误。

    要解决这个问题,就要用到我们abc模块了。

    from abc import ABC
    # 定义基类,继承于ABC
    >>> class Base(ABC):
        @abstractmethod
        def foo(self):
            pass
        @abstractmethod
        def bar(self):
            pass
    
        
    >>> class Concrete(Base):
        def foo(self):
            print("called foo")
        # 实现类并没有实现bar方法
    
    >>> c = Concrete()
    Traceback (most recent call last):
      File "<pyshell#357>", line 1, in <module>
        c = Concrete()
    TypeError: Can't instantiate abstract class Concrete with abstract methods bar
    

    可以看到,在使用Concrete构造方法的时候,就立即抛出TypeError了。

    0x04 使用namedtuple的好处

    关于namedtuple的用法在前面的文章《如何在Python中表示一个对象》 也有提到。
    简单的说namedtuple是一个可以命名的tuple,他是对tuple的扩展,它有跟tuple一样不可变的属性。

    对于一些数据类的定义,namedtuple使用起来非常方便

    >>> from collections import namedtuple
    >>> Point = namedtuple('Point','x y z')
    >>> p = Point(1,3,5)
    >>> p
    Point(x=1, y=3, z=5)
    >>> p.x
    1
    >>> p.y = 3.5
    AttributeError: can't set attribute
    # 可以看出通过namedtuple定义对象,就是一个class类型的
    >>> type(p)
    <class '__main__.Point'>
    
    

    还可以使用它内部的一些工具方法,在实际的编码当中也是非常实用的。

    # 转化为dict
    >>> p._asdict()
    OrderedDict([('x', 1), ('y', 3), ('z', 5)])
    # 更新或替换某个属性值
    >>> p._replace(x=111)
    Point(x=111, y=3, z=5)
    # 使用_make创建新对象
    >>> Point._make([333,666,999])
    Point(x=333, y=666, z=999)
    

    0x05 类变量和实例变量

    Python中对象的属性类型有实例变量和类变量

    类变量是属于类的,它存储在“类的内存空间”里,并能够被它的各个实例对象共享。而实例变量是属于某个特定实例的,它不在“类的内存空间”中,它是独立于各个实例存在的。

    >>> class Cat:
        num_legs = 4
        
    >>> class Cat:
        num_legs = 4
        def __init__(self,name):
            self.name = name
    
    >>> tom = Cat('tom')
    >>> jack = Cat('jack')
    >>> tom.name,jack.name
    ('tom', 'jack')
    >>> tom.num_legs,jack.num_legs
    (4, 4)
    >>> Cat.num_legs
    4
    

    这里定义了一个猫类,它有一个实例变量name,还有一个类变量num_legs,这个是各个实例共享的。

    如果对类变量进行,那么其它实例也会同步修改。而对某个实例对修改,并不会影响都类变量。

    >>> Cat.num_legs = 6
    >>> tom.num_legs,jack.num_legs
    (6, 6)
    >>> tom.num_legs = 2
    >>> tom.num_legs,jack.num_legs
    (2, 6)
    >>> Cat.num_legs
    6
    

    tom.num_legs = 2这个语句都作用其实对tom这个实例增加了一个属性,只不过这个属性名称跟类属性的名称是一致的。

    0x06 实例方法、类方法和静态方法

    为了更好区分,我们还是来看代码

    >>> class MyClass:
        def method(self):
            print(f"instance method at {self}" )
        @classmethod
        def classmethod(cls):
            print(f'classmethod at {cls}')
        @staticmethod
        def staticmethod():
            print('staticmethod')
    
            
    >>> mc = MyClass()
    >>> mc.method
    <bound method MyClass.method of <__main__.MyClass object at 0x10c280b00>>
    >>> mc.classmethod
    <bound method MyClass.classmethod of <class '__main__.MyClass'>>
    >>> mc.staticmethod
    <function MyClass.staticmethod at 0x1090d4378>
    

    可以看到在MyClass中分别定义实例方法(method)、类方法(classmethod)和静态方法(staticmethod)

    Python中一切都是对象,所以我打印来一下各个方法的__repr__输出。

    对于实例方法method是绑定在MyClass的具体实现对象中的,而类方法classmethod是绑定在MyClass中的,而静态方法staticmethod既不绑定在实例里,也不绑定在类中,它就是一个function对象

    实例方法的调用,需要传递一个实例对象到实例方法中,以下两种方法的调用是等价的。

    >>> mc.method()
    instance method at <__main__.MyClass object at 0x10910ada0>
    >>> MyClass.method(mc)
    instance method at <__main__.MyClass object at 0x10910ada0>
    

    类方法的调用和静态方法的调用都是使用ClassName.methodName()的方式。

    >>> MyClass.classmethod()
    classmethod at <class '__main__.MyClass'>
    >>> MyClass.staticmethod()
    staticmethod
    

    类方法和静态方法有什么区别呢?

    • 首先在前面可以看到类方法和静态方法的对象是不一样的,一个是bound method,一个是function
    • 其次类方法可以访问到类对象MyClass,而静态方法不能。
    • 最后静态方法其实跟一个普通的function对象一样,只不过它是属于类命名空间的。

    0x07 总结一下

    本文主要对Python中一些常见的面向对象的相关的一些特性进行了说明。包括对象的比较、输出、拷贝等操作,以及推荐使用namedtuple定义数据类。最后对类变量和实例变量以及类方法、实例方法和静态方法的不同作了分析。

    0x08 学习资料

    • Python Tricks: A Buffet of Awesome Python Features
      ——Dan Bader

    相关文章

      网友评论

          本文标题:Python中面向对象你应该知道的事

          本文链接:https://www.haomeiwen.com/subject/gravsctx.html