Python 之下划线

python 中的标识符可以包含数字、字母和 _,但必须以字母或者 _ 开头,其中以 下划线 (_) 开头的命名一般具有特殊的意义。下划线对 python 的意义不同于其他语言,分单下划线、双下划线;有前缀有后缀。看起来有点繁琐,总结起来,单双划线主要用于变量名、方法名上以及其特殊用法

单双划线的 5 种形式
  • 单一下划线:_
  • 开头单下划线:_var
  • 结尾单下划线:var_
  • 开头双下划线:__var
  • 开头结尾双下划线:__var__

一. 单一下划线:_

1.单一下划线作为变量相当于一个约定,通常用来表示不会被使用的临时变量,只是不可匹配时用来占位的临时变量

>>> car=('red','auto','12','230000')
>>> color,_,_,money=car
>>> color
'red'
>>> money
'230000'

2. 在 python REPL 解释器下,_ 同时也是用来表示上一个表达式结果的特殊变量

>>> 2+3
5
>>> _
5
>>> print(_)
5
>>> list()
[]
>>> _.append(1)
>>> _.append(2)
>>> print(list)
<class 'list'>
>>> _
[1, 2]

二. 开头单下划线:_var

这样的对象叫做保护变量,不能用 from module import * 形式导入,只有类对象和子类对象能访问这些变量

单下划线只是 python 社区一个约定俗成的规定,用来提醒程序员以单下划线开头的变量和方法仅供类内部使用即私有变量或方法,不是公共接口的一部分

python 中并没有严格的私有方法和公有方法,这里单下划线更像一个善意的提醒。就好像告诉程序员:我可以被访问和调用,但请最好不要调用我

class Test:     
    def __init__(self):
        self.foo=11
        self._bar=12.5
 >>> t=Test()
 >>> t.foo
 11
 >>> t._bar
 12.5       

实际上 _bar 可以被访问,因为不是强制而是俗称约定。采取开头单下划线并不会真的阻止你访问内部变量

但以单下划线开头会对模块的导入产生影响

my_model.py 模块

# This is my_model.py

def external_func():
    print("This is a test program")


def _interbal_func():
    print("I can't be import using 'import *'")

测试

>>> from my_model import *
>>> external_func()
This is a test program
>>> _interbal_func()NameError                                 Traceback (most recent call last)
<ipython-input-31-bf01afc9a0b0> in <module>()
----> 1 _interbal_func()

NameError: name '_interbal_func' is not defined

当使用 import * 的时候,python 不会导入开头单下划线的变量和方法,除非这些内容在 all 中被明确定义。当然在 pep8 也不建议使用这种导入方式

扫描二维码关注公众号,回复: 1643508 查看本文章

import 单个模块,并不受影响

import my_model
my_model.external_func()
my_model._internal_func()

输出

This is a test program
I can't be import using 'import *'

三. 结尾单下划线:var_

有的时候,有些变量命被关键字所占用,这个时候在结尾添加一个单下划线,可以避免命名冲突。
这在 pep8 中有明确的定义

>>> def make_object(name, class): File "<ipython-input-5-b2b686955b92>", line 1
    def make_object(name, class):
                              ^
SyntaxError: invalid syntax
>>> def make_object(name, class_):
...     pass

四. 开头双下划线:__var

类中的私有成员,只有类对象自己能访问,子类对象也不能访问到这个成员,但在对象外部可以通过 对象名._类名__xxx 这样的特殊方式来访问。Python中没有纯粹的 C++意义上的私有成员

开头双下划线会使 python 解释器改变当前变量的名字(name mangling)从而避免子类中可能出现的命名冲突

class Test:
    def __init__(self):
        self.foo = 11
        self._bar = 23
        self.__baz = 23

t=Test()
dir(t)

输出

['_Test__baz',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_bar',
 'foo']

使用 dir() 方法,可以得到一个对象的属性列表。从列表中很轻松的找到 foo_bar 变量,但是 __baz 却好像消失了?

两个自定义类

class Test:
    def __init__(self):
        self.foo = 11
        self._bar = 23
        self.__baz = 23

class ExtendedTest(Test):
    def __init__(self):
        super().__init__()
        self.foo = 'overridden'
        self._bar = 'overridden'
        self.__baz = 'overridden'

测试


>>> t2=ExtendedTest()
>>> t2.foo
'overridden'
>>> t2._bar
'overridden'
>>> t2.__baz


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-14-982677203981> in <module>()
----> 1 t2.__baz

AttributeError: 'ExtendedTest' object has no attribute '__baz'


>>> dir(t2)

Out[15]:
['_ExtendedTest__baz',
 '_Test__baz',
 '__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 '_bar',
 'foo']

 >>> t2._ExtendedTest__baz
'overridden'

>>> t2._Test__baz
23

