Python模块与面向对象

主要介绍模块的基本概念和Python里面向对象的相关知识。主要参考廖雪峰的Python教程的内容。原教程地址

模块

模块的创建

每个.py后缀的python程序都是一个可以被import的模块。以创建一个test模块为例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

' a test module '

__author__ = 'Breeze'

import sys

def test():
args = sys.argv
if len(args)==1:
print('This is just a test!')
elif len(args)==2:
print('Test, %s!' % args[1])
else:
print('Too many arguments!')

if __name__=='__main__':
test()

参数管理

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称,例如:

运行python3 test.py获得的sys.argv就是['test.py']

运行python3 test.py 'memory function'获得的sys.argv就是['test.py', 'memory function']

或者使用专有的命令行参数解析包argparse。参考自

假设下面的程序名字叫print_name.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import argparse

def get_parser():
parser = argparse.ArgumentParser(description="Demo of argparse, it has these arguments:...")
parser.add_argument('--name', default='Great')
parser.add_argument('--sex', required=True)
return parser


if __name__ == '__main__':
parser = get_parser()
args = parser.parse_args()
name = args.name
sex = args.sex
print('Hello {}'.format(name))
print(sex)

引入了argparser包,argparser.ArgumentParser函数生成argparser对象。

1.description:

其中这个函数的description表示在命令行显示帮助信息的时候,这个程序的描述信息。(具体就是python print_name.py -h之后会输出描述信息,具体有哪些参数等使用提示)。

2.default:

之后我们通过对象的add_argument函数来增加参数。这里我们增加了一个--name的参数,然后后面的default参数表示如果没提供参数的默认值。

3.required:

又增加了一个--sex参数,添加required参数限制,表示这个参数是必须输入的。

4.type:

默认的参数类型是str类型,如果你的程序需要一个整数或者布尔型参数,你需要设置type=int或type=bool

parser.add_argument('-number', type=int)

5.choices:

参数值只能从几个选项里面选择

parser.add_argument('-arch', required=True, choices=['alexnet', 'vgg'])

1
2
3
$ python choices.py -arch resnet
usage: choices.py [-h] -arch {alexnet,vgg}
choices.py: error: argument -arch: invalid choice: 'resnet' (choose from 'alexnet', 'vgg')

6.help:

help给使用工具的人提供该参数是用来设置什么的说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# file-name: help.py
import argparse

def get_parser():
parser = argparse.ArgumentParser(
description='help demo')
parser.add_argument('-arch', required=True, choices=['alexnet', 'vgg'],
help='the architecture of CNN, at this time we only support alexnet and vgg.')

return parser


if __name__ == '__main__':
parser = get_parser()
args = parser.parse_args()
print('the arch of CNN is '.format(args.arch))
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
$ python help.py -h
usage: help.py [-h] -arch {alexnet,vgg}

choices demo

optional arguments:
-h, --help show this help message and exit
-arch {alexnet,vgg} the architecture of CNN, at this time we only support
alexnet and vgg.

```

7.nargs:

设置参数在使用时候可以提供的个数

`parser.add_argument('-name', nargs=x)`

值|含义
---|---
N|参数的绝对个数(例如:3)
'?'|0或1个参数
'*' | 0或所有参数
'+' | 所有,并且至少一个参数

```python
# file-name: nargs.py
import argparse

def get_parser():
parser = argparse.ArgumentParser(
description='nargs demo')
parser.add_argument('-name', required=True, nargs='+')

return parser


if __name__ == '__main__':
parser = get_parser()
args = parser.parse_args()
names = ', '.join(args.name)
print('Hello to {}'.format(names))
1
2
$ python nargs.py -name A B C
Hello to A, B, C

作用域

