Python中的成员修饰符比Java中少了2个。只有公有和私有两种

Python Version: 3.5+

成员修饰符是用来设置字段、方法和属性的。默认情况下,成员是公有的

公有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C:
def __init__(self, name):
self.name = name

def func(self):
print(self.name) # 在类的内部进行访问

c = C("ps")
print(c.name) # 在类的外部进行访问
c.func()

------------
ps
ps

像以上代码那样,字段从类的内部和外部都可以访问的情况,成员修饰符就是公有的

私有

在类中的成员名前面加上两个下划线__,就可以将该成员的属性设置为私有。私有的成员只有从类的内部访问的权限,出了该类的内部,其他任何地方都不允许访问,包括该类的派生类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class C:
def __init__(self, name):
self.__name = name

def func(self):
print(self.__name) # 在类的内部进行访问

c = C("ps")
print(c.__name) # 在类的外部进行访问

------------
Traceback (most recent call last):
File "/Users/lvrui/PycharmProjects/untitled/8/c5.py", line 9, in <module>
print(c.name) # 在类的外部进行访问
AttributeError: 'C' object has no attribute 'name'
1
2
3
4
5
6
7
8
9
10
11
12
class C:
def __init__(self, name):
self.__name = name

def func(self):
print(self.__name) # 在类的内部进行访问

c = C("ps")
c.func() # 只能通过该方法,从类的内部进行访问

------------
ps

Python访问私有成员的后门

1
2
3
4
5
6
7
8
9
10
class C:
def __init__(self, name):
self.__name = name

def func(self):
print(self.__name) # 在类的内部进行访问

c = C("ps")
# print(c.__name) # 在类的外部进行访问
print(c._C__name)

通过_类名__成员名这样的语法格式来强制访问类中的私有成员

Python面向对象中类的成员总共有三种,分别是字段、方法和属性

Python Version: 3.5+

字段

普通字段

1
2
3
4
5
6
7
8
class A:
def __init__(self, name):
# 我就是字段,普通字段
self.name = "polarsnow"

def show(self):
print(self.name)
return self.name

在上面的代码块中,self.name就是类中的字段。该字段的信息会保存在每一个实例化的对象中

静态字段

1
2
3
4
5
6
7
8
9
10
11
class A:
# 我也是字段,我是静态字段
country = "China"

def __init__(self, name):
# 我就是字段,普通字段
self.name = "polarsnow"

def show(self):
print(self.name)
return self.name

静态字段保存在类中。每个实例化的类中,都保存了一个类对象指针指向一个类。每个对象自己封装了自己的普通字段信息,但是该类所有的对象都共用了一份儿静态字段。

静态字段的应用场景和好处是某个类中,共用的一些信息可以抽离出来当做静态字段,好处是每个对象可以不用再重复性的保存同一份数据。

字段访问规则

一般情况下:自己去访问自己的字段

例如:访问静态字段,前面提到了,静态字段保存在类中,所以原则上需要使用类去访问静态字段;如果访问普通字段,普通字段保存在对象中,所以只能使用对象去访问普通字段。

在Java中,强制性的要求静态字段只能由类去访问;但是在Python中比较特殊,类中的静态字段也是可以通过对象去访问的。你需要坚持的原则就是:自己去访问自己的字段。不到特殊情况下,不要使用对象去访问类中的静态字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A:
# 我也是字段,我是静态字段
country = "China"

def __init__(self, name):
# 我就是字段,普通字段
self.name = "polarsnow"

def show(self):
print(self.name)
return self.name


a = A("polarsnow")
print("使用对象访问普通字段")
print(a.name)

print("使用类访问静态字段")
print(A.country)

print("使用对象访问静态字段")
print(a.country)

------------
使用对象访问普通字段
polarsnow
使用类访问静态字段
China
使用对象访问静态字段
China

由于每个对象都保存了类对象指针,当访问的字段在本对象中找不到的时候会去类中查找有没有对应的静态字段

总结

静态字段在代码加载的时候就已经被创建;而普通字段只有在实例化一个对象时候才被创建

在类的内部访问静态字段,推荐也带上类名来访问A.country符合上面说的自己去访问自己的字段的访问原则

方法

在其他面向对象语言中,类的方法有两种,但是在Python中有三种

普通方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class B:
country = "China"

def __init__(self, name):
self.name = name

# 我是普通方法 由对象调用的方法
def show(self):
print(self.name)
return self.name

b = B("北京")
b.show()

------------
北京

普通方法,由对象去调用执行(方法属于类)

静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class B:
country = "China"

def __init__(self, name):
self.name = name

# 我是普通方法 由对象调用的方法
def show(self):
print(self.name)
return self.name

# 我是静态方法
@staticmethod
def func_static():
print("我是静态方法")
return "我是静态方法"

b = B("北京")

print("使用对象去调用执行普通方法")
b.show()

print("使用类去调用静态方法")
B.func_static()

------------
使用对象去调用执行普通方法
北京
使用类去调用静态方法
我是静态方法

普通方法和静态方法的区别:在上面的字段介绍中,静态字段和普通字段的区别是,静态字段一般由类去调用而普通字段一定是由对象去调用。静态方法和普通方法也是一样,静态方法一般是由类去调用执行,而普通方法是由对象去调用执行

定义一个静态方法

定义一个静态方法,首先需要在方法上面加上一个装饰器@staticmethod,下面的方法参数中,不要再含有self参数。至于静态方法的用法,和普通的函数用法没有任何区别,后面可以添加任意参数进行传值

小结:静态方法虽然在类中,属于类,但是静态方法和对象没有任何关系,它是由类去调用的。这一点在静态方法的参数列表中也可以很明显的看出,它没有接受self参数,也就是没有接受对象。静态方法的调用不依赖与任何一个具体的对象。那为什么这个方法和类没有关系还要放在这个类中呢?一般情况下,我们把某些和类相关的操作都会放在类中,方便查找和使用,所以很多时候,类中会出现和对象不相关,但是和类却息息相关的操作。

类方法

类方法是Python独有的一个方法。本质上静态方法的一种特殊形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class B:
country = "China"

def __init__(self, name):
self.name = name

# 我是普通方法 由对象调用的方法
def show(self):
print(self.name)
return self.name

# 我是静态方法
@staticmethod
def func_static():
print("我是静态方法")
return "我是静态方法"

