美文网首页
Python进阶3

Python进阶3

作者: MetaT1an | 来源:发表于2019-01-12 19:24 被阅读0次

    深入类和对象

    鸭子类型和多态

    引言

    在鸭子类型中,关注的不是对象的类型本身,而是它是如何使用的。

    例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭子的对象,并调用它的 方法。

    在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的 方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的 方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

    使用案例

    class Cat(object):
        def say(self):
            print("I am a cat")
            
    class Dog(object):
        def say(self):
            print("I am a dog")
    
    class Duck(object):
        def say(self):
            print("I am a duck")
    
    # 这里又是一切皆对象的说明,把类作为对象放入 list
    animal_list =  [Cat, Dog, Duck]
    
    for animal in animal_list:
        animal().say()
        
    # result:
    # I am a cat
    # I am a dog
    # I am a duck
    

    上面的案例中,我们并不需要确定 animal 的类型,只要具有了 say() 这个方法,就能满足调用的条件,程序就可以正常运行。如果在 java 中,则需要抽象出一个 Animal的类,其中包含了 say() 方法,让一系列子类来实现这个方法。这就是动态语言的灵活所在。再来看一个平时经常使用的例子:

    li_a, li_b =[1, 2], [3, 4]
    
    # 初学列表时,应该都知道列表的 extend()方法,将两个列表合并
    li_a.extend(li_b)
    print(li_a)
    
    # result:
    # [1, 2, 3, 4]
    

    实际上,extend() 这个方法的原型是什么样的呢

    def extend(self, iterable):
        pass
    

    它的参数并未要求是一个列表,而是一个可迭代对象即可,所以,下面这种使用方都是可以的:

    li_a = [1, 2]
    tuple_b = (3, 4)        # 元组
    set_c = set([5, 6])     # 集合,用列表转换而成
    
    li_a.extend(tuple_b)
    print(li_a)
    
    li_a.extend(set_c)
    print(li_a)
    
    # result:
    # [1, 2, 3, 4]
    # [1, 2, 3, 4, 5, 6]
    

    可迭代对象的本质就是定义了某些魔法函数,使用特定的语法时,魔法函数会隐式地去调用。这里和上一个案例中的 say() 方法是有些类似的。

    抽象基类和abc模块

    引言

    • 可以类比于 java 中的接口
    • 这个类无法实例化
    • 用来检测某个类是否具有某种方法属性
    • 用来模拟实现抽象类

    使用案例

    检测对象属性

    class Language(object):
        def __init__(self, lan_list):
            self.lans = lan_list
        
        # 有了这个魔法函数,它的对象可以用 len()
        def __len__(self):
            return len(self.lans)
    
    
    language = Language(["C"], ["Python"])
    print(len(language))
    
    # result:
    # 2
    

    如果我们不知道某个对象能不能作为 len() 的参数,直接调用会报异常。我们可以采用一种方式来检验这个对象能否用 len()

    print(hasattr(language, "__len__"))
    
    # result:
    # True
    

    不过这种方式语义不够明显,我们可以像 Java 一样,采用更加适合于编码习惯的方式。 collections.abc 模块中定义好了很多抽象基类,具有不同的属性,下一小节会讨论一下 Sized 这个抽象基类。

    抽象基类
    # 这里的 Sized 就是定义了某些必须实现函数的抽象基类
    from collections.abc import Sized
    
    
    print(isinstance(language, Sized))
    """
    isinstance放在下一个话题详细讨论
    这里先理解为 language 有了 Sized 里定义的属性了
    """
    
    # result:
    # True
    

    构建抽象类

    还有一个位于全局的直接 abc 模块,可以通过它来声明一个抽象的类,然后通过其他具体的类去继承实现这个类。强制约束类的行为。具体说明可以查看 这里

    # 首先需要导入这个模块,abstract base class
    import abc
    
    class Base(metaclass=abc.ABCMeta):
        
        # 采用装饰器将这个方法修饰成抽象方法
        @abc.abstractmethod
        def get(self, key):
            pass
            
    class Test(Base):
        pass        # 未实现抽象方法
        
    test = Test()   # 实例化阶段就会报错
    
    # result:
    # TypeError: Can't instantiate abstract class Test with abstract methods get
    

    看看 Sized 这个抽象基类中都有什么东西:

    """
    接上一小节
    """
    
    class Sized(metaclass=ABCMeta):
    
        __slots__ = ()
    
        @abstractmethod
        def __len__(self):      # 上一小节使用 Sized 做检测的关键
            return 0
    
        @classmethod
        def __subclasshook__(cls, C):   # 能打印 True 是因为这个魔法函数自动调用了
            if cls is Sized:
                if any("__len__" in B.__dict__ for B in C.__mro__):
                    return True
            return NotImplemented
    

    关于 __subclasshook__(),参阅:https://docs.python.org/3/library/abc.html#abc.ABCMeta.subclasshook

    小结

    abc 模块可以让我们定义自己的抽象基类,但是你可能已经注意到,这样的功能似乎和 Python 的动态语言灵活特性相违背,所以,这里并不推荐采用这种方式来编写代码,鸭子类型才应该是我们关注的重点。

    collections.abc 中定义了各种抽象基类,每一种都具有专门的特性,这些抽象基类不是为我们提供的,只是以一种类似文档的方式让我们了解 Python 灵活的各种内置对象的构成。

    isinstance 和 type

    引言

    • isinstance(obj, class) : 判断前面的对象是不是后面类的一个实例。
    • type(obj): 获得对象的类型

    使用案例

    class A:
        pass
    
    
    class B(A):
        pass
    
    b = B()
    
    print(isinstance(b, B))
    print(isinstance(b, A))     # 内部会沿着继承链往上找
    
    # result:
    # True
    # True
    
    print(type(b) is B)
    print(type(b) is A)
    
    # result:
    # True
    # False
    

    如果要判断一个对象的类型,应尽量采用 isinstance()

    类变量和实例变量

    引言

    • 类变量属于类,是由所有实例共享的
    • 实例变量只属于特定的实例

    使用案例

    class Vector:
        v = 1
        
        def __init__(self, x, y):
            self.x = x
            self.y = y
            
    
    m = Vector(2, 3)
    print(m.x, m.y, m.v, Vector.v)  # 实例可以访问到类变量
    
    # result:
    # 2, 3, 1, 1
    
    Vector.v = 11
    print(m.x, m.y, m.v, Vector.v)  # 通过类修改了类变量的值
    
    # result:
    # 2, 3, 11,11
    
    m.v = 111
    print(m.x, m.y, m.v, Vector.v)
    """
    通过实例无法修改类变量
    这样写本质上是给实例 m 增加了一个实例变量 v
    """
    
    # result:
    # 2, 3, 111, 11
    

    MRO

    引言

    • MRO:方法解析顺序(Method Resolution Order)
    • 对象在使用属性或者调用方法的时候会按照一定的顺序进行查找

    使用案例

    现在有这样两种多继承的关系结构:

    两种多继承结构

    1 中若在A中调用了某一个方法,但是这个方法并不存在于A中,则它就会按照如下顺序向上查找并调用:A->B->D->C->E

    2 中,会沿着这样的一个顺序:A->B->C->D

    详细的算法步骤比较复杂,就不仔细讲解。我们可以通过__mro__属性来查看这种搜索顺序

    """
    图2中的继承关系
    """
    class D:
      pass
    
    class B(D):
      pass
    
    class C(D):
      pass
    
    class A(B, C):
      pass
    
    print(A.__mro__)
    
    # result:
    # (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)
    

    类方法 静态方法 实例方法

    引言

    • 类方法静态方法在使用时要加上特殊的装饰器
    • 类方法和整个类有关,而且需要引用这个类
    • 静态方法和类有关,但不需要引用类或者实例

    使用案例

    class Date:
        def __init__(self, year, month, day):
            self.year = year
            self.month = month
            self.day = day
            
        def __str__(self):
            return "{y}/{m}/{d}".format(y=self.year, m=self.month, d=self.day)
            
        def tommorrow(self):
            self.day += 1
            
        @classmethod
        def parse_from_str(cls, data_str):
            year, month, day = tuple(data_str.split("-"))
            return cls(int(year), int(month), int(day))
            
        @staticmethod
        def is_valid_str(data_str):
            year, month, day = tuple(data_str.split("-"))
            valid_year = int(year)>0
            valid_month = int(month)>0 and int(month)<=12
            valid_day = int(day)>0 and int(day)<31
            
            return valid_year and valid_month and valid_day
        
        
    # 1.测试实例方法
    new_day = Date(2019, 8, 1)
    new_day.tommorrow()
    """
    实例方法定义的时候会有 self 参数
    实际调用时,不需要进行传递,谁调用谁就是self
    解释器会把它转化成:tommorrow(new_day)
    """
    print(new_day)
    
    # result:
    # 2019/8/2
    
    
    # 2.使用 classmethod 完成初始化
    new_day = Date.parse_from_str("2019-8-1")
    print(new_day)
    
    # result:
    # 2019/8/1
    
    
    # 3.使用 staticmethod 进行格式校验
    date_str = "2019-8-32"
    print(Date.is_valid_str(date_str))
    
    # result:
    # False
    

    数据封装和私有属性

    引言

    • Python中对象的私有属性都是通过双下划线开头
    • Python语言中并没有严格地数据私有化机制,而是通过名字重整,间接私有属性

    使用案例

    class Person:
        def __init__(self, age):
            self.__age = age
            
        def get_age():
            return self.age
    
    person = Person(20)
    print(person.age)
    
    # result:
    # AttributeError: 'Person' object has no attribute 'age'
    
    """
    名字重整就是将双下划线开头的私有变量,在内部用另外一个名字替换掉了
    替换方式:_ClassName__property
    """
    
    print(person._Person__age)
    
    # result:
    # 20
    

    Python自省机制

    引言

    • 自省是通过一定的机制查询到对象的内部结构
    • 通过__dict__来查询属性
    • 通过dir(obj)查看更加详细的属性

    使用案例

    class Person:
        name = "MetaTian"
        
    class Student(Person):
        def __init__(self, school_name):
            self.school_name = school_name
            
    
    me = Student("Rity")
    print(me.__dict__)  # 查看 me 的所有属性
    
    # result:
    # {'school_name': 'Rity'}
    
    print(me.name)
    # result:
    # MetaTian
    
    """
    name 是属于 Person 的属性,能被打印不报错是因为
    按照了一定的查找规则,找到了它,可以调用,但并不属于 me
    """
    
    print(Person.__dict__)
    
    # result:
    # {'__module__': '__main__', 'name': 'MetaTian', '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of'Person' objects>, '__doc__': None}
    
    """
    类也是对象,但是它的属性结构要比对象复杂的多
    """
    
    me.__dict__["hobby"] = "reading"
    print(me.hobby)
    
    # result:
    # reading
    """
    这就是对象属性存储的本质了
    """
    
    print(dir(me))
    
    image

    这里给出的属性会更加详细,不过没有对应的值。通过给出的这些属性,我们大概就能猜到它实现了哪些魔法函数

    super

    引言

    • 调用父类的方法?
    • 多半用在构造函数中,既然重写了父类构造函数,为什么还要去调用?

    使用案例

    """
    定义自己的一个线程
    可以重用父类的构造方法,完成线程创建
    """
    from threading import Thread
    
    
    class MyThread(Thread):
        def __init__(self, myname, user):
            self.user = user
            super().__init__(name=myname)   # 调用父类的构造方法
    
    """
    这里参考 MRO 小结中的第二种继承关系
    可以看到,B被打印后并没有找到其父类D,进行D的打印,而是打印了C
    super() 调用顺序与 mro 中定义的顺序是一样的
    """
    class D:
        def __init__(self):
            print("D")
    
    class B(D):
        def __init__(self):
            print("B")
            super().__init__()
    
    class C(D):
        def __init__(self):
            print("C")
            super().__init__()
    
    class A(B, C):
        def __init__(self):
            print("A")
            super().__init__()
    
    a = A()
    
    # result:
    # A
    # B
    # C
    # D
    # (<class '__main__.A'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.D'>, <class 'object'>)
    

    小结

    super()函数一般用在子类的构造函数中,可以让我们重用父类构造函数的代码,特别是父类构造函数非常复杂的情况下。

    这里的父类也不是严格的继承关系上的父类,而是MRO顺序中的上一个,对于复杂的继承关系结构,把super()简单地理解为调用父类是不准确的。

    with和contexlib

    引言

    • 就是上下文管理器,涉及到两个魔法函数__enter__()__exit__()
    • 用来简化tryfinally的用法
    • 实现了上下文管理器协议的类都可以直接使用with语句

    使用案例

    class Sample:
        def __enter__(self):
            print("enter")      # 获取资源
            return self
            
        def __exit__(self, exc_type, exc_val, exc_tb):
            print("exit")       # 释放资源
            
        def do_sth(self):
            print("doing something")
            
    
    with Sample() as sample:
        sample.do_sth()
        
    # result:
    # enter
    # doing something
    # exit
    
    import contexlib
    """
    使用contexlib库中提供的一个装饰器可以将一个函数变成上下文管理器
    所修饰的函数必须是一个生成器
    yield 语句之前的代码对应 __enter__()中的逻辑
    yield 语句之后的代码对应 __exit__()中的逻辑
    """
    
    @contexlib.contexmanager
    def file_open(file_name):
        print("file open")
        yield {}    # 模拟一下,后面部分会详细讲解生成器
        print("file end")
    
    with file_open("Metatian.txt") as f_opened:
        print("file processing")
        
    # result:
    # file open
    # file processing
    # file end
    

    相关文章

      网友评论

          本文标题:Python进阶3

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