क्यों [] सूची से तेज है ()?


707

मैं हाल ही में की प्रसंस्करण गति की तुलना में []और list()और उस खोज के लिए आश्चर्यचकित था []रन तेजी से तीन बार की तुलना में अधिक से list()। मैं के साथ एक ही परीक्षण भाग गया {}और dict()और परिणाम व्यावहारिक रूप से समान थे: []और {}दोनों के चारों ओर 0.128sec / मिलियन चक्र ले लिया है, जबकि list()और dict()मोटे तौर पर 0.428sec / मिलियन चक्र प्रत्येक ले लिया।

ऐसा क्यों है? क्या []और {}(और शायद ()और ''भी) तुरंत वापस कुछ खाली स्टॉक शाब्दिक की प्रतियां पारित उनकी स्पष्ट रूप से नाम समकक्षों (जबकि list(), dict(), tuple(), str()) पूरी तरह से एक वस्तु बनाने के बारे में जाना है, या नहीं, वे वास्तव में तत्वों है?

मुझे नहीं पता कि ये दोनों तरीके कैसे भिन्न हैं, लेकिन मुझे पता लगाना अच्छा लगेगा। मुझे डॉक्स या एसओ में कोई उत्तर नहीं मिला, और खाली ब्रैकेट की तलाश में मैं जितना उम्मीद कर रहा था उससे अधिक समस्याग्रस्त हो गया।

मुझे क्रमशः सूचियों और शब्दकोशों की तुलना करने के लिए, और , timeit.timeit("[]")और timeit.timeit("list()"), बुलाकर मेरे समय के परिणाम मिले। मैं पायथन 2.7.9 चला रहा हूं।timeit.timeit("{}")timeit.timeit("dict()")

मुझे हाल ही में पता चला है कि " 1 की तुलना में ट्रू स्लो क्यों है? " जो कि प्रदर्शन की तुलना करता if Trueहै if 1और एक समान शाब्दिक-बनाम-वैश्विक परिदृश्य पर स्पर्श करता है। शायद यह विचार करने लायक भी है।


2
ध्यान दें: ()और ''विशेष हैं, क्योंकि वे न केवल खाली हैं, वे अपरिवर्तनीय हैं, और जैसे, यह उन्हें एकल बनाने के लिए एक आसान जीत है; वे नई वस्तुओं का निर्माण भी नहीं करते हैं, सिर्फ खाली tuple/ के लिए सिंगलटन लोड करते हैं str। तकनीकी रूप से एक कार्यान्वयन विवरण, लेकिन मेरे पास यह कल्पना करने का कठिन समय है कि वे प्रदर्शन कारणों से खाली / कैश क्यों नहीं करेंगे । तो एक शेयर शाब्दिक के बारे में और वापस पारित करने का आपका अंतर्ज्ञान गलत था, लेकिन यह लागू होता है और । tuplestr[]{}()''
शैडो रेंजर

जवाबों:


758

क्योंकि []और {}कर रहे हैं शाब्दिक वाक्य रचना । पायथन सिर्फ सूची या शब्दकोश वस्तुओं को बनाने के लिए बायटेकोड बना सकता है:

>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
>>> dis.dis(compile('{}', '', 'eval'))
  1           0 BUILD_MAP                0
              3 RETURN_VALUE        

list()और dict()अलग-अलग वस्तुएं हैं। उनके नामों को हल करने की आवश्यकता है, तर्कों को धक्का देने के लिए शामिल किया जाना है, बाद में पुनः प्राप्त करने के लिए फ्रेम को संग्रहीत करना होगा, और एक कॉल करना होगा। वह सब अधिक समय लेता है।

खाली मामले के लिए, इसका मतलब है कि आपके पास बहुत कम से कम है LOAD_NAME(जिसे वैश्विक नामस्थान और साथ ही __builtin__मॉड्यूल के माध्यम से खोजना होगा ) एक के बाद CALL_FUNCTION, जिसे वर्तमान फ्रेम को संरक्षित करना है:

>>> dis.dis(compile('list()', '', 'eval'))
  1           0 LOAD_NAME                0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
>>> dis.dis(compile('dict()', '', 'eval'))
  1           0 LOAD_NAME                0 (dict)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        

आप नाम देखने का समय अलग से दे सकते हैं timeit:

>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119

समय की विसंगति शायद एक शब्दकोश हैश टक्कर है। उन वस्तुओं को कॉल करने के लिए समय से घटाएं, और शाब्दिक का उपयोग करने के लिए समय के खिलाफ परिणाम की तुलना करें:

>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125

तो वस्तु को कॉल करने के लिए 1.00 - 0.31 - 0.30 == 0.39प्रति 10 मिलियन कॉल पर एक अतिरिक्त सेकंड लगता है ।

आप स्थानीय लोगों के रूप में वैश्विक नामों को अलग करके वैश्विक लुकअप लागत से बच सकते हैं (एक timeitसेटअप का उपयोग करके , आप जो कुछ भी नाम से बांधते हैं वह एक स्थानीय है):

>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137

लेकिन आप कभी भी उस CALL_FUNCTIONलागत को पार नहीं कर सकते ।


150

list()एक वैश्विक खोज और एक फ़ंक्शन कॉल की आवश्यकता होती है, लेकिन []एक निर्देश के लिए संकलित करता है। देख:

Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
  1           0 LOAD_GLOBAL              0 (list)
              3 CALL_FUNCTION            0
              6 RETURN_VALUE        
None
>>> print dis.dis(lambda: [])
  1           0 BUILD_LIST               0
              3 RETURN_VALUE        
None

75

क्योंकि एक स्ट्रिंग को एक सूची ऑब्जेक्ट में बदलने के लिए listएक फ़ंक्शन है, जबकि []बल्ले से एक सूची बनाने के लिए उपयोग किया जाता है। यह कोशिश करो (आप के लिए और अधिक समझ हो सकता है):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

जबकि

y = ["wham bam"]
>>> y
["wham bam"]

आपको एक वास्तविक सूची देता है जिसमें आप जो भी डालते हैं।


7
यह सीधे प्रश्न को संबोधित नहीं करता है। सवाल यह था कि इससे []ज्यादा तेज क्यों है list(), इससे ['wham bam']ज्यादा तेज क्यों नहीं है list('wham bam')
जेरेमी विज़सर

2
मेरे लिए थोड़ा भावना बनाया @JeremyVisser ऐसा इसलिए है क्योंकि []/ list()बिल्कुल के रूप में एक ही है ['wham']/ list('wham')क्योंकि वे एक ही चर मतभेद हैं बस के रूप में 1000/10रूप में ही है 100/1गणित में। आप सिद्धांत रूप में दूर ले जा सकते हैं wham bamऔर यह तथ्य अभी भी वही होगा, जो list()एक फ़ंक्शन नाम को कॉल करके कुछ बदलने की कोशिश करता है जबकि []सीधे चर को बदल देगा। फ़ंक्शन कॉल भिन्न हैं हां, यह समस्या का सिर्फ एक तार्किक अवलोकन है, उदाहरण के लिए किसी कंपनी का नेटवर्क मानचित्र भी समाधान / समस्या का तार्किक है। वोट हालांकि आप चाहते हैं
Torxed

@JeremyVisser इसके विपरीत, यह दर्शाता है कि वे सामग्री पर अलग-अलग ऑपरेशन करते हैं।
बाल्ड्रिक

20

यहाँ उत्तर बहुत अच्छे हैं, इस बिंदु पर और इस प्रश्न को पूरी तरह से कवर करते हैं। मैं रुचि रखने वालों के लिए बाइट-कोड से एक और कदम नीचे छोड़ दूँगा। मैं सीपीथॉन के सबसे हालिया रेपो का उपयोग कर रहा हूं; पुराने संस्करण इस संबंध में समान व्यवहार करते हैं लेकिन इसमें थोड़े बदलाव हो सकते हैं।

