การต่อสองรายการ - ความแตกต่างระหว่าง '+ =' และขยาย ()


243

ฉันเคยเห็นมีสองวิธี (อาจมากกว่า) เชื่อมรายการใน Python: วิธีหนึ่งคือการใช้วิธีขยาย ()

a = [1, 2]
b = [2, 3]
b.extend(a)

อีกอันใช้ตัวดำเนินการบวก (+):

b += a

ตอนนี้ฉันสงสัยว่า: ตัวเลือกใดในสองวิธีนี้คือ 'pythonic' ในการทำรายการ concatenation และมีความแตกต่างระหว่างทั้งสอง (ฉันได้ค้นหา Python tutorial อย่างเป็นทางการ แต่ไม่พบอะไรเกี่ยวกับหัวข้อนี้)


1
อาจจะแตกต่างกันมีผลกระทบมากขึ้นเมื่อมันมาถึงการดักไทปปิงและถ้าคุณอาจจะไม่ได้จริงๆ-a-รายการ แต่เหมือน-a-รายการสนับสนุน.__iadd__()/ .__add__()/ .__radd__()เมื่อเทียบกับ.extend()
นิคที

คำตอบ:


214

ความแตกต่างเพียงอย่างเดียวในระดับ bytecode ก็คือ.extendวิธีนี้เกี่ยวข้องกับการเรียกใช้ฟังก์ชันซึ่งมีราคาแพงกว่าใน Python INPLACE_ADDเล็กน้อย

มันไม่มีอะไรที่คุณควรกังวลเลยนอกจากว่าคุณจะทำสิ่งนี้เป็นพันล้านครั้ง อย่างไรก็ตามมีโอกาสที่คอขวดจะอยู่ที่อื่น


16
อาจจะแตกต่างกันมีผลกระทบมากขึ้นเมื่อมันมาถึงการดักไทปปิงและถ้าคุณอาจจะไม่ได้จริงๆ-a-รายการ แต่เหมือน-a-รายการสนับสนุน.__iadd__()/ .__add__()/ .__radd__()เมื่อเทียบกับ.extend()
นิคที

8
คำตอบนี้ไม่ได้พูดถึงความแตกต่างของการกำหนดขอบเขตที่สำคัญ
Wim

3
ที่จริงแล้วการขยายนั้นเร็วกว่า INPLACE_ADD () เช่นการต่อข้อมูล gist.github.com/mekarpeles/3408081
Archit Kapoor

178

คุณไม่สามารถใช้ + = สำหรับตัวแปรที่ไม่ใช่โลคัล (ตัวแปรที่ไม่ใช่โลคอลสำหรับฟังก์ชั่นและไม่ใช่โกลบอล)

def main():
    l = [1, 2, 3]

    def foo():
        l.extend([4])

    def boo():
        l += [5]

    foo()
    print l
    boo()  # this will fail

main()

เป็นเพราะการขยายตัวรวบรวมเคสจะโหลดตัวแปรlโดยใช้LOAD_DEREFคำสั่ง แต่สำหรับ + ​​= มันจะใช้LOAD_FAST- และคุณจะได้รับ*UnboundLocalError: local variable 'l' referenced before assignment*


4
ฉันมีปัญหากับคำอธิบายของคุณ "ตัวแปรซึ่งไม่ได้อยู่ในฟังก์ชันและยังไม่ใช่สากล " คุณสามารถยกตัวอย่างของตัวแปรได้หรือไม่?
Stephane Rolland

8
ตัวแปร 'l' ในตัวอย่างของฉันเป็นแบบนั้น ไม่ใช่เฉพาะฟังก์ชั่น 'foo' และ 'boo' (นอกขอบเขต) แต่ไม่ใช่ฟังก์ชั่นทั่วโลก (กำหนดไว้ภายใน 'main' func ไม่ใช่ในระดับโมดูล)
monitorius

3
ฉันสามารถยืนยันได้ว่าข้อผิดพลาดนี้ยังคงเกิดขึ้นกับ python 3.4.2 (คุณจะต้องเพิ่มเครื่องหมายวงเล็บในการพิมพ์ แต่ทุกอย่างจะยังคงเหมือนเดิม)
trichoplax

7
ถูกตัอง. แต่อย่างน้อยคุณสามารถใช้คำสั่งnonlocal lในbooใน Python3
monitorius

คอมไพเลอร์ -> ล่าม?
joelb

42

คุณสามารถโยงการเรียกฟังก์ชั่นได้ แต่คุณไม่สามารถ + = การเรียกใช้ฟังก์ชันโดยตรง:

class A:
    def __init__(self):
        self.listFoo = [1, 2]
        self.listBar = [3, 4]

    def get_list(self, which):
        if which == "Foo":
            return self.listFoo
        return self.listBar

a = A()
other_list = [5, 6]

a.get_list("Foo").extend(other_list)
a.get_list("Foo") += other_list  #SyntaxError: can't assign to function call

8

ฉันจะบอกว่ามีความแตกต่างบางอย่างเมื่อมันมาพร้อมกับ numpy (ฉันเพิ่งเห็นว่าคำถามถามเกี่ยวกับการต่อสองรายการไม่ใช่อาร์เรย์ numpy แต่เนื่องจากมันอาจเป็นปัญหาสำหรับมือใหม่เช่นฉันฉันหวังว่าสิ่งนี้จะช่วยคนได้ ผู้ที่หาทางแก้ไขโพสต์นี้) เช่น

import numpy as np
a = np.zeros((4,4,4))
b = []
b += a

มันจะกลับมาพร้อมกับข้อผิดพลาด

ValueError: ตัวถูกดำเนินการไม่สามารถออกอากาศพร้อมกับรูปร่าง (0,) (4,4,4)

b.extend(a) ทำงานได้อย่างสมบูรณ์แบบ



4

ขยาย () ทำงานร่วมกับ iterable ใด ๆ *, + = ทำงานร่วมกับบางส่วน แต่จะได้รับขี้ขลาด

import numpy as np

l = [2, 3, 4]
t = (5, 6, 7)
l += t
l
[2, 3, 4, 5, 6, 7]

l = [2, 3, 4]
t = np.array((5, 6, 7))
l += t
l
array([ 7,  9, 11])

l = [2, 3, 4]
t = np.array((5, 6, 7))
l.extend(t)
l
[2, 3, 4, 5, 6, 7]

Python 3.6
* ค่อนข้างมั่นใจ. ภาคผนวก () ใช้ได้กับ iterable ใด ๆ แต่โปรดแสดงความคิดเห็นหากฉันไม่ถูกต้อง


Tuple แน่นอน iterable แต่ไม่มีวิธีการขยาย () วิธีการขยาย () ไม่มีอะไรจะทำอย่างไรกับการทำซ้ำ
wombatonfire

.extend เป็นวิธีการของรายการระดับ จากเอกสารของ Python: list.extend(iterable) Extend the list by appending all the items from the iterable. Equivalent to a[len(a):] = iterable.ฉันคิดว่าฉันตอบดอกจันของฉันเอง
grofte

โอ้คุณหมายถึงว่าคุณสามารถผ่านการยืดได้ () ฉันอ่านมันว่า "ขยาย () สามารถใช้ได้สำหรับ iterable ใด ๆ " :) ฉันไม่ดี แต่มันฟังดูคลุมเครือเล็กน้อย
wombatonfire

1
โดยรวมแล้วนี่ไม่ใช่ตัวอย่างที่ดีอย่างน้อยก็ไม่ใช่ในบริบทของคำถามนี้ เมื่อคุณใช้+=โอเปอเรเตอร์กับวัตถุประเภทต่าง ๆ (ตรงกันข้ามกับสองรายการดังในคำถาม) คุณไม่สามารถคาดหวังได้ว่าคุณจะได้รับการต่อกันของวัตถุ และคุณไม่สามารถคาดหวังได้ว่าจะมีการlistส่งคืนประเภท มีลักษณะที่รหัสของคุณคุณจะได้รับแทนnumpy.ndarray list
อมแบตไฟร์

2

ที่จริงมีความแตกต่างระหว่างสามตัวเลือก: ADD, และINPLACE_ADD extendอดีตมักจะช้ากว่าเสมอในขณะที่อีกสองคนจะเป็นแบบเดียวกัน

ด้วยข้อมูลนี้ผมค่อนข้างจะใช้extendซึ่งจะเร็วกว่าและดูเหมือนว่าฉันมากขึ้นอย่างชัดเจนของสิ่งที่คุณกำลังทำมากกว่าADDINPLACE_ADD

ลองใช้รหัสต่อไปนี้สองสามครั้ง (สำหรับ Python 3):

import time

def test():
    x = list(range(10000000))
    y = list(range(10000000))
    z = list(range(10000000))

    # INPLACE_ADD
    t0 = time.process_time()
    z += x
    t_inplace_add = time.process_time() - t0

    # ADD
    t0 = time.process_time()
    w = x + y
    t_add = time.process_time() - t0

    # Extend
    t0 = time.process_time()
    x.extend(y)
    t_extend = time.process_time() - t0

    print('ADD {} s'.format(t_add))
    print('INPLACE_ADD {} s'.format(t_inplace_add))
    print('extend {} s'.format(t_extend))
    print()

for i in range(10):
    test()
ADD 0.3540440000000018 s
INPLACE_ADD 0.10896000000000328 s
extend 0.08370399999999734 s

ADD 0.2024550000000005 s
INPLACE_ADD 0.0972940000000051 s
extend 0.09610200000000191 s

ADD 0.1680199999999985 s
INPLACE_ADD 0.08162199999999586 s
extend 0.0815160000000077 s

ADD 0.16708400000000267 s
INPLACE_ADD 0.0797719999999913 s
extend 0.0801490000000058 s

ADD 0.1681250000000034 s
INPLACE_ADD 0.08324399999999343 s
extend 0.08062700000000689 s

ADD 0.1707760000000036 s
INPLACE_ADD 0.08071900000000198 s
extend 0.09226200000000517 s

ADD 0.1668420000000026 s
INPLACE_ADD 0.08047300000001201 s
extend 0.0848089999999928 s

ADD 0.16659500000000094 s
INPLACE_ADD 0.08019399999999166 s
extend 0.07981599999999389 s

ADD 0.1710910000000041 s
INPLACE_ADD 0.0783479999999912 s
extend 0.07987599999999873 s

ADD 0.16435900000000458 s
INPLACE_ADD 0.08131200000001115 s
extend 0.0818660000000051 s

2
คุณไม่สามารถเปรียบเทียบADDกับและINPLACE_ADD สร้างรายการใหม่และคัดลอกองค์ประกอบของสองรายการเดิมไป สำหรับแน่ใจว่ามันจะช้ากว่าการดำเนินงานของ inplace และ extend()ADDINPLACE_ADDextend()
wombatonfire

ฉันรู้แล้ว. จุดตัวอย่างนี้เป็นการเปรียบเทียบวิธีต่าง ๆ ในการมีรายการกับองค์ประกอบทั้งหมดเข้าด้วยกัน แน่นอนว่ามันใช้เวลานานกว่าเพราะมันทำสิ่งที่แตกต่างกัน แต่ก็ยังเป็นเรื่องดีที่จะรู้ในกรณีที่คุณสนใจที่จะอนุรักษ์วัตถุดั้งเดิมไม่เปลี่ยนแปลง
dalonsoa

1

ฉันค้นหาบทช่วยสอนของ Python อย่างเป็นทางการแล้ว แต่ไม่พบสิ่งใดเกี่ยวกับหัวข้อนี้

ข้อมูลนี้จะถูกฝังอยู่ในคำถามที่พบบ่อยการเขียนโปรแกรม :

... สำหรับรายการ__iadd__[ie +=] เทียบเท่ากับการโทรextendในรายการและส่งคืนรายการ นั่นเป็นเหตุผลที่เราพูดว่าสำหรับรายการ+=เป็น "ชวเลข" สำหรับlist.extend

คุณสามารถดูได้ด้วยตัวคุณเองในซอร์สโค้ดของ CPython: https://github.com/python/cpython/blob/v3.8.2/Objects/listobject.c#L1000-L1011


-1

ตาม Python สำหรับการวิเคราะห์ข้อมูล

“ โปรดทราบว่าการต่อข้อมูลแบบเรียงลำดับโดยการเพิ่มเป็นการดำเนินการที่ค่อนข้างแพงเนื่องจากต้องสร้างรายการใหม่และคัดลอกวัตถุไป การใช้ขยายเพื่อผนวกองค์ประกอบเข้ากับรายการที่มีอยู่โดยเฉพาะอย่างยิ่งถ้าคุณกำลังสร้างรายการขนาดใหญ่มักจะดีกว่า ” ดังนั้น

everything = []
for chunk in list_of_lists:
    everything.extend(chunk)

เร็วกว่าทางเลือกที่ต่อกัน:

everything = []
for chunk in list_of_lists:
    everything = everything + chunk

ป้อนคำอธิบายรูปภาพที่นี่ ป้อนคำอธิบายรูปภาพที่นี่


4
everything = everything + tempeverything += tempไม่ได้ดำเนินการจำเป็นต้องอยู่ในลักษณะเดียวกับที่
David Harrison

1
คุณพูดถูก ขอบคุณสำหรับการเตือน แต่ประเด็นของฉันคือความแตกต่างของประสิทธิภาพ :)
littlebear333

6
@ littlebear333 everything += tempมีการใช้งานในลักษณะที่everythingไม่จำเป็นต้องคัดลอก สวยมากทำให้คำตอบของคุณเป็นจุดที่สงสัย
nog642
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.