第三章:深入类和对象

1.鸭子类型和多态

什么是鸭子类型
当一只鸟走起路来像鸭子,游泳起来像鸭子,叫起来像鸭子,就可以称这只鸟为鸭子.
也就是,当我们有很多类都实现了同样的方法,这些类就可以被看成是同一个种类.
Python中的变量本身是没有类型的,所以它本身就代表着多态,它可以表示成为任何的类型.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 14:52'


class Cat(object):
    def say(self):
        print('我是一只猫')


class Dog(object):
    def say(self):
        print('我是一只狗')


class Duck(object):
    def say(self):
        print('我是一只鸭子')


animal_list = [Cat(),Dog(),Duck()]

for animal in animal_list:
    animal.say()

还有一个例子:list的extend方法
lst.extend(self ,iteralbe) 可以看到iterable就是一种鸭子类型,它代表的是一个可迭代的类型

a = [1,2]
b = [2,1]
c = (3,4)
d = dict(x=1,y=2) # 字典也是可迭代对象,但是默认迭代是字典的键
e = set()
e.add(5)
e.add(6)

a.extend(b)
a.extend(c)
a.extend(d)
a.extend(e)
print(a)

output:
1, 2, 2, 1, 3, 4, 'x', 'y', 5, 6]

2.抽象基类(abc abstract class的缩写)

什么是抽象基类
1. 如果有的类继承了抽象基类,它就必须实现抽象基类中的所有的方法
2. 抽象基类是没有办法实例化的,就是通过它不能创建对象

既然Python是基于鸭子类型的,也就是说所有的类增加一些魔法方法就可以实现特殊的特性,它不需要像静态语言那样必须继承某个类才能获取一些特性.那为什么需要抽象基类呢?因为有时候,我们可以通过抽象基类来判断某个是否属于某种类型.

Python的collections.abc模块有一些抽象基类,你可以根据isinstance(object,abcClass)来判定某个类是不是某种类型

如何去模拟一个抽象基类呢?
最简单的方法是通过抛出异常的方式来实现

# 最简单的方法就是在基类中抛出一个异常,但是这种方法有一个缺点.
# 就是在调用方法的时候才会抛出异常
class CacheBase():
    def get(self,key):
        raise NotImplementedError
    def set(self,key,value):
        raise NotImplementedError

class RedisCache(CacheBase):
    pass

redis_cache = RedisCache()
redis_cache.set('key','value')

这样有一个缺点,就是在调用方法的时候才抛出异常,而在初始化的时候没有问题,有没有什么办法可以在初始化的时候就可以抛出异常,告诉你必须实现抽象基类的方法呢.使用abc模块中的@abc.abstractmethod装饰器的方式可以实现.

