暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【自动化运维新手村】Python与面向对象-终篇


自动化运维,你玩转了吗?



【摘要】

首先说明,以下几类读者请自行对号入座:
  1. 对CMDB很了解但对于Python还没有上手的读者,强烈建议阅读前面几篇;
  2. 对Python了解较少只能写出简单脚本的读者,强烈建议阅读此篇;
  3. 已经可以熟练写出Python脚本,但对CMDB不是很了解的读者,建议阅读此篇;
  4. 即了解Python,又了解CMDB的读者,可以出门左转,看下一篇。
上一讲我们已经可以说已经摸到点儿Python中面向对象的门道了,虽然说只摸到一点儿,但这也足够支撑我们开启后面的学习,
今天我们就继续深入到CMDBv1.5的源码中。

【CMDB v1.5源码阅读】

【类的关系】

首先来看完整的入口函数
if __name__ == "__main__":
    try:
        file_path = os.path.join(os.path.dirname(__file__), "data.json")
        file_store = Store("FILE", file_path) # 实例化一个文件存储的存储对象
        cmdb = CMDB(file_store) # 传入读出的数据源实例化一个CMDB的对象
        cmd_params = Params(cmdb.operations) # 实例化从命令行获取参数的对象
        op, args = cmd_params.parse(sys.argv) # 使用参数对象的解析方法解析出要做的操作和具体的参数
        result = cmdb.execute(op, args) # 传入参数对象解析出的操作和具体参数,调用CMDB对象的执行操作方法
        print(result)
    except Exception as e:
        print(e)


CMDBv1.5完整的代码中包括了三个类,分别是Params参数类
Store存取类
以及CMDB类
, 通过入口函数中类的实例化以及参数的传递可以看出三个对象之间的关系。
  • 先实例化存取类负责数据的读取和保存
  • 再实例化CMDB类,并将存取对象传入CMDB类中,负责具体操作时对数据的读写
  • 最后实例化参数类,解析命令行参数,并将解析结果传入CMDB类的实例方法中
如下图所示:

【CMDB类】

今天的重头戏就是CMDB中最重要的类的实现,首先来看CMDB类的实例化
cmdb = CMDB(file_store) # 传入读出的数据源实例化一个CMDB的对象


实例化的方法还是和之前将的一样,但这里传入的参数比较特别,传入的是一个实例对象,将已经实例化过的存取对象传入到CMDB的实例中,这样可以在之后的其他操作中很方便的通过存取对象进行数据的读写。
Tips
这里用到了类之间的组合,这属于一种编程规范:在面向对象的过程中,尽量避免不必要的继承,而是要多使用对象的组合。
一、属性
class CMDB:
    def __init__(self, store):
        self.store = store
        self.operations = self.methods()
        
    def methods(self):
        ops = []
        for m in dir(self): # 获取self变量的所有属性和方法
            if m.startswith("__") or m.startswith("__"): # 过滤掉内置属性和方法
                continue
            if not callable(getattr(self, m)): # 过滤掉属性
                continue
            ops.append(m)
        return ops


CMDB类具有两个属性,一个是存取对象,另一个就是允许执行的操作,存取对象是通过__init__()
函数在实例化类的时候传入的,而允许执行的操作需要我们调用自身类中的一个实例方法去获取。
1. dir()
这个方法不知道大家是否还有印象,在之前的文章中提到过,由于Python中所有变量皆对象,可以用过dir()
这个方法获取到某个变量具有的所有属性和方法,所以这里通过dir(self)
来获取实例对象所有的属性和方法,并循环去进行判断。
2.在Python的类中,一般会将内置的属性前会加__
,而内置的方法前后都会加__
,所以在每次循环时,通过判断这个变量名是否以__
开头或结尾,便可以过滤出内置的属性和方法。
3. callable()
函数会返回一个bool
值,可以判断传入的参数是否是可被调用的,如果返回True就说明传入的参数是一个函数。
4. getattr(obj, name)
函数可以传入一个对象和一个字符串,会根据传入的name返回obj中对应的属性或方法
综上,CMDB类就包含两个属性,分别是store对象,和允许执行的operations,这两个属性都会在实例化CMDB类的时候确定。
二、实例方法


class CMDB:
    def __init__(self, store):
        self.store = store
        self.operations = self.methods()

    def execute(self, op, args):
        if op not in self.operations:
            raise Exception("%s is not valid CMDB operation, should is %s" % (op, ",".join(self.operations)))
        method = getattr(self, op)
        return method(*args)

    def init(self, region):
        data = self.store.read()
        if region in data:
            raise Exception("region %s already exists" % region)
        data[region] = {"idc": region, "switch": {}, "router": {}}
        self.store.save(data)
        return region


1. 上一讲已经提到,类的实例方法是类必须实例化之后才能被调用的,且第一个参数必须是self
,CMDB的已知操作分别是init(), add(), delete(), update(), get()
,这些都应该是实例方法,用法也都几乎一样。

2. execute(self, op, args)
函数是在CMDB类中额外新增一个功能,它要求传入要执行的操作和操作所需的参数。

这个函数的功能是作为CMDB统一对外暴露的入口,执行增删改查的操作都通过这个函数来进行,主要目的是为了增加整个CMDB类的可扩展性。大家可以参照之前没有重构过的代码进行比较一下:


if args[1] == "init":
    init(args[2])
elif args[1] == "add":
    add(*args[2:])
elif args[1] == "get":
    get(args[2])
elif args[1] == "update":
    update(*args[2:])
elif args[1] == "delete":
    delete(*args[2:])
else:
    print("operation must be one of get,update,delete")


同样是调用CMDB的增删改查操作,原先需要写很多的判断逻辑,并且这些函数的调用方式也都是相同的,所以完全可以把这个调用抽象成一个execute()
方法,直接根据operation
的名字去调用对应的函数即可
再假设如果后续新增了其他对CMDB的操作,那么如果用if...else..
的方式是不是每次都需要去修改代码,这其实就是扩展性不够好的体现,现在重构过之后使用execute()
方法,则完全可以不用修改额外的代码,大家可以仔细体会一下。
3. init(self, region)
这个函数是CMDB初始化地域的函数,大家注意与__init__()
区分
def init(self, region):
    data = self.store.read()
    if region in data:
        raise Exception("region %s already exists" % region)
    data[region] = {"idc": region, "switch": {}, "router": {}}
    self.store.save(data)
    return region


初始化地域包括另外的增删改查操作,逻辑都与重构之前的一模一样,唯一一点需要改动的地方就是,原先在面向过程编程的时候,对于数据的读写都是通过直接调用read_file()
或者write_data()
函数,但现在需要通过存取对象来实现。
在实例化CMDB类的时候就已经将存取类通过参数传递给了CMDB对象,所以self.store
此刻就已经是存取对象,可以直接通过self.store.read()
或者self.store.save()
来实现数据的读写功能。
4. 大家可以参照init()
的方法改写一下其他的增删改查操作,将原先的函数改写为实例方法
三、静态方法
Python中除了实例方法还有静态方法类方法,这三种方法在用法上会有所区别,但并没有明确的规定说我这个方法必须定义成实例方法或必须定义成静态方法。
定义成什么类型的方法更多的基于场景的分析,以及编程的习惯,更多关于这三种方法类型的区别会在番外篇中做出解释,因为此处只用到了静态方法,所以就暂时先讲解静态方法的语法和使用。
静态方法的语法为在方法的上面增加一行@staticmethod
,这属于Python中的装饰器,装饰器也会单独在番外篇中详细讲解,也是属于Python中的一大特点。
除了加了一个标识为静态方法的装饰器以外,静态方法并不需要传入self
参数,因为静态方法的特点就是该方法不属于任何类或者实例对象,它是静态的,它只是因为这个方法从面向对象的角度来说,可以让它放在CMDB类中,但它本身并不需要引用到任务与该类或者该类的实例相关的其他属性或方法。
import json

class CMDB:
    @staticmethod
    def check_parse(attrs): 
         # 检查参数的合法性
        if attrs is None:
            return
        try:
            attrs = json.loads(attrs)
            return attrs
        except Exception:
            raise Exception("attributes is not valid json string")

    @staticmethod
    def locate_path(data, path):
        # 根据path定位到data的位置
        target_path = data
        path_seg = path.split("/")[1:]
        for seg in path_seg[:-1]:
            if seg not in target_path:
                print("location path is not exists in data, please use add function")
                return
            target_path = target_path[seg]
        return target_path, path_seg[-1]


大家可以根据之前的讲解了解到,检查参数的合法性以及根据path定位到data中相应的位置,这两个方法都是相对比较独立的功能。
并且从上面代码中也可以看出,这两个函数并不依赖任何其他第三方库或者具体的业务逻辑,这种函数我们一般称之为干净的函数
如果后面我们需要在多处应用到相同的干净的函数,那么就可以将这些干净的函数单独写在一个文件中,供其他地方import调用,这样很多干净的函数就组成了一个工具包
但基于目前CMDB的场景,将这两个函数归到CMDB类中也是十分合适的。
因为静态方法不需要传入self
,并且它本身也与实例对象无关它只是形式上归属于CMDB类,所以可以直接通过CMDB.check_parse()
来调用,如下面的add()

class CMDB:
    def add(self, path, attrs):
        attrs = CMDB.check_parse(attrs)
        if not attrs:
            raise Exception("attrs is invalid json string")
        data = self.store.read()
        target_path, last_seg = CMDB.locate_path(data, path)
        if last_seg in target_path:
            raise Exception("%s already exists in %s, please use update operation" % (last_seg, path))
        target_path[last_seg] = attrs
        self.store.write(data)
        return attrs

【总结】
根据上面的讲解,大家基本已经可以自己写出完整的CMDB类了,但我这里还是给大家附上重构后全部的源代码
希望大家可以自己先根据我上面的讲解自行重构,然后再与我给出的代码做对比,这样才能发现你的思路上可以优化的地方,说不定你能重构出更优雅的代码,到时候希望我们可以互相多多交流,分享思路。 
温馨提示:篇尾彩蛋
import json
import sys
import os
import time


class Store:
    version = None
    update_time = None
    def __init__(self, store_type, store_uri):
        self.store_type = store_type # 存储介质类型
        self.store_uri = store_uri # 存储介质的路径

    def save(self, data):  # 存储方法
        data["version"] = (Store.version or 1) + 1
        data["update_time"] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        with open(self.store_uri, "w+") as f:
            json.dump(data, f, indent=2)

    def read(self):  # 读取方法
        with open(self.store_uri, "r+") as f:
            data = json.load(f)
            try:
                Store.version = data.pop("version")
                Store.update_time = data.pop("update_time")
            except Exception:
                pass
        return data


class CMDB:
    version = None
    update_time = None

    def __init__(self, store):
        self.store = store
        self.operations = self.methods()

    def methods(self):
        ops = []
        for m in dir(self):
            if m.startswith("__") or m.startswith("__"):
                continue
            if not callable(getattr(self, m)):
                continue
            ops.append(m)
        return ops

    def execute(self, op, args):
        if op not in self.operations:
            raise Exception("%s is not valid CMDB operation, should is %s" % (op, ",".join(self.operations)))
        method = getattr(self, op)
        return method(*args)

    def init(self, region):
        data = self.store.read()
        if region in data:
            raise Exception("region %s already exists" % region)
        data[region] = {"idc": region, "switch": {}, "router": {}}
        self.store.save(data)
        return region

    def add(self, path, attrs):
        attrs = CMDB.check_parse(attrs)
        if attrs is None:
            raise Exception("attrs is invalid json string")
        data = self.store.read()
        target_path, last_seg = CMDB.locate_path(data, path)
        if last_seg in target_path:
            raise Exception("%s already exists in %s, please use update operation" % (last_seg, path))
        target_path[last_seg] = attrs
        self.store.save(data)
        return attrs

    def delete(self, path, attrs=None):
        attrs = CMDB.check_parse(attrs)
        data = self.store.read()
        target_path, last_seg = CMDB.locate_path(data, path)
        if attrs is None:
            if last_seg not in target_path:
                raise Exception("%s is not in data" % path)
            target_path.pop(last_seg)
        if isinstance(attrs, list):
            for attr in attrs:
                if attr not in target_path[last_seg]:
                    print("attr %s not in target_path" % attr)
                    continue
                if isinstance(target_path[last_seg], dict):
                    target_path[last_seg].pop(attr)
                if isinstance(target_path[last_seg], list):
                    target_path[last_seg].remove(attr)
        self.store.save(data)
        return attrs

    def update(self, path, attrs):
        attrs = CMDB.check_parse(attrs)
        if attrs is None:
            raise Exception("attrs is invalid json string")
        data = self.store.read()
        target_path, last_seg = CMDB.locate_path(data, path)
        if type(attrs) != type(target_path[last_seg]):
            raise Exception("update attributes and target_path attributes are different type.")
        if isinstance(attrs, dict):
            target_path[last_seg].update(attrs)
        elif isinstance(attrs, list):
            target_path[last_seg].extend(attrs)
            target_path[last_seg] = list(set(target_path[last_seg]))
        else:
            target_path[last_seg] = attrs
        self.store.save(data)
        return attrs

    def get(self, path):
        if "/" not in path:
            raise Exception("please input valid path")
        data = self.store.read()
        if path == "/":
            return json.dumps(data, indent=2)
        try:
            target_path, last_seg = CMDB.locate_path(data, path)
            ret = target_path[last_seg]
        except KeyError:
            raise Exception("path %s is invalid" % path)
        return json.dumps(ret, indent=2)

    @staticmethod
    def check_parse(attrs):
        if attrs is None: # 判断attrs的合法性
            return None
        try:
            attrs = json.loads(attrs)
            return attrs
        except Exception:
            raise Exception("attributes is not valid json string")

    @staticmethod
    def locate_path(data, path):
        target_path = data
        path_seg = path.split("/")[1:]
        for seg in path_seg[:-1]:
            if seg not in target_path:
                print("location path is not exists in data, please use add function")
                return
            target_path = target_path[seg]
        return target_path, path_seg[-1]


class Params:
    def __init__(self, operations) -> None:
        self.operations = operations

    def parse(self, args):
        if len(args) < 3:
            raise Exception("please input operation and args, operations: %s" % ",".join(self.operations))
        operation = args[1]
        params = args[2:]
        return operation, params


if __name__ == "__main__":
    try:
        file_path = os.path.join(os.path.dirname(__file__), "data.json")
        file_store = Store("FILE", file_path) # 实例化一个文件存储的存储对象
        cmdb = CMDB(file_store) # 传入读出的数据源实例化一个CMDB的对象
        cmd_params = Params(cmdb.operations) # 实例化从命令行获取参数的对象
        op, args = cmd_params.parse(sys.argv) # 使用参数对象的解析方法解析出要做的操作和具体的参数
        result = cmdb.execute(op, args) # 传入参数对象解析出的操作和具体参数,调用CMDB对象的执行操作方法
        print(result)
    except Exception as e:
        print(e)


【篇后语】


平时经常会有朋友问我,并且我也在知乎或者其他论坛上看到有人提问有没有好的Python的学习资料,有没有系统的学习Python的方法,其实对此我有一些自己的看法,我觉得问这样问题的朋友肯定是想要学习的,但什么是真正的学习可能鲜有人清楚
1. 学习本质上分为两种:记忆和泛化
记忆顾名思义就是把一些固定的知识记住,比如一年有多少天这种知识。但学习编程语言很明显不属于记忆,它应该属于泛化的范畴。
2. 泛化又分为两种:指令学习和归纳学习
- 指令学习就是提供一个知识点,比如time库中的时间格式转换函数,你这时候需要的就是把这个指令记住,然后在不停的使用这个函数转换时间的过程中去真正掌握它。
- 归纳学习则更多的是需要你根据某个知识点去触类旁通的解决更多的问题,比如我们学习了面向对象的思想,我带领大家去用这个思想实现CMDB,那你是否真的掌握了这个知识呢?这就要看你是不是可以用这样的思想去解决其他的问题,所以归纳学习会更具有难度,他需要你去不停的用新的问题来验证你是否真的学会了这个知识点。
在之前的直播答疑中有的朋友问起我需不需要刻意去背一些库函数的用法,我当时给的答案是肯定的,需要去背,因为他很明显属于指令学习的一种,但我还说了一句话:如果你能更多的去使用他,熟悉了之后你就会有一种感觉,在面对陌生的函数的时候也可以更快速的掌握,这就又是指令学习到归纳学习的转换
所以如果一上来就希望有一本大而全的Python书籍或者资料,且不说有没有这样的书籍,倘若真的有,你拿到了也并不一定就表示你可以学会Python。

因为学习Python是一种泛化学习,需要你在获取新的知识点的过程中去用它解决实际更多的问题,不然就只能是停留在纸上谈兵的阶段。这也是为什么我的文章不愿意一点一点的去挨个讲知识点,我更愿意用从实际的场景出发,教会大家如何用具体的知识点去解决问题。
但更深一层的是虽然我是从实际的场景讲解运用知识点,但仍然是我带领大家去思考的,所以作为读者你仍然需要用去亲自实践它,这次的篇后语略微长了些,但更多的是希望告诉大家一些学习方法和理念,能对学习任何技能都起到帮助作用。

yuefeiyu1024

添加作者微信加入专属学习交流群,获取更多干货秘籍


文章转载自python玩转自动化运维,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论