• Python语法速查: 8. 类与对象


    返回目录

    本篇索引

    (1)类基本用法

    (2)类的进阶知识

    (3)类继承

    (4)property

    (5)描述符

    (6)__slots__

    (7)元类

    (8)类装饰器

     (1)类基本用法

    使用面向对象编程有3个重要的好处:封装(Encapsulation)、继承(Inheritance)、多态(Polymorphism)。 Python中所有的类都是继承自object类(Python2中要使用__metaclass__=type声明,Python3中不用考虑),它提供了一些常用的方法(如__str__()等)的默认实现,在使用中可以不显式指出object。

    因此以下三种定义类的方法都是可以的:

    class MyClass(object):
        pass
        
    class MyClass():
        pass
        
    class MyClass:
        pass

    ● 类中的成员

    供所有实例共享的变量称为“类变量”(class variables)。类的每个实例各自有的成员变量称为“特性”(attribute);而“属性”(property)有另外的含义,后面会详述。 由于以前Java中“属性”这个名词太深入人心,因此在不引起歧义的情况下,也常常把“特性”叫为属性。

    类中定义的函数分为三种:实例方法(绑定方法)、类方法、静态方法。

    以下为类中各种成员的定义:

    class Foo:
        a = 10;   # 类变量(供所有实例共享)
        
        # 函数fi为实例方法
        def fi(self):
            self.b = 20   # 特性(每个实例各自有)
            self.a = 30   # 本句不会改变类变量a,而是为本实例创建新的实例attribute:a
            
        # 函数fc为类方法(使用@classmethod装饰器)
        @clasmethod
        def fc(cls):
            cls.a = 30
            
        # 函数fs为静态方法(使用@staticmethod装饰器)
        def fs():
            print('hello')
            
    f = Foo()
    f.fi()      # 调用实例方法(绑定方法)
    Foo.fc()    # 调用类方法
    Foo.fs()    # 调用静态方法

    ● 初始化类实例

    在创建一个类的实例时,会自动调用类的__init__()方法,这个有点像C++或Java中的构造函数,可以在__init__()方法对实例进行一些初始化工作。 __init__()方法的第一个入参默认为self,后面的入参可任意添加。

    由于在Python中使用变量前无需声明,因此一个比较好的习惯是在__init__()方法中,对所有要用到的“特性”进行初始化。

    初始化实例的例子:

    class Foo:
        def __init__(self, x, y):
            self.x = x
            self.y = y

    ● 绑定与非绑定方法

    通过实例调用方法时,有绑定和非绑定两种用法。绑定方法封装了成员函数和一个对应实例,调用绑定方法时,实例会作为第一个参数self自动传递给方法。而非绑定方法仅封装了成员函数,并没有实例,用户在调用非绑定方法时,需要显式地将实例作为第一个参数传递进去。详见下面2个例子

    绑定用法(bound method):

    class Foo():
        def meth(self, a):
            print(a)
    
    obj = Foo()     # 创建一个实例
    m = obj.meth    # 将meth方法绑定到obj这个实例上
    m(2)            # 调用这个方法时,Python会自动将obj作为self参数传递给meth()方法

    非绑定用法(unbound method):

    class Foo():
        def meth(self, a):
            print(a)
    
    obj = Foo()     # 创建一个实例
    um = Foo.meth   # 非绑定,仅仅是将这个方法赋值给um,并不需要实例存在
    um(obj, 2)      # 调用这个非绑定方法时,用户需要显式地传入obj实例作为第一个参数。

    ● 类方法、静态方法

    类方法用@classmethod装饰器来修饰定义,静态方法用@staticmethod装饰器来修饰定义,它们的区别仅仅在于: 类方法可以访问类变量,而静态方法不能放问类变量。由于类方法需要访问类变量,因此它的第一个参数默认为cls(类名)。

    可以通过类名来调用类方法和静态方法,也可以通过实例名来调用类方法和静态方法。其使用方法见前例。

    ● 数据封装和私有性

    默认情况下,类的所有属性和方法都是“公共的”,可以在外部被访问,也会被派生类继承。为了实现使属性私有,Python提供了点小技巧: 可以将某个属性名或方法名前加双下划线(__),这样Python会对这个名称自动变形,变成:“_类名__成员名”的形式, 这样外部就不能简单通过“成员名”访问内部属性和内部方法了。当然,这只是个小trick,外界通过实例的__dict__属性, 还是可以查看到这个变形后的真实名称的。

    class Foo:
        def __init__(self):
            self.__x = 3    # 变形为 self._Foo__x
            
        def __bar(self):    # 变形为 self._Foo__bar()
            pass

    ● 实例的__dict__、__class__属性

    每个实例内部可以看作都是用字典来实现的,可以通过实例的__dict__属性访问这个字典,其中包含了每个属性的“键值对”。 对实例的修改始终都会反映到局部__dict__属性中。同样,如果直接对__dict__进行修改,所作的修改也会反映在实例的属性中。

    实例的__class__属性保存了这个实例所属的类。

    class Foo:
        a = 10
        def __init__(self):
            pass
            
        def bar(self):
            self.b = 20
            
    f = Foo()
    print(f.__dict__)   # 本句结果为:{}
    f.bar()
    print(f.__dict__)   # 本句运行时,由于bar()方法已运行,故为:{'b':20}
    
    f.__dict__['b'] = 30
    print(f.b)          # 本句运行结果为:30
    
    print(f.__class__)  # 本句结果为:<class '__main__.Foo'>

    ● 类的__dict__、__bases__属性

    类的__dict__属性反映的是和实例的__dict__完全不同的内容,本质上是类对象的属性列表(Python中万物皆对象,类定义本身也是个对象,是type类型的一个实例)。

    类的__bases__属性为一个元组,按顺序列出了这个类的继承链。

    class Foo:
        a = 10
        def __init__(self):
            pass
            
        def bar(self):
            self.b = 20
        
    print(Foo.__dict__)  
    print(Foo.__bases__)
    
    # 结果为:
    {'__module__': '__main__', 'a': 10, '__init__': <function Foo.__init__ at 0x000002A3FFEDB1F8>, 'bar': <function Foo.bar at 0x000002A3FFEDB288>, '__dict__': <attribute '__dict__' of 'Foo' objects>, '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}
    (<class 'object'>,)

    ● 使用dir()内置函数

    使用dir()内置函数可以得到实例内的所有:属性名、方法名。用户可以通过自定义__dir__()方法来使dir()只显示自己想要显示给外界的名称。

    class Foo:
        a = 10
        def bar(self):
            self.b = 20
            
    f = Foo()
    print(dir(f))
    
    # 结果为:
    ['__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__', 'a', 'bar']

     (2)类的进阶知识

    ● 类对象

    Python中万物皆对象,类定义本身也是一个对象,这个“类定义”对象的类型为type。因此,为了表示区别, 由这个类定义生成的对象一般称为“实例对象”(instance),或简称“实例”,不再称为对象(object)。 当然,由于以前“对象”这个名词太深入人心,所以有时也在不引起歧义的情况下,把“实例”叫为“对象”

    类对象和实例对象的区别:

    class Foo:
        pass
    f = Foo()
        
    type(Foo)   # 结果为:<class 'type'>
    type(f)     # 结果为:<class '__main__.Foo'>
    
    t = Foo     # 将类对象(即type类型的对象)赋值给t

    type对象的常用属性:

    属性描述
    t.__doc__ 文档字符串
    t.__name__ 类名称
    t.__bases__ 由基类构成的元组
    t.__mro__ 在实例方法查找时的各基类顺序元组(method resolution)
    t.__dict__ 保存类方法和变量的字典
    t.__module__ 定义类的模块名称
    t.__abstractmethods__ 抽象方法名称的集合

    实例对象的常用属性:

    属性描述
    i.__class__ 实例所属的类
    i.__dict__ 保存实例数据的字典

    ● 对象的特殊方法

    对象的特殊方法名称前后都带有双下划线,当程序执行时,这些方法由解释器自动触发。例如:x + y 被映射为内部方法: x.__add__(y)、索引操作 x[k] 被映射为:x.__getitem__(k),下面介绍一些常见的特殊方法:

    a. 对象的创建与销毁

    方法描述
    __new__(cls [,*args [,**kwargs]]) 创建新实例时调用的类方法
    __init__(self [,*args [,**kwargs]]) 初始化新实例时调用
    __del__(self) 销毁实例时调用

    用户一般很少需要自己定义__new__()方法和__del__()方法,__new__()方法仅在定义元类或继承自不可变类型时才使用。 __del__()基本不会用到,如果要释放某种资源,更好的方法是自定义一个close()方法,然后显式调用来释放资源。

    实例创建的内部过程:

    f = Foo.__new__(Foo)
    if isinstance(f, Foo):
        Foo.__init__(f)

    如果用户自定义了__new__(),通常这个类可能继承自不可变的内置类型(如字符串),因为__new__()是唯一在创建实例之前执行的方法, 如果在__init__()中修改,可能为时已晚,见下例:

    class Upperstr(str):
        def __new__(cls, value=''):
            return str.__new__(cls, value.upper())
            
    u = Upperstr('hello');    # 可以在__new__()中将字符串变为大写:HELLO

    b. 对象的字符串表示

    方法描述
    __format__(self, format_spec) 创建格式化后的表示
    __repr__(self) 创建对象的完整字符串表示
    __str__(self) 创建对象的简单字符串表示

    内建函数repr()会自动调用对象的__repr__()方法,内建函数str()会自动调用对象的__str__()方法。 区别在于__str__()方法返回的字符串更加简明易懂,如该方法未定义,就调用__repr__()方法。

    __format__()方法的调用者是内建函数format()或字符串的format()方法,如:format(x, 'spec') 即为调用:x.__format__('spec')

    c. 对象的比较与排序

    方法描述
    __bool__(self) 由内建函数bool()调用,测试对象真值
    __hash__(self) 由内建函数hash()调用,计算对象的散列值
    __lt__(self, other) self < other
    __le__(self, other) self <= other
    __gt__(self, other) self > other
    __ge__(self, other) self >= other
    __eq__(self, other) self == other
    __ne__(self, other) self != other

    一般来说,用户无需实现所有上面的比较操作,但若要使用对象作为字典键,则必须定义__eq__()方法, 若要为对象排序或使用诸如 min() 或 max() 之类的内建函数,则至少要定义__lt__()方法。

    d. 类型检查

    方法描述
    __instancecheck__(cls, object) 由内建函数 isinstance()调用
    __subclasscheck__(cls, sub) 由内建函数 issubclass()调用

    这个功能通常用于绕过喜欢使用类型检查的用户。在下例中,虽然FooProxy和Foo拥有同样的接口,并且对Foo的功能进行了增强,但由于FooProxy不是继承自Foo, 因此通不过喜欢使用类型检查的用户:

    class Foo(object):
        def bar(self, a, b):
            pass
            
    class FooProxy(object):
        def bar(self, a, b):
            pirnt(a,b)
            
    f = Foo()
    g = Foo()
    isinstance(g, Foo)    # 结果为:False

    下例中,可以通过定义注册方法register(),并重新定义__instancecheck__()和__subclasscheck__(),绕过默认的死板的类型检查, 而实现更灵活的注册制的类型检查。

    class IClass(object):
        def __init__(self):
            self.implementors = set()
        
        def register(self, C):
            self.mplementors.add(C)
        
        def __instancecheck__(self, x):
            return self.__subclasscheck__(type(x))
            
        def __subclasscheck__(self, sub):
            return any(c in self.implementors for c in sub.mro())
    
    IFoo = IClass()
    IFoo.register(Foo)
    IFoo.register(FooProxy)
    
    f = Foo()
    g = FooProxy()
    isinstance(f, IFoo)   # True
    isinstance(g, IFoo)   # True
    
    issubclass(FooProxy, IFoo)    # True  

    上面代码只是演示如何重新灵活定义__instancecheck__()方法的示例。通常如果要使用register()方法的话,抽象基类是更好的选择。

    e. 属性(attribute)访问

    方法描述
    __getattribute__(self, name) 返回属性 self.name
    __getattr__(self, name) 如果上面的__getattribute__()未找到属性,则自动调用本方法
    __setattr__(self, name, value) 设置属性 self.name = value
    __delattr__(self, name) 删除属性 self.name

    只要使用了 obj.name = val 语句设置了属性,实例的__setattr__('name', val)方法就会自动被调用。

    同样的,只要使用了 del obj.name 语句,实例的__delattr__('name')方法就会自动被调用。

    而在读取属性内容时,稍微复杂一点,例如读取 obj.name 时:实例的__getattribute__('name')方法会被自动调用, 该方法执行搜索来查找该属性,这通常涉及检查特性、查找局部__dict__属性、搜索基类等等。如果搜索过程失败, 最终会尝试调用实例的__getattr__()方法(如果已定义)来查找该属性。如果这也失败,就会引发AttributeError异常。

    用户通过重新定义__getattr__()、__setattr__()、__delattr__()方法,可以截取属性操作,并将自定义的操作添加进去。

    下例定义了2个虚拟属性:area, perimeter

    class Circle(object):
        def __init__(self, r):
            self.r = r
            
        def __getattr__(self, name):
            if name == 'arer':
                return 3.14 * self.r ** 2
            elif name == 'perimeter':
                return 2 * 3.14 * self.r
            else:
                return object.__getattr__(self, name)
                
        def __setattr__(self, name, value):
            if name in ('area', 'perimeter'):
                raise TypeError('%s is readonly' %name)
            object.__setattr__(self, name, value)

    f. 属性(property)包装与描述符

    方法描述
    __get__(self, instance, cls) 返回一个属性值,否则引发AttributeError异常
    __set__(self, instance, value) 将属性设为value
    __del__(self, instance) 删除属性

    描述符使用,详见后文“描述符”小节详细说明。

    g. 序列与映射方法

    方法描述
    __len__(self) 返回self的长度,由内建函数 len() 调用
    __getitem__(self, key) 返回 self[key],key甚至可以是slice对象
    __setitem__(self, key, value) 设置 self[key] = value
    __delitem__(self, key) 删除 self[key]
    __contains__(self, obj) 如果obj在self中则返回True(主要由 in 操作符使用)

    h. 数学操作方法

    方法描述
    __add__(self, other) self + other
    __sub__(self, other) self - other
    __mul__(self, other) self * other
    __truediv__(self, other) self / other
    __floordiv__(self, other) self // other
    __mod__(self, other) self % other
    __divmod__(self, other) divmod(self, other)
    __pow__(self, other [,modulo]) self ** other, pow(self, other, modulo)
    __lshift__(self, other) self << other
    __rshift__(self, other) self >> other
    __and__(self, other) self & other
    __or__(self, other) self | other
    __xor__(self, other) self ^ other
    __radd__(self, other) other + self
    __rsub__(self, other) other - self
    __rmul__(self, other) other * self
    __rtruediv__(self, other) other / self
    __rfloordiv__(self, other) other // self
    __rmod__(self, other) other % self
    __rdivmod__(self, other) divmod(other, self)
    __rpow__(self, other [,modulo]) other ** self
    __rlshift__(self, other) other << self
    __rrshift__(self, other) other >> self
    __rand__(self, other) self & other
    __ror__(self, other) self | other
    __rxor__(self, other) other ^ self
    __iadd__(self, other) self += other
    __isub__(self, other) self -= other
    __imul__(self, other) self *= other
    __itruediv__(self, other) self /= other
    __ifloordiv__(self, other) self //= other
    __imod__(self, other) self %= other
    __ipow__(self, other [,modulo]) self **= other
    __ilshift__(self, other) self <<= other
    __irshift__(self, other) self >>= other
    __iand__(self, other) self &= other
    __ior__(self, other) self |= other
    __ixor__(self, other) self ^= other
    __neg__(self, other) -self
    __pos__(self, other) +self
    __abs__(self, other) abs(self)
    __invert__(self, other) ~self
    __int__(self, other) int(self)
    __long__(self, other) long(self)
    __float__(self, other) float(self)
    __complex__(self, other) complex(self)

    通过向类添加以上方法,可以使实例能够使用标准数学运算符进行运算。下例演示了如何定义一个复数类,并让其实例支持加减操作。 从下例中可以看出,__add__()和__sub__()仅适用于Cmpx实例出现在运算符左侧的情形。

    class Cmpx(object):
        def __init__(self, real, imag = 0):
            self.real = float(real)
            self.imag = float(imag)
            
        def __repr__(self):
            return "Complex(%s,%s)" %(self.real, self.imag)
            
        def __str__(self): 
            return "(%g+%gj)" %(self.real, self.imag)
            
        # 重载加法操作符
        def __add__(self, other):
            return Cmpx(self.real + other.real, self.imag + other.imag)
            
        # 重载减法操作符
        def __sub__(self, other):
            return Cmpx(self.real - other.real, self.imag - other.imag)
            
    c = Cmpx(1,2)
    d = Cmpx(3,4)
    
    print(c+d)      # 结果为:(4+6j)
    print(c+3.0)    # 结果为:(4+2j)
    print(c+'a')    # 报错
    print(3.0+c)    # 报错

    以上例子中,用到了other的real和imag属性,如果other对象没有这些属性,那将会报错。另外,倒过来操作(例如3.0+c)也会报错, 因为内置的浮点类型的__add__()方法不知道关于Cmpx类的任何信息。如果需要这样操作,需要向Cmpx类添加:逆向操作数(reversed-operand)方法。

    class Cmpx(object):
        ......
        def __radd__(self, other):
            return Cmpx(self.real + other.real, self.imag + other.imag)
        ......

    如果操作3.0+c失败,Python将在引发TypeError异常前,先尝试c.__radd__(3.0)

    i. dir()与对象检查

    方法描述
    __dir__(self) 由内置函数dir()调用,返回对象内所有的名称列表(属性名、方法名等)

    用户定义该方法后,可以隐藏一些不想让其他人访问的对象内部细节。但是,其他人仍可以通过查看底层的__dict__属性,了解类内部的细节。

     (3)类继承

    通过继承创建新类时,新类将继承其基类定义的属性和方法,子类也可以重新定义这些属性和方法。在class语句中, 以逗号分隔的基类名称列表来表示继承,如果没有指定基类,类将继承object,object是所有Python对象的根类。

    以下为一个基本的类继承例子:

    class Shape:
        def get_name():
            return 'Shape'
        def area():
            return 0
    
    class Square(Shape):
        def get_name():
            return 'square'
            
    sq = Square()
    sq.area()       # 自动调用基类的area()方法,返回0。

    如果搜索一个属性或方法时,未在实例中找到匹配的项,那么会自动搜索其基类。

    ● 子类的初始化

    子类定义__init__()方法时,不会自动调用基类的__init__()方法,因此,如果在子类的__init__()方法中需要调用基类的初始化方法, 需要显式地写出调用语句。有两种方法可以调用基类的__init__()方法:

    class Shape:
        def __init__(self):
            self.name = 'Shape'
            self.area = 0
        def get_name():
            return self.name
    
    # 方法一:
    class Square(Shape):
        def __init__(self):
            Shape.__init__(self)
            self.name = 'Square'
    
            
    # 方法二:
    class Circle(Shape):
        def __init__(self):
            super().__init__()
            self.name = 'circle'

    方法一中,需要显式地指明基类的名称,并将self作为参数,去调用基类的__init__()方法。

    方法二中,使用了super()内置函数,它会返回一个特殊的对象,该对象会自动去查找基类的属性和方法,找到则执行这个方法。

    ● 多重继承和mixin

    虽然Python支持多重继承,但最好避免使用多重继承,因为它会使属性的解析变得非常复杂。

    只有一种情况可以很好地使用多重继承,即使用mixin(混合类)。混合类通常定义了要“混合到”其他类中的一组方法,目的是添加更多的功能。 通常mixin并不单独作为其他类的基类使用,而是有点像插件,插入到其他类中,为其他类添加某种单一的特定功能。

    举个例子:

    class Shape:
        def __init__(self):
            self.name = 'Shape'
            self.area = 0
        def get_name():
            return self.name
    
    class ColorMixin:
        color = 'white'
        def set_color(self,color):
            self.color = color
            
        def get_color(self):
            return self.color
            
    class Square(Shape, ColorMixin):
        def __init__(self):
            Shape.__init__(self)
            self.name = 'Square'
    
    class Circle(Shape, ColorMixin):
        def __init__(self):
            super().__init__()
            self.name = 'circle'
    
    
    if __name__ == '__main__':
        s = Square()
        c = Circle()
    
        s.set_color('red')
    
        print(s.get_color())    # 输出结果为:red
        print(c.get_color())    # 输出结果为:white
        print(Circle.__mro__)   # 结果为:(<class '__main__.Circle'>, <class '__main__.Shape'>, <class '__main__.ColorMixin'>, <class 'object'>)

    在上例中,如果我们要为各种形状的类增加“颜色”功能,就可使用ColorMixin类,代码比较简单,很容易看懂。 使用mixin的前提是它提供的功能比较单一,不会和原有的继承结构发生数据上的依赖和瓜葛。在Django等框架中,就大量使用了Mixin类。

    ● 抽象基类

    要定义抽象基类需要用到abc模块,该模块定义了一个元类(ABCMeta)和一组装饰器(@abstractmethod 和 @abstractproperty)。 抽象基类本身并不能直接实例化,必须要被子类继承,并在子类上实现其抽象方法和抽象属性后,才能在子类上实例化。

    下例定义了一个抽象基类:

    from abc import ABCMeta, abstractmethod, abstractproperty
    class Foo(metaclass=ABCMeta):
        @abstractmethod
        def bar(self, a, b)
            pass
            
        @abstractproperty
        def name(self):
            pass

    抽象基类支持对已经存在的类进行注册(使用register()方法),使其属于该基类,如下例所示:

    class FooA(object)
        def bar(self, a, b):
            print('This is FooA method bar')
            
    Foo.register(FooA)    # 向Foo抽象基类注册

    向抽象基类注册某个类时,Python不会检查该类是否实际实现了任何抽象方法或抽象属性,这种注册过程只影响类型检查(如isinstance()、issubclass()等内置函数)。

    抽象基类主要用于帮助用户建立自定义类的层次结构,一些Python模块中,使用了抽象基类的作用, 如collections模块中、numbers模块中,都实现了层次结构的抽象基类。

     (4)property

    property可以定义一个虚拟的属性,它的值并不保存在实际的内存中,而是每次读取时,由临时计算得到。

    下例中,area即为虚拟属性property:

    import math
    class Circle:
        def __init__(self, r):
            self.r = rad
            
        @property
        def area():
            return math.pi * self.r ** 2
            
    c = Circle(1)
    print(c.area)     # 结果为:3.14

    ● property的设置和删除

    property不仅可以读取,其实也可以为property定义设置函数和删除函数。可以用老式和新式两种写法, 老式的写法更好理解一些,新式的写法结构更清晰简洁。

    老式的写法:

    class Foo:
        def __init__(self, name):
            self.__name = name
    
        def getname(self):
            return self.__name
            
        def setname(self):
            if not isinstance(value, str):
                raise TypeError('Must be a string')
            self.__name = value
              
        def delname(self):
            raise TypeError('Cannot delete name')
            
        name = property(getname, setname, delname)
    

    上面的property函数,定义了虚拟属性 name 的:读取函数、设置函数、删除函数。利用这种方法, 可以将某个真实的成员变量(attribute)隐藏起来,而让用户只能通过property虚拟属性来访问某个成员变量。

    新式写法(使用装饰器):

    class Foo:
        def __init__(self, name):
            self.__name = name
        
        @property
        def name(self):
            return self.__name
        
        @name.setter
        def name(self, value):
            if not isinstance(value, str):
                raise TypeError('Must be a string')
            self.__name = value
            
        @name.deleter
        def name(self):
            raise TypeError('Cannot delete name')

     (5)描述符

    描述符是一种创建托管属性的方法。比如,一个类中有若干个属性都要像上面那样将真实的成员变量(attribute)隐藏起来, 每个属性都要定义:get、set、delete3个函数,写起来太冗长。最好能一次定义好这3个函数,然后后面类似的属性直接拿来用就可以了, 这就是描述符的作用。

    简单来讲,某个类只要内部定义了__get__()方法、__set__()方法、__delete__()方法中的一个或多个,这个类就是个“描述符”。先看一个例子:

    下例中,TypedProperty就是个描述符

    class TypedProperty:
        def __init__(self, name, type, default=None):
            self.name = "_" + name
            self.type = type
            self.default = default if default else type()
            
        def __get__(self, instance, cls):
            return getattr(instance, self.name, self.default)   # 这个getattr是Python内置函数
            
        def __set__(self, instance, value):
            if not isinstance(value, self.type):
                raise TypeError("Must be a %s" %self.type)
            setattr(instance, self.name, value)                 # 这个setattr是Python内置函数
          
        def __delete__(self, instance):
            raise AttributeError("Cannot delete attribute")
            
    class Foo:
        name = TypedProperty("name", str)
        num  = TypedProperty("num", int, 42)
        
    f = Foo()
    g = Foo()
    
    f.name = "abc"  # 调用Foo.name.__set__(f, 'abc')
    g.name = "xyz"
    
    a = f.name      # 调用Foo.name.__get__(f, Foo)
    print(f.name, g.name)   #  本句运行结果为:abc xyz
    del f.name      # 调用Foo.name.__delete__(f)

    在Foo类中,由于name和num都需要类似的属性操作,故都托管给TypedPropery描述符进行管理。需要注意的是, 描述符成员变量虽然是每个实例各自有的(如上例中的name和num),但却只能在类的级别进行定义(在Foo层面定义), 不能放在__init__()方法中初始化或在其他方法中定义。

     (6)__slots__

    通过定义特殊变量__slots__,类可以限制用户设置新的属性名称,如下例所示:

    下例中对实例f设置新的属性会引发AttributeError异常

    class Foo:
        __slots__ = ('name', 'num')
        
    f = Foo();
    f.a = 2     # 本句会引发AttributeError异常

    另外,由于__slots__变量在实例内部不再用字典,而是用列表存储数据,故执行速度会较块并且占用较少内存, 针对需要创建大量对象的程序比较合适。

    如果类继承自使用__slots__的基类,那么它也需要定义__slots__来存储自己的属性(即使不添加任何新属性也是如此), 否则派生类的运行速度将更慢,比不用__slots__还糟。

    __slots__还会破坏底层__dict__属性,这对很多依靠__dict__来调试的代码不利。

     (7)元类

    元类主要用于在创建类时(不是创建实例),添加一些额外的检查和收集关于类定义的信息,甚至可以在创建类之前更改类定义的内容。这个属于高级内容,一般用不到。

    要理解元类,必须先理解用户使用class语句定义新类时发生了些什么事情。前面说过,类定义本身也是个对象,它是type类的实例。 下例展示了类创建时的伪代码过程:

    以下是用户定义一个名为Foo类的普通代码:

    class Foo(object):
        pass

    以下是Python内部创建这个类定义对象的伪代码过程:

    class_name = "Foo"            # 类名
    class_parents = (object,)     # 基类
    class_body="""                # 类主体文本
        pass
    """
    class_dict = {}
    exec{class_body, globals(), class_dict)   # 在局部环境变量class_dict中执行类主体文本,之后类中所有的方法和类变量都会进入class_dict
    
    # 最后一步:创建类对象Foo!
    Foo = type(class_name, class_parents, class_dict)
    

    所谓“元类”,就是用于创建和管理类对象的特殊对象,上例最后一句中,控制Foo创建的元类是一个名为type的类。 最后一句把类名、基类列表、字典传递个元类的构造函数,以创建新的类对象。

    上面创建类对象过程的最后一步:调用元类type()的步骤,可以自己定义,这个就是“元类”的用法。可以通过在基类元组中, 提供metaclass关键字参数来指定自己的元类。下面是一个使用元类的的例子,它要求所有方法在定义时必须拥有一个文档字符串。

    class DocMeta(type):
        def __init__(self, name, bases, dict):
            for key, value in dict.items():
                # 跳过特殊方法和私有方法:
                if key.startswith('__'): continue
                # 跳过不可调用的任何方法
                if not hasattr(value, '__call__'): continue
                # 检查doc字符串
                if not getattr(value, '__doc__'): 
                    raise TypeError('%s must have a docstring' %key)
            type.__init__(self, name, bases, dict)
                
    # 要使用元类,必须先定义一个基类:
    class Documented(metaclass=DocMeta):
        pass
      
    # 然后将该基类用作所有需要这个检查文档字符串功能类的父类:
    class Foo(Documented):
        def bar(self, a, b):
            """This is bar method document."""
            pass

    下面是一个更高级的元类应用,它在创建类之前不止检查,还会更改类定义的内容。这个通过定义元类的__new__()方法来实现。 下例展示了如果将元类和描述符结合使用。虽然使用元类可以改变用户定义的类,但不建议过多使用,因为会造成理解困难。

    class TypedProperty:
        def __init__(self, name, type, default=None):
            self.name = None
            self.type = type
            self.default = default if default else type() 
            
        def __get__(self, instance, cls):
            return getattr(instance, self.name, self.default)   # 这个getattr是Python内置函数
            
        def __set__(self, instance, value):
            if not isinstance(value, self.type):
                raise TypeError("Must be a %s" %self.type)
            setattr(instance, self.name, value)                 # 这个setattr是Python内置函数
          
        def __delete__(self, instance):
            raise AttributeError("Cannot delete attribute")
            
    class TypedMeta(type):
        def __new__(cls, name bases, dict):
            slots = []
            for key, value in dict.items():
                if isinstance(value, TypedProperty):
                    value.name = "_" + key
                    slots.append(value.name)
            dict['__slots__'] = slots
            return type.__new__(cls, name, bases, dict)
            
    # 用户要使用元类功能的基类定义
    class Typed(metaclass=TypedMeta):
        pass
            
    class Foo(Typed):
        name = TypedProperty(str)
        num = TypedProperty(int, 42)

      (8)类装饰器

    类装饰器是一个函数,它接受类作为输入,并返回类作为输出。如果完全理解了前面讲的函数装饰器, 那理解类装饰器是非常简单的。

    下面的例子定义了一个名为register的类装饰器,用它装饰类时,可以将被其装饰的类放入到注册字典中:

    register_dict = {}
    # 定义类装饰器
    def register(cls):
        register_dict[cls.__clsid__] = cls
        return cls
        
    # 使用类装饰器
    @register
    class Foo(object):
        __clsid__ = "123"
    

     

    返回目录

  • 相关阅读:
    RedisDump安装报错
    安装mysql解压版时遇到的错误
    Docker 私有仓库 Harbor搭建与使用
    最好的6个Go语言Web框架
    安裝 drone CLI和使用drone
    使用 Kubernetes Helm 安装 Drone
    从ELK到EFK演进
    搭建helm私服ChartMuseum
    Helm3的使用
    Helm3部署安装
  • 原文地址:https://www.cnblogs.com/initcircuit/p/11868472.html
Copyright © 2020-2023  润新知