第3章_Python进阶(二)

第3章_Python进阶(二)

          • 21.方法重写
          • 22.函数重载
          • 23.钻石继承
          • 24.MixIn混入类
          • 25.多态
          • 26.`__str__`和`__repr__`
          • 27.新类和旧类
          • 28.`MRO`

21.方法重写

重写是指子类重写父类的成员方法。子类可以改变父类方法所实现的功能, 但子类中重写的方法必须与父类中对应的方法具有相同的方法名。也就是说 要实现重写,就必须存在继承。1

class Person():
    def print_info(self):
        print("*************")
        
class ChinesePerson(Person):
    def print_info(self): 
    #子类重写父类的print_info方法
        print("________")

如果想在子类里面调用父类的同名方法:1

class Person():
    def __init__(self,name):
        self.name  = name
 
    def set_name(self,name):
        if len(name)>5:
            return
        self.name = name
 
class ChinesePeople(Person):
    def __init__(self,name,nation):
        Person.__init__(self,name)
        self.nation = nation

    def set_name(self,name):
	#子类中明确的调用父类中被重写的方法
        Person.set_name(self,name)
		#写法一
        #super(ChinesePeople,self).set_name(name)
		#写法二
		#super.set_name()
		#写法三

如果想用子类的实例调用父类被重写的方法,我们可以使用__class__

p = ChinesePeople("吴老师","汉")
p.__class__ = Person
p.set_name("abcd")
p.__class__ = ChinesePeople
# 调用完记得修改回来

__init____new____class__的其他说明参见234

22.函数重载

函数重载指的是有多个同名的函数,但是它们的签名或实现却不同。当调用一个重载函数 fn 时,程序会检验传递给函数的实参/形参,并据此而调用相应的实现。5

int area(int length, int breadth) {
  return length * breadth;
}
 
float area(int radius) {
  return 3.14 * radius * radius;
}

在以上例子中(用 c++编写),函数 area 被重载了两个实现。第一个函数接收两个参数(都是整数),表示矩形的长度和宽度,并返回矩形的面积。另一个函数只接收一个整型参数,表示圆的半径。
 
当我们像 area(7)这样调用函数 area时,它会调用第二个函数,而 area(3,4) 则会调用第一个函数。5

python是没有重载的,第二个同名的函数会把第一个覆盖掉。因为:1

  1. python参数不指定类型
  2. python参数可变

python不需要函数重载
 
函数重载主要是为了解决两个问题。

  1. 可变参数类型。
  2. 可变参数个数。
     

另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。
 
好吧,那么对于情况1,函数功能相同,但是参数类型不同,python 如何处理?答案是根本不需要处理,因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在python 中很可能是相同的代码,没有必要做成两个不同函数。
 
那么对于情况2 ,函数功能相同,但参数个数不同,python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。
 
好了,鉴于情况 1跟 情况2都有了解决方案,python自然就不需要函数重载了。6

但是在某些情况当中,我们又是需要重载的,例如上文的面积计算函数。对于这种情形,我们可以使用singledispatch装饰器:7

from functools import singledispatch
from collections import  abc

@singledispatch
def show(obj):
    print (obj, type(obj), "obj")

# 参数字符串
@show.register(str)
def _(text):
    print (text, type(text), "str")

# 参数int
@show.register(int)
def _(n):
    print (n, type(n), "int")


# 参数元祖或者字典均可
@show.register(tuple)
@show.register(dict)
def _(tup_dic):
    print (tup_dic, type(tup_dic), "int")
    
show(1)
show("xx")
show([1])
show((1,2,3))
show({"a":"b"})

# 1  int
# xx  str
# [1]  obj
# (1, 2, 3)  int
# {'a': 'b'}  int

singledispatch主要针对的是函数,但对于方法不友好,现在可以用singledispatchmethod来做。8

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

关于singledispatch的底层原理解释详见9,其机制类似于下文我们自行实现的函数重载。

我们也可以用装饰器自行实现函数重载,详见5

# 模块:overload.py
from inspect import getfullargspec
 
