第3章 语法最佳实践——类级

本章记录:

  • 子类化内建类型
  • 访问超类中的方法
  • 元编程

3.1 子类化内建类型

内建类型object是所有内建类型的公共祖先。

当需要实现一个与内建类型十分类似的类时,可以使用例如list、tuple、dict这样的内建类型进行子类化。

可以继承父类中的方法在子类中使用,可以让内建类型适用在更多场景下,例如下面这个不能有重复元素的list:

In [17]: class ListError(Exception):
   ....:     pass
   ....: 

In [18]: class SingleList(list):
   ....:     def append(self, elem):
   ....:         if elem in self:
   ....:             raise ListError("{0} aleady in SingleList")
   ....:         else:
   ....:             super(SingleList, self).append(elem)
   ....:             

In [19]: sl = SingleList()

In [20]: sl.append('a')

In [21]: sl.append('b')

In [22]: sl.append('c')

In [23]: sl.append('a')
-----------------------------------------------------------------------
ListError                                 Traceback (most recent call l
 in ()
----> 1 sl.append('a')

 in append(self, elem)
      2     def append(self, elem):
      3         if elem in self:
----> 4             raise ListError("{0} aleady in SingleList")
      5         else:
      6             super(SingleList, self).append(elem)

ListError: {0} aleady in SingleList

3.2 访问超类中的方法

super是一个内建类型,用来访问属于某个对象的超类中的特性(attribute)

访问父类的方法有两种:

  1. super(子类名, self) . 父类方法()
  1. 父类名 . 父类方法(self)

暂且称为super法和self法

先定义一个父类Animal

In [35]: class Animal(object):
   ....:     def walk(self):
   ....:         print "Animal can walk"
   ....:     

super使用父类方法

In [36]: class People(Animal):
   ....:     def fly(self):
   ....:         super(People, self).walk()
   ....:         print "People can fly"
   ....:         

In [37]: person = People()

In [38]: person.fly()
Animal can walk
People can fly

self

In [40]: class People(Animal):
   ....:     def run(self):
   ....:         Animal.walk(self)
   ....:         print "People can run"
   ....:         

In [41]: person = People()

In [42]: person.run()
Animal can walk
People can run

其中super是新方法,对比下两种方法,关键的不同之处在于调用时候self法使用父类名字,super法使用子类名字。这就使得在多继承的时候super变的极其难用。

3.2.1 理解Python中方法解析顺序(MRO)

Python2.3中添加了基于Dylan构建的MRO,即C3的一个新的MRO,它描述了C3构建一个类的线性化(也称优先级,即祖先的一个排序列表)的方法。这个列表被用于特性的查找

书中描述的很理论化,这个本身就是可很理想的假设,因为没有人会这么设计。

说白了,以前的MRO是深度优先,现在的是广度优先。

3.2.2 super的缺陷

在Python中子类不会自动调用父类的__init__,所以手动的调用。

1. 混用super和传统调用

定义两个基类A、B:

In [1]: class A(object):
   ...:     def __init__(self):
   ...:         print 'A'
   ...:         super(A, self).__init__()
   ...:         

In [2]: class B(object):
   ...:     def __init__(self):
   ...:         print 'B'
   ...:         super(B, self).__init__()
   ...:         

定义一个C类继承A、B:

In [3]: class C(A, B):
   ...:     def __init__(self):
   ...:         print 'C'
   ...:         A.__init__(self)
   ...:         B.__init__(self)
   ...:   

这里在C类中使用super和基类使用传统调用,输出结果

In [4]: print "MRO", [x.__name__ for x in C.__mro__]
MRO ['C', 'A', 'B', 'object']

In [5]: C()
C
A
B
B
Out[5]: <__main__.C at 0x7fd2457df810>

In [6]: 

输出了两次B,这不是我们想要的。把C类中改成super方法后就正常了

In [6]: class C(A, B):
   ...:     def __init__(self):
   ...:         print 'C'
   ...:         super(C, self).__init__()
   ...:         

In [7]: print "MRO", [x.__name__ for x in C.__mro__]
MRO ['C', 'A', 'B', 'object']

In [8]: C()
C
A
B
Out[8]: <__main__.C at 0x7fd2457df390>

In [9]: 

2. 不同类型的参数

super的一个缺陷是,每个基类__init__的参数个数不同的话,怎么办?

In [9]: class A(object):
   ...:     def __init__(self):
   ...:         print 'A'
   ...:         super(A, self).__init__()
   ...:
   
In [10]: class B(object):
   ....:     def __init__(self, arg):
   ....:         print 'B'
   ....:         super(B, self).__init__()
   ....:         

In [11]: class C(A, B):
   ....:     def __init__(self, arg):
   ....:         print 'C'
   ....:         super(C, self).__init__(arg)
   ....:         

In [12]: c = C(10)
C
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
 in ()
----> 1 c = C(10)

 in __init__(self, arg)
      2     def __init__(self, arg):
      3         print 'C'
----> 4         super(C, self).__init__(arg)
      5 

TypeError: __init__() takes exactly 1 argument (2 given)

从报出的错误中可以看出是多了一个参数,导致__init__调用失败。

一个妥协的方法就是使用*args和**kw,这样无论是一个还是两个,还是没有参数,都可以成功。但是这么做导致代码变的脆落。另一个解决的办法就是使用传统的__init__,这又会导致第一个问题。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

class Base(object):
    def __init__(self, *args, **kw):
        print 'Base'
        super(Base, self).__init__()

class A(Base):
    def __init__(self, *args, **kw):
        print 'A'
        super(A, self).__init__(*args, **kw)
class B(Base):
    def __init__(self, *args, **kw): 
        print 'B'
        super(B, self).__init__(*args, **kw)
class C(A , B):
    def __init__(self, arg):
        print 'C'
        super(C, self).__init__(arg)

if __name__ == '__main__':
    C(10)

---------------------------------------
C
A
B
Base

书中的例子已经不能运行成功了,可能是版本更新的原因, object的__init__必须为空,否则会报出参数不对的错误。

3.3 最佳实践

  • 应该避免多重继承, 使用一些设计模式替代
  • super的使用必须一致, 在类层次结构中, 应该在所有地方使用super或者彻底不使用它。混用super和传统调用是一种混乱的方法, 人们倾向于避免使用super, 这样使代码更清晰。
  • 不要混用老式和新式的类, 两者都具备的代码库将导致不同的MRO表现。
  • 调用父类时必须检查类层次, 避免出现任何代码问题, 每次调用父类时, 必须查看一下所涉及的MRO(使用mro

3.4 描述符和属性

Python没有private的关键字, 最接近的概念是“name mangling”, 就是在变量或者函数前面加上“__”时,它就重命名。

In [18]: class Class(object):
   ....:     __private_value = 1
   ....:     

In [19]: c = Class()

In [20]: c.__private_value
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
 in ()
----> 1 c.__private_value

AttributeError: 'Class' object has no attribute '__private_value'

这样就和private很相似,找不到这个attribute。

查看一下该类的属性:

In [21]: dir(Class)
Out[21]: 
['_Class__private_value',
 '__class__',
 '__delattr__',
 '__dict__',
 '__doc__',
 '__format__',
 '__getattribute__',
 '__hash__',
 '__init__',
 '__module__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']

属性有有'_Class__private_value'这么一项。和原来的属性类似。

In [22]: c._Class__private_value
Out[22]: 1

In [23]: c._Class__private_value = 2

In [24]: c._Class__private_value
Out[24]: 2

依旧可以访问,修改,所以这个只是换了一个名字而已。它的真正作用是用来避免继承带来的命名冲突,特性被重命名为带有类名前缀的名称。

在实际中从不使用"__",而是使用"_"替代。这个不会改变任何东西,只是标明这是私有的而已。

3.4.1 描述符

描述符用来自定义在引用一个对象上的特性时所应该完成的事情。它们是定义一个另一个类特性可能的访问方式的类。换句话硕,一个类可以委托另一个类来管理其特性。

描述符基于三个必须实现的特殊方法:

  • __set__ 在任何特性被设置的时候调用,在后面是实例中,将其称为setter;
  • __get__ 在任何特性被读取的时调用(被称为getter)
  • __delete__ 在特性请求del时调用

这些方法在__dict__特性之前被调用。

在类特性定义并且有一个getter和一个setter方法时,平常的包含一个对象的实例的所有元素的__dict__映射都将被劫持。

  • 实现了__get__ 和__set__ 的描述符被称作数据描述符
  • 只实现了__get__的描述符被称为非数据描述符

在Python中,访问一个属性的优先级顺序按照如下顺序:

  1. 类属性
  2. 数据描述符
  3. 实例属性
  4. 非数据描述符
  5. __getattr__()方法

下面先创建一个描述符,并实例一个:

In [1]: class UpperString(object):
   ...:     def __init__(self):
   ...:         self._value = ''
   ...:     def __get__(self, object, type):
   ...:         return self._value
   ...:     def __set__(self, object, value):
   ...:         self._value = value.upper()
   ...:         

In [2]: class MyClass(object):
   ...:     attribute = UpperString()
   ...:     

类UpperString是一个数据描述符,通过它实例化了一个attribute,也就是说对attribute的get和set操作都会被UpperString描述符劫持。

In [25]: mc = MyClass()

In [26]: mc.attribute
Out[26]: ''

In [27]: mc.attribute = "my value"

In [28]: mc.attribute
Out[28]: 'MY VALUE'

如果给实例子中添加一个新的特性,它将被保存在__dict__中

In [29]: mc.new_att = 1

In [30]: mc.__dict__
Out[30]: {'new_att': 1}

数据描述符将优先于实例的__dict__

In [35]: MyClass.new_att = UpperString()

In [36]: mc.__dict__
Out[36]: {'new_att': 1}

In [37]: mc.new_att
Out[37]: ''

In [38]: mc.new_att = "other value"

In [39]: mc.new_att
Out[39]: 'OTHER VALUE'

In [40]: mc.__dict__
Out[40]: {'new_att': 1}

对于非数据描述符,实例将优先于描述符

In [42]: class Whatever(object):
   ....:     def __get__(self, object, type):
   ....:         return "whatever"
   ....:     

In [43]: MyClass.whatever = Whatever()

In [44]: mc.__dict__
Out[44]: {'new_att': 1}

In [45]: mc.whatever
Out[45]: 'whatever'

In [46]: mc.whatever = 1

In [47]: mc.__dict__
Out[47]: {'new_att': 1, 'whatever': 1}

描述符除了隐藏类的内容以外还有:

  • 内省描述符——这种描述符将检查宿主类签名,以计算一些信息
  • 元描述符——这种描述符时类方法本身完成值计算

1. 内省描述符

内省描述符就是一种用于自我检查的通用描述符,也就是可以在多个不同的类使用的一种描述符,书中给的例子是类似dir方法的描述符,也就是列出类的属性。
这种描述符就是检查了类中有什么东西和没有什么东西。我也模仿书中例子,写一个类似dir的自省描述符。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

class API(object):
    def __get__(self, obj, type):
        if obj is not None:
            return self._print_doc(obj)
        else:
            print "No method in {0}".format(obj)
        
    def _print_doc(self, obj):
        method_list = [method for method in dir(obj) if not method.startswith('_')]
        for m in method_list:
            print "{0} : {1}".format(m, m.__doc__)
            print '-'*50
        return "Thank you for looking API"

class MyClass():

    __doc__ = API()
    
    def __init__(self):
        ''' init MyClass '''
        pass
    
    def method1(self):
        '''print the msg for method1'''
        print "i'm method1"
    
    def method2(self):
        '''print the msg for method2'''
        print "i'm method2"
        
if __name__ == '__main__':
    mc = MyClass()
    print mc.__doc__

非数据描述符API只有一个__get__方法,用于获取。在非数据描述符API中的_print_doc方法中,过滤掉内置方法,打印出每个方法的doc。

"""
method1 : str(object='') -> string

Return a nice string representation of the object.
If the argument is a string, the return value is the same object.
--------------------------------------------------
method2 : str(object='') -> string

Return a nice string representation of the object.
If the argument is a string, the return value is the same object.
--------------------------------------------------
Thank you for looking API
"""

2. 元描述符

略难,看看即可

3.4.2 属性

属性(Propetry)提供了一个内建的描述符类型,它知道如何将一个特性链接到以组方法上。属性采用fget参数和3个可选参数——fset、fdel、和doc。最后一个参数可以提供用来定义一个链接到特性的docstring,就像是个方法一样。

这个propetry函数和前面的描述符基本类似,用参数来实现了描述符中的_get、_set、_del_方法而已。

#!/usr/bin/env python
# -*- coding: UTF-8 -*-

class MyClass(object):
    def __init__(self):
        print 'init a value = 10'
        self.value = 10;
    
    def get(self):
        print 'get a value'
        return self.value
    
    def set(self, value):
        print 'set value '
        self.value = value
    
    def ddel(self):
        print 'bye bye'
        self.value = 0

    my_value = property(get, set, ddel, "i'm a value")
    
if __name__ == '__main__':
    mc = MyClass()
    mc.my_value
    mc.set(50)
    del mc.my_value

输出为如下:

'''
init a value = 10
get a value
set value 
bye bye
'''

和之前的描述符的现象一模一样,property为描述符提供了简单的接口。
继承的时候会产生混乱,所创建的特性使用当前类创建,不应该在子类中重载。

In [1]: class Base(object):
   ...:     def _get_price(self):
   ...:         return "$ 500"
   ...:     price = property(_get_price)
   ...:     
In [3]: class SubClass(Base):
   ...:     def _get_price(self):
   ...:         return "$ 20"
   ...:  
In [5]: money = SubClass()

In [6]: money.price
Out[6]: '$ 500'

取得的是父类的方法,而不是子类的,这不是我们预期的。重载父类属性不是好的做法,重载自身属性更好一些。

In [1]: class Base(object):
   ...:     def _get_price(self):
   ...:         return "$ 500"
   ...:     price = property(_get_price)
   ...:     

In [2]: class SubClass(Base):
   ...:     def _get_price(self):
   ...:         return "$ 20"
   ...:     price = property(_get_price)
   ...:     

In [3]: money = SubClass()
In [4]: money.price
Out[4]: '$ 20'

3.5 槽

几乎从未被开发人员使用过的一种有趣的特性是槽

嗯,没人用过

3.6 元编程

可以通过_new和_metaclass这两个特殊方法在运行时候修改类和对象的定义

3.6.1 _new_方法

_new_是一个元构造程序,当一个对象被工厂类实例化时候调用。

In [1]: class MyClass(object):
   ...:     def __new__(cls):
   ...:         print '__new__'
   ...:         return object.__new__(cls)
   ...:     def __init__(self):
   ...:         print '__init__'
   ...:    
In [5]: instance = MyClass()
__new__
__init__
  • _new方法是继承object中的_new方法,所以该类必须先继承object。
  • _new_的作用是返回一个实例化完成的类,就像程序中的instance,也可以是别的类。
  • _new_中的cls参数是要实例化的类,就是MyClass,也就是类中self
  • _new的执行在_init之前

在实例化类之前,_new总是在_init之前执行完成更低层次的初始化工作,例如网络套接字或者数据库初始化应该在_new中为不是_init中控制。它在类的工作必须完成这个初始化以及必须被继承的时候通知我们。

3.6.2 _metaclass_方法

廖雪峰关于元类的理解(ORM例子有点难)
深刻理解Python中的元类(metaclass),强烈推荐
元类提供了在类对象通过工厂方法在内存中创建时进行交互的能力,也就是动态的添加方法。内建类型type是内建的基本工厂,它用来生成指定名称、基类以及包含其特性的映射的任何类。

In [7]: klass = type('MyClass', (object,), {'method': method})

In [8]: instance = klass()

In [9]: instance.method()
Out[9]: 1

Python中一切都是对象,包括创建对象的类,它实际上也是一个type对象。等价的类创建方法是:


In [1]: class MyClass(object):
   ...:     def method(self):
   ...:         return 1
   ...:     

In [2]: instance = MyClass()

In [3]: instance.method()
Out[3]: 1

可以在类中显示的给_metaclass赋值,所需要的是一个返回是type实例化后的类。如果某个类指定了_metaclass,那么这个类将从_metaclass中创建。
_metaclass
的特性必须被设置为:

  • 接受和type相同的参数(类名, 一组基类,一个特性映射)
  • 返回一个类对象
In [5]: def metaclass(classname, base_types, func_dicts):
   ...:     return type(classname, base_types, func_dicts)
   ...: 
   
In [6]: class metaclass(object):
   ...:     __metaclass__ = metaclass
   ...:     def echo(self, str):
   ...:         print str
   ...:  
   
In [7]: instance = MyClass()

In [8]: instance.echo("Hello World")
Hello World

元类的强大特性,可以动态的在已经实例化的类定义上创建许多不同的变化。原则上能不使用就不使用。

使用场景:

  • 在框架级别,一个行为在许多类中是强制的时候
  • 当一个特殊的行为被添加的目的不是诸如记录日志这样的类提供的功能交互时

引用一句话:

当你需要动态修改类时,99%的时间里你最好使用上面这两种技术。当然了,其实在99%的时间里你根本就不需要动态修改类

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,724评论 6 342
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,771评论 1 10
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 我爱足球,正如我爱无忧无虑的童年,足球是我人生中的一种颜色,他影响着我,改变着我,完善着我。在我眼中,足球,是一...
    汛之皓阅读 263评论 0 2