zoukankan      html  css  js  c++  java
  • 编写高质量代码 改善Python程序的91个建议——笔记(三)

    建议61:使用更为安全的property

      property是用来实现属性可管理性的built-in数据类型。它实际上是一种实现了__get__(), __set__()方法的类,用户也可以根据自己的需要定义个性化的property,其实质是一种特殊的数据描述符(数据描述符:如果一个对象同时定义了__get__()和__set__()方法,则称为数据描述符,如果仅定义了__get__()方法,则称为非数据描述符)。它和普通描述符的区别在于:普通描述符提供的是一种较为低级的控制属性访问的机制,而property是它的高级应用,它以标注库的形式提供描述符的实现,其签名形式为:

    property(fget=None, fset=None, fdel=None, doc=None)  -> property attribute

      Property常见的使用形式有以下几种

      1)

    class Some_Class(object):
        def __init__(self):
            self._somevalue = 0
    
        def get_value(self):
            print("calling get method to get value")
            return self._somevalue
    
        def set_value(self, value):
            print("calling set method to set value")
            self._somevalue = value
    
        def del_attr(self):
            print("calling delete method to delete value")
            del self._somevalue
    
        x = property(get_value, set_value, del_attr, "I'm the 'x' property.")
    
    obj = Some_Class()
    obj.x = 10
    print(obj.x)
    print(obj.x + 2)
    del obj.x
    print(obj.x)

      2)

    class Some_class(object):
        _x = None
        def __init__(self):
            self._x = None
    
        @property
        def x(self):
            print("calling get method to return value")
            return self._x
    
        @x.setter
        def x(self, value):
            print("calling set method to set value")
            self._x = value
    
        @x.deleter
        def x(self):
            print("calling delete method to delete value")
            del self._x
    
    obj = Some_class()
    obj.x = 10
    print(obj.x)
    del obj.x
    print(obj.x)

      property的优势可言简单概括为以下几点:

      1)代码简洁,可读性更强。这条优势是显而易见的,显然obj.x += 1比 obj.set_value(ojb.get_value()+1)要更简洁易读,而且对于编程人员来说还少敲了几次键盘。

      2)更好的管理属性的访问。property将对属性的访问直接转换为对对应的get / set等相关函数的调用,属性能够更好的被控制和管理。

        创建一个property实际上就是将其属性的访问与特定的函数关联起来,相对于标准属性的访问,其工作原理如图。property的作用相当于一个分发器,对某个属性的访问并不直接操作具体对象,而对标准属性的访问没有中间这一层,直接访问存储属性的对象。

       3)代码可维护性更好; 

      4)控制属性访问权限;提高数据安全性,如果用户想设置某个属性为只读:

    class PropertyTest(object):
        def __init__(self):
            self.__varl = 20
    
        @property
        def x(self):
            return self.__varl
    
    pt = PropertyTest()
    print(pt.x)
    pt.x = 12

      其实:使用property并不能真正完全达到属性只读的目的,正如以双下划线命令的变量并不是真正的私有变量一样,这些方法只是在直接修改属性这条道路上增加了一些障碍。如果用户想访问私有属性,同样能够实现,使用 pt._PropertyTest__varl = 30 来修改属性。

      property本质并不是函数,而是特殊类,既然是类的话,那么就可以被继承,因此用户便可以根据自己的需要定义property:

    def update_meta(self, other):
        self.__name__ = other.__name__
        self.__doc__ = other.__doc__
        self.__dict__.update(other.__dict__)
        return self
    
    class UserProperty(property):
        def __new__(cls, fget=None, fset=None, fdel=None, doc=None):
            if fget is not None:
                def __get__(obj, objtype=None, name=fget.__name__):
                    fget = getattr(obj, name)
                    print("fget name:" + fget.__name__)
                    return fget()
                fget = update_meta(__get__, fget)
    
            if fset is not None:
                def __set__(obj, value, name=fset.__name__):
                    fset = getattr(obj, name)
                    print("fset name:" + fset.__name__)
                    print("setting value:" + str(value))
                    return fset(value)
                fset = update_meta(__set__, fset)
    
            if fdel is not None:
                def __delete__(obj, name=fdel.__name__):
                    fdel = getattr(obj, name)
                    print("warning: you are deleting attribute using fdel.__name__")
                    return fdel()
                fdel = update_meta(__delete__, fdel)
            return property(fget, fset, fdel, doc)
    
    class C(object):
        def get(self):
            print("calling C.getx to get value")
            return self._x
    
        def set(self, x):
            print("calling C.setx to set value")
            self._x = x
    
        def delete(self):
            print("calling C.delx to delete value")
            del self._x
    
        x = UserProperty(get, set, delete)
    
    c = C()
    c.x = 1
    print(c.x)

      回到前面的问题:使用property并不能真正完全达到属性只读的目的,用户仍然可以绕过阻碍来修改变量,那么要真正实现只读属性怎么做呢:

    def ro_property(obj, name, value):
        setattr(obj.__class__, name, property(lambda obj: obj.__dict__["__" + name]))
        setattr(obj, "__" + name, value)
    
    class ROClass(object):
        def __init__(self, name, available):
            ro_property(self, "name", name)
            self.available = available
    
    a = ROClass("read only", True)
    print(a.name)
    a._ROClass__name = "modify"
    print(a.__dict__)
    print(ROClass.__dict__)
    print(a.name)

       a._ROClass__name = "modify" 创建了新的属性,这样就能很好的保护可读属性不被修改。

     建议62:掌握metaclass

      什么是元类(metaclass)?

        1)元类是关于类的类,是类的模板。

        2)元类是用来控制如何创建类的,正如类是创建对象的模板一样。

        3)元类的实例为类,正如类的实例为对象。

      type可以这样使用:

    type(类名, 父类的元组(针对继承的情况,可以为空), 包含属性的字典(名称和值))

      当类中设置了__metaclass__属性的时候,所有继承自该类的子类都将使用所设置的元类来指导类的生成。

      关于元类需要注意的几点:

      1)区别类方法与元方法(定义在元类中的方法)。元方法可以从元类或者类中调用,而不能从类的实例中调用;但是类方法可以从类中调用,也可以从类的实例中调用。

      2)多继承需要严格限制,否则会产生冲突

    建议63:熟悉python对象协议

      参考文章

    建议64:利用操作符重载实现中缀语法

      参考文章

     建议73:理解单元测试概念

      单元测试用来验证程序单元的正确性,一般由开发人员完成,是测试过程的第一个环节,以确保所写的代码符合软件需求和遵循开发目标。好的单元测试可以带来以下好处:

        1)减少了潜在bug,提高了代码质量; 

        2)大大缩减软件修复的成本; 

        3)为集成测试提供基本保障。

      有效的单元测试应该从以下几个方面考虑:

        1)测试先行,遵循单元测试步骤。典型的单元测试步骤如下:

          创建测试计划(Test Plan);

          编写测试用例,准备测试数据;

          编写测试脚本;

          编写被测代码,在代码完成之后执行测试脚本;

          修正代码缺陷,重新测试直到代码可以接受为止。

        2)遵循单元测试基本原则。常见的原则如下:

          一致性:意味着1000次执行和一次执行的结果应该是一样的,产生不确定执行结果的语句应该尽量避免;

          原子性:意味着单元测试的执行结果返回只有两种:True和False,不存在部分成功、部分失败的例子。如果发生这样的情况,往往是测试设计的不够合理; 

          单一职责:测试应该基于情景和行为,而不是方法。如果一个方法对应着多种行为,应该有多个测试用例;而一个行为即使对应多个方法也只能有一个测试用例

          隔离性:不能依赖于具体的环境设置,如数据库的访问、环境变量的设置、系统的时间等;也不能依赖于其他的测试用例以及测试执行的顺序,并且无条件逻辑依赖。单元测试所有的输入应该是确定的,方法的行为和结果应是可以预测的。

        3)使用单元测试框架。python常见的测试框架有PyUnit

    import unittest
    
    
    class MyCal(object):
        def add(self, a, b):
            return a + b
    
        def sub(self, a, b):
            return a - b
    
    
    class MyCalTest(unittest.TestCase):
        def setUp(self):
            print("running set up")
            self.mycal = MyCal()
    
        def tearDown(self):
            print("running teardown")
            self.mycal = None
    
        def testAdd(self):
            self.assertEqual(self.mycal.add(-1, 7), 6)
    
        def testSub(self):
            self.assertEqual(self.mycal.sub(10, 2), 8)
    
    suite = unittest.TestSuite()
    suite.addTest(MyCalTest("testAdd"))
    suite.addTest(MyCalTest("testSub"))
    runner = unittest.TextTestRunner()
    runner.run(suite)

    运行命令:

    python -m 包名 -v MyCalTest

    建议74:为包编写单元测试

      python自带的unittest模块,作为单元测试模块:

    import unittest
    import test.demo1 as a
    
    class TestCase(unittest.TestCase):
        def test_add(self):
            self.assertEqual(a.add(1, 1),2)
    
    if __name__ == "__main__":
        unittest.main()

      框架除了自动调用匹配以test开头的方法之外,unittest.TestCase还有模板方法setUp()和tearDown(),通过覆盖着两个方法,能够实现在测试之前执行一些准备工作,并在测试之后清理现场。

      unittest测试发现功能(test discover)

    python -m unittest discover

      unittest将递归地查找当前目录下匹配test*.py模式的文件,并将其中unittest.TestCase的所有子类都实例化,然后调用相应的测试方法进行测试,这就一举解决了“通过一条命令运行全部测试用例”的问题。

    建议75:利用测试驱动开发提高代码的可测性

      测试驱动开发(TDD)是敏捷开发中一个非常重要的理念,它提倡在真正开始编码之前测试先行,先编写测试代码,再在其基础上通过基本迭代完成编码,并不断完善。其目的是编写可用的干净的代码。所谓可用就是能够通过测试满足基本功能需求,而干净则要求代码设计良好、可读性强、没有冗余。在软件开发过程中引入TDD能带一系列好处,如改进的设计、可测性的增强、更松的耦合度、更强的质量信心、更低的缺陷修复代价等。

      测试驱动开发过程:

        1)编写部分测试用例,并运行测试。

        2)如果测试通过,则回到测试用例编写的步骤,继续添加新的测试用例。

        3)如果测试失败,则修改代码直到通过测试。

        4)当所有测试用例编写完成并通过测试之后,再来考虑对代码进行重构。

      假设开发人员要编写一个求输入数列的平均数的例子:

        1)完成基本的代码框架,便可以开始实施TDD的具体过程了:

    def avg(x):
        pass

        2)编写测试用例:

    import unittest
    from test.demo1 import avg
    
    
    class TestAvg(unittest.TestCase):
        def test_int(self):
            print("test average of integers:")
            self.assertEqual(avg([0, 1, 2]), 1)
    
        def test_float(self):
            print("test average of float:")
            self.assertEqual(avg([1.2, 2.5, 0.8]), 1.5)
    
        def test_empty(self):
            print("test empty input:")
            self.assertFalse(avg([]), False)
    
        def test_mix(self):
            print("test with mix input:")
            self.assertEqual(avg([-1, 3, 7]), 3)
    
        def test_invalid(self):
            print("test with invalid input:")
            self.assertRaises(TypeError, avg, [-1, 3, [1, 2, 3]])
    
    
    if __name__ == "__main__":
        unittest.main()

        3)运行测试用例,测试结果显示失败

        4)开始编码直到所有的测试用例通过,这是一个不停的重复迭代的过程,需要多次重复编码测试,直到上面的测试用例全部执行成功:

    def avg(*x):
        if len(*x) <= 0:
            print("you need inpuit at least one number")
            return False
        try:
            return sum(*x) / len(*x)
        except TypeError:
            raise TypeError("your inpuit is not value with unsupported type")

        5)重构,在测试用例通过之后,现在可以考虑一下重构的问题

      关于测试驱动开发和提高代码可测性方面有以下几点需要说明:

        (1)TDD只是手段而不是目的,因此在实践中尽量只验证正确的事情,并且每次仅仅验证一件事。当遇到问题时不要局限于TDD本身所涉及的一些概念,而应该回头想想采用TDD原本的出发点和目的是什么。

        (2)测试驱动开发本身就是一门学问,不要指望通过一个简单的例子就掌握其精髓。如果需要更深入的了解,推荐阅读相关书籍的同时在实践中不断提高对其的领悟。

        (3)代码的不可测性可以从以下几个方面考量:实践TDD困难;外部依赖太多;需要写很多模拟代码才能完成测试;职责太多导致功能模糊;内部状态过多且没有办法去操作和维护这些状态;函数没有明显返回或者参数过多;低内聚高耦合;等等。如果在测试过程中遇到这些问题,应该停下来想想有没有更好的设计。

    建议81:利用cProfile定位性能瓶颈

      profile是python的标准库。可以统计程序里每一个函数的运行时间,并且提供了多样化的报表,而cProfile则是他的C实现版本,剖析过程本身需要消耗的资源更少。所以在python3.0中,cProfile代替了profile,成为默认的性能剖析模块。使用cProfile来分析一个程序很简单:

    def foo():
        sum = 0
        for i in range(100):
            sum += i
            time.sleep(0.01)
        return sum
    
    if __name__ == "__main__":
        import cProfile
        cProfile.run("foo()")

      cProfile还可以直接用python解释器调用cProfile模块来剖析python程序

    python -m cProfile demo.py

      执行结果

       cProfile解决了我们对程序执行性能剖析的需求,但还有一个需求:以多种形式查看报表以便快速定位瓶颈。我们可以通过pstats模块的另一个类Stats来解决。Stas的构造函数接受一个参数——就是cProfile的输出文件名。Stats提供了对cProfile输出结果进行排序、输出控制等功能:

    if __name__ == "__main__":
        import cProfile
        cProfile.run("foo()", "prof.txt")
        import pstats
        p = pstats.Stats("prof.txt")
        p.sort_stats("time").print_stats()

      执行结果:

     建议84:掌握循环优化的基本技巧

      循环的优化应遵循的原则是尽量减少循环过程中的计算量,多重循环的情形下尽量将内层的计算提到上一层。

      1)减少循环内部的计算。

      2)将显示循环改为隐式循环。

    sun = 0
    for i in range(n+1):
        sum = sum + i

        可以写成:n*(n+1)/2

      3)在循环中尽量引用局部变量。

      4)关注内层嵌套循环。在多层嵌套循环中,重点关注内层嵌套循环,尽量将内层循环的计算往上层移。

      

    人生就是要不断折腾
  • 相关阅读:
    Unity3D之Character Controller(CC)与GameObject的碰撞方法
    Unity3D之资源问题处理
    Unity3D之多个fbx导入场景, 合并多个动画
    Unity3D性能优化--- 收集整理的一堆
    批量删除
    Unity3D插件之Easy Touch 3.1(1): Easy Joystick
    Unity3D与iOS消息交互方法(1)--iOS接收Unity3D发出的消息
    爬虫 requests 模块
    Flask 模型操作
    11.4 Flask session,闪现
  • 原文地址:https://www.cnblogs.com/xiangxiaolin/p/12881982.html
Copyright © 2011-2022 走看看