Python_类全面解析

类(classes)

在Python中,定义类是通过class关键字,class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类。

面向对象重要的概念就是类(Class)和实例(Instance),类是抽象的模板,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同。

先回顾下 OOP 的常用术语:

  • 类:对具有相同数据和方法的一组对象的描述或定义。
  • 对象:对象是一个类的实例。
  • 实例(instance):一个对象的实例化实现。
  • 实例属性(instance attribute):一个对象就是一组属性的集合。
  • 实例方法(instance method):所有存取或者更新对象某个实例一条或者多条属性的函数的集合。
  • 类属性(classattribute):属于一个类中所有对象的属性,不会只在某个实例上发生变化
  • 类方法(classmethod):那些无须特定的对象实例就能够工作的从属于类的函数。

 另外,Python的类机制使用尽可能少的新语法和语义将类引入语言。Python的类提供了面向对象程序设计语言所有的 标准特性:类继承机制允许有多个基类,一个派生类可以覆盖基类中的任何方法,一个方法可以使用相同的名字调用 基类中的方法。

Table of Contents

1.名字和对象
对象有其特性,同一个对象可以有多个名字,这与其它语言中的别名很相似。别名有时候像指针,例如将对象当做 函数参数传递的时候非常高效,因为只传递了指针,这避免了pascal中的两套参数传递机制。
2.Python的域(scopes)和名称空间(namespaces)
在引入类之前,我们讲Python的域规则。类的定义巧妙地运用了名称空间,所以你需要知道域和名称空间如何工作才能理解发生了什么。

首先从定义开始。 名称空间是名字和对象之间的映射。多数名称空间使用Python的字典来实现,但除非出于性能考虑,我们通常不关心具体如何实现。名称空间的例子有,内置的名称例如abs(),内置的异常名,模块的全局名称,函数调用时的局部名称。在某种程度上,对象的属性也构成名称空间。

关于名称空间最重要的一点是:不同名称空间中的名称没有关系。例如 两个不同模块中都可以包含名为maximize的函数,这不会造成混肴,因为使用这些模块时必须加上模块名作为前缀。 另外,我把任何点后的名称叫做属性。例如,在表达式z.real中,real是对象z的属性。严格来说,引用模块中的名称是对属性的引用,在表达式modname.funcname中,modname是一个模块,funcname是它的一个属性。这个例子中模块属性和模块 内定义的全局名称有着直接的映射,它们有着相同的名称空间。 属性可能是只读的或者可写的,上面的例子中,属性就是可写的,例如:modname.the_ answer = 42.可写的属性可以被删除, 例如 del modname.the_ answer 会删除模块 modname中的 the_ answer属性。

名称空间在不同的时刻创建,有着不同的生命周期。包含内置名称的名称空间在Python解释器启动时被创建,且不会被删除。 模块的全局名称空间在模块被导入时被创建,正常情况下,模块的名称空间会持续到解释器退出。来自脚本文件或者交互式环境 被解释器最顶层调用执行的语句,被认为是 __ main __ 模块的一部分,所以他们有着自己的全局名称空间。 函数的局部名称空间当函数被调用时被创建,函数返回时或者出现异常而函数又没有提供处理方式时被删除。当然,在递归调用 中每一次调用都有他们自己的局部名称空间。 域(scpoe)是Python程序的一个名称空间可以直接访问的一个文本范围,“直接访问”在这里的意思时当对一个名字的访问没有 前缀时,会尝试在名称空间内查找这个名字。 在执行的任意时刻,至少有三个嵌套域,它们有名称空间可以直接访问。

  • 最内层的域,它会首先被搜索,包含局部名称
  • 任何封装函数的域,从最近的封装域开始搜索,包含非局部,非全局的名称
  • 倒数第二个域,包含当前模块的全局名称
  • 最外层的域,最后被搜索,包含内置名字的名称空间

如果一个名字被声名为全局的,那么所有的引用和赋值都是针对中间层的域,这一层域包含模块的全局名称。 意识到域是由文本决定是非常重要的,定义在模块中的一个函数的全局域就是这个模块的名称空间,无论这个函数在哪儿, 通过哪个别名被调用。