class Function(object):
  """Function is a wrap over standard python function
  An instance of this Function class is also callable
  just like the python function that it wrapped.
  When the instance is "called" like a function it fetches
  the function to be invoked from the virtual namespace and then
  invokes the same.
  """
  def __init__(self, fn):
    self.fn = fn
  
  def __call__(self, *args, **kwargs):
    """Overriding the __call__ function which makes the
    instance callable.
    """
    # fetching the function to be invoked from the virtual namespace
    # through the arguments.
    fn = Namespace.get_instance().get(self.fn, *args)
    if not fn:
      raise Exception("no matching function found.")
    # invoking the wrapped function and returning the value.
    return fn(*args, **kwargs)
 
  def key(self, args=None):
    """Returns the key that will uniquely identifies
    a function (even when it is overloaded).
    """
    if args is None:
      args = getfullargspec(self.fn).args
    return tuple([
      self.fn.__module__,
      self.fn.__class__,
      self.fn.__name__,
      len(args or []),
    ])
 
class Namespace(object):
  """Namespace is the singleton class that is responsible
  for holding all the functions.
  """
  __instance = None
    
  def __init__(self):
    if self.__instance is None:
      self.function_map = dict()
      Namespace.__instance = self
    else:
      raise Exception("cannot instantiate Namespace again.")
    
  @staticmethod
  def get_instance():
    if Namespace.__instance is None:
      Namespace()
    return Namespace.__instance
 
  def register(self, fn):
    """registers the function in the virtual namespace and returns
    an instance of callable Function that wraps the function fn.
    """
    func = Function(fn)
    specs = getfullargspec(fn)
    self.function_map[func.key()] = fn
    return func
  
  def get(self, fn, *args):
    """get returns the matching function from the virtual namespace.
    return None if it did not fund any matching function.
    """
    func = Function(fn)
    return self.function_map.get(func.key(args=args))
 
def overload(fn):
  """overload is the decorator that wraps the function
  and returns a callable object of type Function.
  """
  return Namespace.get_instance().register(fn)
from overload import overload
 
@overload
def area(length, breadth):
  return length * breadth
 
@overload
def area(radius):
  import math
  return math.pi * radius ** 2
 
@overload
def area(length, breadth, height):
  return 2 * (length * breadth + breadth * height + height * length)
 
@overload
def volume(length, breadth, height):
  return length * breadth * height
 
@overload
def area(length, breadth, height):
  return length + breadth + height
 
@overload
def area():
  return 0
 
print(f"area of cuboid with dimension (4, 3, 6) is: {area(4, 3, 6)}")
print(f"area of rectangle with dimension (7, 2) is: {area(7, 2)}")
print(f"area of circle with radius 7 is: {area(7)}")
print(f"area of nothing is: {area()}")
print(f"volume of cuboid with dimension (4, 3, 6) is: {volume(4, 3, 6)}")
23.钻石继承
A
B
C
D
class A(object):
	def __init__(self):
		print("A init")
class B(A):
	def __init__(self):
		A.__init__(self)
		print("B init")
class C(A):
	def __init__(self):
		A.__init__(self)
		print("C init")
class D(B, C):
	def __init__(self):
		B.__init__(self)
		C.__init__(self)
		print("D init")

d = D()

# 运行结果为:
# A init
# B init
# A init
# C init
# D init
# 从运行结果可以看到A被初始化了两次

class A(object):
  def __init__(self):
	print("A init")
class B(A):
  def __init__(self):
	super(B, self).__init__()
	print("B init")
class C(A):
  def __init__(self):
	super(C, self).__init__()
	print("C init")
class D(B, C):
  def __init__(self):
	super(D, self).__init__()
	print("D init")

d = D()

# 运行结果为:
# A init
# C init
# B init
# D init
# 可以看到使用super调用父类的构造函数A只初始化了一次

Python使用super方法解决钻石继承问题。其中super的内核是MRO(Method Resolution Order,函数解析顺序)MRO使用C3线性算法生成,通过类名.__mro__获取。一般来说,该顺序为当前类、继承类的继承序列、继承类序列的父类序列···,按照上文的例子就是[D, B, C, A]。当子类的多个父类有同一个方法,而子类使用时未指定,即该方法在子类中未找到时,解析器就按照MRO的列表查找父类是否包含该方法,查找到的第一个结果即为返回结果,这也就是上文当中提到的方法遮蔽问题。关于MRO等的其他解释详见10C3线性算法的解释详见1112

24.MixIn混入类

Mix-In是一种拼积木的思想,是多重继承的最后一层。12

class Animal(object):
    def __init__(self, name):
        self.name = name
 
    def eat(self):
        print('%s正在吃东西' % self.name)
 
    def breath(self):
        print('%s正在呼吸' % self.name)
 
class Person(Animal):
    def __init__(self, name, money):
        super().__init__(name)
        self.money = money
 
    def speak(self):
        print('%s说他有%s人民币' % (self.name, self.money))
 
class Spider(Animal):
    def climb(self):
        print('%s正在攀岩' % self.name)
 
    def tusi(self):
        print('%s正在吐丝' % self.name)
 
 
class Spiderman(Person, Spider):
    pass
 
Spiderman = Spiderman('Spiderman', 10)
Spiderman.tusi()
Spiderman.climb()
Spiderman.eat()
Spiderman.breath()

# Spiderman正在吐丝
# Spiderman正在攀岩
# Spiderman正在吃东西
# Spiderman正在呼吸

详细解释参见131415

Mix-In的设计也是一种Duck Type的类型14

25.多态

调用不同对象的相同方法,表现不一样,这就是多态。

import abc
class Animal(metaclass=abc.ABCMeta): 
#同一类事物:动物
    @abc.abstractmethod
    def talk(self):
        pass
        # raise AttributeError('子类必须实现这个方法')
# 上述代码子类是约定俗称的实现这个方法
# 加上@abc.abstractmethod装饰器后要求子类必须实现这个方法
# 但其虚拟子类不用强制实现

class Cat(Animal): 
#动物的形态之一:猫
    def talk(self):
        print('say miaomiao')

class Dog(Animal): 
#动物的形态之二:狗
    def talk(self):
        print('say wangwang')

class Pig(Animal): 
#动物的形态之三:猪
    def talk(self):
        print('say aoao')

c = Cat()
d = Dog()
p = Pig()

def func(obj):
    obj.talk()

func(c)
func(d)
func(p)

# ------------------------------
# 
# >>> say miaomiao
# >>> say wangwang
# >>> say aoao

调用不同的子类将会产生不同的行为,而无须明确知道这个子类实际上是什么,这是多态的重要应用场景。而在python中,因为鸭子类型(duck typing)使得其多态不是那么酷。
 
鸭子类型是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”16

class Duck():
    def walk(self):
         print('I walk like a duck')
    def swim(self):
         print('i swim like a duck')

class Person():
    def walk(self):
       print('this one walk like a duck') 
    def swim(self):
       print('this man swim like a duck')

可以很明显的看出,Person类拥有跟Duck类一样的方法,当有一个函数调用Duck类,并利用到了两个方法walk()swim()。我们传入Person类也一样可以运行,函数并不会检查对象的类型是不是Duck,只要他拥有walk()swim()方法,就可以正确的被调用。
 
再举例,如果一个对象实现了getitem方法,那python的解释器就会把它当做一个collection,就可以在这个对象上使用切片,获取子项等方法;如果一个对象实现了iternext方法,python就会认为它是一个iterator,就可以在这个对象上通过循环来获取各个子项。16

其详细解释参见16

Mix-In的设计也是一种Duck Type的类型14

关于abc.ABCMeta,上述代码子类是约定俗称的实现这个方法,加上@abc.abstractmethod装饰器后严格控制子类必须实现这个方法。17

abc.ABCMeta是一个metaclass,用于在Python程序中创建抽象基类。抽象基类可以不实现具体的方法(当然也可以实现,只不过子类如果想调用抽象基类中定义的接口需要使用super())而是将其留给派生类实现。抽象基类可以被子类直接继承,也可以将其他的类”注册“(register)到其门下当虚拟子类,虚拟子类的好处是你实现的第三方子类不需要直接继承自基类但是仍然可以声称自己子类中的方法实现了基类规定的接口(issubclass(), issubinstance())!18

抽象方法表示基类的一个方法,没有实现,所以基类不能实例化,子类实现了该抽象方法才能被实例化。19

26.__str____repr__

举例如下:20

>>> class Name:
...     def __init__(self,name):
...         self.name = name
...     def __repr__(self):
...         return 'Name: %s' % self.name
...
>>> student = Name('Jack')
>>> student
Name: Jack
>>> print(student)
Name: Jack

仅重构__repr__的情况下,直接输入对象名和调用打印方法输出的都是我们自定义的提示。如果一个对象没有__str__函数,而Python又需要调用它的时(调用print),解释器会用__repr__作为替代。21

>>> class Name:
...     def __init__(self,name):
...         self.name = name
...     def __str__(self):
...         return 'Name: %s' % self.name
...
>>> student = Name('Jack')
>>> student
<__main__.Name object at 0x00000207D3949DC8>
>>> print(student)
Name: Jack

仅重构__str__的情况下,直接输入对象名,返回的是内存地址;调用打印方法输出的都是我们自定义的提示

>>> class Name:
...     def __init__(self,name):
...         self.name = name
...     def __str__(self):
...         return '__str__'
...     def __repr__(self):
...         return '__repr__'
...
>>> student = Name('Jack')
>>> student
__repr__
>>> print(student)
__str__

当两者均被重构时,直接在终端输入对象名,调用的是__repr__;使用打印函数,调用的是__str__函数

另外,repr()函数会默认调用该对象的__repr__,对应的str()会调用__str__,同时,%r调用的是__repr__%s调用的是__str__22举例如下:

import datetime
today = datetime.datetime.today()
print(str(today))
>>> 2019-10-20 20:59:47.003003
print(repr(today))
>>> datetime.datetime(2019, 10, 20, 20, 59, 47, 3003)

__str__的返回结果可读性强。也就是说,__str__的意义是得到便于人们阅读的信息,就像上面的 '2019-10-20 20:59:47.003003' 一样。
 
__repr__的返回结果应更准确。怎么说,__repr__ 存在的目的在于调试,便于开发者使用。将 __repr__ 返回的方式直接复制到命令行上,是可以直接执行的。
 
推荐类中至少添加一个__repr__ 方法,因为__str__的默认实现就是调用__repr__方法。23

27.新类和旧类
  • 相关定义

Python中的类分为新类和旧类。旧类是Python3之前的类,旧类并不是默认继承object类,而是继承type类。
 
Python2中的旧类如下面代码所示:

class oldStyleClass: 
# inherits from 'type'
   pass

Python2中定义一个新类:

class newStyleClass(object): 
# explicitly inherits from 'object'
   pass

Python3中所有的类均默认继承object,所以并不需要显式地指定object为基类。
 
object为基类可以使得所定义的类具有新类所对应的方法(methods)和属性(properties)。24

经典类:classic class
新式类:new-style class

  • python2.2 之前并没有新式类
  • python2.2-2.7 新式类与经典类并存, 默认使用经典类, 除非显式继承object
  • python3.X 中去除了经典类, 用户定义的所有类都隐式继承自object25
  • 统一类(class)和类型(type)

2.2之前,比如2.1版本中,类和类型是不同的,如aClassA的一个实例,那么a.__class__返回 class __main__.ClassAtype(a)返回总是。而引入新类后,比如ClassB是个新类,bClassB的实例,b.__class__type(b)都是返回class '__main__.ClassB',这样就统一了。
 
引入新类后,还有其他的好处,比如更多的内置属性将会引入,描述符的引入,属性可以来计算等等。
 
为了向前兼容,默认情况下用户定义的类为经典类,新类需要继承自所有类的基类object或者继承自object的新类。26

class oldClass:      #经典类
	def __init__( self ):
		pass

class newClass(object):  #新类
	def __init__( self ):
		pass 

c1 = oldClass()
c2 = newClass()

c1.__class__      
# 输出-> 
type(c1)        
# 输出-> 
 
c2.__class__      
# 输出->
type(c2)        
# 输出->

前文提到,在Python 3.x中,所有的类都显式或隐式的派生自object类,type类也不例外。类型自身派生自object类,而object类派生自type,二者组成了一个循环的关系。27
 
通过以下代码来验证

isinstance(object, type)
# True
isinstance(type, object)
# True
  • MRO算法的区别
  • 旧式类 MRO 算法:从左往右,采用深度优先搜索(DFS),从左往右的算法,称为旧式类的 MRO
  • 新式类MRO 算法:自 Python 2.2版本开始,新式类在采用深度优先搜索算法的基础上,对其做了优化
  • C3算法:自 Python 2.3 版本,对新式类采用了 C3算法;由于Python 3.x 仅支持新式类,所以该版本只使用C3算法

C3算法的部分结果与广度优先一致,但其顺序并不是广度优先,举例如下:28

Object
A
C
B
SubClass
class NewStyleClassA(object):
	var = 'New Style Class A'

class NewStyleClassB(NewStyleClassA):
	pass

class NewStyleClassC(object):
	var = 'New Style Class C'

class SubNewStyleClass(NewStyleClassB, NewStyleClassC):
	pass

print(SubNewStyleClass.mro())
print(SubNewStyleClass.var)

# [
# , 
# , 
# , 
# , 
# 
# ]
# New Style Class A

如果按照广度优先应该是Sub->B->C->A->Object

关于C3算法的解释详见29

  • __new____init__的区别

__new____init__的主要区别在于:__new__是用来创造一个类的实例的(constructor),而__init__是用来初始化一个实例的(initializer)。
 
__new__所接收的第一个参数是cls,而__init__所接收的第一个参数是self。这是因为当我们调用__new__的时候,该类的实例还并不存在(也就是self所引用的对象还不存在),所以需要接收一个类作为参数,从而产生一个实例。而当我们调用__init__的时候,实例已经存在,因此__init__接受self作为第一个参数并对该实例进行必要的初始化操作。这也意味着__init__是在__new__之后被调用的。
 
Python的旧类中实际上并没有__new__方法。因为旧类中的__init__实际上起构造器的作用。
 
总结:

  • __init__不能有返回值
  • __new__函数直接上可以返回别的类的实例。如上面例子中的returnExistedObj类的__new__函数返回了一个int值。
  • 只有在__new__返回一个新创建属于该类的实例时当前类的__init__才会被调用。

 
详见24

  • 内置属性__slots__25

而除了继承顺序的差异, 新式类还添加了内置属性__slots__
 
一般来说, 每个实例都有一个字典来管理实例的属性, 我们可以用__dict__来查看(__dict__并不保存类属性),它允许我们动态地修改实例的属性, 但是这也意味着每个实例都会有1个独立的字典需要我们去维护, 当我们需要创建大量的实例时, 这个操作是十分消耗内存的.
 
当我们在定义类时添加了__slots__属性后, 对象在实例化时就不会创建字典来管理实例属性, 而实例只能定义在__slots__里边已经设定好的属性名, 不允许动态添加其他未在__slots__里定义的属性

class Student(object):
	__slots__ = ('id', 'name', 'gender')
	def exam(self):
		pass

s1 = Student()
'__dict__' in dir(s1) # False
s1.id = 10001
s1.class = 1 
# AttributeError: 'Student' object has no attribute 'class'

def func():
	pass

s1.exam = func 
# AttributeError: 'Student' object attribute 'f' is read-only

使用__slots__后我们不再能够动态地修改实例的属性, 那么使用__slots__究竟有什么好处呢?
 
优点:

  1. 节省内存
  2. 提高属性访问速度

 
缺点:

  1. 不能动态修改实例属性

 
当然, 除了继承顺序和__slots__, 新式类添加了__getattribute__方法, 还修改了实例的类型

28.MRO

关于C3线性算法详见1129等,此处暂且不表,只需记得不是广度优先,后面有时间再补充这块。


  1. python学习笔记之—重写与重载 ↩︎ ↩︎ ↩︎

  2. Python常用内建方法:init,new,__class__的使用详解 ↩︎

  3. python使用__new__实现单例模式创建对象 ↩︎

  4. python类属性__class__的理解 ↩︎

  5. 为什么 Python 没有函数重载?如何用装饰器实现函数重载? ↩︎ ↩︎ ↩︎

  6. 为什么 Python 不支持函数重载?而其他语言大都支持? - pansz的回答 - 知乎 ↩︎

  7. python基础-单分派泛函数singledispatch ↩︎

  8. functools.singledispatchmethod(Python 3.8) ↩︎

  9. Python中singledispatch装饰器实现函数重载 ↩︎

  10. Python的继承及super的用法 ↩︎

  11. C3线性优化算法 ↩︎ ↩︎

  12. Python3–继承,多继承 ↩︎ ↩︎

  13. python 混入类MixIn ↩︎

  14. Mixin是什么概念? - 松鼠奥利奥的回答 - 知乎 ↩︎ ↩︎ ↩︎

  15. Is-a、has-a、Like-a区别 ↩︎

  16. python中对多态的理解 ↩︎ ↩︎ ↩︎

  17. 五. python面向对象(多态 和metaclass=abc.ABCMeta) ↩︎

  18. Python装饰器、metaclass、abc模块学习笔记 ↩︎

  19. @abstractmethod的用法 ↩︎

  20. Python学习:__repr__和__str__区别 ↩︎

  21. repr__和__str ↩︎

  22. python中%r和%s的区别 ↩︎

  23. 浅谈python中__str__和__repr__的区别 ↩︎

  24. Python中的__new__和__init__的区别 ↩︎ ↩︎

  25. 浅谈python之新式类 ↩︎ ↩︎

  26. 浅谈python新式类和旧式类区别 ↩︎

  27. Python中的类与类型示例详解 ↩︎

  28. Python中新式类与经典类的区别详析 ↩︎

  29. Python学习之MRO方法搜索顺序 ↩︎ ↩︎

你可能感兴趣的:(Python程序员面试笔试宝典,python,开发语言,后端)