四时宝库

程序员的知识宝库

网络工程师的Python之路 -- 类(Class)

类(Class)作为Python的进阶内容在我的书和专栏文章里是没有讲到的,因为从实用性的角度来看,它对刚入门学习Python,只需要写10几行代码就能SSH登陆网络设备,并完成输入各种配置命令和show/display命令的的网工来说,除了加重网工学习Python的负担外,实际的作用和帮助其实并不大。有鉴于这两年来读过我专栏文章和书的网工读者越来越多,很多读者已经彻底掌握了Python的基础知识,在此基础上,我认为还是有必要把书上以前没讲到的Python进阶知识做个补充,今天就来聊聊类(Class),还是老规矩,所有教学内容和举的例子都将从网工的角度出发。


1. 什么是类

作为一个面向对象编程(object-oriented programming)的语言,你可以把Python中的类(Class)理解为一个模板,我们可以将自己定义好的类(也就是模版)实例化(instantiate)给一个对象(Object),所有被同一个类所实例化的对象都继承了该类下所有的方法(即在该类下面我们自定义的函数),唯一的区别是它们的初始属性(attribute)会因为根据我们在进行实例化初始配置时所传入参数的不同而不相同。

举个例子,全世界有很多的网络设备厂商都在生产或曾经生产过路由器和交换机,虽然它们的性能或各自使用的部分网络协议存在差异,但是他们都具备了一些相同的术语和属性:比如IP地址,MAC地址,VLAN,MTU,设备型号,操作系统版本等等(就好比世界上70多亿人类都有共同的一些属性,比如姓名,身高,年龄,血型,出生地等等),正因为有这些相同的特征和属性,所以我们可以创建一个叫做Router的类或者Switch的类来描述全天下所有的路由器和交换机,每一台路由器和交换机就是将要被我们创建的Router类和Switch类所实例化的对象。

2. 怎么创建类

我们以创建一个Switch类为例子,代码如下:

Bash
class Switch:     
    def __init__(self, model, os_version, ip_add): 
        self.model = model 
        self.os_version = os_version 
        self.ip_add = ip_add

SW1 = Switch('Cisco 9300', '16.12.04', '192.168.2.11')

代码讲解:

  • 这里我们通过class Switch:定义了一个叫做Switch的类,如果你曾经自学过和类相关的知识,肯定会注意到这里的“class Switch:”也可以写成“class Switch(object):”,这个在类名后面加上一个(xxxxx)的做法叫做“继承”(inheritance,继承的概念我们后面会讲到),在Python2中定义类的时候是否使用(object)来继承object这个Python自带的类是有很大区别的,具体的区别是什么大家有兴趣可以自行去扩展阅读,这里就不详述了,因为Python2已经不是我们需要重点关注的内容了。大家只需要记住:在Python3中,在类名后面不管加不加(object)都是没有任何区别的,一般图省事的都可以不加。
Bash
class Switch:
  • 接下来我们创建一个特殊函数,叫做__init__(),注意在Python中,函数名前后各带两个下划线__的函数叫做魔法函数(Magic Methods)(关于魔法函数的讲解已经超出了本文的范围),这里的__init__()就是一个典型的魔法函数,它的作用是在我们将类实例化给一个对象后,立即就要执行该函数让该对象完成初始化配置,__init__()中的参数self代表实例本身,而后面的参数model, os_version, ip_add等则是我们要手动赋值给对象本身的初始参数:
Bash
    def__init__(self, model, os_version, ip_add):          
        self.model = model          
        self.os_version = os_version          
        self.ip_add = ip_add 
  • 定义好Switch类和它的__init__()函数后,我们就可以将Switch类实例化给一个叫做SW1的对象,注意在调用__init__()函数向对象传入参数时,参数self本身不用被传,这里我们只需要传入model, os_version以及ip_add三个参数即可,这三个参数即成为了SW1这个对象的初始属性。
Bash
SW1 = Switch('Cisco 9300','16.12.04','192.168.2.11')
  • 此时我们使用print (SW1.model), print (SW1.os_version)以及print (SW1.ip_add)即可打印出对象SW1的这三个初始属性:


同样的道理,定义好Switch类后,我们还可以借助这个'模板'将Switch类实例化给第二个对象SW2:

Bash
SW2 = Switch('Cisco 9300','16.12.04','192.168.2.12')

这里可以看到,对象SW1和SW2的设备型号以及OS版本都是一样的,它们唯一不同的是IP地址,就像世界上你可以随意找出两个性别和身高一模一样,但是名字不同的两个人(对象)一样。

3. 方法 (Methods)

除了使用__init__()这个魔法函数来定义对象的初始属性外,我们还能在类下面自定义方法,也就是我们通常理解的函数。比如这里我们可以定义一个叫做description()的方法来获取SW1和SW2的三个初始属性, 代码如下:

Bash
class Switch: 
 def __init__(self, model, os_version, ip_add): 
 self.model = model 
 self.os_version = os_version 
 self.ip_add = ip_add
 
 def description(self):
   description = f'Model  : {self.model}\n'\
                 f' OS Version : {self.os_version}\n'\
                 f' IP Address : {self.ip_add}\n'
   return description

SW1 = Switch('Cisco 9300', '16.12.04', '192.168.2.11')
SW2 = Switch('Cisco 9300', '16.12.04', '192.168.2.12')

print (' SW1\n', SW1.description())
print (' SW2\n', SW2.description())

运行脚本看效果:


4. 继承 (Inheritance)

前面讲到了,在类名后面加上一个(xxxxx)的做法叫做“继承”,xxxxx就是你要继承的类的名称。在Python中,继承一方叫做“子类”被继承一方叫做“父类”子类会继承父类所有的属性(比如上面class Switch中的model, os_version, ip_add)和函数(__init__())以及方法(description()),不用重新再造一次轮子

在前面的例子中,我们已经创建了Switch类,现在如果我们要创建Router类的话有两个办法:1. 从头再写一个class Router; 2. 直接用class Router(Switch)来继承已有的Switch类。 显然第二种方法更省事。

来看下面的代码:

Bash
class Switch: 
  def __init__(self, model, os_version, ip_add): 
    self.model = model 
    self.os_version = os_version 
    self.ip_add = ip_add
 
  def description(self):
    description = f'Model  : {self.model}\n'\
                 f' OS Version : {self.os_version}\n'\
                 f' IP Address : {self.ip_add}\n'
    return description

class Router(Switch):
	pass

SW1 = Switch('Cisco 9300', '16.12.04', '192.168.2.11')
SW2 = Switch('Cisco 9300', '16.12.04', '192.168.2.12')
Router1 = Router ('Cisco ISR 4400', '16.10.05', '172.16.1.100')
Router2 = Router ('Cisco ISR 4400', '16.10.05', '172.16.1.101')

print (' SW1\n', SW1.description())
print (' SW2\n', SW2.description())
print (' Router1\n', Router1.description())
print (' Router2\n', Router2.description())

这里我们通过class Router(Switch)来创建Router类,让它继承了Switch类,此时Switch类为父类,Router类为子类(注意,在代码里面,父类必须写在子类的前面(或者说上面)!)。这里我们创建了Router子类后,因为它继承了Switch父类的所有初始属性、函数和方法,因此我们可以什么事都不做,在class Router(Switch):下面只写一个pass了事即可。然后直接创建Router1和Router2两个对象,将Router类直接实例化给它俩,最后再调用description()这个方法将Router1和Router2的型号、OS版本以及IP地址打印出来:

Bash
class Router(Switch):
	pass

Router1 = Router ('Cisco ISR 4400', '16.10.05', '172.16.1.100')
Router2 = Router ('Cisco ISR 4400', '16.10.05', '172.16.1.101')

print (' Router1\n', Router1.description())
print (' Router2\n', Router2.description())

运行脚本效果如下:

5. 在一个脚本中用import引入另一个脚本

在Python中使用类还有另外一个好处,就是可以支持我们在创建一个新脚本时通过import语句调用另一个已经存在的脚本的内容,省去了重复造轮子的工作。举个例子,每次我们使用Paramiko来SSH登录一台网络设备时,我们总免不了要输入下面的14行代码来完成登录交换机的这个过程:

Bash
import paramiko
import time
from getpass import getpass

username = input('Enter your username: ')
password = getpass('Enter you password: ')

f = open('ip_list.txt')

for ips in f.readlines():
    ip = ips.strip()
    ssh_client = paramiko.SSHClient()
    ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh_client.connect(hostname=ip, username=username, password=password, look_for_keys=False)
    command = ssh_client.invoke_shell()
    print (f'Sucessfully login to {ip}')
    command.send('term len 0\n')

因为这是每次写新脚本都要反反复复使用到的代码内容,那么我们就可以创建一个脚本,将上述代码写成类,做成一个模板,以后我们就可以通过import来调用该脚本来使用这个脚本,方法如下 :

首先我们创建一个叫做SSH_Login.py的脚本,放入下列代码:

Bash
#SSH_Login.py

import paramiko
import time
from getpass import getpass
from datetime import datetime

class SSH:
    
    def __init__(self):
        self.username = input('Enter your username: ')
        self.password = getpass('Enter you password: ')
    
    def login_to_device(self, ip):
        self.ip = ip
        self.ssh_client = paramiko.SSHClient()
        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.ssh_client.connect(hostname=self.ip, username=self.username, password=self.password, look_for_keys=False)
        self.command = self.ssh_client.invoke_shell()
        print (f'Sucessfully login to {self.ip}')
        self.command.send('term len 0\n')

然后创建第二个叫做test.py的脚本,并确认当前目录下有一个存放要登录设备IP地址的文件ip_list.txt,放入下列代码:

Bash
#test.py

from SSH_Login import SSH

ssh = SSH()

with open('ip_list.txt') as f:
    for ips in f.readlines():
        ssh.login_to_device(ip=ips.strip())

代码讲解如下:

这里我们通过from SSH_Login import SSH来从脚本SSH_Login.py中导入它的SSH类,并将该SSH类实例化给ssh这个对象。

Bash
from SSH_Login import SSH

ssh = SSH()

然后我们通过open()函数和for循环遍历ip_list.txt里的设备IP地址信息,然后调用SSH_Login.py里SSH类下面的login_to_device()方法,即完成了通过Paramiko依次登录所有设备的操作,将原本14行的代码量缩减到了5行,并且将来还可以反复调用SSH_Login.py这个“模板”。

Bash
with open('ip_list.txt') as f:
    for ips in f.readlines():
        ssh.login_to_device(ip=ips.strip())


如果你用过NAPALM这个第三方模块的话肯定知道它的getter类API下面有很多诸如get_arp_table,get_enviroment, get_interfaces之类的函数:


它可以让你仅仅使用一行代码就完成paramiko里需要4,5行代码才能完成的工作,其背后的原理同样是使用from xxxx import xxxx语法来调用napalm.py这个已经写好的模板里的内容,然后将该模板里的类实例化给一个新脚本里的对象。

照着这个思路我们还可以自己造一个类似napalm的轮子,举例如下,下面我在SSH_Login.py的内容中额外加上了get_interfaces(), get_show_run()以及backup_config()三个方法,方便我们快速完成获取show ip int brief, show run两条命令的输出内容,将设备配置做备份等三个常见的任务。

Bash
#SSH_Login.py

import paramiko
import time
from getpass import getpass
from datetime import datetime

class SSH:
    
    def __init__(self):
        self.username = input('Enter your username: ')
        self.password = getpass('Enter you password: ')
    
    def login_to_device(self, ip):
        self.ip = ip
        self.ssh_client = paramiko.SSHClient()
        self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.ssh_client.connect(hostname=self.ip, username=self.username, password=self.password, look_for_keys=False)
        self.command = self.ssh_client.invoke_shell()
        print (f'Sucessfully login to {self.ip}')
        self.command.send('term len 0\n')

    def print_output(self, sleep):
        time.sleep(sleep)
        print (self.command.recv(65535).decode('ascii'))

    def get_interfaces(self, sleep=1):
        self.command.send('show ip int brief\n')
        self.print_output(sleep)

    def get_show_run(self, sleep=2):
        self.command.send('show run\n')
        self.print_output(sleep)

    def backup_config(self):
        self.command.send('show run\n')
        time.sleep(2)
        output = self.command.recv(65535).decode('ascii')
        today = datetime.today()
        date = '%s-%s-%s'%(today.year, today.month, today.day)
        with open(f'{self.ip}_{date}.txt','w+') as f:
            f.write(output)


然后在test.py里引入调用它们即可:

Bash
from SSH_Login import SSH

ssh = SSH()

with open('ip_list.txt') as f:
    for ips in f.readlines():
        ssh.login_to_device(ip=ips.strip())
        ssh.get_interfaces()
        ssh.get_show_run()
        ssh.backup_config()

有兴趣的读者朋友可以自行拿去将代码使用验证,这里就不做详解了。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接