Python 面向对象编程

作者:最西瓜 来源:《Python核心编程》13章. 面向对象编程

Python 面向对象编程的最大优点就是你不必使用面向对象。Python 的类没有什么访问控制,可以说都是公共的。Python 类没有抽象方法或纯虚方法。Python 类中继承自 object 的称之为新式类,否则就是旧式类,推荐使用的是新式类。下面会详述新式类有什么优点。类的模板如下:

1
2
3
4
5
class MyNewObjectType(bases):
    """define MyNewObjectType class"""
    class_suite

myFirstObject = MyNewObjectType()

创建对象就像调用函数一样的。对象的数据属性是动态的,随时加入,但预先声明或者记录下来是一个好的习惯。定义类的实例方法,第一个参数永远是 self。__init__ 是特殊的实例方法,是在实例化对象后返回此对象引用前的调用的第一个方法,传给它的参数需要在创建对象时传入,此方法只能返回 None 值。类可以有自己的数据属性,推荐将数据属性初始化写在类的起始部分。

Python 中的继承和别的语言中没什么两样,子类会继承父类的各种方法和属性。如果定义了子类 __init__ 方法,父类的就不会自动调用,需要在第一行显式调用,而且需要显式传递 self 参数。从 Python 2.2 之后,可以从内建类中派生子类。Python 类支持多重继承。参考如下:

1
2
3
4
5
class EmplAddrBookEntry(AddrBookEntry):
    def __init__(self, nm, ph, id, em):
        AddrBookEntry.__init__(self, nm, ph)
        self.empid = id
        self.email = em

以上 __init__ 的调用成为非绑定调用,而用实例直接调用成为绑定调用。非绑定调用,必须要给出实例作为第一个参数。可以用 dir() 内建函数和类的特殊属性 __dict__ 来展示类具有哪些属性,同样实例也可以用 dir() 内建函数和特殊属性 __dict__ 来访问实例的数据属性,从本质上说方法属于类的属性,如果是內建类型则只能使用 dir() 来访问,它们没有 __dict__ 属性。类具有一些特殊属性:

  • __name__ 类的名字
  • __doc__ 类的文档字符串
  • __bases__ 类的所有父类构成的元组
  • __dict__ 类的属性
  • __module__ 类所在的模块
  • __class__ 实例对应的类,在经典对象中没有此属性,因为经典类被认为与类型不统一

实例属性和类属性是两种不同的事物,但是当对应的实例属性不存在时,通过实例可以访问类属性,但是假如通过实例去更新这个属性,并不是真正的更新,而是创建一个实例属性。示例如下:

1
2
3
4
5
6
7
class C(object):
    version = 1.2

c = C()
c.version   #1.2
c.version += 1 #创建了c.version
C.version #还是1.2

Python 除了以上方法外还有类方法和静态方法。类方法是默认传入的第一个参数是类本身,静态方法则所有参数都需要显式传入。类方法用 @classmethod 标记,静态方法用 @staticmethod 标记。类方法和静态方法都可以被子类继承或者覆盖。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class TestStaticMethod:
    @staticmethod
    def foo():
        print 'Calling static method foo()'

class TestClassMethod:
    @classmethod
    def foo(cls):
        print 'Calling class method foo()'
        print 'foo() is part of class:', cls.__name__

显式调用父类方法的优雅做法是用 super() 如下:

1
2
3
4
5
6
7
class C(P):
    def __init__(self):
        suepr(C, self).__init__()
        print 'Calling C\'s constructor'
    def foo(self):
        super(C, self).foo()
        print 'Hi, I am C-foo()'

多重继承很复杂,目前最佳的应用场景是 mixin 类,就是将几个完全不同的行为的类合并到一个类中。目前新式类的方法查找类似于广度优先查找,先在父类中查找,如果都找不到再到祖先类中查找。所有类都有一个 __mro__ 元组属性来告知方法被搜索的顺序。

Python 还提供我认为不会经常用到的类方法 __new__ ,这个方法以类本身作为第一个参数,必须返回一个类实例。通常的用法是当我们继承一个不变的内置类型如数字类型时会用到。但更好的方法是组合而不是继承。以下我们继承 float 类型:

1
2
3
class RoundFloat(float):
    def __new__(cls, val):
        return float.__new__(cls, round(val, 2))

调用父类 new 方法的推荐用法是:super(currentclass, cls).__new__(cls[, ...]),new 特殊方法的参数与 init 特殊方法是一样的,非常值得一提的是在类的内部推荐用 self.__class__() 来调用所属类的构造函数。

内建函数

  • issubclass(sub, sup) 判断 sub 是否为 sup 的子类,如果 sub 和 sup 是同一个类也是成立的,sup 可以是父类组成的元组,sub 是要是 sup 中任意一个类的子类就返回 True
  • isinstance(obj1, cls) 判断一个对象是否是一个类的实例,cls 必须是类对象或者内建类型,cls 可以是类对象组成的元组
  • hasattr() getattr() setattr() delattr() 分别用来判断是否有属性、获取属性、设置属性、删除属性

特殊方法

Python 的类有非常的特殊方法,而且还在不断增加,它们的作用在于模拟操作符或者标准类型的行为,有些则是内建函数会隐式调用,通常我们不会显式去调用它们。它们均为实例方法。

__str__ 被 str() 及 print() 调用,__repr__ 被 repr() 隐式调用,__call__ 当将对象当做函数调用时会调用此方法,__nonzero__ bool() 调用,__len__ len() 调用,__cmp__ cmp() 调用

__lt__ __le__ __gt__ __ge__ __eq__ __ne__ 分别小于、小于等于、大于、大于等于、等于、不等于对应关系操作符

迭代器特殊方法 __iter__ 被 iter() 或 for 循环隐式调用,__next__ 被 next() 或 for 隐式调用,当没有下一个元素时应当抛出 StopIteration 异常,__reversed__ 被 reversed() 隐式调用,这个比较少见。

属性特殊方法 __getattr__ 以句点方式或者内建函数 getattr() 获取属性时,当属性不存在时调用,__setattr__ 总是调用来设置属性,__delattr__ 总是调用来删除属性,__getattribute__ 总是调用来获取属性。属性包括数据属性和方法。当你定义了以上方法时最好能够定义 __dir__ 供 dir() 函数使用。当定义了 __getattribute__ 时,最好定义 __setattr__ 否则新设置的属性将无法取得,并且由于 __getattribute__ 会拦截方法的查找,需要仔细实现这个方法。

序列映射特殊方法 __contains__ 被 in 调用,__getitem__ 得到单个元素,__setitem__ __delitem__ 设置和删除单个元素,__hash__ hash() 调用获取散列值,__getslice__ __setslice__ __delslice__ 用来得到、设置、删除切片,__missing__ 用于当映射中没有对应 key 时提供默认值

数字方法 __add__ __sub__ __mul__ __truediv__ __floordiv__ __mod__ __pow__ __lshift__ __rshift__ __and__ __xor__ __or__ 这些方法不一一解释,以上方法都是当对象位于左边时调用,如果对象位于右边,将调用 __radd__ 等。 __iadd__ 用于模拟 += 操作符,i 方法的实现是可选的,因为没有定义的话,Python 会翻译成前面两种形式的隐式调用。

__neg__ __pos__ __abs__ __int__ __float__ __round__ __ceil__ __floor__ __trunc__ 同样用户执行数学计算。

序列化特殊方法 __copy__ __deepcopy__ 用于对象复制 __getstate__ 用于在序列化前获得对象状态,__reduce_ex__(protocol_version) __reduce__ 是真正的序列化对象, __getnewargs__ 控制对象反序列化时的创建,__setstate__ 控制在反序列化之后的状态设置

with 资源协议特殊方法 __enter__ __exit__(exc_type, exc_value, traceback) 进入和退出资源协议时调用的特殊方法。

私有化