# 我是类方法
@classmethod
def func_class(cls):
print("我是类方法")
return "我是类方法"

b = B("北京")

print("使用对象去调用执行普通方法")
b.show()

print("使用类去调用静态方法")
B.func_static()

print("使用类去调用类方法")
B.func_class()

------------
使用对象去调用执行普通方法
北京
使用类去调用静态方法
我是静态方法
使用类去调用类方法
我是类方法

在静态方法中,可以没有任何参数,也可以有任意多个参数,使用类去调用;但是在类方法中,至少要有一个参数cls, 也是使用类去调用。这一特点和普通方法又很像,普通方法也是至少要有一个参数,但普通方法的必有参数为self

  • self在普通方法中,代指了对象
  • cls在类方法中,代指了类。跟self一样会隐式传递

类方法比静态方法只多了一步,就是隐式的把类传递给了类方法

类方法的应用:单例模式的实现

总结

Python中类的三个方法都属于类

  • 普通方法 至少要有一个self参数 通过对象去执行
  • 静态方法 参数可有可无 通过类去执行(也可以对象执行)
  • 类方法 至少有一个cls参数 通过类去执行(也可以对象执行)

属性

属性的特征是:拥有普通方法的样式和表现形式,却拥有字段的访问方式

现有如下需求:需要做一个分页器,每10条内容分一页,最后得出一共分出多少页的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 首先使用普通方法的方式来实现这个功能
class Pageer:

def __init__(self, num):
self.num = num

def all_pager(self):
p1, p2 = divmod(self.num, 10)
if p2 == 0:
return p1
else:
return p1 + 1

p = Pageer(59)
print(p.all_pager())

------------
6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用属性的方式来实现这个功能
class Pageer:

def __init__(self, num):
self.num = num

@property
def all_pager(self):
p1, p2 = divmod(self.num, 10)
if p2 == 0:
return p1
else:
return p1 + 1

p = Pageer(59)
print(p.all_pager)

------------
6

对比两段代码可以看出,有两点区别

  • 在普通方法的上面加了一个@property装饰器
  • 在调用这个普通成员的时候,第一段代码使用了标准的普通方法调用执行的方式p.all_pager();第二段代码则使用了访问普通字段的方式p.all_pager

这就是上面说的属性的特征,既拥有和普通方法一样的表现形式,又有访问普通字段那样的访问形式

对属性的赋值

上面的小例子实现了对属性的取值。对于访问普通字段来说,不仅可以访问,还是为普通字段赋值。那么属性既然有着普通字段的访问形式,能不能也去实现像普通字段那样的赋值呢?p.all_pager = 1991

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Pageer:

def __init__(self, num):
self.num = num

@property
def all_pager(self):
p1, p2 = divmod(self.num, 10)
if p2 == 0:
return p1
else:
return p1 + 1

@all_pager.setter
def all_pager(self, value):
print(value)

p = Pageer(59)
print(p.all_pager)

p.all_pager = 1991

------------
6
1991

对属性进行赋值的关键语法:

  • 一定要使用属性取值方法的方法名.setter装饰器
  • 赋值方法的函数名必须和取值方法的方法名一致
  • 在赋值方法中,接受一个赋值参数value

在为all_pager赋值的时候,会自动执行@all_pager.setter下面的方法(特别注意:属性赋值方法中不能return返回值)

删除属性

在对字段的操作中,可以访问字段,给字段赋值,还可以删除字段。上面的属性中,已经实现了属性的访问和赋值,接下来要实现属性的删除,这样一来,属性就拥有了和字段一样的操作特性了(查,改,删)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Pageer:

def __init__(self, num):
self.num = num

@property
def all_pager(self):
p1, p2 = divmod(self.num, 10)
if p2 == 0:
return p1
else:
return p1 + 1

@all_pager.setter
def all_pager(self, value):
print(value)

@all_pager.deleter
def all_pager(self):
print("del all_pager")

p = Pageer(59)
print(p.all_pager)

print("为属性赋值")
p.all_pager = 1991

print("删除属性")
del p.all_pager

总结

属性具有方法的表现形式,同时具有字段的访问形式(通过不加括号()的方式去执行了一个方法)

由于属性的这个访问特性是我们通过三个方法来伪造出来的,所以每个操作对应了具体什么内容,需要我们来自己定义。就像上面的删除方法,我只是print了一下而已~~ Python只是根据我们操作属性的方式,提供了一种关联方式而已,具体里面需要实现哪些功能都需要我们自己去定义

注意:如果你想真的在内存中实现删除all_pager属性,你可能会这样写:

1
2
3
@all_pager.deleter
def all_pager(self):
del self.all_pager

这样做的结果是会进入一个死循环。因为你在外面调用del函数来删除对象属性的时候,会去deleter里面执行del self.all_pager代码,该代码又会跳到deleter里面执行del self.all_pager,所以会进入一个死循环。属性的删除方法,应该根据实际需要调整他的功能

属性存在意义是:访问属性时可以制造出和访问字段完全相同的假象。属性由方法变种而来,如果Python中没有属性,方法完全可以代替其功能

属性的第二种表现形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Pageer:

def __init__(self, num):
self.num = num

def func1(self):
return 1991

def func2(self, value):
print("set", value)

def func3(self):
print("del")

func = property(fget=func1, fset=func2, fdel=func3)


p = Pageer(59)
print(p.func) # 自动去调用fget=func1
p.func = 2016 # 自动去调用fset=func2
del p.func # 自动去调用fdel=func3

------------
1991
set 2016
del

property的构造方法中有个四个参数

  • 第一个参数是方法名,调用 对象.属性 时自动触发执行方法
  • 第二个参数是方法名,调用 对象.属性 = XXX 时自动触发执行方法
  • 第三个参数是方法名,调用 del 对象.属性 时自动触发执行方法
  • 第四个参数是字符串,调用 对象.属性.doc ,此参数是该属性的描述信息

这种通过静态字段的方式实现属性的操作在很多源码中很常见,使用的频率是最高的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# Python WEB框架 Django 的视图中 request.POST 就是使用的静态字段的方式创建的属性
class WSGIRequest(http.HttpRequest):
def __init__(self, environ):
script_name = get_script_name(environ)
path_info = get_path_info(environ)
if not path_info:
# Sometimes PATH_INFO exists, but is empty (e.g. accessing
# the SCRIPT_NAME URL without a trailing slash). We really need to
# operate as if they'd requested '/'. Not amazingly nice to force
# the path like this, but should be harmless.
path_info = '/'
self.environ = environ
self.path_info = path_info
self.path = '%s/%s' % (script_name.rstrip('/'), path_info.lstrip('/'))
self.META = environ
self.META['PATH_INFO'] = path_info
self.META['SCRIPT_NAME'] = script_name
self.method = environ['REQUEST_METHOD'].upper()
_, content_params = cgi.parse_header(environ.get('CONTENT_TYPE', ''))
if 'charset' in content_params:
try:
codecs.lookup(content_params['charset'])
except LookupError:
pass
else:
self.encoding = content_params['charset']
self._post_parse_error = False
try:
content_length = int(environ.get('CONTENT_LENGTH'))
except (ValueError, TypeError):
content_length = 0
self._stream = LimitedStream(self.environ['wsgi.input'], content_length)
self._read_started = False
self.resolver_match = None

def _get_scheme(self):
return self.environ.get('wsgi.url_scheme')

def _get_request(self):
warnings.warn('`request.REQUEST` is deprecated, use `request.GET` or '
'`request.POST` instead.', RemovedInDjango19Warning, 2)
if not hasattr(self, '_request'):
self._request = datastructures.MergeDict(self.POST, self.GET)
return self._request

@cached_property
def GET(self):
# The WSGI spec says 'QUERY_STRING' may be absent.
raw_query_string = get_bytes_from_wsgi(self.environ, 'QUERY_STRING', '')
return http.QueryDict(raw_query_string, encoding=self._encoding)

def _get_post(self):
if not hasattr(self, '_post'):
self._load_post_and_files()
return self._post

def _set_post(self, post):
self._post = post

@cached_property
def COOKIES(self):
raw_cookie = get_str_from_wsgi(self.environ, 'HTTP_COOKIE', '')
return http.parse_cookie(raw_cookie)

def _get_files(self):
if not hasattr(self, '_files'):
self._load_post_and_files()
return self._files

POST = property(_get_post, _set_post)

FILES = property(_get_files)
REQUEST = property(_get_request)
  • 第70行,定义了属性的两个方法,取值和赋值
  • 第52行,取值
  • 第57行,赋值

特别强调

最后再一次强调,本站关于Python的介绍大多数是基于Python3.5+

上面关于属性通过装饰器的方式来实现不同的操作时,均是在Python3.5+的环境下,如果你在Python2.x的环境下,得到的结果肯定会不一样。因为Python2.x中

  • 经典类中的属性只有一种访问方式,其对应被@property修饰的方法
  • 新式类中的属性有三种访问方式,并分别对应了三个被@property @方法名.setter @方法名.deleter修饰的方法

但是在Python3中,经典类也是拥有这三种访问方式的。

继承了object的类就是新式类,否则为经典类

Python是否支持多态的问题争论已久,在面向对象语言中,有三大特性(封装 继承 多态)Python作为典型的面向对象语言,怎么可能会不支持多态呢

产生这个问题的原因在于Python是一门动态编程语言,在运行的时候可以更改自己的结构。在java等面向对象语言中,定义一个变量,强制要求首先声明他的数据类型;而在Python中,这个步骤是完全可以忽略的。由于Python动态语言的特性,Python天生就支持多态,不需要像其他面向对象的语言那样使用了多态语法来实现多态的特性。大家争论Python是否支持多态,其实都是纠结在Python没有多态的语法。

  • 在java中,如果一个变量被声明是int类型,如果被赋值为一个字符串对象,那一定会报错。在像Java这种强类型的语言中,一个变量被声明数据类型时候,只允许被这个类或这个类的派生类赋值。

  • 而在Python中,声明一个变量,你可以给他赋任何类型的值(对象)

多态可以简单的理解为多种形态,多种类型。在Python中,不需要特殊的多态语法来实现多态的特性。

动态语言的优势与劣势

毋庸置疑,动态语言带来的便捷是显而易见的,我们不需要关心变量的数据类型,因为在赋值的过程中,Python会动态的改变自己的结构。

由于Python动态语言的特性,一个变量可以被任何数据类型赋值,也造成了一个困扰,就是在读别人源码的时候,如果参数只写了一个arg,那我们是不知道它具体是需要传递什么样类型的参数的。

Python中的pickle可以保存Python中的任意对象到文件中。当我们dump一个自定义类实例化出来的对象之后,如果需要在其他Python模块(文件)中load进来,需要注意,一定要先导入这个自定义的类,否则执行对象的方法的时候会报错

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import pickle

class A:
def __init__(self, name):
self.name = name

def show(self):
print(self.name)
return self.name

if __name__ == "__main__":
print("--dump--")
a = A("polarsnow")
pickle.dump(a, open('db', 'wb'))

print("--load--")
ret = pickle.load(open('db', 'rb'))
print(ret)
ret.show()

------------
--dump--
--load--
<__main__.A object at 0x101b77e10>
polarsnow
  • 创建一个自定义的类
  • 实例化这个类的对象
  • 将这个对象从内存中dump到一个文件中
  • 从本程序load pickle文件到内存中
  • 执行对象的一个方法

上面从本程序中,load自定义类实例化出来的对象,执行对象的操作是没有问题的。那么如果是从另一个文件load这个对象呢?

1
2
3
4
5
6
7
8
9
import pickle

ret = pickle.load(open('db', 'rb'))

------------
Traceback (most recent call last):
File "/Users/lvrui/PycharmProjects/untitled/8/loadpickle.py", line 3, in <module>
ret = pickle.load(open('db', 'rb'))
AttributeError: Can't get attribute 'A' on <module '__main__' from '/Users/lvrui/PycharmProjects/untitled/8/loadpickle.py'>

从另一个Python文件中load一个自定义类的对象时报错了!为什么会报错?因为对象是由类而产生的,在新的Python文件中,类没有被加载到内存中,所以load这个类的对象的时候就会报错。在报错信息中也可以看出,没有在当前文件中找到A这个类。

解决这个问题有两个途径:

  • 将类导入到这个Python文件中
  • 将类的代码直接copy到这个文件中
1
2
3
4
5
6
7
8
9
10
import pickle
from dumppickle import A

ret = pickle.load(open('db', 'rb'))
print(ret)
ret.show()

