《Python高级编程》学习心得——第四章 深入类和对象

《Python高级编程》学习心得——第四章 深入类和对象

总览

《Python高级编程》学习心得——第四章 深入类和对象

鸭子类型和多态

Java中多态是通过继承实现的,子类继承父类(或接口),重写父类(或接口)的方法,从而实现多态。而在Python中,一个对象从本质上来说是一个字典的封装,在该字典中,每个key是一个属性(或方法),每个value是属性的值或者方法的实现。因此,在Python中,一个对象的类型本质上是由其属性和方法决定的,比如一个类如果实现了__next__方法和__iter__方法,那该类的对象属于迭代器类型。

下面用一个例子展示Java和Python实现多态方式的不同:

Java多态

public class Main
{
	interface Animal {
		public void run();
	}
	
	class Dog implements Animal {
		@Override
		public void run()
		{
			System.out.println("A dog is running");
		}
	}

	class Cat implements Animal {
		@Override
		public void run()
		{
			System.out.println("A cat is running");
		}
	}

	public void outerMethod()
	{
		Animal[] animals = new Animal[2];
		animals[0] = new Dog();
		animals[1] = new Cat();
		for (Animal animal : animals)
		{
			animal.run();
		}
	}
	
	public static void main(String[] args)
	{
		Main main = new Main();
		main.outerMethod();
	}
}

执行结果

A dog is running
A cat is running

Python多态

class Dog:
    def run(self):
        print ("A dog is running")


class Cat:
    def run(self):
        print ("A cat is running")


if __name__ == '__main__':
    animals = (Dog(), Cat())
    for animal in animals:
        animal.run()

执行结果

A dog is running
A cat is running

抽象基类(abc模块)

Python的抽象基类类似于Java的接口interface的作用,但与interface不同的是,Python的抽象基类可以定义非抽象的方法。类似类方法(@classmethod)和静态方法(@staticmethod),Python中抽象方法也是用装饰器修饰定义。

视频中老师说为了避免过度设计,Python中不推荐使用抽象基类。但是通过阅读源码,我们可以发现Python源代码中使用到了抽象基类,理解抽象基类有助于我们阅读、学习Python源码。

下面用一个例子来看抽象基类的定义和使用。

import abc


class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def run(self):
        pass


class Dog(Animal):
    """
    If class Dog does not implements abstract method run, TypeError will be raised
    """
    pass


if __name__ == '__main__':
    dog = Dog()     

执行结果

Traceback (most recent call last):
  File "AbstractClass.py", line 15, in <module>
    dog = Dog()
TypeError: Can't instantiate abstract class Dog with abstract methods run

但其实用NotImplementedError也可以限制Dog类必须实现run方法,只不过Dog类仍能实例化,报错会发生在Dog类实例调用run方法时。

class Animal:
    def run(self):
        raise NotImplementedError


class Dog(Animal):
    pass


if __name__ == '__main__':
    dog = Dog()   
    dog.run()  

执行结果

Traceback (most recent call last):
  File "AbstractClass.py", line 27, in <module>
    dog.run()
  File "AbstractClass.py", line 18, in run
    raise NotImplementedError
NotImplementedError

使用isinstance而不是type

  • isinstance: 子类对象仍是父类的实例
  • type: 子类对象的类型仅仅是子类,而不能是父类
class Animal:
    def run(self):
        raise NotImplementedError


class Dog(Animal):
    pass


if __name__ == '__main__':
    dog = Dog()   
    print (isinstance(dog, Animal))
    print (type(dog) is Animal)

执行结果

True
False

类属性和对象属性及其查找顺序

类属性和对象属性及其覆盖问题比较简单,仅用下面一个例子说明:

class A:
    aa = 1                      # class property
    def __init__(self, x, y):
        self.x = x              # object property
        self.y = y              # object property


if __name__ == '__main__':
    a = A(2,3)
    b = A(4,5)
    a.aa = 11
    print(a.aa)
    print(b.aa)
    print(A.aa)

执行结果

11
1
1

在多继承时,属性的查找顺序遵循MRO(method resolution order). 在Python2中有经典类和新式类之分,而在Python3中,不管类定义时是否显式地继承了object, 都是新式类。新式类的MRO算法是C3算法,具体算法比较复杂,可以简单理解C3算法既不同于BFS,也不同于DFS,总的原则是优先左边的父类和优先浅层的父类. 作为属性/方法的一个特例,super关键字在调用父类的__init__方法时也遵循MRO的顺序。实际上,super不是调用父类的__init__方法,而是调用__mro__中下一个类的__init__方法。下面用两个例子来说明Python3的MRO:

Case1: 菱形继承

《Python高级编程》学习心得——第四章 深入类和对象

class A:
    def __init__(self):
        print('A')


class B(A):
    def __init__(self):
        super().__init__()
        print('B')


class C(A):
    def __init__(self):
        super().__init__()
        print('C')


class D(B, C):
    def __init__(self):
        super().__init__()
        print('D')


if __name__ == '__main__':
    d = D()
    print(D.__mro__)

执行结果