就像变量或者函数在c++里分public和private一样。Python是如何区分作用域的呢?

  • 公有的函数和变量名(public):可以被直接引用,比如:abc,x123,PI等;

  • 特殊变量:在每个模块里类似于__xxx__这样的变量是特殊变量,可以被直接引用,但是一般都有特殊的含义,如__author____name__,上面的test模块里定义的文档注释也可以用特殊变量__doc__访问。

  • 保护变量:命名形式是_xxx在变量前加一个_。这个保护类型只能允许其本身与子类进行访问。并且当使用“from M import”时,不会将以一个下划线开头的对象引入。保护类型的命名其实是能够在外部访问的。不过原则上还是把这种变量当做私有变量看待。

  • 私有变量:如果想让变量或者函数是私有(private)的,可以用类似__xxx这样的形式命名变量或函数,也就是前面加两个下划线。这样就只允许这个类本身进行访问了,连子类也不可以。双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是。譬如Student类里面定义一个私有变量__name不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量。

所以说Python的变量的动态化使得在命名上需要严格控制。并且私有公有的维护全靠自觉。

附上一篇———Python中变量的命名规范

有一个比较经典的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A(object):

def __init__(self):
self.__private()
self.public()

def __private(self):
print('A.__private()')

def public(self):
print('A.public()')

class B(A):
def __private(self):
print('B.__private()')

def public(self):
print('B.public()')

b = B()

运行之后的结果是

1
2
A.__private() 
B.public()

类A和B都有一个私有方法,命名为__private(),因为B是继承于A,在用B实例化对象b时,程序需要先执行A的__init__ (),执行到__private时, 是A的__private();到public()方法时候,B里面重写了,因此调用的是B的public()方法。

面向对象编程

类和实例

1
2
3
4
5
class Student(object):

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

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

然后构造函数是__init__,__init__方法的第一个参数永远是self,表示创建的实例本身。在创建实例的时候,必须传入与__init__方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去

对于访问限制的问题,在类的构建的时候

self.__name = name__name变为类里面的私有变量。

浅拷贝

“浅拷贝”,只拷贝最外层元素,内层嵌套元素则通过引用方式共享,而非独立分配内存。

用LeetCode上的一个题目
请编写一个函数,使其可以删除某个链表中给定的(非末尾)节点,你将只被给定要求被删除的节点。

现有一个链表 — head = [4,5,1,9],它可以表示为:

从链表里删除一个节点 node 的最常见方法是修改之前节点的 next 指针,使其指向之后的节点。因为,我们无法访问我们想要删除的节点 之前 的节点,我们始终不能修改该节点的 next 指针。相反,我们必须将想要删除的节点的值替换为它后面节点中的值,然后删除它之后的节点。

作者:LeetCode
链接:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/solution/shan-chu-lian-biao-zhong-de-jie-dian-by-leetcode/
来源:力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None

class Solution:
def deleteNode(self, node):
"""
:type node: ListNode
:rtype: void Do not return anything, modify node in-place instead.
"""
node.val, node.next = node.next.val, node.next.next
  • 直接写node = node.next是不行的,因为这里只是改了函数参数指向的对象,而原来传进来的 node 没有任何改变。如果Python的函数得到的参数是可变对象(比如list,set,这样的,内部属性可以改变的),那么我们实际得到的是这个对象的浅拷贝。“浅拷贝”,只拷贝最外层元素,内层嵌套元素则通过引用方式共享,而非独立分配内存。

  • 比如这个函数刚刚开始的时候题目传进来一个参数node,我们设这个节点为A,那么实际上得到的参数node是一个对于A的一个浅拷贝,你可以想象node是一把钥匙,它可以打开真正的节点A的门,如果我们现在让node = node.next,那么我们只是换了钥匙,变成了打开 A.next 的门的对应的钥匙,因此链表没有被修改, A没有被修改,只是我们手里的钥匙变了。而如果我们直接写node.val, node.next = node.next.val, node.next.next,就相当于我们先用钥匙找到 A 的门,然后修改了 A 的属性,链表发生了实际的变化。

链接:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/solution/1-xing-python-xiang-jie-by-knifezhu/

类中函数的互相调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Stu:
name=None
age=None
school="xx大学"#类变量,被所有学生实例共有
def __init__(self,name,age):
self.name=name
self.age=age
def printName_Age(self):
print("我叫"+self.name+","+"今年"+str(self.age)+"岁。")
def printSchool(self):
print("来自",Stu.school)
def printTotal(self):
print("类中方法调用其他方法")
Stu.printName_Age(self)
self.printSchool()
  • 方法一

格式:类名.方法名(self)
注意:方法名内必须传入一个实例对象的指针,self后可根据方法定义放入适当实参

  • 方法二

格式:self.方法名(方法列表).
方法列表不应该包括self

定制类

在上面的作用域里我们提到了特殊变量。这些特殊变量或方法一般都能够辅助定义类或者帮助实现某些功能,所以我们可以用这些特殊变量或方法来定制我们的类。在下面介绍类与对象的属性的时候还会专门讲一下__slots__限制类中变量命名的独特作用。

__iter____next__

如果一个类想被用于for … in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Fib(object):
def __init__(self):
self.a, self.b = 0, 1 # 初始化两个计数器a,b

def __iter__(self):
return self # 实例本身就是迭代对象,故返回自己

def __next__(self):
self.a, self.b = self.b, self.a + self.b # 计算下一个值
if self.a > 1000: # 退出循环的条件
raise StopIteration()
return self.a # 返回下一个值

for n in Fib():
print(n)

__getitem__()``__setitem__``__delitem__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class DictDemo:
def __init__(self,key,value):
self.dict = {}
self.dict[key] = value
def __getitem__(self,key):
return self.dict[key]
def __setitem__(self,key,value):
self.dict[key] = value
def __len__(self):
return len(self.dict)
dictDemo = DictDemo('key0','value0')
print(dictDemo['key0']) #value0
dictDemo['key1'] = 'value1'
print(dictDemo['key1']) #value1
print(len(dictDemo)) #2

输出:

value0
value1
2

要表现得像list那样按照下标取出元素并且支持list里的切片功能,需要实现__getitem__()方法:

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

class Fib(object):

def __getitem__(self, n):
if isinstance(n, int):
a, b = 1, 1
for x in range(n):
a, b = b, a + b
return a
if isinstance(n, slice):
start = n.start
stop = n.stop
if start is None:
start = 0
a, b = 1, 1
L = []
for x in range(stop):
if x >= start:
L.append(a)
a, b = b, a + b
return L

f = Fib()
print(f[0])
print(f[5])
print(f[100])
print(f[0:5])
print(f[:10])

这里的isinstance()是Python中的内建函数,来判断一个对象是否是一个已知的类型。
第一个参数(object)为对象,第二个参数(type)为类型名(int…)或类型名的一个列表((int,list,float)是一个列表)。其返回值为布尔型(True or flase)。

__repr__

默认情况下构建一个类之后,创建一个实例,print这个实例的时候会输出“类名+object at+内存地址”,而如果对__repr__方法进行重写,可以为其制作自定义的描述信息。

1
2
3
4
5
6
7
8
class test:
def __init__(self):
self.name = "a test"

def __repr__(self):
return self.name
test1 = test
print(test1)

当然Python里的特殊变量和方法很多。这个可以到Python官方文档查看。

枚举类

构造

需要枚举的时候Python提供了Enum类帮助我们为一个枚举类型定义一个class类。

1
2
3
from enum import Enum

Month = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr'))

这样我们就获得了Month类型的枚举类,这样构造的话成员的value默认从1开始计数。

另一种构造方式看着可能更能看出Enum为我们构造的是一个类,并且这时候可以自定义从0开始计数。

1
2
3
4
5
from enum import Enum
class Color (Enum):
red=0
orange=1
yellow=2

如果要限制定义枚举时,不能定义相同值的成员。可以使用装饰器@unique。

1
2
3
4
5
6
from enum import Enum, unique

@unique
class Color(Enum):
red = 1
red_alias = 1

这样出现相同值的成员就会报错。

取值

1
2
3
4
5
6
7
8
>>> Month['Jan']
<Month.Jan: 1>
>>> Month(1)
<Month.Jan: 1>
>>> Month.Jan.name
'Jan'
>>> Month.Jan.value
1

下面枚举它的所有成员:

1
2
3
4
5
6
7
8
>>> for item in Month:
... print(item)
...
Month.Jan
Month.Feb
Month.Mar
Month.Apr
>>>

如果想把值重复的成员也遍历出来,要用枚举的一个特殊属性__members__

