一切皆对象——Python面向对象:属性访问的魔法(上)组合的实现

什么是属性

在Python中,简单来说,凡是存在于类里的,不论是数据、方法还是类,都叫做属性。例如我们定义一个类:

classA:

def__init__(self,a):

self.a=a

self.b=1

def__str__(self):

returnstr(

self.a+self.b

)

defadd(self):

returnself.a+self.b

这里,类A有5个属性,分别是两个数值和,两个特殊方法和以及一个普通方法:。注意到这些属性都是属于类A的实例,只能通过实例利用点运算符来访问:

a=A(2)

print(a.a)

# 2

print(a.add)

# >

# 尝试类访问:

print(A.a)

# AttributeError: type object 'A' has no attribute 'a'

通过类是无法访问实例属性的。那么,如何定义类的属性和方法呢?很简单,去掉:

classB:

a=2

defb():

print(B.a)

这里为类定义了两个类的属性和。类的属性的访问由的方式进行:

print(B.a)

# 2

B.b()

# 2

所有类的实例共享同一份类的属性:

b1=B()

b2=B()

print(b1.a)

# 2

B.a=10

print(b2.a)

# 10

可能有人会好奇,能不能通过实例来修改类的属性呢?试一下:

b2.a=5

print(b1.a)

# 10

失败了。为啥呢?因为是给这个实例定义了一个属性它的值为,而不是修改。

组合

在面向对象编程过程中,除去继承(inheritance)之外,另一类比较重要委托(delegation)方式是组合(composition)。所谓组合,即将一个类的对象(设为类A)做为另一个类(设为类B)的属性使用。这样,可以使用所提供的方法,而不必完全继承。例如:

classB:

defp(self,obj):

print(obj)

# 继承方式:

classA(B):

pass

a=A()

a.p('hi')

# hi

# 组合方式:

classA:

def__init__(self):

self.b=B()

a=A()

a.b.p('hi')

面向对象设计的一大方式是委托,即一个类只负责实现它自己相关的功能,其他功能委托给其他类实现。这样能够更好地拆解问题。委托的方式有两类:组合和继承。它们各有优缺点,都是OO中不可或缺的机制。关于组合和继承,可以这样理解:动物和猫的关系是继承关系,因为猫包含了动物的所有特性;而猫腿、猫尾、猫须等部位和猫的关系就是组合关系,它们是猫的组成部分,分管了猫的不同功能。

端午妞妞图片来自微博@回忆专用小马甲,已通知博主,如涉及侵权问题立即删除)知道了组合这种方式,还有一个问题。在上面例子里,想要使用的方法,还需要显式地访问再调用。好像太麻烦了一点,能不能让直接做为的方法来调用呢?例如。这样接口更加统一,使用者也不必关心内部实现机制究竟是Inheritance还是Composition。想要解决这个问题,需要对Python的属性访问机制有个比较深刻的认识。我们都知道属性访问采用点运算符,找不到这个属性就会抛出异常。Python提供了一个机制,允许我们在抛出异常前再尝试调用方法再寻找一次。这个特殊方法接收一个参数作为属性名,并返回该属性。

__getattr__

我们先写一个打印函数来看看它什么时候调用的:

classA:

def__getattr__(self,name):

print('Here')

returnNone

a=A()

# 访问一个不存在的属性

b=a.a

# Here

print(b)

# None

我们看到首先访问一个不存在的属性并没有报错;其次,访问这个属性的过程中调用了方法。所以,我们可以利用来控制我们的属性访问机制,从而实现上面提到的那个问题:

classB:

defp(self,obj):

print(obj)

classA:

def__init__(self):

self.b=B()

def__getattr__(self,name):

returngetattr(self.b,name)

a=A()

a.p('hi')

# hi

这样,组合来的类完美融入了类中。注意到里面用到了一个方法,这个方法和点运算符的作用一样,只不过它是函数的形式,因而属性名可以传递一个变量。熟悉JavaScript的朋友都知道,在js中,对象的属性访问非常方便:

// JavaScript

constobj={

a:'a',

b:2,

c:function() {},

};

console.log(obj.a);

// a

obj.d=3;

console.log(obj);

// { a: 'a', b: 2, c: [Function: c], d: 3 }

而在Python中,字典项的访问不得不使用中括号加字符串完成:

# Python

obj= {

'a':'a',

'b':2,

'c':lambda_:_,

}

print(obj['a'])

# a

obj['d'] =3

print(obj)

# {'d': 3, 'b': 2, 'c':

# at 0x000002D94A3EBF28>,

# 'a': 'a'}

print(obj.b)

# AttributeError: 'dict'

# object has no attribute 'b'

有了刚刚的方法,我们可以改写一下Python的字典,让他能够支持点运算符访问:

classDotDict(dict):

def__getattr__(self,name):

returnself.__getitem__(name)

obj=DotDict({

'a':'a',

'b':2,

'c':lambda_:_,

})

print(obj.a)

# a

这里面我们使用了另一个特殊方法,下面介绍一下它:

__getitem__

同样接收一个参数,只不过它返回的是以索引方式(中括号)访问的属性。例如,序列对象值的访问,背后的方法就是。同样,字典项的访问也是它来实现的。所以在上面例子里,我们仅仅是把的结果通过方法返回,即实现了点运算符访问字典项的功能。来看一个例子:

# 只能访问序列的偶数项

classEvenList:

def__init__(self,lst):

self.lst=lst

def__getitem__(self,key):

returnself.lst[2*key]

l=EvenList([xforxinrange(10)])

print(l[2])

# 4

Ok,访问的问题解决了,修改和删除怎么办呢?

# 上面的DotDict实例obj

obj.e='e'

print(obj)

# {'b': 2, 'a': 'a', 'c': at 0x000001A25A163730>}

根本没有。怎么办呢?Python提供了和访问配套的修改删除操作,只要把对应的换成和即可:

def__setattr__(self,name,val):

self.__setitem__(name,val)

def__delattr__(self,name):

self.__delitem__(name)

DotDict.__setattr__=__setattr__

DotDict.__delattr__=__delattr__

obj.e='e'

delobj.c

print(obj)

# {'b': 2, 'a': 'a', 'e': 'e'}

友情链接: https://vonalex.github.io/

欢迎关注 它不只是Python

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180712G1F0KS00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券