3

温故而知新--day2

 3 years ago
source link: http://www.cnblogs.com/lczmx/p/14289381.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

类与对象

类是一个抽象的概念,是指对现实生活中一类具有共同特征的事物的抽象。其实列化后称为对象。类里面由类属性组成,类属性可以分为数据属性和函数属性(函数属性又称为类方法)。举个例子,人类是一各抽象的概念这就相当于一个类,我们的一个个的人,就是人类这个类实例化后的对象,人可以有钱这个具体的变量,这可以称为数据属性,而钱的多少可以是由父辈继承过来的,也可以靠自己的奋斗组成;一些动作如吃饭,可以是为视为函数属性或者类方法。

数据属性

python中的类属性就是在类中定义的变量,它可以跟随这类为实例化后生成的对象提供数据。

class Foo:
    name = "foo"

f = Foo()       # 实例化

print(f.name)

不过类属性并不是不可被改变的,它可以通过赋值的方式改变。

class Foo:
    name = "foo"


f = Foo()       # 实例化

print(f.name)
f.name = "f2"

print(f.name)

对于某些情况,我们的每个对象赋予不同的属性,这就需要使用一个特殊的方法: __init__() ,这个函数可以视为构造函数。

class Foo:
    def __init__(self, name):
        self.name = name		# 把name设置为对象的属性


f = Foo("xxx")       # 实例化

print(f.name)

类方法

上面说过类方法实质上就是一个个函数,不过这些函数大多要把第一的参数值设为 self (这是一个约定俗成的规则,也可以用其他的名字),表示的是实例化后的对象本身。后面的参数可以和其他函数一样设置。

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

    def eat(self, food):
        print(f"{self.name}吃了{food}")


d = Dog("大黄")

d.eat("狗粮")

上面的例子中,我定义可一个类,里面由name属性和eat方法,实例化后,又调用了eat方法。需要注意的是,我们调用方法的时侯并没有 传入self参数,这是因为python在执行过程中会自动传入把对象本身传入到self中,使用 classmethod 装饰器时的cls也是一样的道理。

面向对象

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想。OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数。

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行。为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度。

而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递。

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类(Class)的概念。在学习面向对象这个编程范式的过程中,有三个名词我们必须了解其概念并熟悉如何运用,那就是封装、继承、多态。

封装

封装就类似于把类当作一个箱子,把数据属性和函数属性装在一起,在使用的这个箱子的过程中,我们只需要根据箱子外面开的洞,把东西从外面放进去或把从里面拿出来。也就是说封装的本质时为了区分内外。

实现封装的方法:

  1. 双下划线: __
    class Dog:
    	__name = "Dog"
    
    d = Dog()
    print(d.__name)   # AttributeError: 'Dog' object has no attribute '__name'
    print(d._Dog__name)
    这种方法,虽然可以不然使用者直接通过字段名访问,但是可以通过 ._类名__字段名 的方式访问,访问起来较为复杂。
  2. 单下划线: _
    class Dog:
    	_name = "Dog"
    
    
    d = Dog()
    print(d._name)  # Dog
    这是python程序员约定俗成的命名方式,表示此字段不允许外部访问。
    在实际开发中,我们并不建议将属性设置为私有的,因为这会导致子类无法访问。但假如直接暴露个用户的话,也不太合适,所以建议使用装饰器:
class Dog:
    _name = "Dog"
    _age = 5
    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):
        self._age = value


d = Dog()

print(d.name)   # Dog
# d.name = 123    # AttributeError: can't set attribute

print(d.age)    # 5
d.age += 1
print(d.age)    # 6

继承

顾名思义,继承是指在使用自己没有的属性时,可以寻找父类的属性,找得到就用,找不到就报错;举个例子,继承有点像现实中的继承财产,假如你没有钱可以使用从父辈那里继承过来的财产使用,但不同的是:假如你已经挣了钱,那么就不能直接从父辈中拿钱过来用了。因此,继承是需要父子关系的,其中一个父类可以有多个子类,而一个子类也可以有多个父类。

class Person:
    """人"""

    def __init__(self, name, age):
        self._name = name
        self._age = age

    @property
    def name(self):
        return self._name

    @property
    def age(self):
        return self._age


class Student:
    """学生"""

    def __init__(self, name, age, grade):
        super().__init__(name, age)
        self._grade = grade

    @property
    def grade(self):
        return self._grade

    @grade.setter
    def grade(self, grade):
        self._grade = grade

    def study(self, course):
        print(f"{self.name}正在学习{course}")


if __name__ == '__main__':
    stu = Student('王二狗', 18, '高三')
    print(stu.name, stu.age, stu.grade)
    stu.study('数学')