A
C
B
D
(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

解释:MRO为D->B->C->A->object. D的super调用B的__init__, B的super调用C的__init__, C的super调用A的__init__.

Case2: 树形继承

《Python高级编程》学习心得——第四章 深入类和对象

class A:
    def __init__(self):
        print('A')


class C(A):
    def __init__(self):
        super().__init__()
        print('C')


class B:
    def __init__(self):
        print('B')


class D(B):
    def __init__(self):
        super().__init__()
        print('D')


class E(C, D):
    def __init__(self):
        super().__init__()
        print('E')


if __name__ == '__main__':
    e = E()
    print(E.__mro__)

执行结果

A
C
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)

解释:MRO为E->C->A->D->B->object. E的super调用C的__init__, C的super调用A的__init__.

如果想要在子类E的__init__中同时调用所有父类的__init__,则可以改写class E为:

class E(C, D):
    def __init__(self):
        C.__init__(self)
        D.__init__(self)
        # super().__init__()
        print('E')

执行结果

A
C
B
D
E
(<class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class '__main__.D'>, <class '__main__.B'>, <class 'object'>)

静态方法,类方法及对象方法

这部分笔者已经比较熟悉了,就简单说一下。静态方法、类方法及对象方法不太好拿Java类比,可能和C++做比较牵强的类比(并不完全相等,在命名空间等方面有所不同):

C++ Python
全局函数 静态方法(@staticmethod修饰)
类方法(static关键字修饰) 类方法(@classmethod修饰)
实例方法 对象方法

写一个Python的类方法的常见使用场景。众所周知,Python是不支持函数重载的(为什么Python不支持函数重载,又可以引出一个话题,在此不表)。因此,一种支持多个构造函数的迂回方案就可以利用类方法实现:

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __str__(self):
        return str(self.year) + '-' + str(self.month) + '-' + str(self.day)


    @classmethod
    def from_str(cls, date_str):
        year, month, day = date_str.split('-')
        return cls(int(year), int(month), int(day))


if __name__ == '__main__':
    day1 = Date(2019, 3, 14)
    print(day1)
    day2 = Date.from_str('2019-03-14')
    print(day2)

注:cls和self一样,是类方法中大家约定俗成的第一个参数,但不是Python关键字

执行结果

2019-3-14
2019-3-14

数据封装和私有属性

Python没有数据访问控制权限关键字,Python类的所有属性和方法默认都是public权限,约定俗成地,用_(一条下划线)开头表示protected, 用__(两条下划线)开头表示private.

class A:
    def __init__(self):
        self.__a = 1
        self._b = 2

    def getA(self):
        return self.__a

    def getB(self):
        return self._b


class B(A):
    def __init__(self):
        super().__init__()
        self._b = 3


if __name__ == '__main__':
    b = B()
    print('a: {}, b: {}'.format(b.getA(), b.getB()))

可以正常执行,而直接访问私有属性

print(b.__a)

则会报错:

Traceback (most recent call last):
  File "private.py", line 22, in <module>
    print(b.__a)
AttributeError: 'B' object has no attribute '__a'

实际上,私有属性是可以通过"_classname__attribute"的方式直接访问的

print(b._A__a)

>> 1

由此可见,Python的访问权限控制并不严格,只是在编译的时候用了一个属性重命名的小技巧。

Python对象的自省机制

用"对象.__dict__"或"dir(对象)"可以得到对象/类的属性/方法列表。其中"对象.__dict__"返回对象相对于父类新增的(说法不严谨,望大神指正)属性名和属性值的字典,"dir(对象)"返回对象的所有属性列表(比__dict__更全,但没有值).

>>> class A:
...     def __init__(self, x):
...             self.x = x
...
>>>
>>> a = A(1)
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'x']
>>> dir(A)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
>>> a.__dict__
{'x': 1}
>>> A.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function A.__init__ at 0x000001DC7FA29620>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})

with语句和上下文管理

首先视频讲了try-except-finally三部曲的一些语法规定,这里Python的try-except-finally的语法和Java的try-catch-finally一样,这里就不再赘述了。

with关键字是对try-except-finally三部曲的改进。在申请和释放资源(打开/关闭文件、数据库、socket连接)时,常用with关键字。

进一步地,with+一个符合上下文管理协议的对象+as+对象别名,构成上下文管理器的正确语法。实现了__enter__和__exit__的类是符合上下文管理协议的类。以我们常用的with关键字操作文件为例,用open创建了文件对象file后,dir(file)可以看到,file是实现了"__enter__“和”__exit__"方法的,因此file是一个上下文管理器。

>>> file = open('Main.java', 'r')
>>> dir(file)
['_CHUNK_SIZE', '__class__', '__del__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', '__new__', '__next__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_checkClosed', '_checkReadable', '_checkSeekable', '_checkWritable', '_finalizing', 'buffer', 'close', 'closed', 'detach', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'line_buffering', 'mode', 'name', 'newlines', 'read', 'readable', 'readline', 'readlines', 'seek', 'seekable', 'tell', 'truncate', 'writable', 'write', 'writelines']
>>> file.close()