type和isinstance用法

  • isinstance() 与 type() 区别:
    • type() 不会认为子类是一种父类类型,不考虑继承关系。
    • isinstance() 会认为子类是一种父类类型,考虑继承关系。
  • 如果要判断两个类型是否相同推荐使用 isinstance()。
  • 语法:

    1. isinstance(object, classinfo)
    2. # example
    3. >>>a = 2
    4. >>> isinstance (a,int)
    5. True
    6. >>> isinstance (a,str)
    7. False
    8. >>> isinstance (a,(str,int,list)) # 是元组中的一个返回 True
    9. True
    • object — 实例对象。
    • classinfo — 可以是直接或间接类名、基本类型或者由它们组成的元组。

      注意字符串的classinfo为str而非string,字典也是简写dict

  • 返回值

    • 如果对象的类型与参数二的类型(classinfo)相同则返回 True,否则返回 False。

      元组相关

      1、(num, ),是元组
      1. num=1
      2. type((num, ))
      3. # <class 'tuple'>

面向对象编程OOP

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student,比如,Bart Simpson和Lisa Simpson是两个具体的Student。
所以,面向对象的设计思想是抽象出Class,根据Class创建Instance。
面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法。

类和实例

:::info self代表类的实例,而非类
类的方法与普通的函数只有一个特别的区别——它们必须有一个额外的第一个参数名称, 按照惯例它的名称是 self。
类有一个名为 init() 的特殊方法(构造方法),该方法在类实例化时会自动调用 :::

  • 在Python中,定义类是通过class关键字:

    1. class Student(object):
    2. pass
    • class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的,继承的概念我们后面再讲,通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。
  • 定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的:

    1. >>> bart = Student()
    2. >>> bart.name = 'Bart Simpson'
    3. >>> bart.name
    4. 'Bart Simpson'
    • 可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性.
  • 由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把namescore等属性绑上去:

    1. class Student(object):
    2. def __init__(self, name, score):
    3. self.name = name
    4. self.score = score
  • 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。 :::info 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同: :::

    1. >>> bart = Student('Bart Simpson', 59)
    2. >>> lisa = Student('Lisa Simpson', 87)
    3. >>> bart.age = 8
    4. >>> bart.age
    5. 8
    6. >>> lisa.age
    7. Traceback (most recent call last):
    8. File "<stdin>", line 1, in <module>
    9. AttributeError: 'Student' object has no attribute 'age'

    访问限制

  • 在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。

  • 但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的namescore属性:
  • 如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:

    1. class Student(object):
    2. def __init__(self, name, score):
    3. self.__name = name
    4. self.__score = score
    5. def print_score(self):
    6. print('%s: %s' % (self.__name, self.__score))
    • 改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name实例变量.__score
    • 这样就确保了外部代码不能随意修改对象内部的状态,这样通过访问限制的保护,代码更加健壮。但是如果外部代码要获取name和score怎么办?可以给Student类增加get_nameget_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):

    1. self.name = name
    2. self.age = age

    def call(self):

    1. 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的方法

  1. > **注意:**一定要用 super(Cat, self).__init__(name,age) 去初始化父类,否则,继承自 Animal Cat子类将没有 nameage两个属性。
  2. > python3中可以直接super().__init__(name, age),效果是一样的。
  3. > 函数super(Cat, self)将返回当前类继承的父类,即 Animal,然后调用__init__()方法,注意self参数已在super()中传入,在__init__()中将隐式传递,不能再写出self
  4. 参考:[类的继承,调用父类的属性和方法基础详解](https://blog.csdn.net/yilulvxing/article/details/85374142);[类的继承](https://zhuanlan.zhihu.com/p/30239694)<br />**类继承注意事项**<br />在继承中基类的构造方法(__init__()方法)不会被自动调用,它需要在其派生类的构造方法中亲自专门调用。<br />Python 总是首先查找对应类的方法,如果它不能在派生类中找到对应的方法,它才开始到基类中逐个查找。(先在本类中查找调用的方法,找不到才去基类中找)<br />**对父类方法的重构** <br />类方法的调用顺序,当我们在子类中重构父类的方法后,Cat子类的实例先会在自己的类 Cat 中查找该方法,当找不到该方法时才会去父类 Animal 中查找对应的方法。
  5. ```python
  6. #!/usr/bin/python3
  7. class Parent: # 定义父类
  8. def myMethod(self):
  9. print ('调用父类方法')
  10. class Child(Parent): # 定义子类
  11. def myMethod(self):
  12. print ('调用子类方法')
  13. c = Child() # 子类实例
  14. c.myMethod() # 子类调用重写方法
  15. super(Child,c).myMethod() #用子类对象调用父类已被覆盖的方法

子类与父类的关系
子类与父类的关系是 “is” 的关系,如上 Cat 继承于 Animal 类,我们可以说:
“A”是 Animal 类的实例,但,“A”不是 Cat 类的实例。
“C”是 Animal 类的实例,“C”也是 Cat 类的实例。
判断对象之间的关系,我们可以通过 isinstance (变量,类型) 来进行判断
多态
类具有继承关系,并且子类类型可以向上转型看做父类类型

  1. def do(all):
  2. all.call()
  3. A = Animal('小黑',4)
  4. C = Cat('喵喵', 2, '男')
  5. D = Dog('旺财', 5, '女')
  6. for x in (A,C,D):
  7. do(x)

这种行为称为多态。也就是说,方法调用将作用在 all 的实际类型上。C 是 Cat 类型,它实际上拥有自己的 call() 方法以及从 Animal 继承的 call 方法,但调用 C .call() 总是先查找它自身的定义,如果没有定义,则顺着继承链向上查找,直到在某个父类中找到为止。
传递给函数 do(all) 的参数 all 不一定是 Animal 或 Animal 的子类型。任何数据类型的实例都可以,只要它有一个 call() 的方法即可。其他类不继承于 Animal,具备 call 方法也可以使用 do 函数。这就是动态语言,动态语言调用实例方法,不检查类型,只要方法存在,参数正确,就可以调用。

继承和多态

继承在此不未做笔记。
要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

  1. def run_twice(animal):
  2. animal.run()
  3. animal.run()
  • 你会发现,新增一个Animal的子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。
  • 多态的好处就是,当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了,因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可。由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思:
  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

    • 对扩展开放:允许新增Animal子类;
    • 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

      静态语言 vs. 动态语言

  • 对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法。

  • 对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。
    • 这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
  • 如果你学的是Java或者C++等静态语言,可能对鸭子类型的理解没那么深刻,因为静态语言中对象的特性取决于其父类。
  • 而动态语言则不一样,比如迭代器,任何实现了 iternext 方法的对象都可称之为迭代器,但对象本身是什么类型不受限制,可以自定义为任何类
  • 鸭子类型依赖文档、清晰的代码和测试来确保正确使用 。这既是优点也是缺点,缺点是需要通过文档才能知道参数类型,为了弥补这方面的不足,Python3.6 引入了类型信息,定义变量的时候可以指定类型

动态语言的鸭子类型特点决定了继承不像静态语言那样是必须的。

获取对象信息

如果要获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

  1. >>> dir('ABC')
  2. ['__add__', '__class__',..., '__subclasshook__', 'capitalize', 'casefold',..., 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

  1. >>> len('ABC')
  2. >>> 'ABC'.__len__()

我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

  1. >>> class MyDog(object):
  2. ... def __len__(self):
  3. ... return 100
  4. >>> dog = MyDog()
  5. >>> len(dog)

实例属性和类属性

如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:
当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。

  1. class Student(object):
  2. 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: 乘方

运算符重载

  1. #!/usr/bin/python3
  2. class Vector:
  3. def __init__(self, a, b):
  4. self.a = a
  5. self.b = b
  6. def __str__(self):
  7. return 'Vector (%d, %d)' % (self.a, self.b)
  8. def __add__(self,other):
  9. return Vector(self.a + other.a, self.b + other.b)
  10. v1 = Vector(2,10)
  11. v2 = Vector(5,-2)
  12. print (v1 + v2)

看到类似__slots__这种形如__xxx__的变量或者函数名就要注意,这些在Python中是有特殊用途的。
__slots__我们已经知道怎么用了,__len__()方法我们也知道是为了能让class作用于len()函数。
除此之外,Python的class中还有许多这样有特殊用途的函数,可以帮助我们定制类。

str

类中定义的方法,用来定义打印类对象时的输出。

如果直接打变量而不用print,打印出来的实例还是不好看。这是因为直接显示变量调用的是repr()。 两者的区别是__str__()返回用户看到的字符串,而__repr__()返回程序开发者看到的字符串,也就是说,__repr__()是为调试服务的。

解决办法是再定义一个__repr__()。但是通常__str__()__repr__()代码都是一样的,所以,有个偷懒的写法:

  1. class Student(object):
  2. def __init__(self, name):
  3. self.name = name
  4. def __str__(self):
  5. return 'Student object (name=%s)' % self.name
  6. __repr__ = __str__

iter

如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。
我们以斐波那契数列为例,写一个Fib类,可以作用于for循环:

  1. class Fib(object):
  2. def __init__(self):
  3. self.a, self.b = 0, 1 # 初始化两个计数器a,b
  4. def __iter__(self):
  5. return self # 实例本身就是迭代对象,故返回自己
  6. def __next__(self):
  7. self.a, self.b = self.b, self.a + self.b # 计算下一个值
  8. if self.a > 100000: # 退出循环的条件
  9. raise StopIteration()
  10. return self.a # 返回下一个值

getitem

Fib实例虽然能作用于for循环,看起来和list有点像,但是,把它当成list来使用还是不行,比如,取第5个元素会报错。要表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

  1. class Fib(object):
  2. def __getitem__(self, n):
  3. a, b = 1, 1
  4. for x in range(n):
  5. a, b = b, a + b
  6. return a
  7. # 现在就可以按下标访问数列的每一项了
  8. f = Fib()
  9. f[0]

但是list有个切片方法,对于Fib仍然报错。原因是__getitem__()传入的参数可能是一个int,也可能是一个切片对象slice,所以要做判断:

  1. class Fib(object):
  2. def __getitem__(self, n):
  3. if isinstance(n, int): # n是索引
  4. a, b = 1, 1
  5. for x in range(n):
  6. a, b = b, a + b
  7. return a
  8. if isinstance(n, slice): # n是切片
  9. start = n.start
  10. stop = n.stop
  11. if start is None:
  12. start = 0
  13. a, b = 1, 1
  14. L = []
  15. for x in range(stop):
  16. if x >= start:
  17. L.append(a)
  18. a, b = b, a + b
  19. return 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__()方法,就可以直接对实例进行调用。请看示例:

    1. class Student(object):
    2. def __init__(self, name):
    3. self.name = name
    4. def __call__(self):
    5. print('My name is %s.' % self.name)
    6. # 调用方式如下
    7. >>> s = Student('Michael')
    8. >>> s() # self参数不要传入
    9. My name is Michael.
  • __call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以你完全可以把对象看成函数,把函数看成对象,因为这两者之间本来就没啥根本的区别。 :::info 如果你把对象看成函数,那么函数本身其实也可以在运行期动态创建出来,因为类的实例都是运行期创建出来的,这么一来,我们就模糊了对象和函数的界限。 ::: 那么,怎么判断一个变量是对象还是函数呢?其实,更多的时候,我们需要判断一个对象是否能被调用,能被调用的对象就是一个Callable对象,比如函数和我们上面定义的带有__call__()的类实例:

    1. >>> callable(Student())
    2. True
    3. >>> callable(max)
    4. True
    5. >>> callable([1, 2, 3])
    6. False
    7. >>> callable(None)
    8. False
    9. >>> callable('str')
    10. False

    通过callable()函数,我们就可以判断一个对象是否是“可调用”对象.