例子中的 super() 是子类调用父类的方法,其格式是:super().方法(参数)。

关于继承的顺序

假如一个类有多个父类,那么它是如何选择从哪个父类开始找的呢?

# 假如逐一注释name字段的话,就会出现注释后面的内容
class A:
    name = 'A'     # AttributeError: 'D' object has no attribute 'name'
    pass


class B(A):
    name = 'B'    # C
    pass


class C(A):
    name = 'C'      # A
    pass


class D(B, C):
    pass


d = D()

print(d.name)

上述例子,B和C继承A,D继承B和C。在Python中的继承顺序有两种:

  • 广度优先

    j67RJjy.png!mobile

python2 经典类是按深度优先来继承的,新式类是按广度优先来继承的。

python3 统一按广度优先来继承的。

另外python3不再区分经典类和新式类,即写的类不继承object也是新式类。

多态

子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写。通过方法重写我们可以让父类的同一个行为在子类中拥有不同的实现版本,当我们调用这个经过子类重写的方法时,不同的子类对象会表现出不同的行为,这个就是多态。比如猫和狗都属于生物,它们都有叫这个方法,但是他们叫之后的效果是不一样的,这种现象就类似于多态。

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

    def jiao(self):
        pass


class Cat(Animal):
    def __init__(self, name):
        super().__init__(name)

    def jiao(self):
        print("%s: 喵喵喵~" % self.name)


class Dog(Animal):
    def __init__(self, name):
        super().__init__(name)

    def jiao(self):
        print("%s: 汪汪汪~" % self.name)


c = Cat("小猫")
d = Dog("大黄")

c.jiao()
d.jiao()

类常用的双下划线方法

如果方法名前如果有两个下划线,则表示该成员是私有成员,私有成员只能由类内部调用。python的类中就有这些双下划线方法,我们在定义自己的类的同时,可以对其进行重写,以达到自己想要的效果。

__doc____name__

表示类的描述信息

class Foo:
    """this is the Foo class"""
    pass


print(Foo.__name__)     # Foo
print(Foo.__doc__)      # this is the Foo class

__module____class__

  • module 表示当前操作的对象在那个模块
  • class 表示当前操作的对象的类是什么
from collections import Counter

print(Counter.__module__)       # collections
print(Counter.__class__)        # <class 'type'>


class Foo:
    pass


f = Foo()
print(f.__module__)        # __main__       # 注意假如是导入的话,__main__就会变为模块名,下面一样
print(f.__class__)         # <class '__main__.Foo'>

__init__

构造函数,实例化时自动执行,接收对象,禁止返回任何值,更多见 __new__

class Foo:
    def __init__(self):
        print("running __init__")


f = Foo()       # running __init__

__del__

析构方法,当对象在内存中被释放时,自动触发执行。由解释器进性垃圾回收时自动触发,无需我们自定义。

典型的应用场景:

创建数据库类,用该类实例化出数据库链接对象,对象本身是存放于用户空间内存中,而链接则是由操作系统管理的,存放于内核空间内存中

当程序结束时,python只会回收自己的内存空间,即用户态内存,而操作系统的资源则没有被回收,这就需要我们定制__del__,在对象被删除前向操作系统发起关闭数据库链接的系统调用,回收资源

__call__

为对象加括号时触发。

class Foo:
    def __call__(self, *args, **kwargs):
        print("__call__")


f = Foo()
f()     # 调用__call__

__dict____slots__

  • dict 以字典的形式存储类或对象的所有成员
  • slots 在元组中存储对象的属性,可以大大减少内存, 该属性不可被继承
class Foo:
    name = 'foo'

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

    def run(self):
        pass


print(Foo.__dict__)
# {'__module__': '__main__', 'name': 'foo', '__init__': <function Foo.__init__ at 0x0000025C3A1FD310>,
# 'run': <function Foo.run at 0x0000025C3A1FD3A0>, '__dict__': <attribute '__dict__' of 'Foo' objects>,
# '__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__doc__': None}

print(Foo("test").__dict__)     # {'arg': 'test'}


class Bar:
    __slots__ = ("name", "value")

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

    def run(self):
        pass


b = Bar("lczmx", "test")
print(Bar.__slots__)    # 'name', 'value')
print(b.__slots__)      # ('name', 'value')
# b.age = 20      # AttributeError: 'Bar' object has no attribute 'age

虽然__slots__可以限制程序员新增对象属性,但是可以通过在__slots__中添加__dict__解决。可我们使用__slots__的主要目的是替换以散列表形式存储数据的字典,降低所需的内存,所以如何使用还看个人的选择。另外假如要把实例作为弱引用的目标,可以加入__weakref__属性。