在 Python 中以双下划线开始的属性或方法,在被外界访问时会被混淆。核心编程这本书里说,__num 这种会被混淆成 _ClassName__num 形式,相当于提供了一种很弱的私有性。需要混淆的原因在于子类同名的属性或方法可能会覆盖父类的。

新式类的高级特性

新式类带来的最大优点就是将用户定义类与 Python 内建类型统一,而且用户定义类可以继承内建类型。以下特性只存在于新式类中。

__slots__ 是一个类属性,类型必须是字符串的元组,此元组中的元素才被允许作为实例的属性,这个特性的好处在于将实例的 __dict__ 给去除了,节约了内存。

1
2
3
4
5
class SlottedClass(object):
    __slots__ = ('foo', 'bar')
c = SlottedClass()
c.foo = 42
c.xxx = "don't think so"  #将抛出AttributeError异常

新式类还给出了描述符的概念:当我们访问属性时,我们会去调用它的代理方法。描述符协议要求代理对象实现 __get__ __set__ __delete__ 方法来响应访问、设置和删除属性事件。我们不必所有都实现,事实上 delete 方法很少实现,如果只实现 get 方法则成为非数据描述符,实现了 get set 称为数据描述符。描述符必须定义为类的属性。

 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 Descriptor(object):

    def __init__(self):
        self._name = ''

    def __get__(self, instance, owner):
        print "Getting: %s" % self._name
        return self._name

    def __set__(self, instance, name):
        print "Setting: %s" % name
        self._name = name.title()

    def __delete__(self, instance):
        print "Deleting: %s" %self._name
        del self._name

class Person(object):
    name = Descriptor()

user = Person()
user.name = 'john smith'
print user.name
del user.name

描述符系统和 __getattribute__ 方法相辅相成,获取属性会执行如下属性的搜索:

  • 类属性
  • 数据描述符
  • 实例属性
  • 非数据描述符
  • __getattr__ 方法

除了用以上方式创建描述符,我们还可以用 property 达到同样的效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person(object):
    def __init__(self):
        self._name = ''

    def fget(self):
        print "Getting: %s" % self._name
        return self._name

    def fset(self, value):
        print "Setting: %s" % value
        self._name = value.title()

    def fdel(self):
        print "Deleting: %s" %self._name
        del self._name
    name = property(fget, fset, fdel, "I'm the property.")

还有很多别的方式来创建描述符,参考:Python 描述符简介

元类和 __metaclass__

Python 区别于别的 OOP 语言就在于它的类也是对象,可以动态的生成、修改。其实你应该明白,在 Python 中一切都是对象。方法 type(name, bases, dict) 可以创建类名为 name,基类为 bases 以及属性为 dict 的动态类,跟在代码中定义是一样的。元类的概念就是创建类对象的类。所有的新式类都是 type 的实例,恰恰 type 是所有用户定义类型的元类。元类使用的频率很低,但我们需要了解这种特性。

metaclass 可以是函数也可以是类,创建类时会去调用 __metaclass__ 然后返回一个新的类,给了元类去修改类的机会。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class UpperAttrMetaclass(type):
    def __new__(cls, name, bases, dict):
        attrs = ((name, value) for name, value in dict.items() if not name.startswith('__')
        uppercase_attr  = dict((name.upper(), value) for name, value in attrs)
        return type.__new__(cls, name, bases, uppercase_attr)

class Foo(object):
    __metaclass__ = UpperAttrMetaclass
    bar = 'bip'

f = Foo()
print f.BAR

UpperAttrMetaclass 元类改变了类 Foo 的属性名大小。通常如果需要改写类,我们需要实现 new 特殊方法,而 init 将传入的参数初始化给对象。当然,UpperAttrMetaclass 完全可以是一个函数。

查找 metaclass 在运用 metaclass 前需要在环境中查找到它,查找顺序是现在本类中查找特殊属性 metaclass,如果没有就向上查找父类中的 __metaclass__ ,还是找不到将查找全局变量 __metaclass__,否则就用 type 作为元类。

关于元类更多的信息请参考 Python 文档中 __metaclass__ 词条,那里会有完整的解释!