代码块的分类

python中分几种代码块类型,它们都有自己的作用域,或者说名称空间:

  • 文件或模块整体是一个代码块,名称空间为全局范围
  • 函数代码块,名称空间为函数自身范围,是本地作用域,在全局范围的内层
    • 函数内部可嵌套函数,嵌套函数有更内一层的名称空间
  • 类代码块,名称空间为类自身
    • 类中可定义函数,类中的函数有自己的名称空间,在类的内层
    • 类的实例对象有自己的名称空间,和类的名称空间独立
    • 类可继承父类,可以链接至父类名称空间

正是这一层层隔离又连接的名称空间将变量、类、对象、函数等等都组织起来,使得它们可以拥有某些属性,可以进行属性查找。

本文详细解释类和对象涉及的名称空间,属于纯理论类的内容,有助于理解python面向对象的细节。期间会涉及全局和本地变量作用域的查找规则,如有不明白之处,可先看文章:Python作用域详述

一个概括全文的示例

以下是一个能在一定程度上概括全文的示例代码段:

x = 11           # 全局变量x

def f():         # 全局变量f
print(x) # 引用全局变量x def g(): # 全局变量g
x = 22 # 定义本地变量x
print(x) # 引用本地变量x class supcls(): # 全局变量supcls
x = 33 # 类变量x
def m(self): # 类变量m,类内函数变量self
x = 44 # 类内函数变量x
self.x = 55 # 对象变量x class cls(supcls): # 全局变量cls
x = supcls.x # 引用父类属性x,并定义cls类属性x
def n(self): # 类变量n
self.x = 66 # 对象变量x

如果能理解上面的每个x属于哪个作用域、哪个名称空间,本文内容基本上就理解了。

类的名称空间

下面有一个类,类中有类属性x、y,有方法m和n。

class supcls():
x = 3
y = 4 def m(self):
x = 33
self.x = 333
self.y = 444
self.z = 555 def n(self):
return self.x, self.y, self.z

当python解释到supcls代码块后,知道这是一个类,类有自己的名称空间。所以,当知道了这个类里面有x、y、m、n后,这几个属性都会放进类supcls的名称空间中。

如下图:

在上图中,类的名称空间中有属性x、y、m和n,它们都称为类属性。需要说明的是,在python中,函数变量m、n和普通变量没什么区别,仅仅只是它保存了指向函数体的地址,函数体即上图中用func m和func n所表示的对象。

因为有名称空间,可以直接使用完全限定名称去访问这个名称空间中的内容。例如:

print(supcls.x)
print(supcls.y)
print(supcls.m)
print(supcls.n)

输出结果:

3
4
<function supcls.m at 0x02B83738>
<function supcls.n at 0x02B836F0>

因为函数m和n也是类的属性,它们也可以直接通过类名来访问执行。例如,新加入一个函数,但不用self参数了,然后执行它。

class testcls():
z = 3
def a():
x = 1
print(x)
# print(z) # 这是错的 testcls.a()

不仅如此,方法也是属性,它实际上就是函数(在python 3.0中确实如此,在以前版本中方法和函数是有点区别的)。所以,可以将方法保存下来,之后再去调用它:

aa = testcls.a
aa()
print(aa)

但是需要注意,方法代码块中看不见类变量。虽然类和方法的作用域关系类似于全局作用域和函数本地作用域,但并不总是等价。例如,方法a()中无法直接访问类变量z。这就像类内部看不到全局变量一样。

上面全都是使用类名.属性这种完全限定名称去访问类中的属性的。如果生成类的对象,则可以通过对象去访问相关对象属性,因为对象有自己的名称空间,且部分属性来源于类。

对象名称空间

类就像一个模板,可以根据这个模板大量生成具有自己特性的对象。在Python中,只需像调用函数一样直接调用类就可以创建对象。

例如,下面创建了两个cls类的对象o1和o2,创建类的时候可以传递参数给类,这个参数可以传递给类的构造函数__init__()。

o1 = cls()
o2 = cls("some args")

对象有自己的名称空间。因为对象是根据类来创建的,类是它们的模板,所以对象名称空间中包含所有类属性,但是对象名称空间中这些属性的值不一定和类名称空间属性的值相同。