__str____repr__

  • str 将python中的对象转换为字符串, 主要面向用户,其目的是可读性
  • repr 将python中的对象转换为字符串, 面向的是python解释器,或者说开发人员,其目的是准确性

使用顺序:

  • print 函数调用:
    • 没有说明用str或repr时,优先__str__
    • 指定为str没有__str__时,使用__repr__
    • 指定为repr没有__repr__时,返回对象内存地址
  • 命令行终端调用:
    • 有__repr__返回__repr__,无则返回对象内存地址
class Foo:
    def __str__(self):
        return "str foo"

    def __repr__(self):
        return "repr foo"


class Bar:
    def __repr__(self):
        return "repr bar"


class Test:
    def __str__(self):
        return "str test"


f = Foo()
b = Bar()
t = Test()

print(str(f))       # str foo
print(repr(f))      # repr foo

print(f)            # str foo
print(b)            # repr bar
print(str(b))       # repr bar
print(repr(t))      # <__main__.Test object at 0x0000020AD6859B80>

使用命令行运行, python -i cls.py (cls.py是代码所在文件)

>>> b
repr bar
>>> f
repr foo
>>> t
<__main__.Test object at 0x000001FA9F3E9B80>
>>>

__format__

可以自定义格式化,使用 format() 方法调用。

class Foo:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __format__(self, value: str):
        fmt_dict = {
            "n-a": "{name}-{age}",
            "n(a)": "{name}({age})",
            "N:A": "name: {name}, age: {age}",
        }
        fmt_string = fmt_dict.get("n-a")
        if fmt_dict.get(value):
            fmt_string = fmt_dict.get(value)

        res_fmt = fmt_string.format(name=self.name, age=self.age)

        return res_fmt


f = Foo("lczmx", 20)

print(format(f))                # lczmx-20
print(format(f, "n(a)"))        # lczmx(20)
print(format(f, "N:A"))         # name: lczmx, age: 20

__getattr____getattribute____setattr____delattr__

  • __getattr__ 访问属性(方法或字段)不存在时触发,即抛出AttributeError时触发
  • __getattribute__ 访问属性时,不管存不存在都触发,不存在时抛出AttributeError,所以其先于 __getattr__
  • __setattr__ 设置属性(增/改)时触发(可以放入__dict__)
  • __delattr__ 删除属性时触发

注意:重写这几个方法(包括下面的__xxxitem__方法),特别容易造成无限递归,注意这里操作的是 __dict__ ,而不是通过 . 的方式。

例子,注意如何操作__dict__,以及这几种方法什么时候执行。

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

    def __getattr__(self, key):
        print("getattr: 知道%s不存在了" % key)

    def __getattribute__(self, item):
        print("getattribute:访问了%s" % item)
        return super().__getattribute__(item)

    def __setattr__(self, key, value):
        print("setattr: 设置了%s=%s" % (key, value))
        # self.key=value                    # 无限递归
        self.__dict__[key] = value

    def __delattr__(self, key):
        if key in self.__dict__:
            print("delattr: 删除了%s" % key)

            # del self.key                  # 无限递归
            self.__dict__.pop(key, None)    # 或  del self.__dict__[key]
        else:
            print("delattr: %s不存在" % key)


f = Foo("lczmx")
# setattr: 设置了name=lczmx
# getattribute:访问了__dict__


f.name = "foo"
# setattr: 设置了name=foo     
# getattribute:访问了__dict__


print(f.name)
# getattribute:访问了name    
# foo


del f.name
getattribute:访问了__dict__
delattr: 删除了name
getattribute:访问了__dict__


f.name
# getattribute:访问了name
# getattr: 知道name不存在了

__getitem____setitem____delitem__

__getitem__
__setitem__
__delitem__

注:索引操作是指用中括号操作,用 . 操作调用的是__xxxattr__。这里操作的也是 __dict__

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

    def __getitem__(self, key):

        print("getitem: 获取%s" % key)
        return self.__dict__[key]

    def __setitem__(self, key, value):
        print("setitem: 设置了%s=%s" % (key, value))
        self.__dict__[key] = value

    def __delitem__(self, key):
        if key in self.__dict__:
            print("delitem: 删除了%s" % key)
            self.__dict__.pop(key, None)    # 或  del self.__dict__[key]
        else:
            print("delitem: %s不存在" % key)


f = Foo('foo')
f['age'] = 18				# setitem: 设置了age=18
f['age1'] = 19				# setitem: 设置了age1=19
del f['age1']				# delitem: 删除了age1
f['name'] = 'lczmx'			# setitem: 设置了name=lczmx
print(f.__dict__)			# {'name': 'lczmx', 'age': 18}

__iter____next__ :迭代器协议

迭代器协议:对象必须提供一个next方法,执行该方法要么返回下一项,要么抛出StopIteration异常,终止迭代器。

class Foo:
    def __init__(self, stop, start=0, seq=1):
        self.start = start
        self.stop = stop
        self.seq = 1 if int(seq) == 0 else int(abs(seq))  # 确保seq是大于0的整数

        self._reverse = False      # stop到start
        if start > stop:
            self._reverse = True

    def __next__(self):
        if (self.start < self.stop and not self._reverse) or \
                (self.start > self.stop and self._reverse):
            if self._reverse:       # stop -> start
                n = self.stop
                self.stop += self.seq
            else:                   # start -> stop
                n = self.start
                self.start += self.seq
            return n
        raise StopIteration

在命令行中运行:

>>> f = Foo(start=1, stop=6, seq=2)
>>> next(f)
1
>>> next(f)
3
>>> next(f)
5
>>> next(f)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "cls.py", line 21, in __next__
    raise StopIteration
StopIteration
>>>

for 循环可以调用 __iter__ 方法,将对象变为可迭代对象,同时for循环内部遇到 StopIteration 异常时,停止迭代。

为了支持for循环,我们可以为上面的类增加一个 __iter__ 方法:

class Foo:
    def __init__(self, stop, start=0, seq=1):
        self.start = start
        self.stop = stop
        self.seq = 1 if int(seq) == 0 else int(abs(seq))  # 确保seq是大于0的整数

        self._reverse = False      # stop到start
        if start > stop:
            self._reverse = True

    def __next__(self):
        if (self.start < self.stop and not self._reverse) or \
                (self.start > self.stop and self._reverse):
            if self._reverse:       # stop -> start
                n = self.stop
                self.stop += self.seq
            else:                   # start -> stop
                n = self.start
                self.start += self.seq
            return n
        raise StopIteration
    def __iter__(self):
        return self

关于 iter 函数:iter函数可以调用 __iter__ 方法,生成可迭代对象,且它有两个参数 iter(object[, sentinel])

  • 第一个参数是遵循可迭代协议或支持序列协议(有 __getitem__() 方法,且数字参数从 0 开始)的对象;
  • 第二个参数有值的话,object 必须是 可调用 的对象。这种情况下生成的迭代器,每次迭代调用它的 __next__() 方法时都会不带实参地调用 object;如果返回的结果是 sentinel 则触发 StopIteration ,否则返回调用结果(这种效果可以作为哨兵使用)。
class Foo:
    def __init__(self, stop, start=0, seq=1):
        self.start = start
        self.stop = stop
        self.seq = 1 if int(seq) == 0 else int(abs(seq))  # 确保seq是大于0的整数

        self._reverse = False      # stop到start
        if start > stop:
            self._reverse = True

    def __next__(self):
        if (self.start < self.stop and not self._reverse) or \
                (self.start > self.stop and self._reverse):
            if self._reverse:       # stop -> start
                n = self.stop
                self.stop += self.seq
            else:                   # start -> stop
                n = self.start
                self.start += self.seq
            return n
        raise StopIteration

    def __call__(self):
        return next(self)

    def __iter__(self):
        return self


if __name__ == '__main__':
    f = Foo(start=1, stop=10)
    i = iter(f, 3)
    print(next(i))	# 1
    print(next(i))	# 2
    print(next(i))
	
"""
Traceback (most recent call last):
  File "cls.py", line 35, in <module>
    print(next(i))
StopIteration
"""

__enter____exit__ :上下文管理协议

上下文管理协议,即with语句,为了让一个对象兼容with语句,必须在这个对象的类中声明 __enter____exit__ 方法。with语句开始时,上下文管理对象会调用 __enter__ 方法,with语句运行结束后,调用 __exit__ 方法。

优点是可以自动释放资源,在一些需要管理资源的场景(文件、网络连接、锁等)大有用处。

class Test:
    def __init__(self, data):
        self.data = data

    def __enter__(self):
        print(self.data)
        return self     # 要使用as 的话,注意返回self

    def __exit__(self, exc_type, exc_value, exc_tb):
        print(exc_type, exc_value, exc_tb)
        # exc_type  异常名
        # exc_value 异常值
        # exc_tb    异常追踪信息
        # 三个都没值时,为None

    def test_func(self, args):
        print(args)


if __name__ == '__main__':
    with Test("123") as t:
        t.test_func("test")

使用标准库实现上下文管理器:

@contextlib,contextmanager + yield :在被contextmanager装饰的生成器中,yield前的部分相当于 __enter__ ,之后的部分相当于 __exit__ 。例子:

from contextlib import contextmanager


def acquire_resource(*args, **kwargs):
    """处理数据的函数"""
    return "已经处理过的数据"


@contextmanager
def test(*args, **kwargs):
    try:		# 必须做异常处理
        # 处理数据的代码
        data = acquire_resource(*args, **kwargs)
        yield data
    finally:
        # 释放资源的代码
        # Code to release resource, e.g.:
        print("release resource")


if __name__ == "__main__":
    with test(12) as t:
        print(t)		# 已经处理过的数据
        #				# release resource

__new____metaclass__

**python一切皆对象。**所有的类也都是对象,那么类是有谁产生的呢?实际上python中的类是由 type 类实例化产生的,而为了避免无限溯源, type 类又是其本身的实例化。为了更好的理解,可以看看下面这幅图:

JjQvemq.png!mobile

type与object

两幅图都是正确的,左边强调str、type和LineItem是object的子类,右边强调str、object、LineItem是type的实例。

object和type的关系很特别:object是type的实例,type是object的子类。

回归到 __new____metaclass__ 上,前面在__init__里说过: __init__ 称为构造函数,实质上用于构造实例的方法是 __new____new__ 是一个经过特殊处理的类方法,不必使用classmethod,它必须返回一个实例,而这个实例会作为 __init__ 的第一个参数(self),所以 __init__ 实质上就是“初始化方法”,一般来说我们不需要重写 __new__ 方法,继承object的就已经够用了。

__metaclass__ 是用来表示该类由谁来实例化创建的。

class Bar:
   def __init__(self):
       super().__init__()

   def __new__(cls, *args, **kwargs):
       print("bar new running")
       return object.__new__(cls, *args, **kwargs)


class Foo:
   __metaclass__ = Bar

   def __new__(cls, *args, **kwargs):
       print("foo new running")
       return object.__new__(cls, *args, **kwargs)


b = Bar()       # bar new running
f = Foo()       # foo new running


print(f.__metaclass__)      # <class '__main__.Bar'>

反射

反射是程序可以访问、测试和修改其本身状态或行为的一种能力,利用反射可以实现可插拔机制,可以提前定义好接口,只有在接口实现后真正执行,提高协同开发的效率。

  1. hasattr(obj, name)
    判断是否可以调用,即可否调用 obj.name
  2. getattr(obj, name, default=None)
    访问某个属性,有则返回 obj.name ; 无就返回 default 的内容
  3. setattr(obj, key, val)
    等同 obj.key = val , 不过val参数可以使用lambda函数
  4. delattr(obj, key)
    等同于 del obj.key

例子:

class HomeWork:
    def __init__(self, course, complete=False):
        self.course = course

    def write(self, content):
        self.content = content
        self.wrong = "写错了"

    def __str__(self):
        """方便显示"""
        show_str = ""
        for k, v in self.__dict__.items():
            show_str += "%s = %s\n" % (k, v)
        return show_str


if __name__ == '__main__':
    h = HomeWork("语文")

    # hasattr
    if hasattr(h, "write"):
        # getattr
        func = getattr(h, "write")
        func("鹅鹅鹅,曲项向天歌!")
    # setattr
    setattr(h, "complate", True)
    # delattr
    delattr(h, "wrong")

    print(h)
    # course = 语文
    # content = 鹅鹅鹅,曲项向天歌!
    # complate = True

装饰器

在我们日常开发的过程中,往往不能做到面面俱到或由于当时业务不需要,所以后期需要用到某些功能时可能要修改某些代码,但是直接写个原函数可能造成不可挽回的损失,为了解决这种情况,我们可以使用装饰器来维护代码。除此之外,装饰器经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,它 是解决这类问题的绝佳设计。装饰器允许向一个现有的对象添加新的功能,同时又不改变其结构。装饰器本质上就是一个函数,其相对于高阶函数+函数嵌套+闭包的组合体,三者合一成为装饰器。

一些基本概念

想要使用装饰器首先要了解一些基本的概念。

  • 语法糖@

    在python中

    @dec
    def test():
    	pass

相对于: test = dec(test)

  • 装饰器会在被装饰函数定义完之后立即执行
    def dec(func):
    	print("dec running")        # dec running
    
    
    @dec
    def test():
    	pass

可以看到,我并没有调用test函数,dec就执行了,所以在导入模块时需要注意装饰器是否允许被执行。

  • 闭包

    闭包是延伸了作用域的函数,可以访问定义体之外定义的非全局变量。其主要的形式就是用一个函数嵌套另一个函数,以达到作用域延伸的效果。

    def make_avg():
    	"""计算平均数"""
    	num_list = []
    
    	def avg(new_value):
    		num_list.append(new_value)
    		return sum(num_list)/len(num_list)
    	return avg
    
    
    a = make_avg()
    print(a(10))        # 10
    print(a(20))        # 15
    print(a(30))        # 20
uAjUvuZ.png!mobile

闭包示意图

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,在调用函数时,虽定义作用域不可用,但仍然可以使用这些绑定。

简单列子

前面说过,装饰器实质上就是高阶函数+函数嵌套+闭包的组合体,所以其基本的格式如下:

def timmer(func):

    def wrapper(*args, **kwargs):
        # 代码      # 此时可以修改传入的参数*args和, **kwargs
        res = func(*args, **kwargs)     # 运行原函数
        # 代码      # 可以修改原函数的返回值res

        return res      # 假如有返回的话,别忘记返回了
    return wrapper

为了更加便于理解,下面举一个例子,用一个装饰器计算函数的运行时间:

from time import time


def clock(func):
    def clocked(*args, **kwargs):
        start = time()
        res = func(*args, **kwargs)
        end = time()
        func_name = func.__name__
        print('{0}({1}) 执行了[{2:.8f}s]'.format(
            func_name, ",".join(repr(i) for i in args), end-start))
        return res
    return clocked


@clock
def factorial(num):
    """计算阶乘"""
    return 1 if num < 2 else num * factorial(num - 1)


if __name__ == '__main__':
    n = 6
    result = factorial(n)
    print("%d! = %d" % (n, result))

需要注意的是,在日常开发过程中,我们应该把装饰器放在其他的文件中,在使用时可以导入到本模块中使用。

使用标准库实现

上面计算函数运行时间这个例子有一个缺点:原函数的 __name____doc__ 属性被覆盖了,为了解决这个问题,我们可以引入 functools.wraps 把原函数相关属性复制到装饰器中。

from functools import wraps
from time import time


def clock(func):
    """计算函数运行时间"""
    @wraps(func)
    def clocked(*args, **kwargs):
        start = time()
        res = func(*args, **kwargs)
        end = time()
        func_name = func.__name__
        print('{0}({1}) 执行了[{2:.8f}s]'.format(
            func_name, ",".join(repr(i) for i in args), end-start))
        return res
    return clocked


@clock
def factorial(num):
    """计算阶乘"""
    return 1 if num < 2 else num * factorial(num - 1)


if __name__ == '__main__':
    n = 6
    result = factorial(n)
    print(factorial.__name__)    # factorial   # 之前:clocked
    print(factorial.__doc__)     # 计算阶乘     # 之前:None
    print("%d! = %d" % (n, result))

如果想查看更多关于 functools 库的内容,可以查看官方文档 functools模块

