学习总结:Python高级编程第2版-语法最佳实践-类级别以上
子类化内置类型
访问超类中的方法
高级属性访问模式
子类化内置类型 Python中的类都有一个共同的祖先object
。当需要实现与某个内置类型相似的行为时,可以将这个内置类型子类化。
子类化dict 我们创建子类 distinctdict 继承 dict ,添加新元素时不允许多个键对应相同的键值,否则触发自定义异常DistinctError
.
1 2 3 4 5 6 7 8 9 class DistinctError (ValueError ): """如果向distinctdict添加重复value,则触发该异常""" class distinctdict (dict ): def __setitem__ (self, key, value ): if value in self.values(): if (key in self and self[key] != value) or key not in self: raise DistinceError("This value already exists for different key" ) super ().__setitem__(key, value)
子类化list 我们创建子类Folder继承list,管理序列。
1 2 3 4 5 6 7 8 9 10 11 12 class Folder (list ): def __init__ (self, name ): self.name = name def dir (self, nesting=0 ): offset = " " *nesting print ("%s%s/" % (offset, self.name)) for element in self: if hasattr (element, 'dir' ): element.dir (nesting+1 ) else : print ("%s %s" % (offset, element))
在交互式shell中测试,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 >>> tree = Folder('project' )>>> tree.append('README.md' )>>> tree.dir ()project/ README.md >>> src = Folder("src" )>>> src.append("script.py" )>>> tree.append(src)>>> tree.dir ()project/ README.md src/ script.py
访问超类中的方法 super
是一个内置类,可用于访问某个对象的超类的属性。
Python官方文档将super作为内置函数给出,虽然它的用法与函数类似,但实际上它是一个内置类。
1 2 >>> super <class 'super '>
旧的写法与新的写法 旧的写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Mama : def say_hello (self ): print ("Mama: Hello" ) def says (self ): print ("do your homework" ) self.say_hello() class Sister (Mama ): def say_hello (self ): print ("Sister: Hello" ) def says (self ): Mama.says(self) print ("and clean your bedroom" )
上述例子中Mama.says(self)
表明调用超类中的方法,传入的是子类本身self, 下面是执行案例。
1 2 3 4 >>> Sister().says()do your homework Sister: Hello and clean your bedroom
下面展示使用super的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Mama : def say_hello (self ): print ("Mama: Hello" ) def says (self ): print ("do your homework" ) self.say_hello() class Sister (Mama ): def say_hello (self ): print ("Sister: Hello" ) def says (self ): super (Sister, self).says() print ("and clean your bedroom" )
super 可以在方法内部使用,也可以在方法外部使用,下面的例子是在方法外部使用。
1 2 3 4 5 6 >>> anita = Sister()>>> anita.__class__<class '__main__ .Sister '> >>> super (anita.__class__, anita ).says () do your homework Sister : Hello
super 的第二个参数是可选的,如果只提供第一个参数,那么super返回的是一个未绑定的类型,这一点与classmethod 在一起使用时特别有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Pizza : def __init__ (self, toppings ): self.toppings = toppings def __repr__ (self ): return "Pizza with: " + " and " .join(self.toppings) @classmethod def recommend (cls ): return cls(["spam" , "ham" , "eggs" ]) class VikingPizza (Pizza ): @classmethod def recommend (cls ): recommended = super ().recommend() recommended.toppings += ['spam' ] * 5 return recommended
多继承中super将变得非常难用
需要理解何时避免使用 super
需要理解方法解析顺序(Method Resolution Order, MRO)
理解Python的MRO Python的解析方法基于C3,C3是为Dylan编程语言构建的MRO,参考文档位于 https://www.python.org/download/releases/2.3/mro/ 。描述了C3是如何构建一个类的线性化(优先级,祖先的有序列表)。
L[MyClass(Basel, Base2)] = MyClass + merge(L[Basel], L[Base2], Basel, Base2)
C的线性化是C加上父类的线性化和父类列表的合并的总和
merge算法负责删除重复项并保持正确的顺序
类的__mro__
属性保存了线性化的计算结果,也可以调用 .mro()
获取。
使用super易犯的错误 如果在多继承下使用了 super ,这是非常危险的,主要原因在于类的初始化。在Python中,基类不会在__init__()
中被隐式的调用,所以需要开发人员来调用它们。
混用super与显示类调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class A : def __init__ (self ): print ("A" , end=" " ) super ().__init__() class B : def __init__ (self ): print ("B" , end=" " ) super ().__init__() class C (A, B ): def __init__ (self ): print ("C" , end=" " ) A.__init__(self) B.__init__(self) >>> C.mro()[<class '__main__ .C '>, <class '__main__ .A '>, <class '__main__ .B '>, <class 'object '>] >>> print ("MRO:" , [x.__name__ for x in C.mro( )] ) MRO : ['C' , 'A' , 'B' , 'object' ]>>> C()C A B B <__main__.C object at 0x0000028B7F5FBD08 >
出现上面输出的原因是C的实例调用了A的__init__()
函数,使得super(A, self).__init__()
调用了B.__init__()
方法。
super 应被用到整个类的层次结构中,但不幸的是,有时这种层次结构位于第三方代码中,你无法确定外部包代码中是否使用了 super 。如果需要对第三方类进行子类化,最好总是查看其内部代码以及MRO中其他类的内部代码。
不同种类的参数 使用 super 的另一个问题是初始化过程中的参数传递。如果没有相同的签名,一个类怎么能调用其基类的__init__()
代码呢?这会导致下列问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class CommonBase : def __init__ (self ): print ("CommonBase" ) super ().__init__() class Base1 (CommonBase ): def __init__ (self ): print ("Base1" ) super ().__init__() class Base2 (CommonBase ): def __init__ (self, arg ): print ("Base2" ) super ().__init__() class MyClass (Base1, Base2 ): def __init__ (self, arg ): print ("MyClass" ) super ().__init__(arg) >>> MyClass()Traceback (most recent call last): File "<stdin>" , line 1 , in <module> TypeError: __init__() missing 1 required positional argument: 'arg'
创建MyClass会报错,原因是其与父类Bas1的__init__()
签名不匹配。
解决这种问题的办法之一是将所有类的参数都写成*args, **kwargs
这种形式,但这会导致代码很脆弱,任何参数都能被接受。
另一种方式是在MyClass中显示调用__init__()
,但这会导致和上一小节相同的问题。
最佳实践
避免使用多继承 :使用设计模式来代替它
super的使用必须一致 :在类的层次结构中,要么不用super,要么全用super
调用父类时必须查看类的层次结构
高级属性访问模式 C++和JAVA中有private关键字保护属性,但是在Python中与其最接近的是名称修饰 (name mangling),每当在一个属性前面加上__
前缀,解释器就会立即将其重命名 。
1 2 3 4 5 6 7 8 9 10 11 >>> class A :... __secret = 1 ... >>> a = A()>>> a.__secretTraceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: 'A' object has no attribute '__secret' >>> dir (a)['_A__secret' , '__class__' , '__delattr__' , '__dict__' , '__dir__' , '__doc__' , '__eq__' , '__format__' , '__ge__' , '__getattribute__' , '__gt__' , '__hash__' , '__init__' , '__init_subclass__' , '__le__' , '__lt__' , '__module__' , '__ne__' , '__new__' , '__reduce__' , '__reduce_ex__' , '__repr__' , '__setattr__' , '__sizeof__' , '__str__' , '__subclasshook__' , '__weakref__' ] >>>
可以看出 __secret
属性被重命名为_A__secret
。
Python提供这一特性是为了避免继承中的名称冲突 。
在Python中,如果一个属性不是公有的,约定以前缀_
开头,这是流行的写法。
Python中还有其他可用机制来构建类的公有代码和私有代码,应该使用descriptor和property这些OOP设计的关键特性来设计一个清晰的API。
descriptor - 描述符
描述符允许我们自定义在引用一个对象的属性应该完成的事情。
描述符是Python中复杂属性访问的基础。
描述符在内部用于实现 property
、方法
、类方法
、静态方法
、super类型
。
描述符是一个类,定义了另一个类的属性访问方式,即一个类可以将属性管理委托给另一个类。
描述符协议 :描述符类基于下面三个特殊方法
__set__(self, obj, type=None)
: 设置属性时将调用这一方法,称为setter
__get__(self, obj, value)
: 获取属性时将调用这一方法,称为getter
__delete__(self, obj)
: 对属性调用 del 或 调用delattr方法时将调用这一方法
数据描述符 :实现了__set__
和__get__
的描述符
非数据描述符 :只实现了__get__
的描述符
在每次属性查找时,这个协议的方法实际上由对象的特殊方法__getattribute__()
调用(不要与__getattr__()
混淆,后者用于其他目的)。
每次通过点号,形式为(instance.attribute),或者getattr(instance, 'attribute')
函数执行来执行这样的查找时,都会隐式地调用__getattribute__()
方法,它按下列顺序查找属性:
验证该属性是否为对象的数据描述符
如果不是,查看该属性是否能在实例对象的__dict__
中找到。
最后,查看该属性是否为对象的非数据描述符
一个简单的例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class RevealAccess : """一个数据描述符""" def __init__ (self, initval=None , name="var" ): self.val = initval self.name = name def __get__ (self, obj, objtype ): print ("Get" , self.name) return self.val def __set__ (self, obj, val ): print ("Set" , self.name) self.val = val class MyClass : x = RevealAccess(10 , 'var "x"' ) y = 5 >>> m = MyClass()>>> m.xGet var "x" 10 >>> m.x = 20 Set var "x" >>> m.xGet var "x" 20 >>> m.y5
数据描述符和非数据描述符的区别很重要。
函数和lambda表达式是非数据描述符:
1 2 3 4 5 6 7 8 9 10 >>> def func (): pass ... >>> hasattr (func, '__get__' )True >>> hasattr (fun, '__set__' )False >>> hasattr (lambda : None , '__get__' )True >>> hasattr (lambda : None , '__set__' )False
因此,如果没有__dict__
优先于非数据描述符,我们不可能在运行时在已经构建好的实例上动态覆写特定的方法。幸运的是,多亏了Python的描述符工作方式,由于这一工作方法,使得开发人员可以使用一种叫做猴子补丁 (monkey-patching)的流行技术来改变实例的工作方式,而不需要子类化。
例子 - 延迟求值属性 将类属性的初始化延迟到被实例访问时。
情况一:这些属性的初始化依赖全局上下文
情况二:初始化的代价很大,在导入类的时候不知道是否会用到这个属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class InitOnAccess : def __init__ (self, klass, *args, **kwargs ): self.klass = klass self.args = args self.kwargs = kwargs self._initialized = None def __get__ (self, instance, owner ): if self._initialized is None : self._initialized = self.klass(*self.args, **self.kwargs) print ("initialized" ) else : print ("cached" ) return self._initialized >>> class MyClass :... value = InitOnAccess(list , "argument" )... >>> m = MyClass()>>> m.valueinitialized ['a' , 'r' , 'g' , 'u' , 'm' , 'e' , 'n' , 't' ] >>> m.valuecached ['a' , 'r' , 'g' , 'u' , 'm' , 'e' , 'n' , 't' ]
property property提供了一个内置的描述符类型,它知道如何将一个属性链接到一组方法上。
property接受4个可选参数:fget、fset、fdel和doc。最后一个参数可以用来定义一个链接到属性的 docstring,就像是一个方法一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Rectangle : def __init__ (self, x1, y1, x2, y2 ): self.x1, self.y1 = x1, y1 self.x2, self.y2 = x2, y2 @property def width (self ): """rectangle width""" return self.x2 - self.x1 @width.setter def width (self, value ): self.x2 = self.x1 + value @property def height (self ): """rectangle height""" return self.y2 - self.y1 @height.setter def height (self, value ): self.y2 = self.y1 + value def __repr__ (self ): return "{}({}, {}, {}, {})" .format ( self.__class__.__name__, self.x1, self.y1, self.x2, self.y2 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 >>> rec = Rectangle(10 , 10 , 25 , 24 )>>> rec.width, rec.height(15 , 14 ) >>> rec.width = 100 >>> recRectangle(10 , 10 , 110 , 24 ) >>> rec.height = 100 >>> recRectangle(10 , 10 , 110 , 110 ) >>> help (Rectangle)Help on class Rectangle in module __main__ : class Rectangle (builtins.object ) | Rectangle (x1, y1, x2, y2 ) | | Methods defined here : | | __init__(self, x1, y1, x2, y2) | Initialize self. See help (type (self)) for accurate signature. | | __repr__(self) | Return repr (self). | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __dict__ | dictionary for instance variables (if defined) | | __weakref__ | list of weak references to the object (if defined) | | height | rectangle height | | width | rectangle width
槽 允许开发人员使用__slots__
属性为指定的类设置一个静态属性列表,并在类的每个实例中跳过__dict__
字典的创建过程,它可以为属性很少的类解决内存空间,因为每个实例都没有创建__dict__
。
除此之外,它还可以有助于签名需要被冻结的类,但是对派生类就解除了这个限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 >>> class Frozen :... __slots__ = ['ice' , 'cream' ]... >>> '__dict__' in dir (Frozen)False >>> 'ice' in dir (Frozen)True >>> frozen = Frozen()>>> frozen.ice = True >>> frozen.cream = None >>> frozen.icy = True Traceback (most recent call last): File "<stdin>" , line 1 , in <module> AttributeError: 'Frozen' object has no attribute 'icy'