关于python:Python基础之Python中的类

71次阅读

共计 8221 个字符,预计需要花费 21 分钟才能阅读完成。

简介

class 是面向对象编程的一个十分重要的概念,python 中也有 class,并且反对面向对象编程的所有规范个性:继承,多态等。

本文将会具体解说 Python 中 class 的信息。

作用域和命名空间

在具体解说 class 之前,咱们来看一下作用域和命名空间的概念。

命名空间 (Namespace) 是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。

命名空间次要是为了防止程序中的名字抵触。只有名字在同一个命名空间中放弃惟一即可,不同的命令空间中的名字互不影响。

Python 中有三种命名空间:

  • 内置名称(built-in names),Python 语言内置的名称,比方函数名 abs、char 和异样名称 BaseException、Exception 等等。
  • 全局名称(global names),模块中定义的名称,记录了模块的变量,包含函数、类、其它导入的模块、模块级的变量和常量。
  • 部分名称(local names),函数中定义的名称,记录了函数的变量,包含函数的参数和部分定义的变量。(类中定义的也是)

命名空间的搜寻程序是 部分名称 -》全局名称 -》内置名称。

在不同时刻创立的命名空间领有不同的生存期。蕴含内置名称的命名空间是在 Python 解释器启动时创立的,永远不会被删除。模块的全局命名空间是在在模块定义被读入时创立.

通常,模块命名空间也会继续到解释器退出。

被解释器的顶层调用执行的语句,比方从一个脚本文件读取的程序或交互式地读取的程序,被认为是 __main__ 模块调用的一部分,因而它们也领有本人的全局命名空间。(内置名称实际上也存在于一个模块中;这个模块称作 builtins。)

一个 作用域 是一个命名空间可间接拜访的 Python 程序的文本区域。

Python 中有四种作用域:

  • Local:最内层,蕴含局部变量,比方一个函数 / 办法外部。
  • Enclosing:蕴含了非部分 (non-local) 也非全局 (non-global) 的变量。比方两个嵌套函数,一个函数(或类)A 外面又蕴含了一个函数 B,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。
  • Global:以后脚本的最外层,比方以后模块的全局变量。
  • Built-in:蕴含了内建的变量 / 关键字等。,最初被搜寻

作用域的搜寻程序是 Local -> Enclosing -> Global -> Built-in

Python 中用 nonlocal 关键字申明为 Enclosing 范畴,用 global 关键字申明为全局范畴。

咱们来看一个 global 和 nonlocal 会如何影响变量绑定的例子:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

下面程序输入:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

函数内的变量默认是 local 作用域,如果要在函数的函数中批改内部函数的变量,那么须要将这个变量申明为 nonlocal, 最初在模块顶层或者程序文件顶层的变量是全局作用域,如果须要援用批改的话须要申明为 global 作用域。

class

Python 中的类是用 class 来定义的,咱们看一个最简略的 class 定义:

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

类定义中的代码将创立一个新的命名空间,外面的变量都被看做是部分作用域。所有对局部变量的赋值都是在这个新命名空间之内。

类对象

class 定义类之后,就会生成一个类对象。咱们能够通过这个类对象来拜访类中定义的属性和办法。

比方咱们定义了上面的类:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

类中定义了一个属性 i 和一个办法 f。那么咱们能够通过 MyClass.iMyClass.f 来拜访他们。

留神,Python 中没有像 java 中的 private,public 这一种变量拜访范畴管制。你能够把 Python class 中的变量和办法都看做是 public 的。

咱们能够间接通过给 MyClass.i 赋值来扭转 i 变量的值。

In [2]: MyClass.__doc__
Out[2]: 'A simple example class'

In [3]: MyClass.i=100

In [4]: MyClass
Out[4]: __main__.MyClass

In [5]: MyClass.i
Out[5]: 100

Class 中,咱们还定义了 class 的文档,能够间接通过 __doc__ 来拜访。

类的实例

实例化一个类对象,能够将类看做是无参的函数即可。

In [6]: x = MyClass()

In [7]: x.i
Out[7]: 100

下面咱们创立了一个 MyClass 的实例,并且赋值给 x。

通过拜访 x 中的 i 值,咱们能够发现这个 i 值是和 MyClass 类变量中的 i 值是统一的。

实例化操作(“调用”类对象)会创立一个空对象。如果你想在实例化的时候做一些自定义操作,那么能够在类中定义一个 __init__() 办法时,类的实例化操作会主动为新创建的类实例发动调用 __init__()

def __init__(self):
    self.data = []

__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)

实例对象的属性

还是下面 class,咱们定义了一个 i 属性和一个 f 办法:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

咱们能够通过实例对象来拜访这个属性:

In [6]: x = MyClass()

In [7]: x.i
Out[7]: 100

甚至咱们能够在实例对象中创立一个不属于类对象的属性:

In [8]: x.y=200

In [9]: x.y
Out[9]: 200

甚至应用完之后,不保留任何记录:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

办法对象

咱们有两种形式来拜访函数中定义的办法,一种是通过类对象,一种是通过实例对象,看下两者有什么不同:

In [10]: x.f
Out[10]: <bound method MyClass.f of <__main__.MyClass object at 0x7fb69fc5f438>>

In [11]: x.f()
Out[11]: 'hello world'

In [12]:  MyClass.f
Out[12]: <function __main__.MyClass.f>

In [13]:  MyClass.f()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-e50d25278077> in <module>()
----> 1 MyClass.f()

TypeError: f() missing 1 required positional argument: 'self'

从下面的输入咱们能够看出,MyClass.f 是一个函数,而 x.f 是一个 object 对象。

还记得 f 办法的定义吗?f 办法有一个 self 参数,如果作为函数来调用的话,肯定要传入所有须要的参数才能够,这也就是为什么间接调用 MyClass.f() 报错,而 x.f() 能够间接运行的起因。

尽管办法的第一个参数经常被命名为 self。这也不过就是一个约定: self 这一名称在 Python 中相对没有非凡含意。

办法对象的非凡之处就在于实例对象会作为函数的第一个参数被传入。在咱们的示例中,调用 x.f() 其实就相当于 MyClass.f(x)。总之,调用一个具备 n 个参数的办法就相当于调用再多一个参数的对应函数,这个参数值为办法所属实例对象,地位在其余参数之前。

为什么办法对象不须要传入 self 这个参数呢?从 x.f 的输入咱们能够看出,这个办法曾经绑定到了一个实例对象,所以 self 参数会被主动传入。

办法能够通过应用 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)

类变量和实例变量

在类变量和实例变量的应用中,咱们须要留神哪些问题呢?

一般来说,实例变量用于每个实例的惟一数据,而类变量用于类的所有实例共享的属性和办法。

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 Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

如果同样的属性名称同时呈现在实例和类中,则属性查找会优先选择实例:

>>> class Warehouse:
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

继承

看下 Python 中继承的语法:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

如果基类定义在另一个模块中的时候:

class DerivedClassName(modname.BaseClassName):

如果申请的属性在类中找不到,搜寻将转往基类中进行查找。如果基类自身也派生自其余某个类,则此规定将被递归地利用。

派生类可能会重写其基类的办法。因为办法在调用同一对象的其余办法时没有非凡权限,所以调用同一基类中定义的另一办法的基类办法最终可能会调用笼罩它的派生类的办法。

Python 中有两个内置函数能够用来不便的判断是继承还是实例:

  • 应用 isinstance() 来查看一个实例的类型:

    例如:isinstance(obj, int) 仅会在 obj.__class__ 为 int 或某个派生自 int 的类时为 True。

  • 应用 issubclass() 来查看类的继承关系:

    例如:issubclass(bool, int) 为 True,因为 bool 是 int 的子类。然而,issubclass(float, int) 为 False,因为 float 不是 int 的子类。

Python 也反对多重继承:

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

如果某一属性在 DerivedClassName 中未找到,则会到 Base1 中搜寻它,而后(递归地)到 Base1 的基类中搜寻,如果在那里未找到,再到 Base2 中搜寻,依此类推。

公有变量

尽管 Python 中并没有强制的语法规定公有变量,然而大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有局部 (无论它是函数、办法或是数据成员)。

这只是咱们在写 Python 程序时候的一个实现细节,并不是语法的强制标准。

既然有公有变量,那么在继承的状况下就有可能呈现公有变量笼罩的状况,Python 是怎么解决的呢?

Python 中能够通过变量名改写的形式来防止公有变量的笼罩。

任何模式为 __spam 的标识符(至多带有两个前缀下划线,至少一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的以后类名称。这种改写不思考标识符的句法地位,只有它呈现在类定义外部就会进行。

举个例子:


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)

下面的示例即便在 MappingSubclass 引入了一个 __update 标识符的状况下也不会出错,因为它会在 Mapping 类中被替换为 _Mapping__update 而在 MappingSubclass 类中被替换为 _MappingSubclass__update

请留神传递给 exec()eval() 的代码不会将发动调用类的类名视作以后类;这相似于 global 语句的成果,因而这种成果仅限于同时通过字节码编译的代码。

迭代器

对于大多数容器对象来说,能够应用 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, end='')

其底层原理就是 for 语句会在容器对象上调用 iter()办法。该函数返回一个定义了 __next__() 办法的迭代器对象,此办法将逐个拜访容器中的元素。当元素用尽时,__next__() 将引发 StopIteration 异样来告诉终止 for 循环。

你能够应用 next() 内置函数来调用 __next__() 办法;上面的例子展现了如何应用:

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

晓得了迭代器的原理之后,咱们就能够为自定义的 class 增加迭代器对象了,咱们须要定义一个 __iter__() 办法来返回一个带有 __next__() 办法的对象。如果类已定义了 __next__(),则 __iter__() 能够简略地返回 self:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    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]

生成器

生成器 是一个用于创立迭代器的简略而弱小的工具。它们的写法相似于规范的函数,但当它们要返回数据时会应用 yield 语句。每次在生成器上调用 next() 时,它会从上次来到的地位复原执行(它会记住上次执行语句时的所有数据值)。

看一个生成器的例子:


def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>>
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

能够用生成器来实现的操作同样能够用前一节所形容的基于类的迭代器来实现。但生成器的写法更为紧凑,因为它会主动创立 __iter__()__next__() 办法。

生成器还能够用表达式代码的形式来执行,这样的写法和列表推导式相似,但外层为圆括号而非方括号。

>>> 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

>>> 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']

本文已收录于 http://www.flydean.com/10-python-class/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!

正文完
 0