import abc
class CacheBase(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def get(self,key):
        pass

    @abc.abstractmethod
    def set(self,key,value):
        pass

class RedisCache(CacheBase):
    def get(self,key):
        pass
    def set(self,key,value):
        pass

cache = RedisCache()

3.isinstance和type的区别?

type只会判断这个对象或者类的直属对象,而isinstance可以向上追溯到它的父类.所有有时候type没有isinstance准确.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 16:53'


class A:
    pass


class B(A):
    pass
b = B()
print(isinstance(b,B))
print(isinstance(b,A))
print(type(b) is B)
print(type(b) is A)

4. 类变量和实例变量

1. 类变量属于类,也属于实例.所有的实例对象和类共有一份类变量.并且类和实例都可以调用类变量
2. 实例变量属于实例,不属于类.实例变量是具体的实例单独拥有.并且实例变量只能由具体的实例来调用.不能由类变量来调用.
3. 如果实例变量和类变量重名,则实例变量调用的就是自己的实例变量,如果通过实例对象对类变量进行赋值,则它不会调用类变量,而是重新增加一个同名的实例属性.这点是个坑,有点类似全局变量和局部变量的味道.所以我们在使用的时候,最好的习惯是使用类来调用类变量,而减少使用实例对象调用类变量的情况.其实这里如果用类来调用,就相当于是加了一个global声明,表示这个变量是类变量.而如果你用实例对象来调用的时候,如果是访问,它就会向上查找,如果是修改,例如赋值,它就不会向上查找,如果没有这个属性,它就会直接创建

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 16:59'

class A:
    aa = 1 # 类变量
    def __init__(self,x,y):
        self.x = x  # 实例变量
        self.y = y  # 实例变量

a = A(2,3) # a是一个实例
A.aa = 11  # 类变量是所有的实例和类共有的变量,只有一份.
a.aa = 100 # 如果创建的实例变量和类变量重合,则实例变量就多出来了一个属性aa
           # 这样在调用的时候获取的就是实例的属性,有点类似作用域局部和全局的味道
print(a.x,a.y,a.aa) # 实例找变量的时候,会先找自己的,再找类拥有的
print(A.aa) # 类变量也可以通过类来调用
# print(A.x,A.y) # 实例变量不能通过类来方法

打印结果:
2 3 100
11

5.类属性和实例属性以及查找顺序

属性: 变量和方法统称为属性
Python3采用的属性查找顺序是C3算法,也就是先广度优先,然后有子类会调用子类的方式.
通过一个类的mro属性可以查看一个类的属性查找顺序.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:03'
# 新式类
class D:
    pass
class E:
    pass
class C(E):
    pass
class B(D):
    pass
class A(B,C):
    pass
print(A.__mro__)

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

6.类方法,静态方法,实例方法

类方法是属于类的,所有的对象和类公有这个类方法,用@classmethod装饰器来装饰.实例对象和类对象都可以调用类方法,但是类方法不能使用实例属性,里面必须有cls参数,也就是一个类参数

静态方法,跟一个普通的函数定义没有什么不同,对参数也没有要求,它就是普通的函数放到一个类中,然后加上@staticmethod装饰器.静态方法,一般不能使用实例变量.类对象和实例对象都可以调用静态方法

实例方法,第一个参数必须是具体的实例对象,我们一般定义的时候使用self来代替,不需要添加额外的装饰器.实例方法一般只有实例对象才可以调用,类对象不可以调用

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:08'


class Date:
    # 构造初始化函数
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day
    @staticmethod
    def parse_from_string(date_str):
        year,month,day = tuple(date_str.split('-'))
        return Date(int(year),int(month),int(day))

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

    def tomorrow(self):
        self.day += 1
        return self.day

    def __str__(self):
        return "{year}/{month}/{day}".format(year=self.year, month=self.month, day=self.day)

if __name__ == '__main__':
    new_day = Date(2019,1,2)
    print(new_day)

    print(new_day.tomorrow())

    date_str = '2018-12-31'
    # 用staticmethod来完成初始化
    new_day = Date.parse_from_string(date_str)
    print(new_day)

    # 用classmethod来完成初始化
    new_day = Date.from_string(date_str)
    print(new_day)

7.数据封装和私有属性.

私有属性:
通过双下划线来声明私有属性,不可以直接访问,但是它只是改变了一个名字不可以直接访问.但是还是可以通过_classname__var的方式来访问.

这种方法,其实就是为了使得我们写程序的时候更加规范,更多的是一种提示程序员这个变量该怎么使用的作用.
这种方法还有一个好处,就是有效的区分了继承之后变量名重名的问题,因为它会在变量前面加上自己的_classname

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:45'


class User:
    def __init__(self, birthyear):
        self.__birthyear = birthyear

    def get_age(self):
        # 返回年龄
        return 2019 - self.__birthyear


if __name__ == '__main__':
    user = User(1990)
    # print(user.__birthyear) # 这里访问不到,以双下划线开头的私有属性
    # 不可以直接通过实例对象来访问,但是可以通过_User__birthyear来访问.
    print(user.get_age())

8.Python的自省

什么是自省
自省就是在Python中一个对象在运行的时候可以通过某种机制知道自己的类型.
dir()可以获取一个对象的多有的属性列表,只有属性没有值.
__dict__变量可以获取一个对象的自己的属性,一般是对象自身拥有的,不是继承的,也不是内置的.
并且它返回的是一个字典,字典的键就是属性名,字典的值就是属性变量对应的值.可以通过这个字典进行添加和修改属性的操作.

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 19:57'


class Person:
    name = 'user'


class Student(Person):
    def __init__(self, school_name):
        self.school_name = school_name


if __name__ == '__main__':
    user = Student('慕课网')
    person = Person()
    # 通过__dict__查询这个类都有哪些属性
    print(user.__dict__) #{'school_name': '慕课网'}
    print(person.__dict__) # {}
    print(Person.__dict__)
    # {'__module__': '__main__', 'name': 'user', '__dict__': <attribute '__dict__' of 'Person' objects>,
    # '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}

    # 可以通过__dict__添加和修改一个对象的属性
    user.__dict__['school_addr'] = '北京市'
    print(user.school_addr) # 北京市

    print(dir(user))
    # dir()函数会列出一个对象的所有的属性,包括它继承到的,还有内置的各种属性

Python中的super()函数的使用

两个问题?
1.比如我们已经重写了某个方法,为什么还要用super()调用它的上一级的这个方法
因为有时候,我们需要重用它的上一级完成的某些功能.比如我们重写Thread类的时候,就可以重用它的__init__里面的一些功能

# encoding:utf-8
__author__ = 'Fioman'
__time__ = '2019/3/14 20:12'
# 既然我们重写了构造方法,为什么还要去调用super()呢?
# 因为我们有时候要重用它的上一级的类的方法去实现某种功能.

# 还有super()并不是简单的调用父类的方法,它的调用顺序又是怎么样的呢?
# super()的调用顺序是按照__mro__的顺序来调用的,也就是说它是和通过.
# 来查询属性的顺序是一致的.

from threading import Thread


class MyThread(Thread):
    def __init__(self, name, target, time):
        self.time = time
        super().__init__(target=target, name=name)

    def run(self):
        pass

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

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

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

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

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

[output:] D B C A
(<class 'main.D'>, <class 'main.B'>, <class 'main.C'>, <class 'main.A'>, <class 'object'>)

如果只是简单的调用父类super()方法,则上面的打印结果应该是 d=D()的时候应该是:D,B,A,C,A
但是实际的调用情况是D B C A和D.__mro__
的顺序是一致的

Python中的with语句

首先看下try ... except .. else ... finally的用法
try: 先执行的模块,这里会捕获异常.
except:如果捕获到异常就会执行except语句的代码
else: 如果没有捕获到异常,就会执行else语句的代码
finally: 无论except和else是否执行了,finally一定会执行.

def exe_try():
    try:
        print('code started')
        raise KeyError
        return 1
    except KeyError as e:
        print('key error')
        return 2
    else:  # 没有抛异常的时候会运行
        print('other error')
        return 3
    finally:  # 无论是否抛异常都会执行,如果finally里面有return语句.则
        # 返回的永远都是finally里面的return语句
        print('finally')
        return 4

总结:finally一般用来释放资源,做最后的处理.

Python的with语句
with语句又叫上下文管理器,之所以一个对象可以使用with语句,是因为这个对象实现了上下文管理器协议.
而这个协议的实现,就是通过魔法方法__enter__()__exit__来实现的.
只要我们使用了with语句,它后面跟的对象,在初始化的时候就会调用这个对象的__enter__()方法,而在结束的时候就会自动调用它的__exit__()方法.

class Sample():
    def __enter__(self):
        print('__enter__')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('exit')

    def do_something(self):
        print('doing something')

# with表示的含义是初始化的时候先调用对象的__enter__方法
# 结束的时候会调用__exit__方法  __enter__ 和__exit__方法就是
# 用来实现上下文管理器协议的,然后实现了这个协议的就可以使用with语句.
with Sample() as simple:
    simple.do_something()

还有另外一种方式可以实现上下文管理器,使用contexlib的contextmanager装饰器可以将一个函数变成上下文管理器对象.


注意:这里的函数必须是使用yield,yield之前的代码可以理解成都是__enter__()实现的逻辑代码.
而yield之后的代码可以理解成都是__exit__()实现的代码逻辑.

@contextlib.contextmanager
def file_open(file_name):
    print('file open')
    yield {}
    print('file end')

with file_open('bob.txt') as f_opened:
    print('中间')

output:
file open
中间
file end

这种书写的好处是,代码比较容易理解,更加的直观.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容