type和isinstance用法
- isinstance() 与 type() 区别:
- type() 不会认为子类是一种父类类型,不考虑继承关系。
- isinstance() 会认为子类是一种父类类型,考虑继承关系。
- 如果要判断两个类型是否相同推荐使用 isinstance()。
语法:
isinstance(object, classinfo)# example>>>a = 2>>> isinstance (a,int)True>>> isinstance (a,str)False>>> isinstance (a,(str,int,list)) # 是元组中的一个返回 TrueTrue
- object — 实例对象。
- classinfo — 可以是直接或间接类名、基本类型或者由它们组成的元组。
注意字符串的classinfo为str而非string,字典也是简写dict
返回值
面向对象编程OOP
面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student,比如,Bart Simpson和Lisa Simpson是两个具体的Student。
所以,面向对象的设计思想是抽象出Class,根据Class创建Instance。
面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。
类和实例
:::info
self代表类的实例,而非类
类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
类有一个名为 init() 的特殊方法(构造方法),该方法在类实例化时会自动调用
:::
在Python中,定义类是通过
class关键字:class Student(object):pass
class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
定义好了
Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:>>> bart = Student()>>> bart.name = 'Bart Simpson'>>> bart.name'Bart Simpson'
- 可以自由地给一个实例变量绑定属性,比如,给实例
bart绑定一个name属性.
由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的
__init__方法,在创建实例的时候,就把name,score等属性绑上去:class Student(object):def __init__(self, name, score):self.name = nameself.score = score
和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量
self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。 :::info 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同: :::>>> bart = Student('Bart Simpson', 59)>>> lisa = Student('Lisa Simpson', 87)>>> bart.age = 8>>> bart.age8>>> lisa.ageTraceback (most recent call last):File "<stdin>", line 1, in <module>AttributeError: 'Student' object has no attribute 'age'
访问限制
在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。
- 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的
name、score属性: 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线
__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:class Student(object):def __init__(self, name, score):self.__name = nameself.__score = scoredef print_score(self):print('%s: %s' % (self.__name, self.__score))
- 改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问
实例变量.__name和实例变量.__score了 - 这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。但是如果外部代码要获取name和score怎么办?可以给Student类增加
get_name和get_score这样的方法 - 原本可以直接访问,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数。
双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。不能直接访问
__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量: 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名。 总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉。
有些时候,你会看到以一个下划线开头的实例变量名,比如
_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。继承
子类继承父类时,可以不写初始化方法/定义自己的初始化方法(初始化方法中可以调用父类的init方法也可不调用)。
什么时候使用继承:假如我需要定义几个类,而类与类之间有一些公共的属性和方法,这时我就可以把相同的属性和方法作为基类的成员,而特殊的方法及属性则在本类中定义。这样子类只需要继承基类(父类),子类就可以访问到基类(父类)的属性和方法了,它提高了代码的可扩展性和重用性。 ```python class Animal(object): # python3中所有类都可以继承于object基类 def init(self, name, age):self.name = nameself.age = age
def call(self):
print(self.name, '会叫')
#
现在我们需要定义一个Cat 猫类继承于Animal,猫类比动物类多一个sex属性。
#
class Cat(Animal): def init(self,name,age,sex): super(Cat, self).init(name,age) # 不要忘记从Animal类引入属性 self.sex=sex
if name == ‘main‘: # 单模块被引用时下面代码不会受影响,用于调试 c = Cat(‘喵喵’, 2, ‘男’) # Cat继承了父类Animal的属性 c.call() # 输出 喵喵 会叫 ,Cat继承了父类Animal的方法
> **注意:**一定要用 super(Cat, self).__init__(name,age) 去初始化父类,否则,继承自 Animal的 Cat子类将没有 name和age两个属性。> python3中可以直接super().__init__(name, age),效果是一样的。> 函数super(Cat, self)将返回当前类继承的父类,即 Animal,然后调用__init__()方法,注意self参数已在super()中传入,在__init__()中将隐式传递,不能再写出self。参考:[类的继承,调用父类的属性和方法基础详解](https://blog.csdn.net/yilulvxing/article/details/85374142);[类的继承](https://zhuanlan.zhihu.com/p/30239694)<br />**类继承注意事项**<br />在继承中基类的构造方法(__init__()方法)不会被自动调用,它需要在其派生类的构造方法中亲自专门调用。<br />Python 总是首先查找对应类的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)<br />**对父类方法的重构** <br />类方法的调用顺序,当我们在子类中重构父类的方法后,Cat子类的实例先会在自己的类 Cat 中查找该方法,当找不到该方法时才会去父类 Animal 中查找对应的方法。```python#!/usr/bin/python3class Parent: # 定义父类def myMethod(self):print ('调用父类方法')class Child(Parent): # 定义子类def myMethod(self):print ('调用子类方法')c = Child() # 子类实例c.myMethod() # 子类调用重写方法super(Child,c).myMethod() #用子类对象调用父类已被覆盖的方法
子类与父类的关系
子类与父类的关系是 “is” 的关系,如上 Cat 继承于 Animal 类,我们可以说:
“A”是 Animal 类的实例,但,“A”不是 Cat 类的实例。
“C”是 Animal 类的实例,“C”也是 Cat 类的实例。
判断对象之间的关系,我们可以通过 isinstance (变量,类型) 来进行判断
多态
类具有继承关系,并且子类类型可以向上转型看做父类类型
def do(all):all.call()A = Animal('小黑',4)C = Cat('喵喵', 2, '男')D = Dog('旺财', 5, '女')for x in (A,C,D):do(x)
这种行为称为多态。也就是说,方法调用将作用在 all 的实际类型上。C 是 Cat 类型,它实际上拥有自己的 call() 方法以及从 Animal 继承的 call 方法,但调用 C .call() 总是先查找它自身的定义,如果没有定义,则顺着继承链向上查找,直到在某个父类中找到为止。
传递给函数 do(all) 的参数 all 不一定是 Animal 或 Animal 的子类型。任何数据类型的实例都可以,只要它有一个 call() 的方法即可。其他类不继承于 Animal,具备 call 方法也可以使用 do 函数。这就是动态语言,动态语言调用实例方法,不检查类型,只要方法存在,参数正确,就可以调用。
继承和多态
继承在此不未做笔记。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:
def run_twice(animal):animal.run()animal.run()
- 你会发现,新增一个
Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。 - 多态的好处就是,当我们需要传入
Dog、Cat、Tortoise……时,我们只需要接收Animal类型就可以了,因为Dog、Cat、Tortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思: 对于一个变量,我们只需要知道它是
Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:对于静态语言(例如Java)来说,如果需要传入
Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。- 对于Python这样的动态语言来说,则不一定需要传入
Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。- 这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
- 如果你学的是Java或者C++等静态语言,可能对鸭子类型的理解没那么深刻,因为静态语言中对象的特性取决于其父类。
- 而动态语言则不一样,比如迭代器,任何实现了 iter 和 next 方法的对象都可称之为迭代器,但对象本身是什么类型不受限制,可以自定义为任何类
- 鸭子类型依赖文档、清晰的代码和测试来确保正确使用 。这既是优点也是缺点,缺点是需要通过文档才能知道参数类型,为了弥补这方面的不足,Python3.6 引入了类型信息,定义变量的时候可以指定类型
获取对象信息
如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:
>>> dir('ABC')['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']
类似
__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:
>>> len('ABC')>>> 'ABC'.__len__()
我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:
>>> class MyDog(object):... def __len__(self):... return 100>>> dog = MyDog()>>> len(dog)
实例属性和类属性
如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。
class Student(object):name = 'Student'
在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。
类的私有属性与方法
- 类的私有属性:private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.private_attrs。
- 类的私有方法:private_method:两个下划线开头,声明该方法为私有方法,只能在类的内部调用 ,不能在类的外部调用。self.private_methods。
面向对象高级编程
类的专有方法
init : 构造函数,在生成对象时调用
del : 析构函数,释放对象时使用
repr : 打印,转换
setitem : 按照索引赋值
getitem: 按照索引获取值
len: 获得长度
cmp: 比较运算
call: 函数调用
add: 加运算
sub: 减运算
mul: 乘运算
truediv: 除运算
mod: 求余运算
pow: 乘方
运算符重载
#!/usr/bin/python3class Vector:def __init__(self, a, b):self.a = aself.b = bdef __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)
看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。__slots__我们已经知道怎么用了,__len__()方法我们也知道是为了能让class作用于len()函数。
除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。
str
类中定义的方法,用来定义打印类对象时的输出。
如果直接打变量而不用print,打印出来的实例还是不好看。这是因为直接显示变量调用的是repr()。 两者的区别是
__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。
解决办法是再定义一个__repr__()。但是通常__str__()和__repr__()代码都是一样的,所以,有个偷懒的写法:
class Student(object):def __init__(self, name):self.name = namedef __str__(self):return 'Student object (name=%s)' % self.name__repr__ = __str__
iter
如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:
class Fib(object):def __init__(self):self.a, self.b = 0, 1 # 初始化两个计数器a,bdef __iter__(self):return self # 实例本身就是迭代对象,故返回自己def __next__(self):self.a, self.b = self.b, self.a + self.b # 计算下一个值if self.a > 100000: # 退出循环的条件raise StopIteration()return self.a # 返回下一个值
getitem
Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素会报错。要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:
class Fib(object):def __getitem__(self, n):a, b = 1, 1for x in range(n):a, b = b, a + breturn a# 现在就可以按下标访问数列的每一项了f = Fib()f[0]
但是list有个切片方法,对于Fib仍然报错。原因是
__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:
class Fib(object):def __getitem__(self, n):if isinstance(n, int): # n是索引a, b = 1, 1for x in range(n):a, b = b, a + breturn aif isinstance(n, slice): # n是切片start = n.startstop = n.stopif start is None:start = 0a, b = 1, 1L = []for x in range(stop):if x >= start:L.append(a)a, b = b, a + breturn L
此时对于step参数未作处理。比如:
f[:10:2],也没有对负数作处理,因此要正确实现一个__getitem__还是有很多工作要做的。
与之对应的是__setitem__()方法,把对象视作list或dict来对集合赋值。最后,还有一个__delitem__()方法,用于删除某个元素。
:::info
总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。
:::
getattr
正常情况下,当我们调用类的方法或属性时,如果不存在,就会报错。
要避免这个错误,除了可以加上一个属性外,Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。返回函数也是完全可以的。
这实际上可以把一个类的所有属性和方法调用全部动态化处理了,不需要任何特殊手段。 这种完全动态调用的特性有什么实际作用呢?作用就是,可以针对完全动态的情况作调用。 博客中以实现REST API的链式调用为例。参考:https://www.liaoxuefeng.com/wiki/1016959663602400/1017590712115904#0
call
- 一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用
instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,答案是肯定的。 任何类,只需要定义一个
__call__()方法,就可以直接对实例进行调用。请看示例:class Student(object):def __init__(self, name):self.name = namedef __call__(self):print('My name is %s.' % self.name)# 调用方式如下>>> s = Student('Michael')>>> s() # self参数不要传入My name is Michael.
__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。 :::info 如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。 ::: 那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:>>> callable(Student())True>>> callable(max)True>>> callable([1, 2, 3])False>>> callable(None)False>>> callable('str')False
通过
callable()函数,我们就可以判断一个对象是否是“可调用”对象.