子类中通过开头双下划线定义的变量依然不能对象实例直接访问,但是通过 dir() 方法发现,子类中定义的 __baz 并未覆盖掉父类中 __baz ,他们分别是 _ExtendedTest__baz_Test__baz , 但是对内部实现来说,他们也是完全透明的,解释器并未做限制

class ManglingTest:
    def __init__(self):
        self.__mangled = 'hello'

    def get_mangled(self):
        return self.__mangled

>>> ManglingTest().get_mangled()
'hello'
>>> ManglingTest().__mangled
AttributeError: "'ManglingTest' object has no attribute '__mangled'"

除了变量名,对方法名也是同样适用的

class MangledMethod:
    def __method(self):
        return 42

    def call_it(self):
        return self.__method()

>>> MangledMethod().__method()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-20-af42293a628a> in <module>()
----> 1 ManglingTest().__mangled
      2 

AttributeError: 'ManglingTest' object has no attribute '__mangled'

>>> MangledMethod().call_it()
42

由于命名重置(name mangling)的存在,解释器在对双下划线开头变量做展开的时候,也会使用到命名空间里面的全局变量

_MangledGlobal__mangled = 23

class MangledGlobal:
    def test(self):
        return __mangled

>>> MangledGlobal().test()
23

五. 开头和结尾双下划线:__var__

一般用于特殊方法的命名,用来实现对象的一些行为或者功能,比如 __new__() 方法用来创建实例;__init__() 方法用来初始化对象;x + y 操作被映射为方法 x.__add__(y) ,序列或者字典的索引操作x[k]映射为x.getitem(k),len()、str()分别被内置函数len()、str()调用等等

开头和结尾双下划线的用法又被称为 魔术方法,命名重置(name mangling)对它不生效,不会修改名称

由于魔术方法通常是 python 中预留的一些方法,比如 __init____call__ , 可以复写这些方法,但是不建议添加一些自定义魔术方法,这个可能会和将来 python 的改动相冲突

class PrefixPostfixTest:
    def __init__(self):
        self.__bam__ = 42

>>> PrefixPostfixTest().__bam__
42

六. 魔术方法

Python 解释器碰到特殊的句法时, 会使用特殊方法去激活一些基本的对象操作, 这些特殊方法的名字以两个下划线开头, 以两个下划线结尾( 例如 __getitem__) 。 比如 obj[key] 的背后就是__getitem__ 方法, 为了能求得 my_collection[key] 的值, 解释器实际上会调用 my_collection.__getitem__(key)

魔术方法( magic method) 是特殊方法的昵称,特殊方法也叫双下方法( dunder method)

用一个非常简单的例子来展示如何实现 __getitme____len__ 这两个特殊方法, 通过这个例子也能见识到特殊方法的强大

示例 代码建立了一个纸牌类

import collections
Card = collections.namedtuple('Card', ['rank', 'suit'])
class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]
    def __len__(self):
        return len(self._cards)
    def __getitem__(self, position):
        return self._cards[position]

这个代码中, collections.namedtuple 构建了一个简单的类来表示一张纸牌,利用 namedtuple, 得到一个纸牌对象

>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

关于魔术方法,在示例代码中,从一叠牌中抽取特定的一张纸牌, 比如说第一张或最后一张, 是 deck[0] , deck[-1]。 实际调用的是__getitem__ 方法。 因为 __getitem__方法把 []操作交给了 self._cards 列表, 所以我们的 deck 类自动支持切片( slicing) 操作

>>> deck[0]
Card(rank='2', suit='spades')
>>> deck[-1]
Card(rank='A', suit='hearts')
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'),
Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'),
Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

虽然 FrenchDeck 隐式地继承了 object 类, 但功能却不是继承而来的。 可以通过数据模型和一些合成来实现这些功能。 通过实现 __len____getitem__ 这两个特殊方法, FrenchDeck 就跟一个 Python 自有的序列数据类型一样,可以体现出 Python 的核心语言特性( 例如迭代和切片)。 同时这个类还可以用于标准库中诸如random.choicereversedsorted 这些函数。 另外, 对合成的运用使得 __len____getitem__的具体实现可以代理给 self._cards 这个 Python 列表( 即 list 对象)

很多时候, 特殊方法的调用是隐式的, 比如 for i in x: 这个语句,背后其实用的是iter(x), 而这个函数的背后则是x.__iter__()方 法。特殊方法的存在是为了被 Python 解释器调用的, 你自己并不需要调用它们。 也就是说没有 my_object.__len__() 这种写法,而应该使用 len(my_object)。 唯一的例外可能是 __init__ 方法,代码里可能经常会用到它, 目的是在的子类的 __init__ 方法中调用超类的构造器

特殊方法一览表

Model 一章列出了 83 个特殊方法的名字, 其中 47 个用于实现算术运算、 位运算和比较操作

1. 类的基础方法

序号 目的 所编写代码 Python 实际调用
初始化一个实例 x = MyClass() x.__init__()
字符串的 “官方” 表现形式 repr(x) x.__repr__()
字符串的“非正式”值 str(x) x.__str__()
字节数组的“非正式”值 bytes(x) x.__bytes__()
格式化字符串的值 format(x, format_spec) x.__format__(format_spec)
  • 按照约定, repr() 方法所返回的字符串为合法的 Python 表达式
  • 在调用 print(x) 的同时也调用了 str() 方法

2.迭代枚举

序号 目的 所编写代码 Python 实际调用
遍历某个序列 iter(seq) seq.__iter__()
从迭代器中获取下一个值 next(seq) seq.__next__()
按逆序创建一个迭代器 reversed(seq) seq.__reversed__()
  • 无论何时创建迭代器都将调用 iter() 方法。这是用初始值对迭代器进行初始化的绝佳之处。
  • 无论何时从迭代器中获取下一个值都将调用 next() 方法。
  • __reversed__() 方法并不常用。它以一个现有序列为参数,并将该序列中所有元素从尾到头以逆序排列生成一个新的迭代器

3. 属性管理

序号 目的 所编写代码 Python 实际调用
获取一个计算属性(无条件的) x.my_property x.__getattribute__('my_property')
获取一个计算属性(后备) x.my_property x.__getattr__('my_property')
设置某属性 x.my_property = value x.__setattr__('my_property',value)
删除某属性 del x.my_property x.__delattr__('my_property')
列出所有属性和方法 dir(x) x.__dir__()
  • 如果某个类定义了 __getattribute__() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。
  • 如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性color, x.color 将 不会 调用 x.__getattr__('color');而只会返回x.color 已定义好的值
  • 无论何时给属性赋值,都会调用 __setattr__()方法
  • 无论何时删除一个属性,都将调用__delattr__()方法
  • 如果定义了 __getattr__()__getattribute__() 方法, __dir__() 方法将非常有用。通常,调用 dir(x)将只显示正常的属性和方法。如果__getattr()__方法动态处理 color 属性, dir(x) 将不会将 color 列为可用属性。可通过覆盖 __dir__() 方法允许将 color 列为可用属性,对于想使用你的类但却不想深入其内部的人来说,该方法非常有益

4. 可序列化的类

Python 支持 任意对象的序列化和反序列化。(多数 Python 参考资料称该过程为 picklingunpickling)。该技术对与将状态保存为文件并在稍后恢复它非常有意义。所有的 内置数据类型 均已支持 pickling 。如果创建了自定义类,且希望它能够 pickle,阅读 pickle 协议 了解下列特殊方法何时以及如何被调用

序号 目的 所编写代码 Python 实际调用
自定义对象的复制 copy.copy(x) x.__copy__()
自定义对象的深度复制 copy.deepcopy(x) x.__deepcopy__()
在 pickling 之前获取对象的状态 pickle.dump(x, file) x.__getstate__()
序列化某对象 pickle.dump(x, file) x.__reduce__()
序列化某对象(新 pickling 协议) pickle.dump(x, file, protocol_version) x.__reduce_ex__(protocol_version)
控制 unpickling 过程中对象的创建方式 x = pickle.load(file) x.__getnewargs__()
在 unpickling 之后还原对象的状态 x = pickle.load(file) x.__setstate__()

要重建序列化对象,Python 需要创建一个和被序列化的对象看起来一样的新对象,然后设置新对象的所有属性。__getnewargs__() 方法控制新对象的创建过程,而 __setstate__() 方法控制属性值的还原方式

七. 总结

单下划线 (_)

  1. 在 CPython 等解释器中代表交互式解释器会话中上一条执行的语句的结果

  2. 作为临时性的名称使用,分配了一个特定的名称但是在后面不会用到该名称

  3. 用于实现国际化和本地化字符串之间翻译查找的函数名称(遵循相应的C约定)

  4. 名称前的单下划线,用于指定该名称属性为 私有,这并不是语法规定而是惯例,在使用这些代码时将大家会知道以 _ 开头的名称只供内部使用,在 from <Package> import * 时,以 _ 开头的名称都不会被导入,除非模块或包中的 __all__ 列表显式地包含了它们

双下划线 (__)

  1. 名称(具体为一个方法名)前双下划线(__)的用法并不是一种惯例,对解释器来说它有特定的意义。Python 中的这种用法是为了避免与子类定义的名称冲突。Python文档指出,__spam 这种形式(至少两个前导下划线,最多一个后续下划线)的任何标识符将会被 _classname__spam 这种形式原文取代,在这里 classname 是去掉前导下划线的当前类名

  2. 名称前后的双下划线表示 Python 中特殊的方法名。这只是一种惯例,对 Python 来说,这将确保不会与用户自定义的名称冲突。通常,程序员会重写这些方法,并在里面实现所需要的功能,以便Python 调用

文章资料学习引用:Python代码中下划线的含义下划线与 PythonPython中下划线—完全解读 、书籍《流畅的 Python》

猜你喜欢

转载自blog.csdn.net/qq_36148847/article/details/79470271