इनमें से प्रत्येक के BUILD_LISTलिए , []और इसके CALL_FUNCTIONलिए निष्पादन का ब्रेक डाउन है list()


BUILD_LISTअनुदेश:

आपको बस हॉरर देखना चाहिए:

PyObject *list =  PyList_New(oparg);
if (list == NULL)
    goto error;
while (--oparg >= 0) {
    PyObject *item = POP();
    PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();

मैं बहुत जानता हूं। यह कितना सरल है:

  • स्टैक पर तर्कों की संख्या को इंगित करते हुए एक नई सूची बनाएं PyList_New(यह मुख्य रूप से एक नई सूची ऑब्जेक्ट के लिए मेमोरी आवंटित करता है) oparg। सीधा मुद्दे पर।
  • जांचें कि कुछ भी गलत नहीं हुआ if (list==NULL)
  • PyList_SET_ITEM(मैक्रो) के साथ स्टैक पर स्थित किसी भी तर्क (हमारे मामले में इसे निष्पादित नहीं किया गया है) में जोड़ें ।

कोई आश्चर्य नहीं कि यह तेज है! यह नई सूची बनाने के लिए कस्टम बनाया गया है, और कुछ नहीं :-)

CALL_FUNCTIONअनुदेश:

जब आप कोड हैंडलिंग पर नज़र डालते हैं, तो सबसे पहले यह देखते हैं CALL_FUNCTION:

PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
    goto error;
}
DISPATCH();

बहुत हानिरहित लगता है, है ना? खैर, नहीं, दुर्भाग्य से, call_functionएक सीधा आदमी नहीं है जो तुरंत फ़ंक्शन को कॉल करेगा, यह नहीं कर सकता। इसके बजाय, यह स्टैक से ऑब्जेक्ट को पकड़ता है, स्टैक के सभी तर्कों को पकड़ लेता है और फिर ऑब्जेक्ट के प्रकार के आधार पर स्विच करता है; क्या यह:

  • PyCFunction_Type? नहीं, यह है list, listप्रकार की नहीं हैPyCFunction
  • PyMethodType? नहीं, पिछले देखें।
  • PyFunctionType? Nopee, पिछले देखें।

हम listटाइप कर रहे हैं , में दिया गया तर्क call_functionहै PyList_Type। CPython को अब किसी भी कॉल करने योग्य ऑब्जेक्ट को संभालने के लिए एक जेनेरिक फ़ंक्शन को _PyObject_FastCallKeywordsकॉल करना होगा, yay अधिक फ़ंक्शन कॉल करेगा।

यह फ़ंक्शन फिर से कुछ फ़ंक्शन प्रकारों के लिए कुछ जांच करता है (जो मुझे समझ में नहीं आता कि क्यों) और फिर, यदि आवश्यक हो तो kwargs के लिए एक तानाशाही बनाने के बाद , कॉल करने के लिए जाता है _PyObject_FastCallDict

_PyObject_FastCallDictअंत में हमें कहीं मिलता है! प्रदर्शन करने के बाद और भी अधिक चेकों यह पकड़ लेता tp_callसे स्लॉटtype की typeहम में पारित कर दिया गया है, यह है कि, यह पकड़ लेता है type.tp_call। यह तब के साथ पारित तर्कों से बाहर एक ट्यूपल बनाने के लिए आगे बढ़ता है _PyStack_AsTupleऔर, आखिरकार, आखिरकार एक कॉल किया जा सकता है !