1
2
3
4
5
6
7
8
>>> for item in Month.__members__.items():
... print(item)
...
('Jan', <Month.Jan: 1>)
('Feb', <Month.Feb: 2>)
('Mar', <Month.Mar: 3>)
('Apr', <Month.Apr: 4>)
>>>

变量和方法的获取

  • dir() 函数不带参数时,返回当前模块内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法__dir__(),该方法将被调用。如果参数不包含__dir__(),该方法将最大限度地收集参数信息。
  • vars():默认打印当前模块的所有属性和属性值,如果传一个对象参数则打印当前对象的属性和属性值
  • globals():以字典类型返回当前位置的全部全局变量。
  • locals():会以字典类型返回当前位置的全部局部变量。

这里讲一下更常用的dir()和vars()

dir()

dir([object]) object — 对象、变量、类型

1
2
3

>>> dir()
['Enum', 'Month', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'item', 'member', 'name', 'test']

vars()

通过vars()可以获取类的所有成员属性

这里给出一个例子

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
class Market(object):
def __init__(self):
self.title = 'apple'
self.count = '20'
def list_all_member(self):
for name,value in vars(self).items():
print('%s=%s'%(name,value))
if __name__== '__main__':
market= Market()
market.list_all_member()

因为vars返回 __dict__ 属性, 比如模块, 类, 实例, 或者其他带有 __dict__ 属性的object.

所以其实上面的例子也可以改成

1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*-
class Market(object):
def __init__(self):
self.title = 'apple'
self.count = '20'
def list_all_member(self):
for name,value in self. __dict__.items():
print('%s=%s'%(name,value))
if __name__== '__main__':
market= Market()
market.list_all_member()

结果都是一样的

1
2
title=apple
count=20

继承与多态

首先来复习一下这两个概念:

  • 继承可以让子类获得父类的全部功能,父类实现过的方法,子类不需要重新定义就能自动拥有。当然也可以为子类增加新的方法,这些新方法父类不会掌握。如果子类新定义的方法与父类的方法相同,则子类的方法覆盖父类的方法。在程序运行时总是会调用子类的方法而不是父类的方法。

  • 在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行。
    比如参数为Bird的一个函数fly(),新增一个继承自Bird的子类叫Duck,当我们传入Duck的实例时,fly()仍然可以正常执行。
    对于一个对象,我们只需要知道它是Bird类型,无需确切地知道它的子类型,就可以放心地调用其拥有的方法,而调用的方法是作用在Bird还是Duck抑或是诸如Chicken其他子类上,由该对象拥有的实际生效方法决定,这就是多态。

这上面的概念在所有面向对象的语言中都试用。对于Python来讲有和C++在这方面很不一样的地方在于它是动态语言,所以有“鸭子类型”这个说法。

对于这个网上看到一个解释比较清楚原文

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
class Bird:
def wings():
print('l have wings')

# Duck类是继承Bird的子类。
class Duck(Bird):
def swim():
print('Duck can swim')

# Human类是另一个构建的类,但也有swim这个function
class Human:
def swim():
print('People can swim as well')


# 在Java或C++中定义函数参数时,必须指定参数的类型,也即是说,我们如果用
# C++写下面的Func,需要告知,obj具体是Duck类还是Human类的实例。如 void Func(Duck &obj)
# 如果限定了Duck,那么Human实例是不可以被采纳的。
# 然而,在Python中,它关心的是你传入的对象包含不包含swim()这个功能函数,如果
# 没有会调用错误,如果有就一切正常,并不需要care这个对象是从哪个类构造出来的。

def Func(obj):
# 如果真的想要控制好obj的来源就需要程序里备注
# 如:Func函数需要接收一个Duck类型
obj.swim()

obj1 = Duck()
Func(obj1) # 结果:Duck can swim

obj2 = Human()
Func(obj2) # 结果:People can swim as well

多重继承

对于多重继承,java有interface关键字来实现,不过java不是很了解。c++实现多重继承的方法其实和Python差不多。例如

1
2
3
4
5
6
7
8
class ZooAnimal{
};
class Bear : public ZooAnimal{
};
class Endangered{
};
class Panda : public Bear, public Endangered{
};

那么Python大致是如何实现这个功能的呢?譬如对上面的进行转变。

1
2
3
4
5
6
7
8
9
10
11
class ZooAnimal(object):
pass

class Bear(ZooAnimal):
pass

class EndangeredMixIn(object):
pass

class Panda(Bear, EndangeredMixIn):
pass

Python中的MixIn其实是一种约定俗成的命名规矩,表示这个继承的类只代表一种特殊的属性或者功能。如上面的Panda类虽然继承了Bear和EndangeredMixIn两个类。但Bear才是Panda继承下来的主线。EndangeredMixIn只是算作额外的属性描述所需要的类。

使用Mixin实现多重继承要非常小心:

  • 首先它必须表示某一种功能,而不是某个物品,如同Java中的Runnable,Callable等
  • 其次它必须责任单一,如果有多个功能,那就写多个Mixin类
  • 然后,它不依赖于子类的实现

实例属性和类属性

动态添加

作为一个动态语言,Python可以随时随地给已经构建或者定义完的实例和类添加属性或者方法。

这段演示里值得注意的点:

  • 给一个实例绑定的方法,对另一个实例是不起作用的
  • 绑定方法适合需要用到types里的MethodType模块
  • Student.set_age = set_age 和 Student.set_age = MethodType(set_age,Student)其实是一个作用和意思,前者是简写。

__slots__

上面的动态添加的方法太过于自由。如果我们想要限制实例的属性,可以用__slots__变量。

具体写法如下:

1
2
class Student(object):
__slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

1.使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。

1
2
3
4
5
>>> class GraduateStudent(Student):
... pass
...
>>> g = GraduateStudent()
>>> g.score = 9999

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__

2.将方法绑定给类后,类调用方法后,类和实例都可以访问类中的属性与方法,这不受slots范围限制。方法没有绑定给类而直接绑定给实例时,需要在slots规定范围中加入该方法和方法中的属性。

@property

在绑定属性时,如果我们直接把属性暴露出去,虽然写起来很简单,但是,没办法检查参数,更恐怖的是可以把成绩随便改,没有限定。你可以给成绩赋个字符串,并且成绩可以设置地很大很离谱。

s = Student() s.score = 9999

这时候我们可以借助于@property装饰器来解决,这是Python内置的一个decorator。

从一个例子来看,在上面我们用s.score = 9999的方式修改属性有缺点,那么现在我们这样进行修改。使用两个函数进行成绩的设置和获取。

1
2
3
4
5
6
7
8
9
10
11
class Student(object):

def get_score(self):
return self._score

def set_score(self, value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value

看上去没啥问题了,那么这样做的话我们访问这个score属性和想要对score进行设置的时候需要s.set_score(60)或者s.get_score()会很麻烦。

我们肯定希望用s.score这种方式更明晰地访问,同时希望这个score属性的格式和数值能够被规定。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):

@property
def score(self):
return self._score

@score.setter
def score(self,value):
if not isinstance(value, int):
raise ValueError('score must be an integer!')
if value < 0 or value > 100:
raise ValueError('score must between 0 ~ 100!')
self._score = value

把get方法变为属性只需要加上@property装饰器即可,此时@property本身又会创建另外一个装饰器@score.setter,负责把set方法变成给属性赋值,这么做完后,我们调用起来既可控又方便。

1
2
3
4
5
6
7
8
>>> s = Student()
>>> s.score = 60 # OK,实际转化为s.set_score(60)
>>> s.score # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!

还可以定义只读属性,只定义getter方法,不定义setter方法就是一个只读属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(object):

@property
def birth(self):
return self._birth

@birth.setter
def birth(self, value):
self._birth = value

@property
def age(self):
return 2015 - self._birth

上面的birth是可读写属性,而age就是一个只读属性。

值得注意的点:

1
2
def score(self):
return self._score

这样命名是为了变量名不与函数名相冲突。如果改成return self.score
self.score可以看作函数,也可以看作变量。如果你不把score变量声明为_score, 那么score(self, value)为了得出最后的self.score = value,会对self.score这个函数进行循环调用。所以这里的_score的命名不仅仅是可以为了把这个变量变为保护变量,更多是为了和同名函数区别开。