现在根据supcls类构造两个对象s1和s2:

class supcls():
x = 3
y = 4
def m(self):
x = 33
self.x = 333
self.y = 444
self.z = 555
def n(self):
return self.x, self.y, self.z s1 = supcls()
s2 = supcls()

那么它们的名称空间,以及类的名称空间的关系如下图所示:

现在仅仅只是对象s1、s2连接到了类supcls,对象s1和s2有自己的名称空间。但因为类supcls中没有构造方法__init__()初始化对象属性,所以它们的名称空间中除了python内部设置的一些"其它"属性,没有任何属于自己的属性。

但因为s1、s2连接到了supcls类,所以可以进行对象属性查找,如果对象中没有,将会向上找到supcls。例如:

print(s1.x)    # 输出3,搜索到类名称空间
print(s1.y) # 输出4,搜索到类名称空间
# print(s1.z) # 这是错的

上面不再是通过完全限定的名称去访问类中的属性,而是通过对象属性查找的方式搜索到了类属性。但上面访问z属性将报错,因为还没有调用m方法。

当调用m方法后,将会通过self.xxx的方式设置完全属于对象自身的属性,包括x、y、z。

s1.m()
s2.m()

现在,它们的名称空间以及类的名称空间的关系如下图所示:

现在对象名称空间中有x、y和z共3个属性(不考虑其它python内部设置的属性),再通过对象名去访问对象属性,仍然会查找属性,但对于这3个属性的搜索不会进一步搜索到类的名称空间。但如果访问对象中没有的属性,比如m和n,它们不存在于对象的名称空间中,所以会搜索到类名称空间。

print(s1.x)  # 对象属性333,搜索到对象名称空间
print(s1.y) # 对象属性444,搜索到对象名称空间
print(s1.z) # 对象属性555,搜索到对象名称空间
s1.m() # 搜索到类名称空间
s1.n() # 搜索到类名称空间

对象与对象之间的名称空间是完全隔离的,对象与类之间的名称空间存在连接关系。所以,s1和s2中的x和y和z是互不影响的,谁也看不见谁。

但现在想要访问类变量x、y,而不是对象变量,该怎么办?直接通过类名的完全限定方式即可:

print(s1.x)      # 输出333,对象属性,搜索到对象名称空间
print(supcls.x) # 输出3,类属性,搜索到类名称空间

和类一样,通过对象可以访问到方法,方法可以赋值给一个变量保存下来,以后再执行:

mm = s1.m
mm()

实际上,s1.m是一个function类的实例对象。在s1.xxx搜索索性的时候,如果发现xxx不是对象或类的数据属性,说明这是方法属性,python会将这个方法与对象(obj.method)或类(cls.method)进行关联。无论如何,方法都是类的属性。

因为对象有了自己的名称空间,就可以直接向这个名称空间添加属性或设置属性。例如,下面为s1对象添加一个新的属性,但并不是在类内部设置,而是在类的外部设置:

s1.x = 3333       # 在外部设置已有属性x
s1.var1 = "aaa" # 在外部添加新属性var1

新属性var1将只存在于s1,不存在于s2和类supcls中。

类属性和对象属性

属于类的属性称为类属性,即那些存在于类名称空间的属性。类属性分为类变量和方法。方法分为3种:

  • 实例方法:普通的方法,要通过self参数传递实例对象
  • 类方法:传递的是类名而非实例对象的方法
  • 静态方法:不通过self传递对象的方法

类似的,属于对象名称空间的属性称为对象属性。对象属性脱离类属性,和其它对象属性相互隔离。

例如:

class cls:
x=3
def f():
y=4
print(y)
def m(self):
self.z=3

上面的x、f、m都是类属性,x是类变量,f和m是方法,z是对象属性。

  • x可以通过类名和对象名来访问。
  • f没有参数,不能通过对象来调用(通过对象调用时默认会传递对象名作为方法的第一个参数),只能通过类名来调用,所以f属于静态方法。
  • m可以通过对象名来调用,也可以通过类名来调用(在子类继承父类,扩展父类方法的时候很常用)。
  • z通过self设置,独属于每个self参数代表的对象,所以是对象属性。

子类继承时的名称空间