一些常用的内置装饰器

  1. property
    静态属性。可以使方法在调用时不需要加括号就可以直接使用。被装饰后不能被赋值,除非使用 xx.setter 再装饰一遍这个同名方法。
    class Foo:
     	def __init__(self, name, age):
     		self._name = name
     		self._age = age
    
     	@property
     	def name(self):
     		return self._name
    
     	@property
     	def age(self):
     		return self._age
    
     	@age.setter
     	def age(self, value):
     		self._age = value
    
    
     f = Foo("lczmx", 18)
     print(f.name)
    
     # f.name = "xxx"      # AttributeError: can't set attribut
     f.age = 20
  2. classmethod
    类方法。可以直接通过 类名.方法名() 调用,不需要实例化,也不需要创cls参数。
    class Foo:
     	@classmethod
     	def get_name(cls, data: dict):
     		return data.get('name')
    
     data = {'name': 'test'}
     print(Foo.get_name(data))
  3. staticmethod
    静态方法。类似于普通的函数,第一个参数并不是self或cls,调用时可以由类调用,也可以由对象调用。
    class Foo:
     	@staticmethod
     	def get_name(data: dict):
     		return data.get('name')
    
     data = {'name': 'test'}
     print(Foo.get_name(data))
    
     f = Foo()
     print(f.get_name(data))
  4. functools.lru_cache
    一个为函数提供缓存功能的装饰器,缓存 maxsize 组传入参数,在下次以相同参数调用时直接返回上一次的结果。用以节约高开销或I/O函数的调用时间。
    from functools import wraps
    from time import time
    
    
    def clock(func):
    	"""计算函数运行时间"""
    	@wraps(func)
    	def clocked(*args, **kwargs):
    		start = time()
    		res = func(*args, **kwargs)
    		end = time()
    		func_name = func.__name__
    		print('{0}({1}) 执行了[{2:.8f}s]'.format(
    			func_name, ",".join(repr(i) for i in args), end-start))
    		return res
    	return clocked
    
    
    @clock
    def fib(num):
    	"""计算斐波那契数列"""
    	return num if num < 2 else (fib(num - 2) + fib(num - 1))
    
    
    print(fib(5))
    结果
    fib(1) 执行了[0.00000000s] 
    fib(0) 执行了[0.00000000s]
    fib(1) 执行了[0.00000000s]
    fib(2) 执行了[0.00099683s]
    fib(3) 执行了[0.00299120s]
    fib(0) 执行了[0.00000000s]
    fib(1) 执行了[0.00000000s]
    fib(2) 执行了[0.00101113s]
    fib(1) 执行了[0.00000000s]
    fib(0) 执行了[0.00000000s]
    fib(1) 执行了[0.00000000s]
    fib(2) 执行了[0.02294302s]
    fib(3) 执行了[0.02392578s]
    fib(4) 执行了[0.02593160s]
    fib(5) 执行了[0.02892280s]
    5
    使用lru_cache后:
    from functools import wraps, lru_cache
    from time import time
    
    
    def clock(func):
    	"""计算函数运行时间"""
    	@wraps(func)
    	def clocked(*args, **kwargs):
    		start = time()
    		res = func(*args, **kwargs)
    		end = time()
    		func_name = func.__name__
    		print('{0}({1}) 执行了[{2:.8f}s]'.format(
    			func_name, ",".join(repr(i) for i in args), end-start))
    		return res
    	return clocked
    
    @lru_cache()
    @clock
    def fib(num):
    	"""计算斐波那契数列"""
    	return num if num < 2 else (fib(num - 2) + fib(num - 1))
    
    
    print(fib(5))
    结果
    fib(1) 执行了[0.00000000s]
    fib(0) 执行了[0.00000000s]
    fib(2) 执行了[0.00000000s]
    fib(3) 执行了[0.00000000s]
    fib(4) 执行了[0.00000000s]
    fib(5) 执行了[0.00098634s]
    5
    可以发现,使用可lru_cache可以大大减少对于某些重复计算,极大地优化性能,其除了可以在递归算法中使用,也可以在web获取信息的应用中发挥作用。
    lru_cache 有两个参数可以配置 lru_cache(maxsize=128, type=False) ,所以使用lru_cache时需要用像调用函数一样使用。
    • maxsize 指定能存储多少个调用的结果
    • typed 为True时,把不同类型的结果分开保存,如把1和1.0区分开来。
  5. functools.singledispatch
    由于python没有函数重载的概念,所以假如有 用一个函数处理不同的事情的情况就很难处理,最简单的方法是使用大量的if-elif-else进性判断。但是,这样做会造成函数的越写越臃肿,而且耦合度高。为此经过深思熟虑python3.4最终把singledispatch加入了标准库,使解决这类问题可以使用模块化的方法处理。
    使用@singledispatch可以根据第一个参数的类型,以不同的方式处理数据,使原本的函数变为泛函数。
    from functools import singledispatch
    from numbers import Integral
    from collections import abc
    
    
    @singledispatch
    def print_typed(data):
    	"""处理object类型的基函数"""
    	print("object (%s):" % type(data), data)
    
    
    @print_typed.register(str)
    def _(s):
    	print("字符串:%s" % s)
    
    
    @print_typed.register(Integral)     # Integral是int的虚拟超类
    def _(num):
    	print("数字: %d" % num)
    
    
    # 可以放多个
    @print_typed.register(tuple)
    @print_typed.register(abc.MutableSequence)      # list等
    def _(seq):
    	print("seq: ", seq)
    
    
    if __name__ == '__main__':
    	print_typed("123")      # 字符串:123
    	print_typed([1, 2, 3])  # seq:  [1, 2, 3]
    	print_typed({"name": "lczmx"})
    	# object (<class 'dict'>): {'name': 'lczmx'}

参数化装饰器

functools.lru_cache 可以看出,装饰器是可以加参数的,其具体的本质就是一个函数包裹着一个装饰器,如

@dec(ars=123)
def test()

等同于 test = dec(arg=123)(test) ,下面举个例子,优化之前计算函数运行时间的装饰器,为其加上一个参数,可以自定义格式化输出:

from time import time


def clock(fmt='{func_name}({args_str}) 执行了[{elapsed}]s'):
    def decorated(func):
        def clocked(*args, **kwargs):
            start = time()
            res = func(*args, **kwargs)
            end = time()
            func_name = func.__name__
            args_str = ",".join(repr(i) for i in args)
            elapsed = end - start       # 所用时间
            print(fmt.format(**locals()))	# 引用clocked中的全部局部变量
            return res
        return clocked
    return decorated