3 初识类
3.1 定义类
class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类定义,像函数定义一样,在执行时才会起作用。你可以把类定义放在任何地方比如if语句的分支,或者在函数内部。 在实际应用时,定义在类中的语句通常都是函数定义,但是其它语句也是允许出现的,并且有的时候非常有用。 当进入一个类定义时,一个新的名称空间被创建,并且被当作局部域来使用。

3.2 类对象
类对象提供两种操作,属性引用和实例化。 属性引用使用标准句法:obj.name. 有效的属性名是类对象创建时类的名称空间内的所有名字。 例如下面的类定义中,MyClass.i和MyClass.f都是有效的属性名。
>>> class MyClass:
...     """A simple example class"""
...     i = 123
...     def f(self):
...         return 'hello world'
... 
>>> MyClass.i
123
>>> MyClass.i = 10

类的实例化使用函数记号,例如:

>>> x = MyClass()
>>> x.i
10

这个实例化操作创建了一个空对象,许多类在实例化时定义了一些初始化操作。例如:

>>> class MyClass():
...     def __init__(self):
...          self.data = []

当一个类定义了__ init __ 方法后,类实例化时会自动调用 __ init __ ().

__ init __ 函数还可以有其它参数,例如:

>>> class Complex:
...     def __init__(self, realpart, imagpart):
...         self.r = realpart
...         self.i = imagpart
...
>>> x = Complex(3.0, -4.5)
>>> x.r, x.i
(3.0, -4.5)
3.3 实例化对象
现在我们可以用实例化的对象做些什么呢?它唯一可以进行的操作是属性引用。有两类有效的属性名,数据属性和方法。 数据属性(data attributes)对应c++中的数据成员,数据属性无需声明,第一次给它赋值时就表明了它的存在。 另一种实例化的属性引用叫做方法(Method).方法是对象内的一个函数。
3.4 方法对象
通常我们调用一个方法的方式是:
x.f()

但是,由于x.f是一个方法对象,所以它可以存储起来,以便以后调用

>>> class MyClass:
...     """A simple example class"""
...     i = 12345
...     def f(self):
...         return 'hello world'
... 
>>> x = MyClass()
>>> x.f()
'hello world'
>>> xf = x.f
>>> xf()
'hello world'

你可能已经发现,f名名有一个参数,但是调用时为什么没有使用呢。其实,原因在于 x.f() 与 MyClass.f(x) 是等价的。

>>> MyClass.f(x)
'hello world'
3.5 漫谈
数据属性如果和方法属性名称相同,前者会覆盖后者。所以为了避免名称冲突,最好养成一些习惯,比如方法名称大写,数据属性 名称前加一个短小,唯一的前缀。或者数据属性用名词,方法属性用动词。 数据属性可以被方法引用,也可以被对象的使用者引用。换句话说,类不能实现为纯抽象数据类型。 通常,方法的第一个参数是self.虽然这只是一个习惯用法,但是遵循一些习惯用法会让你的程序可读性更强。 函数定义没有必要非在类里面,例如:
# 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)
... 
>>> b = Bag()
>>> b.data
[]
>>> b.add('1')
>>> b.data
['1']
>>> b.addtwice('x')
>>> b.data
['1', 'x', 'x']
4.方法
4.1实例方法(instance method)
Python 的实例方法用得最多,也最常见。
class Kls(object):
    def __init__(self, data):
        self.data = data

    def printd(self):
        print(self.data)


ik1 = Kls('leo')
ik2 = Kls('lee')

ik1.printd()
ik2.printd()

输出:

leo 
lee

上述例子中,printd为一个实例方法。实例方法第一个参数为self,当使用ik1.printd()调用实例方法时,实例ik1会传递给self参数,这样self参数就可以引用当前正在调用实例方法的实例。利用实例方法的这个特性,上述代码正确输出了两个实例的成员数据。

4.2类方法
Python 的类方法采用装饰器@classmethod来定义,我们直接看例子。
class Kls(object):
    num_inst = 0

    def __init__(self):
        Kls.num_inst = Kls.num_inst + 1

    @classmethod
    def get_no_of_instance(cls):
        return cls.num_inst


ik1 = Kls()
ik2 = Kls()

print ik1.get_no_of_instance()
print Kls.get_no_of_instance()

输出:

2 
2

