Classes of Python | Python中的类详解

作者: 只记录自己的声音 | 来源:发表于2017-01-04 16:25 被阅读558次

    Classes of Python(Version 2.7.13)

    全文主要内容翻译自 Python 2.7.13官方文档,示例代码及讲解皆为本人撰写,转载请先联系

    A First Look at Classes

    Classes introduce a little bit of new syntax, three new object types, and some new semantics

    类引入了一些新的语法,三个新的对象类型,和一些新的语义。(下文一一讲解)

    Class Definition Syntax

    class ClassName:
        <statement-1>
        .
        .
        .
        <statement-N>
    

    类的定义,就像函数定义(def)一样,必须在他们作用之前被执行。可能会将函数
    定义放入 if 分支中,这时候就要记住上一句话了。

    if 1:
        def f():
            print "calling f()"
        f() # calling f()
    '''
     # 这时候因为上面的 if 分支是肯定被执行的
     # f() 函数是被加到上下文的 namespaces中
    '''
    f() # calling f()
    
    '''
     # 可是如果 if 分支不一定执行的时候呢?  比如肯定不执行的 if 0:
     # 这时候就会发生 `undefined name f` 错误了!
     # 将定义放入分支的时候,切记先让函数定义被执行之后,再引用
    '''
    

    Class Objects

    Class 对象支持两种操作:属性引用、实例化

    属性引用( Attribute references ) 用 obj.name 形式的语法,
    class 被创建后,他命名空间内的所有 name 都是有效的属性 name
    (即可以被 obj.name 引用到)。[静态变量可以不创建 class 也能引用]

    类实例化使用函数符号(foo()),假使 class 对象是一个无参函数,返回值是
    class 的新的实例。

    x = MyClass()
    

    实例化操作创建了一个空对象,大多数时候我们希望自定义地创建一个实例,这个实例有
    他特殊的初始状态。例如,新建一个 people 对象,我们希望他是个10岁的孩子,即
    people.age 是10,这是在创建好对象的时候就已经初始化的状态。因此我们定义了
    一个特殊的方法 __init__

    def __init__(self):
       self.age = 10
    

    当一个 class(类) 定义了 __init__() 方法,类的实例化操作会自动调用该方法
    去实例化新的类实例。当然,为了更好的灵活性,__init__() 方法可以有参数。这样我们可以在创建 people 的时候就显式的初始化他的年纪。

    class People:
      def __init__(self,age=1):
          self.age = age
    
    baby = People() # default age is 1
    young = People(20) # age 20
    old = People(60) # age 60
    

    静态变量和实例变量

    class Clazz:
        static_var = 0
        def __init__(self):
            self.i = 1
    
        def f():
            print "f()"
    # 静态变量可以不用创建对象也能引用,所有实例共享同一个变量
    print Clazz.static_var # 0
    # 实例化 class
    c = Clazz()
    # Attribute references 属性引用
    print c.i # 1
    print c.f
    print c.f() # f()
    

    Instance Objects

    实例对象(instance objects) 只接受 属性引用(attribute references) 操作。
    这有两种有效的属性名( attribute names ),数据属性和方法(data attributes and methods)。

    数据属性对应着 Smalltalk 中的“实例变量”,以及 C++ 中的“数据成员”。数据属性不需要被直接声明;就像 local variables(局域变量),在她们第一次被赋值的时候就开始存在了。

    class Clazz:
        def foo():
            print "foo()"
    c = Clazz()
    '''
     # 可以发现,在实例对象中引用一个不存在的变量并赋值,该变量就存在在实力对象中了
     # 而不需要在定义的时候显式的声明该变量
     # 这里我们也可以给变量赋值一个函数对象(function object)
    '''
    c.i = 1 # i = 1
    '''
     # 删除实例中的数据属性
    '''
    del c.i
    

    另一种实例属性引用是 方法(method)。方法是一个属于对象的函数( A method is a function that “belongs to” an object.)。(在 Python 中,(术语)方法并不是只有类实例才有:其他对象类型也可以有方法。比如说,list 对象有很多方法叫做 append,insert,remove 之类的。不过,在下面的讨论中,我们只用(术语)方法表示类实例对象的方法,除非另有明确声明)

    一个实例对象中有效的函数名取决于他的类。在定义中,一个类中定义的所有函数对象属性对应着类实例的方法。(类中的 functions 对应着 实例中的 methods)。这段有点绕,看不懂的看原文

    Method Objects

    这里只讲两点吧:

    • 第一点:
    class Clazz:
        def f(self):
            print 'Hello World'
    c = Clazz()
    c.f() # Hello World
    # right way
    # reusable
    cf = c.f
    cf() # Hello World
    
    • 第二点:

    我们看到了在定义方法 f(self) 的时候传入了一个参数 self,而在调用的时候 c.f() 并没有传入任何参数。这里我们实际上调用方法的时候是追寻到类定义的方法。在我们的例子中,c.f()Clazz.f(x) 是完全等价的。

    # 接上一点的 Clazz
    Clazz.f(c) # Hello World
    

    Class and Instance Variables

    通常来说,实例变量(instance variables)是为了保持每个实例的数据唯一性,而类变量(class variables)是为了在所有类的实例之间共享属性(attributes)和方法(method)。

    class Dog:
        kind = 'canine'         # class variable shared by all instances
        def __init__(self, name):
            self.name = name    # instance variable unique to each instance
    >>> d = Dog('Fido')
    >>> e = Dog('Buddy')
    >>> d.kind                  # shared by all dogs
    'canine'
    >>> e.kind                  # shared by all dogs
    'canine'
    >>> d.name                  # unique to d
    'Fido'
    >>> e.name                  # unique to e
    'Buddy'
    
    '''
    既然 class variables 作为共享的数据源,
    任何一个实例更改了变量值,全部实例都会"看到"
    '''
    d.king = 'Dtime'
    print e.kind # Dtime
    

    Random Remarks

    如果名称一样的话,数据属性会覆盖( override ) 方法属性;为了避免偶然发生的、在大型程序中难以发现的命名冲突,用一些公约(惯例)来最小化冲突发生的可能是非常明智的。

    Data attributes may be referenced by methods as well as by ordinary users (“clients”) of an object. In other words, classes are not usable to implement pure abstract data types. In fact, nothing in Python makes it possible to enforce data hiding — it is all based upon convention. (On the other hand, the Python implementation, written in C, can completely hide implementation details and control access to an object if necessary; this can be used by extensions to Python written in C.)

    没有严格地定义函数定义必须要在类定义中function,也可以将函数赋值给类中的一个局部变量。(不建议这么使用)

    # Function defined outside the class
    def f1(self, x, y):
        return min(x, x+y)
    
    class C:
        f = f1
    
        def g(self):
            return 'hello world'
    
        h = g
    

    方法可以通过 self 的方法属性调用其他方法:

    class Bag:
        def __init__(self):
            self.data = []
    
        def add(self, x):
            self.data.append(x)
    
        def addtwice(self, x):
            self.add(x)
            self.add(x)
    

    Methods may reference global names in the same way as ordinary functions. The global scope associated with a method is the module containing its definition. (A class is never used as a global scope.) While one rarely encounters a good reason for using global data in a method, there are many legitimate uses of the global scope: for one thing, functions and modules imported into the global scope can be used by methods, as well as functions and classes defined in it. Usually, the class containing the method is itself defined in this global scope, and in the next section we’ll find some good reasons why a method would want to reference its own class.[这部分我也蒙圈... 不大清楚说啥,以后再来填吧]

    每个值都是一个对象,因此有一个类(也成为他的类型)。他存储为 object.__class__

    Inheritance

    派生类的定义语法如下:

    class DerivedClassName(BaseClassName):
        <statement ...>
    

    父类 BaseClassName 必须定义在包含派生类 DerivedClassName 的作用域中。也可以使用其他任意表达式代替基类名称。比如,当基类定义在其他模块中时:

    class DerivedClassName(modulename.BaseClassName):
    

    父类和派生类名词解释
    基类: 下面的程序中,Base 就是基类,也称父类。
    派生类: Derive 继承了 Base 类,他就是 Base 类的派生类,也称 Base 类的子类。

    class Base:
        pass # 定义一个空类
    
    class Derive(Base):
        pass 
    

    派生类定义的执行与基类的执行相同。当类对象被构造(创建)时,基类被记住(remembered)。这用于解析属性引用:如果在类中找不到请求的属性,则搜索继续查找基类。如果基类本身是从某个其他类派生的,则此规则将递归应用。

    对派生类的实例化没有什么特别的:DerivedClassName()创建一个新的类实例。方法引用解析如下:如果需要,搜索相应的类属性,如果有必要,则降序排列基类链,如果这产生一个函数对象,方法引用是有效的。

    '''
    可以发现 Derive 类打印了 num 属性,但是 Derive 类并没有定义 num 属性,
    所以这时候在解析属性的时候,会去父类 Base 类中寻找,这就是根据降序排列基类链去搜索属性。
    '''
    class Base:
         num = 10
    
    class Derive(Base):
        def p(self):
             print self.num
    

    派生类可以覆盖其基类的方法。因为方法在调用同一对象的其他方法时没有特殊的权限,
    所以一个基类中的方法再调用定义在同个基类中其他方法的时候,最终可能会调用派生类的同名方法来覆盖它。(对于C ++程序员:Python中的所有方法都是虚拟的。)

    派生类中的重写方法实际上可能希望扩展而不是简单地替换同名的基类方法。有一种简单的方法来直接调用基类方法:只需调用 BaseClassName.methodname(self,arguments)。这有时对客户有用。 (请注意,这只有在基类可以作为 BaseClassName 在全局范围内访问时才有效。)

    Python有两个内置的函数作用在继承上:

    • 使用 isinstance() 来检查实例的类型:isinstance(obj,int) 将只有当 obj .__ class__int 或从 int 派生的类时才为 True
    • 使用 issubclass() 检查类继承:issubclass(bool,int)True,因为 boolint 的子类。但是,issubclass(unicode,str)False,因为 unicode不是 str 的子类(它们只共享一个共同的祖先,basestring)。

    Multiple Inheritance

    Python也支持有限形式的多重继承。具有多个基类的类定义如下所示:

    class DerivedClassName(Base1, Base2, Base3):
        <statement ...>
    

    对于旧式(old-style)类,唯一的规则是深度优先,从左到右。因此,如果在 DerivedClassName 中找不到属性,则在 Base1 中搜索,然后(递归地)在 Base1 的基类中搜索,并且仅当在那里找不到时,才会在 Base2 中搜索,以此类推。

    对于新式(new-style)类, 方法解析顺序(MRO)会动态更改,以支持对 super()的协作调用。这种方法在一些其他多继承语言中称为 call-next-method,比单继承语言中的 super 调用更强大。[多重继承下的方法解析不了解,谷歌了一下 Python 的多重继承新式类的方法解析顺序]

    '''
    以 object 作为基类,这就是新式(new-style)类。
    因为 Base 类是以 object 为基类,所以 Derive 类也是新式类了。
    '''
    class Base(object):
        pass
    
    class Derive(Base):
        pass
    
    '''
    # Python3 之后就默认的是新式类了,不需要专门的去继承 object 了。
    # 这篇例子是讲新式类的继承顺序
    # Python 3.5
    '''
    class A:
        def __new__(cls, *argv, **kwargs):
            print('nA')
            return super().__new__(cls)
    
        def __init__(self, a):
            print('A')
            self.a = a
    
        def comeon(self):
            print('A.comeon')
    
    
    class B(A):
        def __new__(cls, *argv, **kwargs):
            print('nB')
            return super().__new__(cls)
    
        def __init__(self, b):
            super().__init__(b)
            print('B')
            self.b = b
    
        def comeon(self):
            print('B.comeon')
    
    
    class C(A):
        def __new__(cls, *argv, **kwargs):
            print('nC')
            return super().__new__(cls)
    
        def __init__(self, c):
            super().__init__(c)
            print('C')
            self.c = c
    
        def comeon(self):
            print('C.comeon')
    
    
    class D(B, C):
        def __new__(cls, *argv, **kwargs):
            print('nD')
            return super().__new__(cls)
    
        def __init__(self, d):
            super().__init__(d)
            print('D')
            self.d = d
    
    
    d = D('d')
    d.comeon()
    
    '''
    # output
    nD
    nB
    nC
    nA
    A
    C
    B
    D
    B.comeon
    '''
    

    最后执行 d.comeon() 实际上调用的方法是从左自右得来的左边的那个 B 的 comeon() 方法 。那么如何实现这样的效果呢?很简单,让 B 的 __init__() 最后一个执行,就能覆盖掉 C 和 D 写入的 comeon() 方法。

    所以实际调用new的顺序就是 D--B--C--A,之后递归栈回过头来初始化,调用 __init__() 的顺序就是 A--C--B--D,只有这样才能保证 B 里的 comeon() 方法能够覆盖掉 A 和 C 创建的 comeon() 方法,同样保证如果你的 D 里有 comeon(),它是最后一个被初始化的,将最后写入而覆盖掉其它的。
    栗子来源于偶然看见的 Coldwings 的知乎回答。不过很明显这里是讲的新式类的,旧式类与此不同(参上)。

    Private Variables and Class-local References

    除了在对象内部不能访问的“私有”实例变量在Python中不存在。然而,大多数 Python code 遵守着这样一个约定:如果前缀为一个下划线(比如 "_attr")的命名,应该被当作是 API 的非公有部分(不论它是一个函数,方法亦或是数据成员)。 它应被视为细节的实现,如有更改,恕不另行通知。

    (It should be considered an implementation detail and subject to change without notice.
    )

    class Base:
        class_var = 1 # 可以被子类 "看到"
        __private_var = 0 # 不可以被子类 '看到',这就是Python实现私有(private) 变量的方式
    
    class Derive(Base):
        def p(self):
            print self.class_var
            print self.__private_var
    # instantiation
    Derive().p() 
    
    # output:
    1
    Traceback (most recent call last):
      File "D:/workspace_py/python-doc/classdefinition/demonstrate.py", line 10, in <module>
        Derive().p()
      File "D:/workspace_py/python-doc/classdefinition/demonstrate.py", line 8, in p
        print self.__private_var
    AttributeError: Derive instance has no attribute '_Derive__private_var'
    

    因为类私有(class-private)成员有一个有效的用例(即避免名称与子类定义的名称冲突),所以对这种机制的有限的支持,称为名称调整(name mangling)。任何标识符 __spam(至少两个前导下划线,最多一个尾部下划线)在文本上替换为 _classname__spam,其中 classname 是带有前导下划线的当前类名。这种调整是在不考虑标识符的句法位置的情况下完成的,只要它出现在类的定义内即可。

    名称调整(name mangling)有助于让子类重写方法,而不会打断类内方法调用。例如:

    class Mapping:
        def __init__(self, iterable):
            self.items_list = []
            self.__update(iterable)
    
        def update(self, iterable):
            for item in iterable:
                self.items_list.append(item)
    
        __update = update   # private copy of original update() method
    
    class MappingSubclass(Mapping):
    
        def update(self, keys, values):
            # provides new signature for update()
            # but does not break __init__()
            for item in zip(keys, values):
                self.items_list.append(item)
    

    注意,传递给 execeval()execfile()的代码不会将调用类的类名视为当前类;这类似于全局语句global statement的效果,其效果同样限于字节编译在一起的代码。同样的限制适用于 getattr()setattr()delattr(),以及直接引用 __dict__ 时。 [持续懵逼... 没用过 exec之类的,不知道咋回事,以后用过再来补]

    Odds and Ends

    有时,使用类似于 Pascal的"record"或C的"struct"的数据类型,捆绑几个命名的数据项,这样的结构是有用的。一个空类定义可以很好的做到这点:

    class People:
        pass
    
    me = People() # create an empty class People
    me.name = "Chan"
    me.age = 16
    me.weight = 1000
    

    一段需要特定抽象数据类型的 Python 代码,通常可以通过一个类来模拟该数据类型的方法。例如,如果你有一个函数来格式化一个文件对象的一些数据,你可以定义一个类方法 read()readline() 从字符串缓冲区获取数据,并传递它作为参数。

    实例方法对象也有属性:m.im_self 是拥有方法m()的实例对象,m.im_func 是对应于方法的函数对象。

    class Clazz:
        def f(self):
            pass
    
    c = Clazz() 
    print c.f.im_self # <__main__.Clazz instance at 0x0000000002915BC8>
    print c.f.im_func # <function f at 0x0000000002A9E9E8>
    

    Exceptions Are Classes Too

    我们还可以使用类来标识 (创建) 用户定义的异常( exception )。用这种机制的话,在扩展异常的层次结构上将更加便捷。 raise 语句添加了两种有效的语义形式:

    raise Class, instance
    
    raise instance
    

    第一种形式,instance 必须是 Class的或者是 Class 的派生类的实例。
    第二种形式是一个简写形式,实际上是这样的:

    raise instance.__class__, instance
    

    except 子句( clause )中,捕捉的异常类会兼容其派生类,也就是说一个捕捉类似 IOException 的基类,所有的继承自 IOException的派生类都会被捕捉到。
    举个栗子:

    class B:
        pass
    class C(B):
        pass
    class D(C):
        pass
    
    for c in [B, C, D]:
        try:
            raise c()
        except D:
            print "D"
        except C:
            print "C"
        except B:
            print "B"
    '''
    # output
    > B
    > C
    > D
    '''
    

    如果将上面代码中的 except 子句的顺序改变为 BCD 的顺序,会发现输出是 "BBB" ,因为 except B 把其所有的派生类都给捕捉了。

    如果出现了没有处理的异常,会打印错误信息,格式是这样子的:

    异常类名,后跟一个冒号再一个空格,最后跟上用内建方法 `str()` 将实例转换成字符串输出
    __main__.B: <__main__.B instance at 0x00000000024EB0C8>
    

    Iterators

    你可能已经发现大多数容器对象可以使用 for 语句来循环了。

    for element in [1, 2, 3]:
        print element
    for element in (1, 2, 3):
        print element
    for key in {'one':1, 'two':2}:
        print key
    for char in "123":
        print char
    for line in open("myfile.txt"):
        print line,
    

    这种操作方式非常的清晰明确且便捷,统一使用迭代器的操作方式已经渗入 Python。在这便捷之下,for 语句调用了 iter() 方法作用在容器对象上。该函数返回了一个迭代器对象,在迭代器对象内定义了 next() 方法,next() 方法一次一个地访问容器中的元素。当没有了可访问元素时, next() 方法抛出了一个 StopIteration 异常,这个异常会终止 for 循环。这有一个栗子,展示这是怎么工作的:

    >>> s = 'abc'
    >>> it = iter(s)
    >>> it
    <iterator object at 0x00A1DB50>
    >>> it.next()
    'a'
    >>> it.next()
    'b'
    >>> it.next()
    'c'
    >>> it.next()
    Traceback (most recent call last):
      File "<stdin>", line 1, in ?
        it.next()
    StopIteration
    

    在看完了迭代器协议背后的机制后,可以开始将迭代器行为到你的类中了。定义一个 __iter__() 方法,该方法返回一个有 next() 方法的对象。如果类中定义了 next() 方法的话,可以在 __iter__() 方法中直接返回 self

    class Dict:
        """fool dict"""
        def __init__(self, key, data):
            self.key = key
            self.data = data
            self.index = -1
    
        def __iter__(self):
            return self
    
        def next(self):
            self.index += 1
            if self.index > len(self.key)-1:
                raise StopIteration
            return self.key[self.index],self.data[self.index]
    
        def add(self,add_key,add_data):
            self.key.append(add_key)
            self.data.append(add_data)
    
    key = [0,1,2,3]
    data = ['a','b','c','d']
    d = Dict(key,data)
    d.add(7,'g')
    for key,data in d:
        print key,data
    
    '''
    # output
    0 a
    1 b
    2 c
    3 d
    7 g
    '''
    

    Generators

    生成器是一个用于生成迭代器的简单且强大的工具。它写起来就和常规的函数一样,但每当需要返回数据的时候,会使用 yield 语句。每次next() 方法被调用,生成器会回到上次它处理过的地方(它会记住所有的数据值以及上次执行的语句)。创建一个生成器非常简单,看下面的栗子:

    def access(data):
        for index in range(len(data)):
            yield data[index]
    
    for char in access('access'):
        print char;
    '''
    # output
    a
    c
    c
    e
    s
    s
    '''
    

    所有能被生成器做到的事情,都可以用基于类的迭代器来完成(就在上一节提到的迭代器)。生成器会自动生成 __iter()__next() 方法,这使得生成器看起来异常的紧凑(和基于类的迭代器相比)。

    另一个重要的特性是,局部变量以及执行状态在每次调用之间都被自动的保存了。与使用实例变量 (如 self.keyself.data) 相比,使得函数变得更易编写,且更加清楚。

    除了自动创建方法和保存程序状态,当生成器终止时,它们自动抛出 StopIteration。结合起来,这些特性使得创建迭代器和编写常规函数一样容易。

    Generator Expressions

    一些简单的生成器可以简洁地编码为表达式,使用类似于列表推导的语法,但使用括号而不是方括号。这些表达式设计用于生成器立即被封闭它的函数使用的情况。

    与完整的生成器定义相比,生成器表达式更紧凑,但特性也少了。生成器表达式比等效的列表推导更内存友好。

    上栗子:

    >>> sum(i*i for i in range(10))                 # sum of squares
    285
    
    >>> xvec = [10, 20, 30]
    >>> yvec = [7, 5, 3]
    >>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
    260
    
    >>> from math import pi, sin
    >>> sine_table = dict((x, sin(x*pi/180)) for x in range(0, 91))
    
    >>> unique_words = set(word  for line in page  for word in line.split())
    
    >>> valedictorian = max((student.gpa, student.name) for student in graduates)
    
    >>> data = 'golf'
    >>> list(data[i] for i in range(len(data)-1,-1,-1))
    ['f', 'l', 'o', 'g']
    

    ===========日志分割线===========

    update log:

    • 更新于 2017年1月19日01:03:16 | 添加 Coldwings 的关于新式类继承的栗子
    • 初次完成于 2017年1月18日19:04:44 | 全篇翻译完毕
    • 更新于 2017年1月18日16:55:08 | 添加 Exceptions Are Classes Too 至文末
    • 更新于 2017年1月5日 21:34:00 | 添加 Inheritance 至文末
    • 更新于 2017年1月4日 23:00:36 | 添加 Class and Instance Variables
    • 创建于 2017年1月4日 16:26:36

    相关文章

      网友评论

        本文标题:Classes of Python | Python中的类详解

        本文链接:https://www.haomeiwen.com/subject/btaovttx.html