------------
<dumppickle.A object at 0x1013777b8>
polarsnow

注意:一个对象,有它对应的类才有意义。所以在load pickle文件中的对象的时候,一定要保证它的类先被导入

在Java和C#这类面向对象的语言中,一个类只允许有一个父类。而在Python中,可以有多个父类。继承,顾名思义,就是子类默认获得父类中的所有成员。当子类中不包含与父类同名的成员时,Python会自动去父类中查找并使用父类的成员;当子类中的成员与父类中的成员名称相同时,优先使用子类的成员。这种覆盖父类成员属性的特性被称之为“重写”或“复写”

什么是继承

继承可以简单的理解为,将父类中的所有成员copy一份到子类中,子类和父类如果有相同的成员名,优先使用子类的代码。

1
2
3
4
5
6
7
class A:
def show(self):
print('show')

class B(A):
def get(self):
print('get')

上面的代码中,我让B类去继承了A类,也就是说A类是B类的父类。现在B类和A类中没有名字相同的成员,所以可以简单的看做是:

1
2
3
4
5
6
class B:
def show(self):
print('show')

def get(self):
print('get')

接下来执行如下代码

1
2
3
4
5
b = B()
b.show()

------------
show

通过上面的额代码可以验证,在B类中,并没有show方法,但是B类继承了A类,相当于继承了A类所有的代码,所以B类的对象可以使用A类的成员方法。

重写的特性

上面我们说到,只有在子类中与父类的成员方法名字不冲突的时候,才会将父类中的成员方法copy到子类中使用(继承到子类)那么,假如子类和父类的某些方法名字冲突了会怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
def show(self):
print('A show')

class B(A):
def get(self):
print('get')

def show(self):
print('B show')

b = B()
b.show()

------------
B show

在父类中调用子类的成员属性或方法

  • 实例一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A:
def show(self):
print('A show')

def func(self):
print(self.name)

def func2(self):
self.get()


class B(A):
def __init__(self, name):
self.name = name

def get(self):
print('get')

def show(self):
print('B show')


b = B('polarsnow')
b.func()
b.func2()

猜一猜会有什么样的结果?会打印出polarsnowget吗?

按照之前的说法,我们可以把上面的继承看成下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B:
def __init__(self, name):
self.name = name

def func(self):
print(self.name)

def func2(self):
self.get()

def get(self):
print('get')

def show(self):
print('B show')

b = B('polarsnow')
b.func()

这样看答案就清楚了吧,肯定是会打印polarsnowget

这里只是举个例子,实际使用中,不推荐在父类中去调用子类的属性,因为有的时候,你不能确定所有继承这个父类的子类都拥有这个成员属性。

  • 实例二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A:
def f1(self):
self.f2()

def f2(self):
print('A f2')

class B(A):
def f3(self):
self.f1()

def f2(self):
print('B f2')

b = B()
b.f3()

创建了B类的对象b,然后执行了对象b的f3方法,在f3方法中又执行了f1方法,f1方法在B类中没有,但是B类继承了A类的代码,(注意这里是继承了A类的成员,可以理解为copy过来的成员,而不是去A类中找相应的成员)执行了f1方法后,又调用了f2方法,思考,这个f2是调用了A类中的f2还是B类中的f2?

根据继承的原理,我们把上面的代码看做如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A:
def f1(self):
self.f2()

def f2(self):
print('A f2')

class B(A):
def f1(self):
self.f2()

def f3(self):
self.f1()

def f2(self):
print('B f2')

b = B()
b.f3()

将父类中有而子类中没有的成员copy到子类中,子类中有而父类中也有的优先使用子类的成员(重写父类成员)所以A类中只有f1这个成员方法被copy到了B类中。

当B类的对象b调用f3方法时,调用本类中的f1方法(继承过来的成员方法),接着在f1方法中又调用了本类中已经存在的f2方法。所以最后调用的是B类中的f2方法。

如果执行以下命令,执行的f2是哪里的f2?

1
2
a = A()
a.f1()

当然会执行本类中的f2方法,也就是A类中的f2方法

总结:由于Python类中self隐式传递对象自己本身的特性,可以看出,如果调用了子类中不存在的成员,那么去父类中查找的时候,self变量仍然等于原对象自己本身,也就是说,继承的每次查找,都会返回到最底层的(调用对象的那一层)类中去找,如果找不到,才会去父类中查找。(记住每次寻找失败都回到原点)

多继承

  • 一个类同时继承两个类的情况下,会优先去左边的父类中查找,如果没有则去右边的类中查找

  • 一个类同时继承两个类,左边的类又继承了一个类的情况下,会优先从最左边的类开始找,如果没有,则继续向上查找,如果上面的类还有单继承,就继续向上查找,直到找到没有继承的类为止

  • 如果多继承出现了如下的继承关系,那么左边的类将找到没有继承或公共继承的类为止,然后就切到了第二个类中继续向上查找,公共继承的类会在最右边的类中被找到

查看Python源码的技巧

在Python的源码中,我们看到好多的pass,却不知道真正的函数体或方法体写在了哪里,那么看完Python的多继承,将完全解决这个问题!

1
2
3
4
import socketserver

s = socketserver.ThreadingTCPServer()
s.serve_forever()

socketserver.ThreadingTCPServer()创建了一个对象,而创建对象时需要执行__init__方法,我们来看看源码中有没有提供这个init方法

1
2
# 源码
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

从上面的源码中可以看出,这个类直接被pass掉了。根据上面的多继承的知识,本类中没有init方法,我们需要去他的父类中去查找,首先去左边第一个父类中去查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 源码
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""

# Decides how threads will act upon termination of the
# main process
daemon_threads = False

def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.

In addition, exception handling is done here.

"""
try:
self.finish_request(request, client_address)
self.shutdown_request(request)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)

def process_request(self, request, client_address):
"""Start a new thread to process the request."""
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
t.daemon = self.daemon_threads
t.start()

这边也没有init方法,这个时候由于这个类不再有继承关系,所以需要回到原点查找第二个父类中有没有init方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 源码
class TCPServer(BaseServer):
...
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True):
"""Constructor. May be extended, do not override."""
BaseServer.__init__(self, server_address, RequestHandlerClass)
self.socket = socket.socket(self.address_family,
self.socket_type)
if bind_and_activate:
try:
self.server_bind()
self.server_activate()
except:
self.server_close()
raise

...

哎~ 在这里找到了init方法,那么最开始创建的对象的代码的init方法,就会使用这里的代码啦!

至此,s = socketserver.TreadingTCPServer()这一行代码就执行完毕啦。

接下来需要执行这个对象的server.forver()方法

接着又回到这个类中查找

1
2
# 源码
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

没有,从左边的父类找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 源码
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""

# Decides how threads will act upon termination of the
# main process
daemon_threads = False

def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.

In addition, exception handling is done here.

"""
try:
self.finish_request(request, client_address)
self.shutdown_request(request)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)