在上述例子中,我们需要统计类Kls实例的个数,因此定义了一个类变量num_inst来存放实例个数。通过装饰器@classmethod的使用,方法get_no_of_instance被定义成一个类方法。在调用类方法时,Python 会将类(class Kls)传递给cls,这样在get_no_of_instance内部就可以引用类变量num_inst。
由于在调用类方法时,只需要将类型本身传递给类方法,因此,既可以通过类也可以通过实例来调用类方法。

4.3静态方法
在开发中,我们常常需要定义一些方法,这些方法跟类有关,但在实现时并不需要引用类或者实例,例如,设置环境变量,修改另一个类的变量,等。这个时候,我们可以使用静态方法。 Python 使用装饰器@staticmethod来定义一个静态方法。
IND = 'ON'


class Kls(object):
    def __init__(self, data):
        self.data = data

    @staticmethod
    def checkind():
        return IND == 'ON'

    def do_reset(self):
        if self.checkind():
            print('Reset done for: %s' % self.data)

    def set_db(self):
        if self.checkind():
            print('DB connection made for: %s' % self.data)


ik1 = Kls(24)
ik1.do_reset()
ik1.set_db()

输出:

Reset done for: 24 
DB connection made for: 24

在代码中,我们定义了一个全局变量IND,由于IND跟类Kls相关,所以我们将方法checkind放置在类Kls中定义。方法checkind只需检查IND的值,而不需要引用类或者实例,因此,我们将方法checkind定义为静态方法。
对于静态方法,Python 并不需要传递类或者实例,因此,既可以使用类也可以使用实例来调用静态方法。

4.4实例方法,类方法与静态方法的区别
我们用代码说明实例方法,类方法,静态方法的区别。注意下述代码中方法foo,class_foo,static_foo的定义以及使用。
class Kls(object):
    def foo(self, x):
        print('executing foo(%s,%s)' % (self, x))

    @classmethod
    def class_foo(cls,x):
        print('executing class_foo(%s,%s)' % (cls,x))

    @staticmethod
    def static_foo(x):
        print('executing static_foo(%s)' % x)


ik = Kls()

# 实例方法
ik.foo(1)
print(ik.foo)
print('==========================================')

# 类方法
ik.class_foo(1)
Kls.class_foo(1)
print(ik.class_foo)
print('==========================================')

# 静态方法
ik.static_foo(1)
Kls.static_foo('hi')
print(ik.static_foo)

输出:

executing foo(<__main__.Kls object at 0x0551E190>,1)
<bound method Kls.foo of <__main__.Kls object at 0x0551E190>>
==========================================
executing class_foo(<class '__main__.Kls'>,1)
executing class_foo(<class '__main__.Kls'>,1)
<bound method type.class_foo of <class '__main__.Kls'>>
==========================================
executing static_foo(1)
executing static_foo(hi)
<function static_foo at 0x055238B0>

对于实例方法,调用时会把实例ik作为第一个参数传递给self参数。因此,调用ik.foo(1)时输出了实例ik的地址。

对于类方法,调用时会把类Kls作为第一个参数传递给cls参数。因此,调用ik.class_foo(1)时输出了Kls类型信息。
前面提到,可以通过类也可以通过实例来调用类方法,在上述代码中,我们再一次进行了验证。

对于静态方法,调用时并不需要传递类或者实例。其实,静态方法很像我们在类外定义的函数,只不过静态方法可以通过类或者实例来调用而已。

值得注意的是,在上述例子中,foo只是个函数,但当调用ik.foo的时候我们得到的是一个已经跟实例ik绑定的函数。调用foo时需要两个参数,但调用ik.foo时只需要一个参数。foo跟ik进行了绑定,因此,当我们打印ik.foo时,会看到以下输出 :

<bound method Kls.foo of <__main__.Kls object at 0x0551E190>>

当调用ik.class_foo时,由于class_foo是类方法,因此,class_foo跟Kls进行了绑定(而不是跟ik绑定)。当我们打印ik.class_foo时,输出:

<bound method type.class_foo of <class '__main__.Kls'>>

当调用ik.static_foo时,静态方法并不会与类或者实例绑定,因此,打印ik.static_foo(或者Kls.static_foo)时输出:

<function static_foo at 0x055238B0>

概括来说,是否与类或者实例进行绑定,这就是实例方法,类方法,静态方法的区别

我们用代码说明实例方法,类方法,静态方法的区别。

5. 派生
派生类的形式如下:
class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

BaseClassName必须在包含派生类的域内定义,BaseClassName可以是一个表达式,例如:

class DerivedClassName(modname.BaseClassName):

当派生类的对象引用了一个属性时,会先在派生类内查找这个属性名,如果找不到,再到基类中查找。 派生类可以覆盖基类中的方法,即使基类中的方法被覆盖了,也可以使用下面的方法来调用

BaseClassName.methodname(self, arguments)
6 多重继承
Python 支持有限的多重继承:
class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

在旧风格的类中,唯一的规则是深度优先,从左到右。以上述类定义为例,如果一个属性没有在 DerivedClassName中被 找到,那么会继续搜索Base1,Base2等等 在新风格的类中,对方法的解析次序是动态改变的,这是因为类的继承关系会呈现出一个或多个菱形。例如新风格的类都由 object类派生出,这样就会就多条路径通向object。为了避免基类被多次搜索,使用了线性化算法将所有基类排列成从左 到右的顺序

7 私有变量和类局部引用
实例的私有变量只能在对象内部使用,python中常常使用例如 _ spam 的形式来代表API的非公有部分,无论是函数,方法还是 数据成员。类私有成员的特性的一种有效的用法是可以避免与子类中定义的名字冲突,这种机制叫做 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)

注意上述代码中 __ update 的使用,避免了子类中对update的覆盖影响到基类 __ init__ 中的 update.

8 结构体
有时候我们可能需要像C中的struct那样的数据类型,把少量的数据项放在一起。Python中可以使用定义一个空类来实现这一点:
# filename:p.py
class Employee:
    pass
 
john = Employee() # Create an empty employee record
 
# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
>>> import p
>>> p.john
<p.Employee instance at 0xb71f50ac>
>>> p.john.name
'John Doe'
>>> p.john.dept
'computer lab'
>>> p.john.salary
1000
9 异常(Exceptions)也是类
用户定义的异常也可以用类来表示,使用这种机制可以创建出可扩展,层次化的异常。 raise 语句有两种新的形式
raise Class, instance
raise instance

第一种形式中,instance必须是Class的一个实例,或者是由它派生出的类。 第二种形式是下面这种形式的缩写

raise instance.__class__, instance

下面这个例子会依次打印出B,C,D

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"
>>> import f
B
C
D

注意如果 B写在最前面,会打印出BBB,这是因为raise C和raise D时,执行到except B是都会 print “B”. 因为B是C,D的基类.

10 迭代器
现在你可能已经注意到了多数容器对象都可以使用for语句来循环
>>> for elem in [1,2,3]:
...     print elem
... 
1
2
3
>>> for elem in (1,2,3):
...     print elem
... 
1
2
3

这一风格清晰,简捷,方便。迭代器的使用在Python中非常普便。for语句的背后,其实是对容器对象调用 iter(). 这个函数返回一个迭代器对象,它定义了next()函数,每次访问容器中的一个元素。当没有元素的时候,next()返回一个 StopIteration异常,告诉for语句循环结束了。

>>> s = 'asdf'
>>> it = iter(s)
>>> it
<iterator object at 0xb71f590c>
>>> it.next()
'a'
>>> it.next()
's'
>>> it.next()
'd'
>>> it.next()
'f'
>>> it.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

理解了迭代机制,就可以很容易地把迭代器加入你的类中,定义__ iter__ ()方法,返回一个有next()方法的对象。 如果一个类定义了next()函数,__ iter__ () 可以仅返回 self:

# q.py
class Reverse:
    def __init__(self, data):
        self.data = data
        self.index = len(data)
    def __iter__(self):
        return self
    def next(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index -1
        return self.data[self.index]
11 生成器(Generators)
生成器是创建迭代器的一个简单而强大的工具。它们像正常函数一样,只是需要返回数据时使用 yield语句。
# d.py
def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> import d
>>> for char in d.reverse('golf'):
...     print char
... 
f
l
o
g

任何可以使用生成器做的事,都可以使用前一版本的reverse实现,生成器之所以实现紧凑是因为自动创建了 __ iter() 和 next() 方法。



转载链接:
link.
link.

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值