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 也不建議使用這種導入方式

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》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章