子类和父类之间有继承关系,它们的名称空间也通过一种特殊的方式进行了连接:子类可以继承父类的属性。

例如下面的例子,子类class childcls(supcls)表示childcls继承了父类supcls。

class supcls():
x = 3
y = 4
def m(self):
x = 33
self.x = 333
self.y = 444
self.z = 555
def n(self):
return self.x, self.y, self.z class childcls(supcls):
y = supcls.y + 1 # 通过类名访问父类属性
def n(self):
self.z = 5555

当python解释完这两段代码块时,初始时的名称空间结构图如下:

当执行完class childcls(supcls)代码块之后,子类childcls就有了自己的名称空间。初始时,这个名称空间中除了连接到父类supcls外,还有自己的类变量y和方法n(),子类中的方法n()重写了父类supcls的方法n()。

因为有自己的名称空间,所以可以访问类属性。当访问的属性不存在于子类中时,将自动向上搜索到父类。

print(childcls.x)   # 父类属性,搜索到父类名称空间
print(childcls.y) # 子类自身属性,搜索到子类名称空间
print(childcls.z) # 错误,子类和父类都没有该属性

当创建子类对象的时候,子类对象的变量搜索规则:

  1. 子类对象自身名称空间
  2. 子类的类名称空间
  3. 父类的类名称空间

例如,创建子类对象c1,并调用子类的方法n():

c1 = childcls()
c1.n()

现在,子类对象c1、子类childcls和父类supcls的关系如下图所示:

通过前面的说明,想必已经不用过多解释。

多重继承时的名称空间

python支持多重继承,只需将需要继承的父类放进子类定义的括号中即可。

class cls1():
... class cls2():
... class cls3(cls1,cls2):
...

上面cls3继承了cls1和cls2,它的名称空间将连接到两个父类名称空间,也就是说只要cls1或cls2拥有的属性,cls3构造的对象就拥有(注意,cls3类是不拥有的,只有cls3类的对象才拥有)。

但多重继承时,如果cls1和cls2都具有同一个属性,比如cls1.x和cls2.x,那么cls3的对象c3.x取哪一个?会取cls1中的属性x,因为规则是按照(括号中)从左向右的方式搜索父类。

再考虑一个问题,如果cls1中没有属性x,但它继承自cls0,而cls0有x属性,那么,c3.x取哪个属性。

这在python 3.x中是一个比较复杂的问题,涉及到动态调整访问顺序。但基本上可以根据MRO(Method Resolution Order)算法总结为:先左后右,先深度再广度,但必须遵循共同的超类最后搜索。

此处只给一个它们的访问顺序图示,这也是__mro__属性的默认顺序:

类自身就是一个全局属性

在python中,类并没有什么特殊的,它存在于模块文件中,是全局名称空间中的一个属性。

例如,在模块文件中定义了一个类cls,那么这个cls就是一个全局变量,只不过这个变量中保存的地址是类代码块所在数据对象。

# 模块文件顶层
class cls():
n = 3

而模块本身是一个对象,有自己的模块对象名称空间(即全局名称空间),所以类是这个模块对象名称空间中的一个属性,仅此而已。

另外需要注意的是,类代码块和函数代码块不一样,涉及到类代码块中的变量搜索时,只会根据对象与类的连接、子类与父类的继承连接进行搜索。不会像全局变量和函数一样,函数内可以向上搜索全局变量、嵌套函数可以搜索外层函数。

例如:

# 全局范围
x = 3
def f():
print(x) # 搜索到全局变量x class sup():
# print(x) # 这是错的,不会搜索全局变量
y = 3
print(y) # 这是对的,存在类属性y
def m(self):
# print(y) # 这是错的,不会搜索到类变量
self.z = 4 class childcls(sup):
# print(y) # 这是错的,不会搜索到父类

其实很容易理解为什么面向对象要有自己的搜索规则。对象和类之间是is a的关系,子类和父类也是is a的关系,这两个is a是面向对象时名称空间之间的连接关系,在搜索属性的时候可以顺着"这根树"不断向上爬,直到搜索到属性。

__dict__就是名称空间

前面一直说名称空间,这个抽象的东西用来描述作用域,比如全局作用域、本地作用域等等。

在其他语言中可能很难直接查看名称空间,但是在python中非常容易,因为只要是数据对象,只要有属性,就有自己的__dict__属性,它是一个字典,表示的就是名称空间。__dict__内的所有东西,都可以直接通过点"."的方式去访问、设置、删除,还可以直接向__dict__中增加属性。

例如:

class supcls():
x=3 class childcls(supcls):
y=4
def f(self):
self.z=5 >>> c=childcls()
>>> c.__dict__.keys()
dict_keys([]) >>> c.f()
>>> c.__dict__
{'z': 5}

可以直接去增、删、改这个dict,所作的修改都会直接对名称空间起作用。

>>> c.newkey = "NEWKEY"
>>> c.__dict__["hello"] = "world"
>>> c.__dict__
{'z': 5, 'newkey': 'NEWKEY', 'hello': 'world'}

注意,__dict__表示的是名称空间,所以不会显示类的属性以及父类的属性。正如上面刚创建childcls的实例时,dict中是空的,只有在c.f()之后才设置独属于对象的属性。

如果要显示类以及继承自父类的属性,可以使用dir()。

例如:

>>> c1 = childcls()
>>> c1.__dict__
{}
>>> dir(c1)
['__class__', '__delattr__', '__dict__',
......
'f', 'x', 'y']

关于__dict__和dir()的详细说明和区别,参见dir()和__dict__的区别。

__class__和__bases__

前面多次提到对象和类之间有连接关系,子类与父类也有连接关系。但是到底是怎么连接的?

  • 对象与类之间,通过__class__进行连接:对象的__class__的值为所属类的名称
  • 子类与父类之间,通过__bases__进行连接:子类的__bases__的值为父类的名称

例如:

class supcls():
x=3 class childcls(supcls):
y=4
def f(self):
self.z=5 c = childcls()

c是childcls类的一个实例对象:

>>> c.__class__
<class '__main__.childcls'>

childcls继承自父类supcls,父类supcls继承自祖先类object:

>>> childcls.__bases__
(<class '__main__.supcls'>,) >>> supcls.__bases__
(<class 'object'>,)

查看类的继承层次

下面通过__class__和__bases__属性来查看对象所在类的继承树结构。

代码如下:

def classtree(cls, indent):
print("." * indent + cls.__name__)
for supcls in cls.__bases__:
classtree(supcls, indent + 3) def objecttree(obj):
print("Tree for %s" % obj)
classtree(obj.__class__, 3) class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
class E: pass
class F(D, E): pass objecttree(B())
print("==============")
objecttree(F())

运行结果:

Tree for <__main__.B object at 0x037D1630>
...B
......A
.........object
==============
Tree for <__main__.F object at 0x037D1630>
...F
......D
.........B
............A
...............object
.........C
............A
...............object
......E
.........object

搞懂Python的类和对象名称空间的更多相关文章

  1. python基础学习Day17 面向对象的三大特性之继承、类与对象名称空间小试

    一.课前回顾 类:具有相同属性和方法的一类事物 实例化:类名() 过程: 开辟了一块内存空间 执行init方法 封装属性 自动的把self返回给实例化对象的地方 对象:实例 一个实实在在存在的实体 组 ...

  2. day 22 面向对象 类与对象 名称空间操作

    一.面向对象 1.面向过程编程 核心是"过程"二字,过程指的是解决问题的步骤,即先干什么再干什么 基于该思想编写程序就好比在编写一条流水线,是一种机械式的思维方式. 优点:复杂的问 ...

  3. python面向对象——类和对象

    一.三大编程范式 编程范式即编程的方法论,标识一种编程风格 三大编程范式: 1.面向过程编程(流水线式) 优点:极大的降低了程序的复杂度 缺点:应用场景固定住了,可扩展性差 2.函数式编程 特点: a ...

  4. 1.面向过程编程 2.面向对象编程 3.类和对象 4.python 创建类和对象 如何使用对象 5.属性的查找顺序 6.初始化函数 7.绑定方法 与非绑定方法

    1.面向过程编程 面向过程:一种编程思想在编写代码时 要时刻想着过程这个两个字过程指的是什么? 解决问题的步骤 流程,即第一步干什么 第二步干什么,其目的是将一个复杂的问题,拆分为若干的小的问题,按照 ...

  5. python 2 类与对象

    1.类与对象的概念 类即类别.种类,是面向对象设计最重要的概念,从一小节我们得知对象是特征与技能的结合体,而类则是一系列对象相似的特征与技能的结合体. 那么问题来了,先有的一个个具体存在的对象(比如一 ...

  6. Python初识类与对象

    Python初识类与对象 类与对象 世界观角度分析类与对象 类是一个抽象的概念,而对象是一个实体的存在,对象由类创造而出,每个对象之间互相独立互不影响,一个对象可以同时拥有多个类的方法,实例化就是通过 ...

  7. python的类和对象2(self参数)

    python的类和对象2(self参数) 1.python里面对象的方法都会有self参数,它就相当于C++里面的this指针:绑定方法,据说有了这个参数,Python 再也不会傻傻分不清是哪个对象在 ...

  8. day_12函数默认值,数据类型的补充,函数对象名称空间与作用域,函数的嵌套定义

    复习, 昨天讲了字符串的比较,按照从左往右比较每一个字符,通过字符对应的ASCII码进行比较 函数的参数,‘ 实参与形参 形参:在函数定义时()中出现的参数 实参,在函数调用时()中出现的参数 实参的 ...

  9. Python小世界:彻底搞懂Python一切皆对象!!!

    前言 犹记得当初学习Python的时候,对于Python一切皆对象很是懵逼,因为Python是面向对象的动态型语言,而在函数及高阶函数的应用中,如若对于一切皆对象不是有很透彻的了解,基础不是那么牢固的 ...

随机推荐

  1. bootstrap-datepicker简单使用

    粗略整理,可能存在其他的方式请大家多多指教 选择年份 html <div class="dropdown"> <label class="search- ...

  2. lua 使用中遇到的坑总结

    前言 竹子是 java 程序员一枚,最近在做一个登录的改造,用 lua 实现,现在基本算是告一段落,然后在此分享下在过程中遇到的坑吧. 一定要注意使用 lua 的版本,版本不同,可能有的函数就没有了, ...

  3. 安卓端 - H5页面在微信分享、收藏、保存图片不成功

    经过代码实践: 原因是微信在分享.收藏和保存时会获取到图片信息,当图片过大时,造成失败

  4. HDFS JAVA客户端的权限错误:Permission denied

    HDFS JAVA客户端的权限错误:Permission denied 转自:http://blog.csdn.net/kkdelta/article/details/50393413 搭建了一个Ha ...

  5. 【备忘】ASP.NET MVC 5 升级到 ASP.NET Core MVC 的部分变化

    正在将一个 .net 4.5 的项目(MVC 5)升级到 .net core 2.1,中间遇到了许多的修改,记在下面,帮大家少走弯路. System.Drawing 下面很多类已经不存在(如Bitma ...

  6. Javascript高级编程学习笔记(44)—— 动态样式

    动态样式 动态样式和昨天的动态脚本一样,都是一种动态引入外部样式(脚本的方式) 由于样式是由 link 元素引入的,所以动态样式自然也就是动态生成link元素插入文档的方式 不过和动态脚本不同的是,动 ...

  7. 14.不同条目的listview

    分类界面 整个项目的逻辑就是这样的 CategoryInfo  public class CategoryInfo { private String title; private String url ...

  8. laravel migrate时报错:Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes

    今天在学习laravel的路由模型绑定时,在按照文档执行php artisan migrate时报错. In Connection.php line 664: SQLSTATE[42000]: Syn ...

  9. 抓包工具 Fiddler 使用介绍

    简介 Fiddler是一个抓包工具,可以将网络传输发送与接收的数据包进行截获.重发.编辑等操作.也可以用来检测流量.原理是以web代理服务器的形式进行工作的,使用的代理地址是:127.0.0.1,端口 ...

  10. HTTP 协议中 GET 和 POST 方法详解

    GET请求报文分析 1.请求行 请求方法 GET(描述该请求采用了什么请求方法),HTTP 1.0 和 1.1 协议中共包含10种请求方法.不过 HTTP 1.1 中只有8种方法. URI 请求WEB ...