def process_request(self, request, client_address):
"""Start a new thread to process the request."""
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
t.daemon = self.daemon_threads
t.start()

左边没有,去右边查找

1
2
3
# 源码
class TCPServer(BaseServer):
...

右边也没有找到,但是右边的父类中,还有一个继承,继续向上查找!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 源码
class BaseServer:
...
def serve_forever(self, poll_interval=0.5):
"""Handle one request at a time until shutdown.

Polls for shutdown every poll_interval seconds. Ignores
self.timeout. If you need to do periodic tasks, do them in
another thread.
"""
self.__is_shut_down.clear()
try:
# XXX: Consider using another file descriptor or connecting to the
# socket to wake this up instead of polling. Polling reduces our
# responsiveness to a shutdown request and wastes cpu at all other
# times.
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)

while not self.__shutdown_request:
ready = selector.select(poll_interval)
if ready:
self._handle_request_noblock()

self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
...

啊哈,原来在这里找到了它!!!但是这里面又调用了self._handle_request_noblock()这个方法,根据之前的继承原理,这里的self指的是调用对象的地址,所以需要重新回到socketserver.ThreadingTCPServer()开始查找

1
2
# 源码
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

没有,找左边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 源码
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""

# Decides how threads will act upon termination of the
# main process
daemon_threads = False

def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.

In addition, exception handling is done here.

"""
try:
self.finish_request(request, client_address)
self.shutdown_request(request)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)

def process_request(self, request, client_address):
"""Start a new thread to process the request."""
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
t.daemon = self.daemon_threads
t.start()

左边没有找右边

1
2
3
# 源码
class TCPServer(BaseServer):
...

右边也没有,继续向上找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 源码
class BaseServer:
...
def _handle_request_noblock(self):
"""Handle one request, without blocking.

I assume that selector.select() has returned that the socket is
readable before this function was called, so there should be no risk of
blocking in get_request().
"""
try:
request, client_address = self.get_request()
except OSError:
return
if self.verify_request(request, client_address):
try:
self.process_request(request, client_address)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)
...

找到了!但是这里面又执行了self.process_request(),有self,仍需要回到最开始的地方查找

1
2
# 源码
class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass

没有,找左边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 源码
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""

# Decides how threads will act upon termination of the
# main process
daemon_threads = False

def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.

In addition, exception handling is done here.

"""
try:
self.finish_request(request, client_address)
self.shutdown_request(request)
except:
self.handle_error(request, client_address)
self.shutdown_request(request)

def process_request(self, request, client_address):
"""Start a new thread to process the request."""
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
t.daemon = self.daemon_threads
t.start()

找到啦!!!


这就是socketserver的源码阅读方法,也适用于所有源码的阅读方法,再次强调,遇到self一定要回到原点再次寻找

在上一篇Python面向对象的开篇中,简单介绍了Python下的封装特性,主要封装的对象是基本数据类型,Python中除了可以封装基本数据类型外,还可以封装类似于list,tuple,set等常见的引用数据类型,甚至可以封装我们自定义的对象。

本篇就是主要介绍Python的对象中封装对象的功能及实现。

对象中封装对象实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A:
def __init__(self, host, b_obj):
self.host = host
self.b_obj = b_obj

class B:
def __init__(self, username, password):
self.username = username
self.password = password

def show(self):
print('class B func show')

b = B('ps', 'password')
a = A('192.168.1.1', b)
print(a.b_obj.username)
print(a.b_obj.password)
a.b_obj.show()
print(a.host)

------------
ps
password
class B func show
192.168.1.1
  • 第1-4行:定义了一个A类,构造方法中,接收两个参数,一个是主机名,一个是账户对象(注意,这里接收的是一个对象)
  • 第6-9行:定义了一个B类,构造方法接收用户名和密码两个字符串参数
  • 第11-12行:B类中的一个对象方法
  • 第14行:实例化一个B类的对象
  • 第15行:实例化一个A类的对象,并将B类的对象b当做参数传递给A类的构造方法
  • 第16行:a对象的第二个成员变量是一个B类型的对象,a.b_obj指向的就是B类的对象b,而a.b_obj.username等同于执行了b.username
  • 第17-18行:同理,等同于执行了b.passwordb.show()

接下来感受一下对象中封装两层对象的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class A:
def __init__(self, host, b_obj):
self.host = host
self.b_obj = b_obj

class B:
def __init__(self, username, password):
self.username = username
self.password = password

def show(self):
print('class B func show')

class C:
def __init__(self, db, a_obj):
self.db = db
self.a_obj = a_obj

b = B('ps', 'password')
a = A('192.168.1.1', b)
c = C('MySQL', a)

print(c.a_obj.b_obj.username)
print(c.a_obj.b_obj.password)
c.a_obj.b_obj.show()

------------
ps
password
class B func show

Python是一门非常适合初学者学习的语言。Python本身是支持面向对象编程的。但是在运维界,Python很多时候被称之为脚本语言,因为Python不仅仅支持面向对象编程,还支持函数式编程。这是Java和C#这种OOP语言所不支持的

Linux中的bash shell被当做一种脚本语言,本身也是支持函数式编程的,但是很多时候,shell的面向过程编程简单、粗暴、高效

而Python不仅仅支持OOP面向对象编程,还支持脚本语言的特性,相面过程编程和函数式编程

概述

  • 面向过程:根据业务逻辑从上到下写代码
  • 函数式编程:将某个功能的代码封装到一个函数中,后面用到该功能的时候无需重新编写,仅需要调用函数即可
  • 面向过程:对函数进行分类和封装,终极目标“消除重复代码”

我的OOP启蒙导师之一 Mars(之前是千峰教育的负责人,2016年离职创业,现在是育知同创的创始人之一)曾经给OOP一个比喻,说重复代码就好比是别人往你的脸上吐了口痰,我们的终极目标就是“消除重复代码”

