导语
在Python中,类和 OOP 都不是日常编程所必需的。尽管它从一开始设计就是面向对象的,并且结构上支持 OOP,但 Python 没有限定或要求你在你的应用中写 OOP 的代码。
不过!不学面向对象可以么?嘿嘿!那是必须的不可以!
相关概念
• 类(Class):
用来描述具有相同的属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。
• 方法 :
类中定义的函数。
• 类变量 :
类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。
• 实例变量:
定义在方法中的变量,只作用于当前实例
• 数据成员:
类变量或者实例变量,用于处理类及其实例对象的相关的数据。
• 方法重写:
如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。
• 继承 :
即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。例如,有这样一个设计 :一个Dog类型的对象派生自Animal类,这是模拟"是一个(is-a)"关系(例如,Dog是一个Animal)。
• 实例化 :
创建一个类的实例,类的具体对象。
• 对象 :
通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。
创建类
使用class语句来创建一个新类,class之后为类的名称并以冒号结尾
class ClassName:
'类的帮助信息' #类文档字符串,类的帮助信息可以通过ClassName.__doc__查看
class_suite #类体,class_suite 由类成员,方法,数据属性组成。
例:
#!/usr/bin/python
class Employee:
'所有员工的基类'
empCount = 0
def init(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print "Total Employee %d" % Employee.empCount
def displayEmployee(self):
print "Name : ", self.name, ", Salary: ", self.salary
• empCount变量是一个类变量,它的值将在这个类的所有实例之间共享。你可以在内部类或外部类使用Employee.empCount访问。
• init()方法是一种特殊的方法,被称为类的构造函数或初始化方法,当创建了这个类的实例时就会调用该方法
创建实例对象
要创建一个类的实例,可以使用类的名称,并通过init方法接受参数。
创建 Employee 类的第一个对象
emp1 = Employee("wing", 2000)
创建 Employee 类的第二个对象
emp2 = Employee("liuchao", 5000)
访问属性
使用点(.)来访问对象的属性:
emp1.displayEmployee()
emp2.displayEmployee()
print "Total Employee %d" % Employee.empCount
使用函数的方式来访问属性:
• getattr(obj, name[, default]) : 访问对象的属性。
• hasattr(obj,name) : 检查是否存在一个属性。
• setattr(obj,name,value) : 设置一个属性。如果属性不存在,会创建一个新属性。
• delattr(obj, name) : 删除属性。
例:
hasattr(emp1, 'age') # 如果存在 'age' 属性返回 True。
getattr(emp1, 'age') # 返回 'age' 属性的值
setattr(emp1, 'age', 8) # 添加属性 'age' 值为 8
delattr(emp1, 'age') # 删除属性 'age'
完整实例:
#!/usr/bin/python
class Employee:
'所有员工的基类'
empCount = 0
def init(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print "Total Employee %d" % Employee.empCount
def displayEmployee(self):
print "Name : ", self.name, ", Salary: ", self.salary
emp1 = Employee("Zara", 2000)
emp2 = Employee("Manni", 5000)
emp1.displayEmployee()
emp2.displayEmployee()
print "Total Employee %d" % Employee.empCount
输出结果:
Name : Zara ,Salary: 2000
Name : Manni ,Salary: 5000
Total Employee 2
添加、删除、修改类的属性:
emp1.age = 7 # 添加一个 'age' 属性
emp1.age = 8 # 修改 'age' 属性
del emp1.age # 删除 'age' 属性
静态变量和实例变量
亦可称为静态数据(类数据属性,也叫静态字段,静态属性)。
这些数据是与它们所属的类对象绑定的,不依赖于任何类实例。
如果你熟悉 Java 或 C++,这种类型的数据相当于在一个变量声明前加上 static 关键字。
静态成员通常仅用来跟踪与类相关的值。
一般,我们会考虑用实例属性(也叫普通变量或实例变量),而不是类属性。
静态变量在内存中只保存一份
实例变量在每个对象中都保存一份
应用场景: 通过类创建对象时,如果每个对象都具有相同的字段,那么就使用静态变量
例1:类数据属性(foo):
class C(object):
... foo = 100
print C.foo
100
C.foo = C.foo + 1
print C.foo
101
注意,上面的代码中,看不到任何类实例的引用
例2:
class Province:
# 静态变量
country = '中国'
def __init__(self, name):
# 实例变量
self.name = name
# 直接访问普通字段
obj = Province('河北省')
print obj.name
#直接访问静态字段
Province.country
类属性与方法
类的私有属性(私有字段)
__private_attrs:两个下划线开头,声明该属性为私有,不能在类地外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs。
类的方法
在类地内部,使用def关键字可以为类定义一个方法,与一般函数定义不同,类方法必须包含参数self,且为第一个参数
类的私有方法
__private_method:两个下划线开头,声明该方法为私有方法,不能在类地外部调用。在类的内部调用 self.__private_methods
实例
#!/usr/bin/python
class JustCounter:
__secretCount = 0 # 私有变量
publicCount = 0 # 公开变量
def count(self):
self.__secretCount += 1
self.publicCount += 1
print self.__secretCount
counter = JustCounter()
counter.count()
counter.count()
print counter.publicCount
print counter.__secretCount # 报错,实例不能访问私有变量Python 通过改变名称来包含类名:
1
2
2
Traceback (most recent call last):
File "test.py", line 17, in
print counter.__secretCount # 报错,实例不能访问私有变量
AttributeError: JustCounter instance has no attribute '__secretCount'
Python不允许实例化的类访问私有数据,但你可以使用 object._className__attrName 访问属性,将如下代码替换以上代码的最后一行代码:
.........................
print counter._JustCounter__secretCount
执行结果:
1
2
2
2
特殊类属性(Python内置类属性):
C.__dict__ :类C的属性(包含一个字典,由类的数据属性组成)
C.__doc__ :类C的文档字符串
C.__name__:类C类名(字符串)
C.__module__:类C定义所在的模块(类的全名是'__main__.className',如果类位于一个导入
模块mymod中,那么className.__module__ 等于 mymod)
C.__bases__ : 类C的所有父类构成元素(包含了一个由所有父类组成的元组)
C.__class__:实例对应的类(仅新式类中)
根据上面定义的类 MyClass,有如下结果:
>>> MyClass.__name__
'MyClass'
>>> MyClass.__doc__
'MyClass class definition'
>>> MyClass.__bases__
(,)
>>> print MyClass.__dict__
{'__doc__': None, 'myVersion': 1, 'showMyVersion':
, '__module__': '__main__'}
>>> MyClass.__module__
'__main__'
>>> MyClass.__class__
__name__
是给定类的字符名字。
它适用于那种只需要字符串(类对象的名字),而非类对象本身的情况。一些内建的类型也有这个属性,我们将会用
到其中之一来展示__name__字符串的益处。
类型对象是一个内建类型的例子,它有__name__的属性。回忆一下,type()返回被调用对象的类型。
我们可以使用类型对象的__name__属性来取得相应的字符串名。
如下例示:
>>> stype = type('What is your quest?')
>>> stype # stype is a type object stype 是一个类型对象
>>> stype.__name__ # get type as a string 得到类型名(字符串表示)
'string'
>>> type(3.14159265) # also a type object 又一个类型对象
>>> type(3.14159265).__name__ # get type as a string 得到类型名(字符串表示)
'float'
__doc__
是类的文档字符串,与函数及模块的文档字符串相似,必须紧随头行(header line) 后的字符串。
文档字符串不能被派生类继承,也就是说派生类必须含有它们自己的文档字符串。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__dict__
包含一个字典,由类的数据属性组成。
访问一个类属性的时候,Python 解释器将会搜索字典以得到需要的属性。
如果在__dict__中没有找到,将会在基类的字典中进行搜索, 采用“深度优先搜索”顺序。
基类集的搜索是按顺序的,从左到右,按其在类定义时,定义父类参数时的顺序。
对类的修改会仅影响到此类的字典;基类的__dict__属性不会被改动的
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__module__
Python 支持模块间的类继承。为更清晰地对类进行描述,1.5 版本中引入了__module__, 这样类名就完全由模
块名所限定。看一下下面的例子:
>>> class C(object):
... pass
...
>>> C
>>> C.__module__
'__main__'
类 C 的全名是“__main__.C”,比如,source_module.class_name。
如果类 C 位于一个导入的模块中,如 mymod,像下面的:
>>> from mymod import C
>>> C
>>> C.__module__
'mymod'
在以前的版本中,没有特殊属性__module__,很难简单定位类的位置,因为类没有使用它们的全名。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
__class__
最后,由于类型和类的统一性,当访问任何类的__class__属性时,你将发现它就是一个类型对象的实例。换句话说,一个类已是一种类型了。因为经典类并不认同这种等价性(一个经典类是一 个类对象,一个类型是一个类型对象),对这些对象来说,这个属性并未定义。
__class__到底是什么意思?看例子
# cat a.py
#!/usr/bin/python
# -*- coding: UTF-8 -*-
class A:
def __init__(self,url):
self.url = url
def out(self):
return self.url
a = A('news.163.com')
print a.out()
b = a.__class__('www.bccn.net')
print b.out()
print A
print a.__class__
# python a.py
news.163.com
www.bccn.net
__main__.A
__main__.A
可以看出a.__class__就等效于a的类A
内置类属性调用实例:
#!/usr/bin/python
class Employee:
'所有员工的基类'
empCount = 0
def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.empCount += 1
def displayCount(self):
print "Total Employee %d" % Employee.empCount
def displayEmployee(self):
print "Name : ", self.name, ", Salary: ", self.salary
print "Employee.__doc__:", Employee.__doc__
print "Employee.__name__:", Employee.__name__
print "Employee.__module__:", Employee.__module__
print "Employee.__bases__:", Employee.__bases__
print "Employee.__dict__:", Employee.__dict__
输出结果:
Employee.__doc__: 所有员工的基类
Employee.__name__: Employee
Employee.__module__: __main__
Employee.__bases__: ()
Employee.__dict__: {'__module__': '__main__', 'displayCount': , 'empCount': 0, 'displayEmployee': , '__doc__': '\xe6\x89\x80\xe6\x9c\x89\xe5\x91\x98\xe5\xb7\xa5\xe7\x9a\x84\xe5\x9f\xba\xe7\xb1\xbb', '__init__': }
类的继承
面向对象的编程带来的主要好处之一是代码的重用,实现这种重用的方法之一是通过继承机制。
继承完全可以理解成类之间的类型和子类型关系。
继承语法
class 派生类名(基类名):
python继承中的特点:
1:在继承中基类的构造(init()方法)不会被自动调用,它需要在其派生类的构造中亲自专门调用。
2:在调用基类的方法时,需要加上基类的类名前缀,且需要带上self参数变量。区别于在类中调用普通函数时并不需要带上self参数
3:Python总是首先查找对应类型的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)。
多重继承
如果在继承元组中列了一个以上的类,那么它就被称作"多重继承" 。
语法:
派生类的声明,与他们的父类类似,继承的基类列表跟在类名之后,如下所示:
class SubClassName (ParentClass1[, ParentClass2, ...]):
'Optional class documentation string'
class_suite
实例:
#!/usr/bin/python
class Parent: # 定义父类
parentAttr = 100
def init(self):
print "调用父类构造函数"
def parentMethod(self):
print '调用父类方法'
def setAttr(self, attr):
Parent.parentAttr = attr
def getAttr(self):
print "父类属性 :", Parent.parentAttr
class Child(Parent): # 定义子类
def __init__(self):
print "调用子类构造方法"
def childMethod(self):
print '调用子类方法 child method'
c = Child() # 实例化子类
c.childMethod() # 调用子类的方法
c.parentMethod() # 调用父类方法
c.setAttr(200) # 再次调用父类的方法
c.getAttr() # 再次调用父类的方法
以上代码执行结果如下:
调用子类构造方法
调用子类方法 child method
调用父类方法
父类属性 : 200
继承多个类
class A: # 定义类 A
.....
class B: # 定义类 B
.....
class C(A, B): # 继承类 A 和 B
.....
使用issubclass()或者isinstance()方法来检测。
issubclass()布尔函数判断一个类是另一个类的子类或者子孙类,语法:
issubclass(sub,sup)
例子:
def foo():
pass
print issubclass(foo.__class__, object)
结果:
True
上述代码说明了Python 中的函数是 object 的子类
isinstance(obj, Class) 布尔函数如果obj是Class类的实例对象或者是一个Class子类的实例对象则返回true。
方法重写
如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法:
实例:
#!/usr/bin/python
class Parent: # 定义父类
def myMethod(self):
print '调用父类方法'
class Child(Parent): # 定义子类
def myMethod(self):
print '调用子类方法'
c = Child() # 子类实例
c.myMethod() # 子类调用重写方法
输出结果:
调用子类方法
基础重载方法
下表列出了一些通用的功能,你可以在自己的类重写:
| 序号 | 方法 描述 简单的调用 |
| 1 | __init__ ( self [,args...] )
构造函数
简单的调用方法: obj = className(args) |
| 2 | __del__( self )
析构方法, 删除一个对象
简单的调用方法 : del obj |
| 3 | __repr__( self )
转化为供解释器读取的形式
简单的调用方法 : repr(obj) |
| 4 | __str__( self )
用于将值转化为适于人阅读的形式
简单的调用方法 : str(obj) |
| 5 | __cmp__ ( self, x )
对象比较
简单的调用方法 : cmp(obj, x) |
运算符重载
Python同样支持运算符重载,实例如下:
#!/usr/bin/python
class Vector:
def init(self, a, b):
self.a = a
self.b = b
def str(self):
return 'Vector (%d, %d)' % (self.a, self.b)
def add(self,other):
return Vector(self.a + other.a, self.b + other.b)
v1 = Vector(2,10)
v2 = Vector(5,-2)
print v1 + v2
以上代码执行结果如下所示:
Vector(7,8)
类、实例和其他对象的内建函数
issubclass()
issubclass() 布尔函数判断一个类是另一个类的子类或子孙类。
语法:
issubclass(sub, sup)
issubclass() 返回True的情况:
1. 给出的子类sub确实是父类sup的一个子类(反之,则为False)。
2. 这个函数也允许“不严格”的子类,意味着,一个类可视为其自身的子类,所以,这个函数如果当 sub 就是 sup,或者从 sup 派生而来,则返回 True。(一个“严格的”子类是严格意义上的从一个类派生而来的子类。)
3. 从 Python 2.3 开始,issubclass()的第二个参数可以是可能的父类组成的 tuple(元组),这时, 只要第一个参数是给定元组中任何一个候选类的子类时,就会返回 True。
isinstance()
isinstance() 布尔函数在判定一个对象是否是另一个给定类的实例时,非常有用。
语法:
isinstance(obj1, obj2)
isinstance()在 obj1 是类 obj2 的一个实例,或者是 obj2 的子类的一个实例时,返回 True (反之,则为 False)
例子:
>>> class C1(object): pass
>>> class C2(object): pass
>>> c1 = C1()
>>> c2 = C2()
>>> isinstance(c1, C1) True
>>> isinstance(c2, C1) False
>>> isinstance(c1, C2) False
>>> isinstance(c2, C2) True
>>> isinstance(C2, c2)
Traceback (innermost last):
File "", line 1, in ?
isinstance(C2, c2)
TypeError: second argument must be a class
注意:第二个参数应当是类;不然,你会得到一个 TypeError。但如果第二个参数是一个类型对象,则不会出现异常。这是允许的,因为你也可以使用 isinstance()来检查一个对象 obj1 是否是 obj2 的类型,比如:
>>> isinstance(4, int)
True
>>> isinstance(4, str)
False
>>> isinstance('4', str)
True
isinstance()也可以使用一个元组(tuple)作为第二个参数。如果第一个参数是第二个参数中给定元组的任何一个候选类型或类的实例时,就会返回 True。
hasattr(), getattr(),setattr(), delattr()
*attr()系列函数可以在各种对象下工作,不限于类(class)和实例(instances),因为在类和实例中使用极其频繁,就在这里列出来了。
hasattr()函数是 Boolean 型的,它的目的就是为了决定一个对象是否有一个特定的属性,一般用于访问某属性前先作一下检查。
getattr()取得对象的属性,会在你试图读取一个不存在的属性时,引发 AttributeError 异常,除非给出那个可选的默认参数。
setattr()将要么加入一个新的属性,要么取代一个已存在的属性。
delattr()函数会从一个对象中删除属性。
下面一些例子使用到了*attr()系列函数:
>>> class myClass(object):
... def __init__(self):
... self.foo = 100
>>> myInst = myClass()
>>> hasattr(myInst, 'foo')
True
>>> getattr(myInst, 'foo')
100
>>> hasattr(myInst, 'bar')
False
>>> getattr(myInst, 'bar')
Traceback (most recent call last):
File "", line 1, in ?
getattr(myInst, 'bar')
AttributeError: myClass instance has no attribute 'bar'
>>> getattr(c, 'bar', 'oops!')
'oops!'
>>> setattr(myInst, 'bar', 'my attr')
>>> dir(myInst)
['__doc__', '__module__', 'bar', 'foo']
>>> getattr(myInst, 'bar') # same as myInst.bar
'my attr'
>>> delattr(myInst, 'foo')
>>> dir(myInst)
['__doc__', '__module__', 'bar']
>>> hasattr(myInst, 'foo')
False
dir()
用 dir()列出一个模块所有属性的信息, dir()还可以用在对象上。
dir()提供的信息比以前更加详尽。根据文档,“除了实例变量名和常用方法外,它还显示那些通过特殊标记来调用的方法,像iadd(+=),len(len()), ne(!=)。
dir()作用在实例上(经典类或新式类)时,显示实例变量,还有在实例所在的类及所有它的基类中定义的方法和类属性.
dir()作用在类上(经典类或新式类)时,则显示类以及它的所有基类的dict中的内容。 但它不会显示定义在元类(metaclass)中的类属性.
dir()作用在模块上时,则显示模块的dict的内容.
dir()不带参数时,则显示调用者的局部变量.
super()
super()函数的目的就是帮助程序员找出相应的父类, 然后方便调用相关的属性。一般情况下,程序员可能仅仅采用非绑定方式调用祖先类方法。使用 super()可以简化搜索一个合适祖先的任务,并且在调用它时,替你传入实例或类型对象。
每个定义的类,都有一个名为mro的属性,它是一个元组,按照他们被搜索时的顺序,列出了备搜索的类。
语法如下:
super(type[, obj])
给出 type,super()“返回此 type 的父类”。如果你希望父类被绑定,你可以传入 obj 参数(obj 必须是
type 类型的).否则父类不会被绑定。obj 参数也可以是一个类型,但它应当是 type 的一个子类。通常,当给出 obj 时:
- 如果obj是一个实例,isinstance(obj,type)就必须返回True
- 如果obj是一个类或类型,issubclass(obj,type)就必须返回True
事实上,super()是一个工厂函数,它创造了一个 super object,为一个给定的类使用mro 去查找相应的父类。很明显,它从当前所找到的类开始搜索 MRO。
super()的主要用途,是来查找父类的属性,比如, super(MyClass,self).init()。如果你没有执行这样的查找,你可能不需要使用 super()。
vars()
vars()内建函数与 dir()相似,只是给定的对象参数都必须有一个dict属性。vars()返回一个字典,它包含了对象存储于其dict中的属性(键)及值。如果提供的对象没有这样一个属性, 则会引发一个 TypeError 异常。如果没有提供对象作为 vars()的一个参数,它将显示一个包含本地名字空间的属性(键)及其值的字典,也就是locals()。
例子,使用类实例调用 vars():
>>> class C(object):
... pass
>>> c = C()
>>> c.foo = 100
>>> c.bar = 'Python'
>>> c.__dict__
{'foo': 100, 'bar': 'Python'}
>>> vars(c)
{'foo': 100, 'bar': 'Python'}
Python新式类和经典类
Python从2.2开始,引入了 new style class(新式类)
Python 2.x中默认都是经典类,只有显式继承了object才是新式类
Python 3.x中默认都是新式类,不必显式的继承object
新式类跟经典类的差别主要是以下几点:
• 新式类对象可以直接通过class属性获取自身类型:type
• 继承搜索的顺序发生了改变
• 新式类增加了slots内置属性, 可以把实例属性的种类锁定到slots规定的范围之中
• 新式类增加了getattribute方法
1. 新式类对象可以直接通过class属性获取自身类型:type
#cat test1.py
#/usr/bin/env python2.7
class E:
#经典类
pass
class E1(object):
#新式类
pass
e = E()
print "经典类"
print e
print type(e)
print e.__class__
print "新式类"
e1 = E1()
print e1
print e1.__class__
print type(e1)
输出结果:
经典类
<__main__.E instance at 0x0000000002250B08>
__main__.E
新式类
<__main__.E1 object at 0x0000000002248710>
E1是定义的新式类。那么输出e1的时候,不论是type(e1),还是e1.__class__都是输出的。
2. 继承搜索的顺序发生了改变
经典类多继承属性搜索顺序: 先深入继承树左侧,再返回,开始找右侧
新式类多继承属性搜索顺序: 先水平搜索,然后再向上移动
#cat test2.py
class A(object):
'新式类 ,作为所有类的基类'
def foo(self):
print "class A"
class A1():
'经典类,作为所有类的基类 '
def foo(self):
print "class A1"
class C(A):
pass
class C1(A1):
pass
class D(A):
def foo(self):
print "class D"
class D1(A1):
def foo(self):
print "class D1"
class E(C, D):
pass
class E1(C1, D1):
pass
e = E()
e.foo()
e1 = E1()
e1.foo()
输出结果:
class D
class A1
因为A新式类,对于继承A类都是新式类,首先要查找类E中是否有foo(),如果没有则按顺序查找C->D->A。它是一种广度优先查找方式。
因为A1经典类,对于继承A1类都是经典类,首先要查找类E1中是否有foo(),如果没有则按顺序查找C1->A1->D1。它是一种深度优先查找方式。
3. 新式类增加了slots内置属性, 可以把实例属性的种类锁定到slots规定的范围之中。
比如只允许对A实例添加name和age属性:
#cat test3.py
class A(object):
__slots__ = ('name', 'age')
class A1():
__slots__ = ('name', 'age')
a1 = A1()
a = A()
a1.name1 = "a1"
a.name1 = "a"
A是新式类添加了__slots__ 属性,所以只允许添加 name age
A1经典类__slots__ 属性没用
执行结果:
Traceback (most recent call last):
File "t.py", line 13, in
a.name1 = "a"
AttributeError: 'A' object has no attribute 'name1'
所以a.name是会出错的
通常每一个实例都会有一个__dict__属性,用来记录实例中所有的属性和方法,也是通过这个字典,可以让实例绑定任意的属性。
__slots__属性作用就是,当类C有比较少的变量,而且拥有__slots__属性时,类C的实例就没有__dict__属性,而是把变量的值存在一个固定的地方。如果试图访问一个__slots__中没有的属性,实例就会报错。
这样操作的好处:__slots__属性虽然令实例失去了绑定任意属性的便利,但是因为每一个实例没有__dict__属性,却能有效节省每一个实例的内存消耗,有利于生成小而精干的实例。
4. 新式类增加了getattribute方法
#cat test4.py
class A(object):
def __getattribute__(self, *args, **kwargs):
print "A.__getattribute__"
class A1():
def __getattribute__(self, *args, **kwargs):
print "A1.__getattribute__"
a1 = A1()
a = A()
a.test
print "========="
a1.test
A.__getattribute__
=========
Traceback (most recent call last):
File "t.py", line 18, in
a1.test
AttributeError: A1 instance has no attribute 'test'
A是新式类,每次通过实例访问属性,都会经过__getattribute__函数,
A1不会调用__getattribute__所以出错了