@clock()
def fib(num):
    """计算斐波那契数列"""
    return num if num < 2 else (fib(num - 2) + fib(num - 1))


@clock(fmt='{args_str:10} {elapsed:.8f}s  ')
def factorial(num):
    """计算阶乘"""
    return 1 if num < 2 else num * factorial(num - 1)


print("fib(2) =", fib(2))
print("factorial(5) =", factorial(5))

结果:

fib(0) 执行了[0.0]s
fib(1) 执行了[0.0]s
fib(2) 执行了[0.0049860477447509766]s
fib(2) = 1
1          0.00000000s
2          0.00000000s
3          0.00000000s
4          0.00099707s
5          0.00199723s
factorial(5) = 120

描述符

描述符是对多个属性运用相同存取逻辑的一种方式,是实现了特定协议的类,这个协议包括: __get____set____delete__ ,property就实现了这三个,在实际开发过程中,一般只需要实现部分即可。

根据实现的方法可以把描述符分为数据描述符和非数据描述符:

  • 数据描述符:至少实现 __set__
  • 非数据描述符:没有实现 __set__

描述符的用法是: 创建一个实例,作为另一个类的属性。

下面展示 __get____set__ 的一般用法,以及对应的参数设置和怎么在其他的类中使用。

class Viladate:

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

    def __get__(self, instance, owner):
        print("get: instances: %s, owner: %s" % (instance, owner))
        # get: instances: <__main__.Item object at 0x00000146F4F354C0>, owner: <class '__main__.Item'>


        if instance is None:
            return self
        return instance.__dict__.get(self.storage_name)

    def __set__(self, instance, value):
        print("set instance: %s, value: %s" % (instance, value))
        # set instance: <__main__.Item object at 0x0000022095E054C0>, value: 3

        
        if value <= 0:
            raise ValueError("value必须大于0")
        instance.__dict__[self.storage_name] = value


class Item:
    price = Viladate("price")
    weight = Viladate("weight")

    def __init__(self, price, weight):
        self.price = price
        self.weight = weight

    def total(self):
        return self.price * self.weight


if __name__ == '__main__':
    i = Item(2.99, 3)
    print(i.price)
    i.weight = 50
    print(i.total())

例子中有几个要点:1. 操作的是托管类(Item)的对象的__dict__;2. 托管类中两次定义属性,init中的实例属性在实例化时会委托给描述符,所以类属性不会被替换掉。

注意事项:

一 描述符本身应该定义成新式类,被代理的类也应该是新式类

二 必须把描述符定义成这个类的类属性,不能为定义到构造函数中

三 要严格遵循该优先级,优先级由高到底分别是

  1. 非数据描述符
  2. 找不到的属性触发__getattr__()

描述符与类装饰器结合

上面这个例子可以进一步改进,把 price = Viladate("price") 换为 price = Viladate() ,即动态生成字段名。要实现这个功能,需要用到类装饰器,它和函数装饰器差不多,差别就是把参数的func换成类,返回值是原来的类或者新类。

为了更加贴近真实场景,这次分别用不同的文件定义

./model.py :

# model.py
class Viladate:
    storage_name = None     # 作为在托管对象的__dict__中的key

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.storage_name)     # 可以用getattr()

    def __set__(self, instance, value):
        if value <= 0:
            raise ValueError("value必须大于0")
        instance.__dict__[self.storage_name] = value        # 可以用setattr()


def entity(cls):
	"""类装饰器"""
    for k, v in cls.__dict__.items():
        if isinstance(v, Viladate):
            class_name = type(v).__name__

            v.storage_name = f"_{class_name}#{k}"
            # 格式形如: _Viladata#price
            # 加'#'是防止在操作 被托管类对象时被替换
			# '#'是可以被__dict__或setattr()/getattr()识别的
    return cls

./main.py :

# main.py
import model


@model.entity
class Item:
    price = model.Viladate()		# 像Django的ORM了吧
    weight = model.Viladate()

    def __init__(self, price, weight):
        self.price = price
        self.weight = weight

    def total(self):
        return self.price * self.weight


if __name__ == '__main__':
    i = Item(2.99, 3)
    print(i.price)
    i.weight = 50
    # i.price = -100		# 会报错
    print(i.total())

描述符用法总结:

  1. 要设置只读属性可以使用 property
  2. 用于验证的描述符可以只有 __set__
  3. 仅有 __get__ 的描述符可以实现高效缓存
  4. 没有 __set__ 的描述符可以被覆盖

参考资料:

  1. 《Fluent Python》
  2. 面向对象进阶

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK