python 笔记(二)类与实例

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/duxiangwushirenfei/article/details/77800247

前言

Python类的定义在前面文章中零散提到些,包括类的方法定义,__init__,__new__,__call__等magic method的作用。其实python中的类和实例还有很多细节值得深究,以下做简单梳理。

旧式类和新式类

Python 2.x中定义类A时如果不明显指定类从object继承,那么A类就是旧式类,明显指定从object继承则是新式类。在Python 3.x中类的定就默认就是新式类。看下面小示例:

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

a = A()
print(a)
print(type(a))

在python 2.7和python3.5中运行结果:
这里写图片描述
可以看出python2.7中,a的类型是instance,而python3.5中a的类型是A。换而言之新式类和旧式类的核心区别在于:
新式类中统一了 类 和 类型,也就是说定义了新式类就是定义了一种类型,该类新建出的实例就属于这种类型。旧式类实例化多的实例,是属于instance类型。

上下文协议管理

在Python 2.5后开始支持的一种语法形式,以with…as…使用形式。实质是具有__enter__,__exit__方法的实例。最长见的文件操作,普通写法:

# 打开文件对象写入内容后,手动关闭
f = open("new.txt", "w")
f.write("Hello World!")
f.close()

使用上下文协议管理写法:

# 无需手动管理,会自动调用open对象的__exit__方法
with open("new.txt", "w") as f:
    f.write("Hello World!")

open所返回对象是内置类 TextIOWrapper 实例,TextIOWrapper父类 _TextIOBase 中定义了__enter__,__exit__方法,因此open所得对象支持上下文管理。此种方法的好处是,避免手动管理的遗漏,并且编写方式也更为简洁。
注意:
1,调用with关键字时,会触发__enter__方法,返回结果赋值给as后的变量。
2,在with体的执行过程结束,一定会调用__exit__方法,不论正常执行完,还是with体中出现异常。如果__exit__返回True,则所有的异常信息会被清空,继续后面程序执行!!
3,上下文管理的嵌套嵌套使用,尤其是自定义上下文管理类的使用时,要清楚知道自定义类的实现原理,以防不支持嵌套使用上下文管理。

属性

定义类必然会涉及属性管理,python在定义类时,也有多种属性管理方式。

__slots__ 内存优化

Python 2.2及以后在类定时可以嵌入类变量__slots__列表,用以优化该类实例化后对象的存储,减小内存开销。定义方式如下:

class Date:
    __slots__ = ['year', 'month', 'day']

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

a = Date('08', '2017', '14')
b = Date('08', '2016', '29')
print(dir(a))  # a实例中有 'day', 'month', 'year' 无 \_\_dict\_\_ 内置属性
print(dir(b))
print(a.year)
print(b.year)
a.name = 'hello'  # 抛出一场,因为name属性不在 \_\_slots\_\_列表中。 


注意:
__slots__类属性可以限定类实例属性,防止用户赋予实例的多余属性,但是不建议这么做,因为__slots__根本目的是为了减小实例所占的内存,特别对于一些数据类的定义,可以用__slots__节省资源。

属性封装

在强类型语言中,例如c++可以通过关键字去定义公有,保护,私有的属性和方法。
Python中对于方法和属性定义常见下划线分割命名,以单 “_” 下划线起始表示保护属性和方法,以双”__”下划线起始表示私有属性和方法:

class A:
    def __init__(self, private_value):
        self.public = 0
        self._protect = 1
        self.__private = private_value

    def get_private_value(self):
        return self.__private

a = A('a_private')
b = A('b_private')
print(dir(a))
print(a._protect)  # 具有 '_A__private' 属性
print(a.get_private_value())
print(b.get_private_value())
print(b.__private)  # 抛出异常

对于双下划线起始的变量,除了在类定义的内部能够使用外,不能直接通过实例点调用。此外__private变量会被自动处理为所属类名加下划线起始,就是 _A__private属性。对于类方法也是如此。
至于 @property请参见之前文章。

方法调用

Python是面向对象语言,支持继承,由此便会存在实例调用父类方法,不仅如此Python还支持多继承,那么方法调用的搜寻更是遵循一定的顺序。看以下示例代码:

class Base:
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def spam(self):
        print('A.spam')
        super().spam()

class B(Base):
    def spam(self):
        print('B.spam')
        # super().spam()

class C(A, B):
    pass

c = C()
print(C.__mro__)  # (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
c.spam()

super

super 函数时python2.2之后加上的一个函数,返回类型的父类,仅仅能在新式类中使用。常见的super的使用是在__init__方法中用以初始化父类,优势在于可以避免重复初始化。如果class D不使用super进行初始化,而是手动调用A.__init__和B.__init__初始化会造成多次初始化Base类。

__mro__

__mro__方法解析顺序类表,python类定义时会计算出一个该类的__mro__,该类的实例调用方法时的搜寻顺序就依据该列表。
因此c实例调用spam方法的搜寻顺序 为C类,之后是B,最后是object。
先运行A中的spam方法,而后还会运行B中的spam方法。
注意:对于有super函数的方法,其运行结束后,会继续搜寻__mro__紧接着后面的类中定义的同名方法运行

重读描述器

python描述器的详细介绍请参见之前文章:python 描述符descriptor,描述器核心效用是对类的属性做定制处理,配合上装饰器完成一些高级功能。以下给出摘录的非常具有代表性一个示例代码:

class Typed:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, cls):
        print('Descriptor __get__ method running....')
        if instance is None:
            return self
        else:
            return instance.__dict__[self.name]

    def __set__(self, instance, value):
        print('Descriptor __set__ method running....')
        if not isinstance(value, self.expected_type):
            raise TypeError('Expected ' + str(self.expected_type))
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]


# Class decorator that applies it to selected attributes
def type_assert(**kwargs):
    def decorate(cls):
        for name, expected_type in kwargs.items():
            # Attach a Typed descriptor to the class
            setattr(cls, name, Typed(name, expected_type))
        return cls
    return decorate


# Example use
@type_assert(name=str, shares=int, price=float)
class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

如果能够顺利理解上面代码,无需再看这段内容,如果理解困难,下面简单分析。
1,语法特性可以很快定位:
- Typed 定义了__get__, __set__和__delete__,是一个data-descriptor
- type_assert语法实现是一个装饰器
- Stock是一个类,不同于一般类的定义,在Stock定义上加了一个type_assert装饰

2,通常装饰器用来装饰方法的用法很常见,但是此处写在一个类的头上,如何理解?
@type_assert(name=str, shares=int, price=float)
class Stock:
装饰器的语法就是一个语法糖,将上面定义做等价:
Stock = type_assert(name=str, shares=int, price=float)(Stock)
type_assert(name=str, shares=int, price=float)运行结果是返回内层定义的 decorate 函数,之后再将定义的类 Stock 作为参数传入运行。

3,decorate运行核心是for循环遍历外层的kwargs,此处就是{‘name’: str, ‘shares’: int, ‘price’: float} 字典,给Stock设置类属性,属性名称就是字典中的key,value是Typed类初始化的实例。

4,Typed(name, expected_type)是以 kwargs中某一个一组key,value初始化处理,如:name=shares,expected_type=int,调用Typed中的__init__方法初始化实例返回。
上述代码核心功能完成了对类Stock属性所属类型的限定。

描述器的使用还有几个注意点:
1,为了正确使用描述器,必须将描述器实例定义成类属性
2,在上面示例代码中,如果描述器对应的类属性不是通过实例访问,而是通过类Stock.shares形式访问,则__get__方法传入的instance为None,标准定义方式就是返回描述符对象本身,可以运行print(Stock.shares)看下结果。
3,之前文档中提过的__getattr__,对于大部分以双下划线(__)开始和结尾的属性并不适用

接口或抽象基类

在c++中通过纯虚函说来定义抽象基类,子类实现父类中的抽象方法。python中类似也有抽象基类,接口类定义,以下是一个常见示例:

from abc import ABCMeta, abstractmethod


class IStream(metaclass=ABCMeta):
    @staticmethod
    @abstractmethod
    def method():
        pass

    @abstractmethod
    def read(self, maxbytes=-1):
        pass

    @abstractmethod
    def write(self, data):
        pass


class SocketStream(IStream):
    def read(self, maxbytes=-1):
        pass

    def write(self, data):
        pass

这是一种程式化的定义,继承方式,但是必须注意:
1,IStream不能够实例化对象
2,SocketStream继承IStream必须复写所有抽象方法,否则也无法实例化对象
3,IStream如果想要具有抽象类的1,2特性,必须指定metaclass=ABCMeta,否则不具备1,2的抽象类特性。
4,abstractmethod还可注解类的静态方法,类方法等,注意顺序

上面元类指定方式是python3.x的语法,python2.x中通过__metaclass__类属性指定。

小结

Python类和实例常见的知识暂且梳理到此,后续如有再行补充。

猜你喜欢

转载自blog.csdn.net/duxiangwushirenfei/article/details/77800247
今日推荐