Python语法特点总结

1. Python对象模型
  • Python中一切皆为对象
  • 对象拥有三个特性:id类型
  • 把数据与功能绑定在一起。创建新类就是创建新的对象类型,从而创建该类型的新实例
类型对象与实例化对象
  • Python中的类型也是一种对象 ,称为类型对象
>>> int
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
  • type对象是所有类型对象的元类。
  • 通过类型对象实例化的对象称为实例化对象:
>>> int('1')
1
>>> type(1)
<class 'int'>
  • object对象是所有对象的基类:
>>> issubclass(int,object)
True
>>> type(object)
<class 'type'>
>>> issubclass(type, object)
True
函数对象
  • Python中函数也是对象,称为函数对象
def func(x, y):
    return x + y

print(type(func))

>>>
<class 'function'>
  • 我们也可以使用lambda定义函数:
func = lambda x,y: x + y
print func(3,4)
  • 函数可以作为一个对象,进行参数传递:
def print_func_output(f, x, y):
    print(f(x, y))

func = lambda x,y: x + y
f = func
print_func_output(f, 1, 2)

>>>
3
  • Python中所提供的map(), filter(), reduce()等函数的第一个参数都是一个函数对象:
# map()将第一个参数中的函数应用于列表的每一个元素,返回一个迭代器
[2, 4, 6]
>>> list(map((lambda x: x * 2), [1, 2, 3]))

# filter()将第一个参数中的函数应用于列表的每一个元素,过滤掉函数结果为False的元素,返回一个迭代器
>>> list(filter((lambda x: x % 2 == 0), [1, 2, 3, 4, 5])) 
[2, 4] 

# reduce累进地将第一个参数中的函数作用于列表的每一个元素。
>>> from functools import reduce
>>> reduce((lambda x, y: x + y), [1, 2, 3])
6
2. 类型对象与实例对象
类型对象
  • 每个自定义类型对象都有一个属性空间,信息存储于 dict 中:
class Person:
    num = 0

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

    def introduce(self):
        print('My name is ' + self.name)

    @classmethod
    def get_num(cls):
        return cls.num

    @staticmethod
    def say_hello():
        print('Hello')


for k, v in Person.__dict__.items():
    print(f'{k}:{v}')

>>>  
__module__:__main__
num:0
__init__:<function Person.__init__ at 0x000001EE4E9C12F0>
introduce:<function Person.introduce at 0x000001EE4E9C1378>
get_num:<classmethod object at 0x000001EE4E9BE9B0>
say_hello:<staticmethod object at 0x000001EE4E9BEC50>
__dict__:<attribute '__dict__' of 'Person' objects>
__weakref__:<attribute '__weakref__' of 'Person' objects>
__doc__:None
  • 由此可见, 类的属性空间中存放了类变量,静态方法,类方法,实例方法和一些内置属性。这些方法都是以一个普通的函数对象方式保存在类的属性空间中。

  • 静态方法其实就是普通的函数,只是在类的定义体中,而不是模块层定义。它的第一个参数不是特殊的值,不能访问实例变量,可以通过类名访问类变量。可以通过类名的方式调用静态方法:

Person.say_hello()

>>> 
Hello
  • 类方法只能访问类变量,不能访问实例变量。它的第一个参数是类本身。但在调用类方法时不需要为该参数传递值:
print(Person.get_num())

>>> 
0
  • 实例方法可以访问实例变量,它的第一个参数是当前对象。我们可以通过对象名调用实例方法,也可以通过类来调用,但需要显式传递 self 参数:
p = Person('Alice', '20')
Person.introduce(p)
p.introduce()


>>>  
My name is Alice
My name is Alice
实例对象
  • 实例对象的属性信息也存储于 dict 中:
p = Person('Alice', '20')
for k, v in p.__dict__.items():
    print(f'{k}:{v}')
    
>>> 
name:Alice
age:20
  • 通过实例对象访问属性时,默认先在实例对象的属性空间字典中查找;再到类型对象的属性空间查找:
class Person:
    name = 'person'
    num = 0

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

p = Person('Alice')
print(p.name)
print(p.num)

>>> 
Alice
0
属性和方法可见度
  • Python的属性/方法可见度只有public和private两种。
  • 在定义属性或方法时,在属性/方法名前面加了2个下划线'__'。则表明该属性/方法是私有属性/方法。不能在对象上通过 . 操作符直接访问:
class Person:
    def __init__(self, name, age):
        self.name = name
        self.__age = age
        
    def __get_age():
        return self.__age()

p = Person('Alice', '20')
p.__age
p.__get_age()

>>>
AttributeError: 'Person' object has no attribute '__age'
AttributeError: 'Person' object has no attribute '__get_age'
  • 但子类之所以无法直接访问私有属性/方法,是因为私有属性/方法名被更换成了 _类名__属性名,与我们所访问的属性/方法名不同所致。Python并未从语法上严格保证其私有性:
print(p.__dict__)
print(p._Person__age)
print(p._Person__get_age())

>>>
{'name': 'Alice', '_Person__age': '20'}
20
20
  • 因此Python中也可以用单下划线_开头的字段,通过约定表示不希望类的用户直接访问该属性。
@property和setter方法
  • 通过@property和setter方法可以在对属性的访问和设置时设置自定义的行为:
class Person:
    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height

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

    @age.setter
    def age(self, value):
        if value > 0:
            self.__age = value
        else:
            raise ValueError('Age must > 0')

p = Person('Alice', 20, 160)
print(p.age)
p.age = -10

>>>
Querying age
20
ValueError: Age must > 0
  • @property和setter方法的一个缺点是不能复用,例如我们想对height属性实现同样的方法,则需要再创建两个方法。描述符可以解决这个问题。
描述符
  • 描述符是实现了描述符协议的类。描述符协议方法有:
__get__(),在设置属性时调用该方法;
__set__() ,在读取属性时调用该方法;
__delete__() ,在删除属性时调用该方法;
  • 类里实现了上述其中一个方法,就称该类为描述符。使用描述符能够对多个属性运用相同的自定义存取逻辑:
class PositiveNumber:  # 描述符类

    def __init__(self, attr_name):
        self.attr_name = attr_name  # attr_name为托管实例中存储值的属性的名称。

    def __set__(self, instance, value):  # instance是托管实例,self是描述符实例。
        if value > 0:
            instance.__dict__[self.attr_name] = value  # 此处必须直接存入__dict__,否则会调用setattr函数会导致无限递归。
        else:
            raise ValueError(f'{self.attr_name} must > 0')

    def __get__(self, instance, owner):  # instance是托管实例,owner是托管类
        print(f'Querying {self.attr_name}')
        return instance.__dict__[self.attr_name]


class Person:  # 托管类
    age = PositiveNumber('age')
    height = PositiveNumber('height')

    def __init__(self, name, age, height):
        self.name = name
        self.age = age
        self.height = height


p = Person('Alice', 20, 160)
print(p.age)
p.height = -10

>>>
Querying age
20
ValueError: age must > 0
  • 如果一个类同时定义了__get__方法和__set__方法,则称之为数据描述符,如果只有__get__方法,则称之为非数据描述符
  • 每次使用类名.属性名,或者 getattr(类名,属性名)的调用方式访问属性时,属性查找优先级顺序为:数据描述符 > 实例的__dict__ > 非数据描述符 > 类的__dict__
__getattribut____setattr____getattr__,
  • 程序每次访问属性时都会调用__getattribut__。每次设置属性时都会调用__setattr__
  • 如果类定义了__getattr__,在查找不到该属性时,将会调用这个方法:
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattr__(self, item):
        print(f'Person do not have field {item}')

p = Person('Alice', 20)
p.height

>>>
Person do not have attr height
继承
  • 加入继承关系后的完整属性查找优先级顺序为:

数据描述符 > 实例的__dict__ > 非数据描述符 > 类的__dict__ > 父类 - >父类的父类 ->Object->调用类的__getattr__->若仍不存在,会引发一个 AttributeError 异常

  • 一个问题
class Parent(object):
 x = 1
 
class Child1(Parent):
 pass
 
class Child2(Parent):
 pass
 
print Parent.x, Child1.x, Child2.x
Child1.x = 2
print Parent.x, Child1.x, Child2.x
Parent.x = 3
print Parent.x, Child1.x, Child2.x

>>>
1 1 1
1 2 1
3 2 3

这个答案的关键是,在 Python 中,类变量在内部是作为字典处理的。如果一个变量的名字没有在当前类的字典中发现,将搜索祖先类(比如父类)直到被引用的变量名被找到。

因此,在父类中设置 x = 1 会使得类变量 X 在引用该类和其任何子类中的值为 1。这就是因为第一个 print 语句的输出是1 1 1。

随后,如果任何它的子类重写了该值(例如,我们执行语句 Child1.x = 2),然后,该值仅仅在子类中被改变。这就是为什么第二个 print 语句的输出是 1 2 1

最后,如果该值在父类中被改变(例如,我们执行语句 Parent.x = 3),这个改变会影响到任何未重写该值的子类当中的值(在这个示例中被影响的子类是 Child2)。这就是为什么第三个 print 输出是 3 2 3。

3. 函数:作用域,闭包,装饰器
作用域
  • Python作用域分为:
  1. local,局部作用域,即当前函数作用域
  2. enclosing,嵌套的父级函数的局部作用域,即包含此函数的上级函数的局部作用域
  3. global,全局作用域,即代码所在模块的作用域
  4. built-in,内置作用域,系统固定模块的作用域
  • 在表达式中引用变量时,Python解释器会按照上述顺序遍历各作用域以解释该引用,如果都没有定义过名称相符的变量,将抛出NameError异常。
闭包
  • 闭包是一种定义在某个作用域中的函数,这种函数引用了那个作用域里面的变量:
def func_outer(prefix):
    def func_inner(name):
        return prefix + ' ' + name
    return func_inner

func_inner = func_outer('Hello')
print(func_inner('A'))

>>>
Hello A
  • nonlocal关键字可以让相关变量赋值时从local作用域延伸到enclosing作用域中查找该变量:
def func_outer(prefix):
    count = 0
    def func_inner(name):
        nonlocal count
        count += 1
        return '{} {}, id {}'.format(prefix, name, count)
    return func_inner

func_inner = func_outer('Hello')
print(func_inner('A'))
print(func_inner('B'))

>>>
Hello A, id 1
Hello B, id 2
  • global关键字 可以让相关变量赋值时从enclosing作用域延伸到global作用域中查找该变量:
a = 1
def func():
    global a
    a = 2

func()
print(a)

>>>
2
装饰器
  • 装饰器一般用来修饰函数。能够在执行受到封装的原函数执行之前和执行完毕后运行一些附加代码,实现公共功能,达到代码复用的目的:
# 定义一个装饰器
def trace(func):
    def wrapper(*args, **kwargs):
        res = func(*args, **kwargs)
        print(f'function: {func.__name__}, parameter: {args},{kwargs}, result: {res}')
        return res
    return wrapper
    
# 使用@修饰函数,其效果等于以该函数为参数调用修饰器,
# 再把修饰器所返回的结果赋给同一个作用域中与原函数同名的变量,
# 即: fibonacci = trace(fibonacci)
@trace
def fibonacci(n):
    if n in (0, 1):
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)

>>>    
function: fibonacci, parameter: (1,),{}, result: 1
function: fibonacci, parameter: (0,),{}, result: 0
function: fibonacci, parameter: (1,),{}, result: 1
function: fibonacci, parameter: (2,),{}, result: 1
function: fibonacci, parameter: (3,),{}, result: 2
4. 列表推导式,生成器,迭代器
列表推导式
  • 列表推导式用于生成一个list
>>> ['x' for n in range(5)]
['x', 'x', 'x', 'x', 'x']

>>> a=[1,2,3,4,5]
>>> [x for x in a if x % 2 == 0]
[2, 4]
  • 列表推导式在推导过程中,对于输入序列中的每个值可能都要创建仅含一项元素的全新列表。输入数据非常多时,会消耗大量内存。
生成器,迭代器
  • 将列表推导式所用的方括号变成圆括号,就构成了生成器。它们区别在于,生成器表达式运行时不会把整个输出序列呈现出来,而是返回一个迭代器,这个迭代器每次根据生成器表达式产生一项数据。
>>> a=[1,2,3,4,5]
>>> it = (x for x in a if x % 2 == 0)
>>> next(it)
2
>>> next(it)
4
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
  • enumerate可用将各种迭代器(也包括序列和支持迭代的对象)包装为生成器:
a = [1, 2, 3, 4, 5]
it = (x for x in a if x % 2 == 0)
for index, value in enumerate(it):
    print('{}:{}'.format(index, value))

for index, value in enumerate(it):
    print('{}:{}'.format(index, value))
    
>>>
0:2
1:4
  • 需注意,迭代器只能产生一轮结果,继续执行第二轮将不会输出,也不会报错
  • zip函数可用于同时把两个或两个以上的迭代器封装为生成器,将值汇聚成元组(tuple)。当其中一个遍历结束时,zip将不再产生元组:
a = [1, 2, 3, 4, 5]
it1 = (x for x in a if x % 2 == 0)
it2 = (x for x in a if x % 2 != 0)
for v1, v2 in zip(it1, it2):
    print('{},{}'.format(v1, v2))

>>>
2,1
4,3
  • 使用yield关键字的函数也称为生成器。每次在返回的迭代器上调用next函数时,迭代器会把生成器推进到下一个yield关键字处:
def func():
    for i in range(1000):
        yield [i] * 1000

gen = func()
for item in gen:
    print(item)
5. 并发与并行

进程,线程,协程与python的实现

6. 模块加载
  • import关键字用于导入模块
  • 引入模块时,Python会按照深度优先的顺序执行下列操作:

1)在由sys.path所指定的路径中,搜寻待引入的模块。

2)从模块中加载代码,并保证这段代码能够正确编译。

3)创建与该模块对应的空对象。

4)把这个空的模块对象,添加到sys.modules里

5)运行模块对象中的代码,以定义其内容。

  • 需注意循环依赖的问题。解决方法包括:1)调整引入顺序 2)动态引入,即在函数或方法内部使用import,会等到真正运行相关代码时,才触发模块的引入操作。但多次调用会带来更多的import开销
7. 内存管理
引用计数
  • Python使用引用计数 ,为每个对象维护引用次数,并据此回收不再需要的垃圾对象。当引用次数变为 0 时就将资源释放:
  • sys.getrefcount可输出变量的引用计数。
>>> import sys
>>> s = '123'
>>> sys.getrefcount(s)
2
>>> n1 = s
>>> sys.getrefcount(s)
3
>>> l = [s]
>>> sys.getrefcount(s)
4
>>> del n1
>>> sys.getrefcount(s)
3
>>> del l
>>> sys.getrefcount(s)
2
  • 当一个对象作为参数传个函数后,它的引用计数将加一;当函数返回,局部名字空间销毁后,对象引用计数又减一。
标记清除法
  • Python还采用 标记清除法 来回收存在循环引用的垃圾对象。
  • 将程序内部对象跟踪起来,是实现垃圾回收的第一步。一个对象是否需要跟踪,取决于它会不会形成循环引用。按照引用特征,Python 对象可以分为两类:

1)内向型对象 ,例如 int 、float 、 str 等,这类对象不会引用其他对象,因此无法形成循环引用,无须跟踪;

2)外向型对象 ,例如 tuple 、 list 、 dict 等容器对象,以及函数、类实例等复杂对象,这类对象一般都会引用其他对象,存在形成循环引用的风险,因此是垃圾回收算法关注的重点;

  • Python 为外向型对象分配内存时,对象头部之前预留了一些内存空间,以便垃圾回收模块用链表将它们跟踪起来。

  • 标记清除法首先找出 根对象 ( root object )集合。所谓根对象,就是指被全局引用或者在栈中引用的对象,这部对象是不能被删除的。根对象集合不难确定:我们只需遍历每个对象引用的对象,将它们的引用计数减一,最后计数不为零的就是根对象。

  • 根对象本身是 可达的 ( reachable ),不能删除;被根对象引用的对象也是可达的,同样不能删除;以此类推。沿着引用关系遍历,遍历到的所有对象都是可达的,不能删除。 不可达 ( unreachable )的垃圾对象,就可以被安全回收。

分代回收机制
  • 如果每次执行标记清除法时,都需要遍历所有对象,会影响程序性能。为了提高垃圾回收效率,Python 还引入了分代回收机制 对象分为若干“代”( generation ),每次只处理某个代中的对象,因此 GC 卡顿时间更短。
  • 一个对象存活的时间越长,它下一刻被释放的概率就越低,可以适当降低回收频率。因此,Python根据对象的存活时间进行分代,分为:初生代中生代老生代
  • 回收策略:
    1)每新增 701 个需要 GC 的对象,触发一次新生代 GC ;

2)每执行 11 次新生代 GC ,触发一次中生代 GC ;

3)每执行 11 次中生代 GC ,触发一次老生代 GC (老生代 GC 还受其他策略影响,频率更低);

4)执行某个生代 GC 前,年轻生代对象链表也移入该代,一起 GC ;

5)一个对象创建后,随着时间推移将被逐步移入老生代,回收频率逐渐降低;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,646评论 4 366
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,979评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,391评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,356评论 0 215
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,740评论 3 293
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,836评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,022评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,764评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,487评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,728评论 2 252
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,200评论 1 263
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,548评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,217评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,134评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,921评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,919评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,766评论 2 274