tp_call, जो मैच type.__call__लेता है और अंत में सूची ऑब्जेक्ट बनाता है। यह उन सूचियों को कॉल करता है __new__जो PyType_GenericNewइसके साथ मेमोरी से मेल खाती और आवंटित करती हैं PyType_GenericAlloc: यह वास्तव में वह हिस्सा है जहां यह PyList_Newआखिरकार पकड़ लेता है । सभी पिछले एक सामान्य फैशन में वस्तुओं को संभालने के लिए आवश्यक हैं।

अंत में, किसी भी उपलब्ध तर्कों के साथ सूची को type_callकॉल करता है list.__init__और आरंभ करता है, फिर हम जिस तरह से आए थे, उसी तरह वापस लौटते हैं। :-)

अंत में, रिमेम्बर LOAD_NAME, यह एक और लड़का है जो यहां योगदान देता है।


यह देखना आसान है कि, हमारे इनपुट से निपटने के दौरान, पायथन को आमतौर पर हुप्स के माध्यम से कूदना पड़ता है ताकि वास्तव Cमें काम करने के लिए उपयुक्त फ़ंक्शन का पता लगाया जा सके । इसमें तुरंत कॉल करने की शंका नहीं है क्योंकि यह गतिशील है, कोई व्यक्ति मुखौटा लगा सकता है list( और लड़का बहुत से लोग करते हैं ) और दूसरा रास्ता लेना होगा।

यह वह जगह है जहां list()बहुत कुछ खो देता है: पायथन की खोज यह जानने के लिए करने की आवश्यकता है कि इसे क्या करना चाहिए।

दूसरी ओर, साहित्यिक वाक्य रचना, वास्तव में एक चीज का मतलब है; इसे बदला नहीं जा सकता है और हमेशा पूर्व-निर्धारित तरीके से व्यवहार किया जाता है।

फुटनोट: सभी फ़ंक्शन नाम एक रिलीज़ से दूसरे में बदलने के अधीन हैं। बिंदु अभी भी खड़ा है और सबसे अधिक संभावना किसी भी भविष्य के संस्करणों में खड़ी होगी, यह गतिशील लुक-अप है जो चीजों को धीमा कर देती है।


13

से []तेज क्यों है list()?

सबसे बड़ा कारण यह है कि पायथन list()सिर्फ एक उपयोगकर्ता-परिभाषित फ़ंक्शन की तरह व्यवहार करता है , जिसका अर्थ है कि आप इसे किसी अन्य listचीज़ से अलग करके और कुछ अलग करने के लिए इसे रोक सकते हैं (जैसे कि अपनी स्वयं की उपवर्ग सूची या शायद एक छल का उपयोग करें)।

यह तुरंत एक अंतर्निहित सूची का एक नया उदाहरण बनाता है []

मेरी व्याख्या आपको इसके लिए अंतर्ज्ञान देना चाहती है।

व्याख्या

[] आमतौर पर शाब्दिक वाक्य रचना के रूप में जाना जाता है।

व्याकरण में, इसे "सूची प्रदर्शन" के रूप में संदर्भित किया जाता है। डॉक्स से :

एक सूची प्रदर्शन वर्ग कोष्ठक में संलग्न भावों की संभवतः एक खाली श्रृंखला है:

list_display ::=  "[" [starred_list | comprehension] "]"

एक सूची प्रदर्शन एक नई सूची ऑब्जेक्ट, अभिव्यक्ति की एक सूची या एक समझ द्वारा निर्दिष्ट की जा रही सामग्री देता है। जब अभिव्यक्तियों की अल्पविराम से अलग की गई सूची की आपूर्ति की जाती है, तो उसके तत्वों का मूल्यांकन बाएं से दाएं किया जाता है और उस क्रम में सूची ऑब्जेक्ट में रखा जाता है। जब एक समझ की आपूर्ति की जाती है, तो सूची का निर्माण उन तत्वों से किया जाता है जो समझ से उत्पन्न होते हैं।

संक्षेप में, इसका मतलब है कि प्रकार की एक निर्मित वस्तु listबनाई गई है।

इसको दरकिनार नहीं किया जाता है - जिसका अर्थ है कि पायथन इसे जल्दी से जल्दी कर सकता है।

दूसरी ओर, बिलिन सूची के निर्माणकर्ता का उपयोग करके list()एक बेसिन बनाने से रोक दिया जा सकता है list

उदाहरण के लिए, मान लें कि हम चाहते हैं कि हमारी सूचियाँ उत्तरोत्तर बनाई जाएं:

class List(list):
    def __init__(self, iterable=None):
        if iterable is None:
            super().__init__()
        else:
            super().__init__(iterable)
        print('List initialized.')

हम तब listमॉड्यूल स्तर के वैश्विक दायरे पर नाम को रोक सकते हैं , और फिर जब हम एक बनाते हैं list, तो हम वास्तव में हमारी उप-सूचीबद्ध सूची बनाते हैं:

>>> list = List
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

इसी तरह हम इसे वैश्विक नाम स्थान से हटा सकते हैं

del list

और इसे बिलियन नेमस्पेस में रखा:

import builtins
builtins.list = List

और अब:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

और ध्यान दें कि सूची प्रदर्शन बिना शर्त के एक सूची बनाता है:

>>> list_1 = []
>>> type(list_1)
<class 'list'>

हम शायद केवल अस्थायी रूप से ऐसा करते हैं, इसलिए हमारे परिवर्तनों को पूर्ववत करें - पहले Listबिलिंस से नई वस्तु हटा दें :

>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined

ओह, नहीं, हमने मूल का ट्रैक खो दिया है।

चिंता न करें, हम अभी भी प्राप्त कर सकते हैं list- यह सूची शाब्दिक का प्रकार है:

>>> builtins.list = type([])
>>> list()
[]

इसलिए...

से []तेज क्यों है list()?

जैसा कि हमने देखा है - हम अधिलेखित कर सकते हैं list- लेकिन हम शाब्दिक प्रकार के निर्माण को रोक नहीं सकते हैं। जब हम उपयोग listकरते हैं तो हमें यह देखने के लिए कि क्या कुछ है, देखने के लिए क्या करना है।

फिर हमें जो भी कॉल करने योग्य है, उसे देखना होगा। व्याकरण से:

एक कॉल एक कॉल करने योग्य वस्तु (उदाहरण के लिए, एक फ़ंक्शन) में संभवतः तर्कों की खाली श्रृंखला होती है:

call                 ::=  primary "(" [argument_list [","] | comprehension] ")"

हम देख सकते हैं कि यह किसी भी नाम के लिए समान है, न कि केवल सूची:

>>> import dis
>>> dis.dis('list()')
  1           0 LOAD_NAME                0 (list)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
  1           0 LOAD_NAME                0 (doesnotexist)
              2 CALL_FUNCTION            0
              4 RETURN_VALUE

के लिए []वहाँ अजगर बाईटकोड स्तर पर कोई समारोह कॉल है:

>>> dis.dis('[]')
  1           0 BUILD_LIST               0
              2 RETURN_VALUE

यह बस सीधे किसी भी लुकअप या कॉल के बिना सूची का निर्माण करने के लिए जाता है।

निष्कर्ष

हमने प्रदर्शित किया है कि listस्कूपिंग नियमों का उपयोग करके उपयोगकर्ता कोड के साथ अवरोधन किया जा सकता है, और वहlist() एक कॉल करने योग्य लगता है और फिर इसे कॉल करता है।

जबकि []एक सूची प्रदर्शन, या एक शाब्दिक है, और इस प्रकार नाम देखने और फ़ंक्शन कॉल से बचा जाता है।


2
+1 यह इंगित करने के लिए कि आप अपहरण कर सकते हैं listऔर अजगर कंपाइलर सुनिश्चित नहीं कर सकता है कि क्या यह वास्तव में एक खाली सूची लौटाएगा।
बीफ़स्टर
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.