首先来感受下面向过程编程和函数式编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 面向过程编程
while True
if cpu利用率 > 90%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接

if 硬盘使用空间 > 90%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接

if 内存占用 > 80%:
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 函数式编程
def 发送邮件(内容)
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接

while True

if cpu利用率 > 90%:
发送邮件('CPU报警')

if 硬盘使用空间 > 90%:
发送邮件('硬盘报警')

if 内存占用 > 80%:
发送邮件('内存报警')

每种写法都有自己存在的价值,世界上没有最好的编程方式,只有最合适的编程方式

创建类和对象

1
2
3
4
5
6
7
8
9
10
11
12
# 创建类
class Foo:

# 创建类中的函数(类中的函数又被称作类的方法)
def func(self):

# 函数体 方法体
pass

# 创建Foo这个类的对象
# 创建一个类的对象,又被称作类的实例化对象
obj = Foo()

体验一下上面的报警功能用OOP是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class mail():
def 发送邮件(self, 内容):
#发送邮件提醒
连接邮箱服务器
发送邮件
关闭连接

m = mail()
while True
if cpu利用率 > 90%:
m.发送邮件('CPU报警')

if 硬盘使用空间 > 90%:
m.发送邮件('硬盘报警')

if 内存占用 > 80%:
m.发送邮件('内存报警')

为什么用了OOP,反而代码量比函数式编程还要多了呢,而且调用方法时不能直接调用了,需要使用mail类的对象来调用。

总结:OOP的优势在极少的代码量的情况下很难体现出来。而且应对不同场景,使用不通的方式编程很重要。函数式编程的应用场景–>各个函数之间是独立的且没有共用的数据。

类和对象的创建

1
2
3
4
5
6
7
8
9
# 创建类
class Foo:

# 创建类中的函数
def func(self):
pass

# 根据类Foo创建对象obj
obj = Foo()

对象和类的关系

创建类

1
2
3
class Foo:
def func(self):
pass

创建对象

1
2
3
4
5
obj1 = Foo()
obj2 = Foo()

obj1.func()
obj2.func()

上面的代码中,我们使用Foo类分别创建了obj1obj2两个对象,在调用Foo类中的func方法时(调用对象方法),Python帮我们隐式地将这个对象传递给了func函数的self参数。在一个类的众多对象中,就是通过隐式的把自己传递给self参数,从而实现了每个对象个性之处。

每一个由类实例化出来的对象,都保存了一个类对象指针指向自己的类

形式参数self

在创建类的时候,如果我们需要让一个方法在不同的对象操作时实现不同的效果,那么我们就需要把这个方法创建成为对象方法。类中的对象方法有个很明显的标记就是self关键字,每个对象方法的头一个参数,都是self!

这个self起到的作用就是区分不同的对象,从而实现对象不同,方法执行的效果就不同。我们在实例化一个对象的时候,该对象调用类中的对象方法,不需要显示的去给self参数传值,因为Python会隐式的帮我们把对象的名字传递给对象方法中的第一个self参数。简单的说,就是哪个对象去执行对象方法,self就是谁。例如上面的例子中,obj1obj2分别调用了对象方法,那么obj1调用func时,self就等于obj1obj2调用func时,self就等于obj2

构造方法/构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 操作MySQL数据库的类
class ConnMySQL:
# 构造方法 __init__(self)
def __init__(self, host, port, username, pasword):
print(self, '每一个对象被创建的时候都会执行这个方法')
self.host = host
self.port = port
self.username = username
self.password = possword

def select(self, sql):
pass

def add(self, sql):
pass

def delete(self, sql):
pass

def modify(self, sql):
pass

# 实例化操作MySQL数据库的对象
my1 = ConnMySQL('192.168.1.1', 3306, 'root', 'p@55\/\/0rd')
my2 = ConnMySQL('192.168.1.2', 3307, 'admin', 'password')

# 使用对象去操作数据库
# 分别去两个数据库中操作sql语句
my1.select('select uid, username, password from userinfo where age=26')
my2.select('select uid, username, password from userinfo where age=26')

注意

  • 在函数式编程中,Python解释器从上向下解释执行,如果一个方法A中依赖另一个方法B,那么B必须要在A之前定义,否则Python解释器会报错。但是在类中不需要,类中的函数可以调用任意位置的函数。

Python中的zipfile和tarfile两个模块可以实现对文件的压缩和解压缩。并且支持向压缩包中压入新的文件,和在压缩包中指定文件进行解压缩

Python Version: 3.5+

zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import zipfile

# 创建新的压缩文件
z = zipfile.ZipFile('docs.zip', 'w')
# 添加需要压缩的文件
z.write("c1.py")
z.write("c2.py")
z.close()

# 向已存在的压缩包中添加文件
z = zipfile.ZipFile('docs.zip', 'a')
# 添加需要压缩的文件
z.write("test.conf")
z.close()

# 解压缩所有文件
z = zipfile.ZipFile('docs.zip', 'r')
z.extractall()

# 解压缩指定文件

# 列出压缩包中的所有文件名
z = zipfile.ZipFile('docs.zip', 'r')
for i in z.namelist():
print(i)

# 指定文件名称进行解压
z.extract("c1.py")
z.close()

解压缩补充:

  • path 可以指定解压缩之后的文件的路径
  • pwd 可以指定去哪个目录下解压缩文件
1
2
3
4
5
6
def extract(self, member, path=None, pwd=None):
"""Extract a member from the archive to the current working directory,
using its full name. Its file information is extracted as accurately
as possible. `member' may be a filename or a ZipInfo object. You can
specify a different directory using `path'.
"""

tar.gz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import tarfile

# 创建新的压缩文件
t = tarfile.open('docs.zip', 'w')
# 添加需要压缩的文件
t.add("c1.py")
t.add("c2.py", arcname="tc2.py")
t.close()

# 向已存在的压缩包中添加文件
t = tarfile.open('docs.zip', 'a')
# 添加需要压缩的文件
t.add("test.conf")
t.close()

# 解压缩所有文件
t = tarfile.open('docs.zip', 'r')
t.extractall()

# 解压缩指定文件
# 列出压缩包中的所有文件名
t = tarfile.open('docs.zip', 'r')
for i in t.getnames():
print(i)

# 指定文件名称进行解压
t.extract("tc2.py")
t.close()

# 使用文件名获取该文件对象,指定该文件对象进行解压缩
o = t.getmember('tc2.py')
t.extract(o)
t.close()

解压缩补充:

  • tarfile解压缩单个文件时支持两种方式进行解压,一种是指定文件名,一种是指定文件对象。
  • 解压方法支持解压缩文件到指定目录中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def extractall(self, path=".", members=None, *, numeric_owner=False):
"""Extract all members from the archive to the current working
directory and set owner, modification time and permissions on
directories afterwards. `path' specifies a different directory
to extract to. `members' is optional and must be a subset of the
list returned by getmembers(). If `numeric_owner` is True, only
the numbers for user/group names are used and not the names.
"""

def extract(self, member, path="", set_attrs=True, *, numeric_owner=False):
"""Extract a member from the archive to the current working directory,
using its full name. Its file information is extracted as accurately
as possible. `member' may be a filename or a TarInfo object. You can
specify a different directory using `path'. File attributes (owner,
mtime, mode) are set unless `set_attrs' is False. If `numeric_owner`
is True, only the numbers for user/group names are used and not
the names.
"""

shutil是Python中的高级文件处理模块,可以使用它来对文件进行更细粒度的操作,如仅复制权限、内容、属组、用户、状态等信息,而且shutil模块还可以做到对压缩文件进行简单的处理。

Python Version: 3.5+

将文件内容拷贝到另一个文件中

shutil.copyfileobj(fsrc, fdst[, length])

1
2
3
import shutil

shutil.copyfileobj(open('old.xml','r'), open('new.xml', 'w'))

拷贝文件

shutil.copyfile(src, dst)

1
shutil.copyfile('f1.log', 'f2.log')

仅拷贝权限。内容、组、用户均不变

shutil.copymode(src, dst)

1
shutil.copymode('f1.log', 'f2.log')

仅拷贝状态的信息,包括:mode bits, atime, mtime, flags

shutil.copystat(src, dst)

1
shutil.copystat('f1.log', 'f2.log')

拷贝文件和权限

shutil.copy(src, dst)

1
2
3
import shutil

shutil.copy('f1.log', 'f2.log')

拷贝文件和状态信息

shutil.copy2(src, dst)

1
2
3
import shutil

shutil.copy2('f1.log', 'f2.log')

递归的去拷贝文件夹

shutil.ignore_patterns(*patterns)
shutil.copytree(src, dst, symlinks=False, ignore=None)

1
2
3
import shutil

shutil.copytree('folder1', 'folder2', ignore=shutil.ignore_patterns('*.pyc', 'tmp*'))

复制中软连接的问题:如果symlinks为True,那么软连接的文件会被copy到目标目录中,如果symlinks为False,那么只会将软连接进行copy,假如链接的目录不存在则会抛出异常

1
2
3
import shutil

shutil.copytree('f1', 'f2', symlinks=True, ignore=shutil.ignore_patterns('*.pyc', 'tmp*'))

递归的去删除文件

shutil.rmtree(path[, ignore_errors[, onerror]])

1
2
3
import shutil

shutil.rmtree('folder1')

递归的去移动文件/重命名,类似mv命令

shutil.move(src, dst)

1
2
3
import shutil

shutil.move('folder1', 'folder3')

压缩文件的处理

shutil.make_archive(base_name, format,…)

参数说明:

  • base_name: 压缩包的文件名,也可以是压缩包的路径。只是文件名时,则保存至当前目录,否则保存至指定路径,
    • 如:docs.tar.gz => 保存至当前路径,并以此为文件名
    • 如:/Users/PolarSnow/docs.tar.gz =>保存至/Users/PolarSnow/ 并以docs.tar.gz的文件名保存
  • format: 压缩包种类,“zip”, “tar”, “bztar”,“gztar”
  • root_dir: 要压缩的文件夹路径(默认当前目录)
  • owner: 用户,默认当前用户
  • group: 组,默认当前组
  • logger: 用于记录日志,通常是logging.Logger对象
1
2
3
4
5
6
7
8
9
# 将 /Users/polarsnow/Downloads/docs 下的文件打包放置当前程序目录
# base_name不需要指定文件名的后缀,压缩后会自动在后面加上tar.gz的后缀
import shutil
ret = shutil.make_archive("docs", 'gztar', root_dir='/Users/wupeiqi/Downloads/docs')


#将 /Users/polarsnow/Downloads/docs 下的文件打包放置 /Users/polarsnow/目录下,并以docs.tar.gz命名
import shutil
ret = shutil.make_archive("/Users/wupeiqi/docs", 'gztar', root_dir='/Users/wupeiqi/Downloads/test')

在Python3中,commands模块被移除掉,commands模块执行shell命令的相关模块和函数的功能,在subprocess模块总均可实现,并且提供了更加丰富的功能

Python Version: 3.5+

call

执行命令,返回状态码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> ret = subprocess.call(["df","-h"], shell=False)
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
>>> print(ret)
0
------------
# 以shell的模式运行
>>> ret = subprocess.call("df -h", shell=True)
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
>>> print(ret)
0

check_call

检查执行命令后的返回值,如果是0则返回,如果不是则抛出异常

1
2
3
4
5
6
>>> ret = subprocess.check_call("exit 1", shell=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.5/lib/python3.5/subprocess.py", line 584, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command 'exit 1' returned non-zero exit status 1

check_output

检查执行命令后的返回值,如果是0则返回执行结果,如果不是则抛出异常

1
2
3
>>> ret = subprocess.check_output("df -h", shell=True)
>>> print(ret)
b'Filesystem Size Used Avail Use% Mounted on\n/dev/disk1 233G 107G 127G 46% /\n/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk\n'

Popen

用于执行复杂的系统命令

参数:

  • args:shell命令,可以是字符串或者序列类型(如:list,元组)
  • bufsize:指定缓冲。0 无缓冲,1 行缓冲,其他 缓冲区大小,负值 系统缓冲
  • stdin, stdout, stderr:分别表示程序的标准输入、输出、错误句柄
  • preexec_fn:只在Unix平台下有效,用于指定一个可执行对象(callable object),它将在子进程运行之前被调用
  • close_sfs:在windows平台下,如果close_fds被设置为True,则新创建的子进程将不会继承父进程的输入、输出、错误管道。
    所以不能将close_fds设置为True同时重定向子进程的标准输入、输出与错误(stdin, stdout, stderr)。
  • shell:同上
  • cwd:用于设置子进程的当前目录
  • env:用于指定子进程的环境变量。如果env = None,子进程的环境变量将从父进程中继承。
  • universal_newlines:不同系统的换行符不同,True -> 同意使用 \n
  • startupinfo与createionflags只在windows下有效
    将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如:主窗口的外观,进程的优先级等等
1
2
3
import subprocess
ret1 = subprocess.Popen(["mkdir","t1"])
ret2 = subprocess.Popen("mkdir t2", shell=True)

非交互模式的命令

1
2
3
4
5
6
7
8
9
10
11
import subprocess

obj = subprocess.Popen("df -h", shell=True)

print("polarsnow")

------------
polarsnow
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk

从上面的例子🌰看到,父进程在开启子进程之后并没有等待子进程结束,而是直接运行了print

1
2
3
4
5
6
7
8
9
10
11
import subprocess

obj = subprocess.Popen("df -h", shell=True)
obj.wait()
print("polarsnow")

------------
Filesystem Size Used Avail Use% Mounted on
/dev/disk1 233G 107G 127G 46% /
/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk
polarsnow

加入了wait之后,可以看到,父进程会等待子进程运行结束后再执行print

此外,你还可以在父进程中对子进程进行如下其他的操作:

  • obj.poll() 检查子进程的状态
  • obj.kill() 终止子进程
  • obj.sent_signal() 向子进程发送信号
  • obj.terminate() 终止子进程
  • obj.pid 查看子进程的pid
  • obj.args 查看shell命令

子进程的文本流控制

  • obj.stdin 标准输入
  • obj.stdout 标准输出
  • obj.stderr 标准错误

可以在Popen建立子进程的时候改变子进程的标准输入、标准输出和标准错误。并可以利用subprocess.PIPE将多个子进程的输入输出连接在一起,构成管道

1
2
3
4
import subprocess

obj = subprocess.Popen("df -h", shell=True, stdout=subprocess.PIPE)
print(obj.stdout.read())

使用了subprocess提供的管道,子进程执行命令后的输出不会再默认输出到屏幕上了,而是放到了管道里,等待用户提取管道中的数据

1
2
3
4
5
6
7
import subprocess

obj = subprocess.Popen("df -h", shell=True, stdout=subprocess.PIPE)
print(obj.communicate())

------------
(b'Filesystem Size Used Avail Use% Mounted on\n/dev/disk1 233G 107G 127G 46% /\n/dev/disk2s1 58G 36G 22G 63% /Volumes/SanDisk\n', None)

communicate()会返回执行命令的结果和标准错误,但是上面的例子中,我只是将子进程的标准输出放到的subprocess的管道中,并没有对标准错误应用subprocess的管道,所以,不管有没有报错,这里的返回值返回的标准错误永远是None。下面来验证一下

1
2
3
4
5
6
7
8
9
import subprocess

obj = subprocess.Popen("df -y", shell=True, stdout=subprocess.PIPE)
print(obj.communicate())

------------
df: invalid option -- 'y'
Try 'df --help' for more information.
(b'', None)

果然,报错默认打在了屏幕上,而communicate的返回值中的标准错误依然是None

实现Linux中 | 管道的效果

1
2
3
4
5
6
7
8
9
import subprocess

obj1 = subprocess.Popen("cat /etc/passwd", shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
obj2 = subprocess.Popen("grep root", shell=True, stdin=obj1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out = obj2.communicate()
print(out)

------------
(b'root:*:0:0:System Administrator:/var/root:/bin/sh\ndaemon:*:1:1:System Services:/var/root:/usr/bin/false\n_cvmsroot:*:212:212:CVMS Root:/var/empty:/usr/bin/false\n', b'')

第3行:创建了一个子进程去执行命令,并将标准输入输出和错误放到了subprocess提供的管道中。

第4行:创建了另一个子进程去执行命令,命令的标准输入是上一条命令的标准输出,所以把obj1中的标准输出从subprocess的管道中取出当做标准输入传递给第二个子进程

你可能会想,在之前的例子中,如果不加wait的话,父进程不管子进程有没有执行完都会往下执行,那么假如第一条命令执行时间比较长,父进程如果继续向下走,执行第二条命令时,还没有拿到第一条的标准输出,会不会报错呢?首先,这是个好问题!下面详细分析一下

  • 程序会继续向下执行,但是会分两种情况,一种情况是第二条命令是阻塞的命令,第二种情况是非阻塞的命令。
    • 假如第二条是阻塞的命令,进程会一直卡住,等待接收输入,这种情况下,程序会一直等下去,等到第一条命令执行结束,把标准输出放到管道为止。
    • 假如第二条命令是非阻塞的命令,父进程会立即向下执行,虽然第一条命令还没有把标准输出放到管道中,第二条命令会认为你要给我传的就是空,所以第二条命令会被迅速执行完毕,而上面的例子中,调用了第二条命令对象的communicate方法,只会等待第二条命令执行完毕,一旦第二条命令执行完毕,不管第一条有没有执行完都会立即退出

subprocess.PIPE实际上为文本流提供一个缓存区。child1的stdout将文本输出到缓存区,随后child2的stdin从该PIPE中将文本读取走。child2的输出文本也被存放在PIPE中,直到communicate()方法从PIPE中读取出PIPE中的文本。
注意:communicate()是Popen对象的一个方法,该方法会阻塞父进程,直到子进程完成

实现类似pexpect的交互式命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
obj.stdin.write("123\n")
obj.stdin.write("123\n")
obj.stdin.close()

cmd_out = obj.stdout.read()
obj.stdout.close()
cmd_error = obj.stderr.read()
obj.stderr.close()

print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
obj.stdin.write("123\n")
obj.stdin.write("123\n")

cmd_out, cmd_error = obj.communicate()
print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import subprocess

obj = subprocess.Popen("passwd ps", shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
cmd_out, cmd_error = obj.communicate('123\n123\n')
print(cmd_out)
print(cmd_error)

------------
Changing password for user ps.
passwd: all authentication tokens updated successfully.

New password: BAD PASSWORD: it is WAY too short
BAD PASSWORD: is too simple
Retype new password: