类型

基本环境

名字空间

Python 中,每个模块(源码文件)有一个全局名字空间,根据代码作用域,有当前名字(本地名字)空间,如果直接在模块级别执行,本地名字空间和全局名字空间没有区别,但在函数内,当前名字空间指函数作用域指的是函数作用域


In [1]:
x = 100
print(id(globals))
print(id(locals))


140669702279960
140669702280680

In [2]:
globals()


Out[2]:
{'In': ['', 'x = 100\nprint(id(globals))\nprint(id(locals))', 'globals()'],
 'Out': {},
 '_': '',
 '__': '',
 '___': '',
 '__builtin__': <module 'builtins' (built-in)>,
 '__builtins__': <module 'builtins' (built-in)>,
 '__doc__': 'Automatically created module for IPython interactive environment',
 '__loader__': None,
 '__name__': '__main__',
 '__package__': None,
 '__spec__': None,
 '_dh': ['/home/kaka/blog/my_blog/content/python3-note'],
 '_i': 'x = 100\nprint(id(globals))\nprint(id(locals))',
 '_i1': 'x = 100\nprint(id(globals))\nprint(id(locals))',
 '_i2': 'globals()',
 '_ih': ['', 'x = 100\nprint(id(globals))\nprint(id(locals))', 'globals()'],
 '_ii': '',
 '_iii': '',
 '_oh': {},
 'exit': <IPython.core.autocall.ZMQExitAutocall at 0x7ff02dab05f8>,
 'get_ipython': <bound method InteractiveShell.get_ipython of <ipykernel.zmqshell.ZMQInteractiveShell object at 0x7ff02dae6cf8>>,
 'quit': <IPython.core.autocall.ZMQExitAutocall at 0x7ff02dab05f8>,
 'x': 100}

In [4]:
def test():
    x = "hello world"
    print(locals())
    print(id(globals))
    print(id(locals))
    
test()


{'x': 'hello world'}
140669702279960
140669702280680

所以,我们可以直接修改名字空间建立关联引用


In [5]:
globals()["hello"] = "hello world"
hello


Out[5]:
'hello world'

并非所有时候都能直接操作名字空间,函数执行使用缓存机制,直接修改本地名字空间未必有效。正常编码时候尽量避免直接修改名字空间

在名字空间字典中,名字只是简单的字符串主键,所以,名字可以重新关联另一个对象,不用在乎类型是否相同


In [6]:
x = 100
print(id(x))
x = "hello"  # 重新关联对象,而不是修改原对象
print(id(x))


140669700048672
140669508307464

一个对象可以用多个名字


In [7]:
x = 1234
y = x
y is x # 必须用 is 判断是否引用同一个对象,因为相等操作符是可以重载的,有时候只判断值


Out[7]:
True

命名规则建议:

- 类型名称使用 CapWords 格式
- 模块文件名、函数、方法成员等使用 lower_case_with_underscores 格式
- 全局变量使用 UPPER_CASE_WITH_UNDERSCORES 格式
- 避免与内置函数或标准库的常用类型同名,以免造成误解

以下划线开头的名字,代表特殊含义:

- 模块成员以单下划线开头 `(_x)`,属于私有成员,不会被星号导入
- 类型成员以双下划线开头,但无结尾 `(__x)` 属于自动命名私有成员
- 以双下划线开头和结尾 `(__x__)` 通常是系统成员,应避免使用
- 交互模式下,单下划线 `(_)` 返回最后一个表达式结果

In [9]:
1 + 2 + 3


Out[9]:
6

In [11]:
_


Out[11]:
6

强引用


In [15]:
import sys  
a = 1234
b = a
print(sys.getrefcount(a)) # getrefcount() 也会通过参数引用目标对象,导致引用计数 +1
del a
print(sys.getrefcount(b))


4
3

弱引用

弱引用(weak reference)在保留引用前提下,不增加计数也不阻止目标被回收(int tuple 等不支持弱引用)


In [26]:
import weakref

class X:
    def __del__(self):
        print(id(self), "dead.")
        
c = X()
sys.getrefcount(c)


Out[26]:
2

In [27]:
w = weakref.ref(c)
w() is c


Out[27]:
True

In [28]:
sys.getrefcount(c)


Out[28]:
2

In [29]:
del c


140669507549728 dead.

In [30]:
w() is None


Out[30]:
True

弱引用经常用来缓存,监控等 “外挂” 场景,不影响目标对象,也不能阻止它们被回收,弱引用另一个典型应用是实现 Finalizer,也就是在对象被回收时执行额外的 “清理” 操作


In [36]:
d = X()
def callback(w):
    print(w, w() is None)

w = weakref.ref(d, callback) # 创建弱引用时设置回调函数

In [37]:
del d


140669397326592 dead.
<weakref at 0x7ff0257547c8; dead> True

这里不用析构方法的原因是析构函数作为目标成员,用途是完成对象的内部资源清理,它并应该处理与之无关的外部场景,所有用 Finalizer 是一个合理的选择

弱引用与普通名字最大区别在于类函数的调用和语法。可以用 proxy 改进,使其和名字引用语法保持一致


In [38]:
a = X()
a.name = "kaka"
w = weakref.ref(a)
w.name


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-38-86b23ebf3104> in <module>()
      2 a.name = "kaka"
      3 w = weakref.ref(a)
----> 4 w.name

AttributeError: 'weakref' object has no attribute 'name'

In [39]:
w().name


Out[39]:
'kaka'

In [40]:
p = weakref.proxy(a)
p


Out[40]:
<__main__.X at 0x7ff02575cb88>

In [41]:
p.name


Out[41]:
'kaka'

In [43]:
p.age = 60 # 可以直接赋值
p.age


Out[43]:
60

对象复制

浅拷贝复制复制名字引用,深拷贝复制所有引用成员


In [45]:
class X: pass
x = X()
x.data = [1, 2]

import copy 

x2 = copy.copy(x)
x2 is x


Out[45]:
False

In [46]:
x2.data is x.data # 成员 data 仍然指向原列表,仅仅复制了引用


Out[46]:
True

In [47]:
x3 = copy.deepcopy(x)
x3 is x


Out[47]:
False

In [48]:
x3.data is x.data


Out[48]:
False

循环引用垃圾回收


In [54]:
class X: 
    def __del__(self): 
        print(self, "dead.")
        
import gc
gc.disable() # 在性能测试时,要关闭 gc, 避免垃圾回收对执行器计时造成影响
a = X()
b = X()
a.x = b
b.x = a # 构建循环引用

del a
del b # 删除所有名字后对象并未回收,引用计数失效

gc.enable()
gc.collect()


<__main__.X object at 0x7ff02c12ee48> dead.
<__main__.X object at 0x7ff02c12e978> dead.
Out[54]:
51

编译

除了交互模式和手工编译,源码在被导入(import)时完成编译,编译后的字节码数据被缓存复用,通常还会保存到硬盘

Python 3 使用专门目录保存字节码缓存文件(__pycache__/*.pyc) 这样程序在下次启动时,可以避免再次编译,提升导入速度。缓存文件头中存储了编译信息,用来判断源码文件是否被更新

除了作为执行指令的字节码外,还有很多元数据,共同组成执行单元。从这些元数据中,可以获得参数,闭包等信息


In [1]:
def  add(x, y):
    return x + y

add.__code__


Out[1]:
<code object add at 0x7f7f240d4b70, file "<ipython-input-1-fae8dc1acce6>", line 1>

In [2]:
dir(add.__code__)


Out[2]:
['__class__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__le__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'co_argcount',
 'co_cellvars',
 'co_code',
 'co_consts',
 'co_filename',
 'co_firstlineno',
 'co_flags',
 'co_freevars',
 'co_kwonlyargcount',
 'co_lnotab',
 'co_name',
 'co_names',
 'co_nlocals',
 'co_stacksize',
 'co_varnames']

In [3]:
add.__code__.co_varnames


Out[3]:
('x', 'y')

In [4]:
add.__code__.co_code


Out[4]:
b'|\x00\x00|\x01\x00\x17S'

我们无法直接阅读机器码,可以反编译


In [5]:
import dis

dis.dis(add)


  2           0 LOAD_FAST                0 (x)
              3 LOAD_FAST                1 (y)
              6 BINARY_ADD
              7 RETURN_VALUE

某些时候,需要手工编译


In [8]:
source = """
print("hello, world")
print(1 + 2)
"""
code = compile(source, "demo", "exec") # 提供一个文件名用于输出提示
dis.show_code(code)


Name:              <module>
Filename:          demo
Argument count:    0
Kw-only arguments: 0
Number of locals:  0
Stack size:        3
Flags:             NOFREE
Constants:
   0: 'hello, world'
   1: 1
   2: 2
   3: None
   4: 3
Names:
   0: print

In [9]:
dis.dis(code)


  2           0 LOAD_NAME                0 (print)
              3 LOAD_CONST               0 ('hello, world')
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP

  3          10 LOAD_NAME                0 (print)
             13 LOAD_CONST               4 (3)
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 POP_TOP
             20 LOAD_CONST               3 (None)
             23 RETURN_VALUE

In [11]:
import py_compile, compileall

path = '/home/kaka/kaka/udacity/1-LaneLines/tune.py'
py_compile.compile(path)


Out[11]:
'/home/kaka/kaka/udacity/1-LaneLines/__pycache__/tune.cpython-35.pyc'

In [12]:
compileall.compile_dir('.') # python -m compileall .


Listing '.'...
Listing './.ipynb_checkpoints'...
Out[12]:
1

执行

程序可以再运行期间动态执行 “未知” 代码,常用于实现动态生成的设计,例如 namedtuple 可以在运行期间构建新的类型


In [14]:
import collections

User = collections.namedtuple("User", "name, age", verbose=True)


from builtins import property as _property, tuple as _tuple
from operator import itemgetter as _itemgetter
from collections import OrderedDict

class User(tuple):
    'User(name, age)'

    __slots__ = ()

    _fields = ('name', 'age')

    def __new__(_cls, name, age):
        'Create new instance of User(name, age)'
        return _tuple.__new__(_cls, (name, age))

    @classmethod
    def _make(cls, iterable, new=tuple.__new__, len=len):
        'Make a new User object from a sequence or iterable'
        result = new(cls, iterable)
        if len(result) != 2:
            raise TypeError('Expected 2 arguments, got %d' % len(result))
        return result

    def _replace(_self, **kwds):
        'Return a new User object replacing specified fields with new values'
        result = _self._make(map(kwds.pop, ('name', 'age'), _self))
        if kwds:
            raise ValueError('Got unexpected field names: %r' % list(kwds))
        return result

    def __repr__(self):
        'Return a nicely formatted representation string'
        return self.__class__.__name__ + '(name=%r, age=%r)' % self

    def _asdict(self):
        'Return a new OrderedDict which maps field names to their values.'
        return OrderedDict(zip(self._fields, self))

    def __getnewargs__(self):
        'Return self as a plain tuple.  Used by copy and pickle.'
        return tuple(self)

    name = _property(_itemgetter(0), doc='Alias for field number 0')

    age = _property(_itemgetter(1), doc='Alias for field number 1')



In [15]:
User


Out[15]:
__main__.User

In [16]:
u = User("kaka", 30)
u


Out[16]:
User(name='kaka', age=30)

且不管代码如何生成,最终都要以模块导入执行,要么调用 eval,exec 函数执行。eval 执行单个表达式,exec 对应代码块执行,接受字符串或已编译好的代码对象(code) 作为参数。如果是字符串,就会检查是否符合语法规则


In [17]:
s = "1 + 2 + 3"
eval(s)


Out[17]:
6

In [18]:
s = """
def test():
    print("hello world")
test()
"""
exec(s)


hello world

无论哪种方式,都必须有对应的上下文环境,默认直接使用 当前全局和本地名字空间


In [19]:
x = 100
def test ():
    y = 200
    print(eval("x + y")) # 从上下文空间获取 x, y
    
test()


300

In [23]:
def test():
    print("test:", id(globals), id(locals))
    exec('print("exec:", id(globals), id(locals))')
    
test()


test: 140184209593112 140184209593832
exec: 140184209593112 140184209593832

有了操作上下文名字空间能力,我们就可以向外部环境注入新的成员,新的类型算法等等。最终达到动态逻辑或结果融入,成为当前体系组成的设计目标


In [25]:
s = """
class X: pass
def hello():
    print("hello, world")
"""
exec(s)

In [26]:
X


Out[26]:
__main__.X

In [27]:
X()


Out[27]:
<__main__.X at 0x7f7f20130860>

In [28]:
hello()


hello, world

某些时候,动态代码来源不确定,基于安全考虑,必须对执行过程进行隔离,阻止其直接读写环境数据。如此,就必须传入容器对象作为动态代码的专用名字空间,以类似简易沙箱(sandbox)的方式执行

根据需要,分别提供 globals,locals参数,也可共用同一空间字典

为保证代码正确执行,解释器会自动导入 __builtins__ 模块。以便导入内置函数


In [29]:
g = {"x": 100}
l = {"y": 200} 
eval("x+y", g, l) # 为 globals 和  locals 分别指定字典


Out[29]:
300

In [31]:
ns = { }
exec("class X: pass", ns) # globals 和 locals 共用一个字典
# ns 太多了,不打印了

同时提供两个名字空间参数时,默认总是 locals 优先,除非在动态代码中明确指定使用 globals


In [35]:
s = """
print(x) # locals
global y # globals
y += 100

z = x + y # locals
"""

g = {"x": 10, "y": 20}
l = {"x": 1000}
exec(s, g, l)


1000

在函数作用域内,locals 函数总是返回执行栈帧(stack frame) 名字空间。因此,即便显示提供 locals 名字看完我,也无法将其注入到动态代码中


In [38]:
s = """
print(id(locals()))
def test():
    print(id(locals()))
test()
"""
ns = {}
id(ns)


Out[38]:
140183978264840

In [40]:
exec(s, ns, ns) # test.locals() 和 ns.locals() 不同


140183978264840
140183975734536

In [ ]: