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在定义类时,也有多种属性管理方式。
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__) # (, , , )
c.spam()
super 函数时python2.2之后加上的一个函数,返回类型的父类,仅仅能在新式类中使用。常见的super的使用是在__init__方法中用以初始化父类,优势在于可以避免重复初始化。如果class D不使用super进行初始化,而是手动调用A.__init__和B.__init__初始化会造成多次初始化Base类。
__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类和实例常见的知识暂且梳理到此,后续如有再行补充。