ตัวดำเนินการ“ is” ทำงานแบบไม่คาดคิดกับจำนวนเต็ม


509

ทำไมสิ่งต่อไปนี้ถึงทำงานโดยไม่คาดหมายใน Python

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result
>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?
>>> 257 is 257
True           # Yet the literal numbers compare properly

ฉันใช้ Python 2.5.2 ลองใช้ Python เวอร์ชั่นอื่นพบว่า Python 2.3.3 แสดงพฤติกรรมข้างต้นระหว่าง 99 ถึง 100

จากข้างต้นฉันสามารถตั้งสมมติฐานได้ว่า Python ถูกนำไปใช้ภายในซึ่งจำนวนเต็ม "เล็ก" จะถูกเก็บไว้ในวิธีที่แตกต่างจากจำนวนเต็มขนาดใหญ่กว่าและisผู้ดำเนินการสามารถบอกความแตกต่างได้ ทำไมสิ่งที่เป็นนามธรรมรั่ว? เป็นวิธีที่ดีกว่าของการเปรียบเทียบวัตถุสองโดยพลอะไรเพื่อดูว่าพวกเขาจะเหมือนกันเมื่อฉันไม่ทราบล่วงหน้าว่าพวกเขาเป็นตัวเลขหรือไม่


1
ดูที่นี่ > การใช้งานปัจจุบันเก็บอาร์เรย์ของวัตถุจำนวนเต็มสำหรับ> จำนวนเต็มทั้งหมดระหว่าง -5 และ 256 เมื่อคุณสร้าง int ในช่วงนั้นคุณจริง ๆ แล้วเพิ่งได้รับการอ้างอิงกลับไปยังวัตถุที่มีอยู่
user5319825

2
นี่เป็นรายละเอียดการติดตั้งเฉพาะของ CPython และพฤติกรรมที่ไม่ได้กำหนดใช้กับข้อควรระวัง
ospider

คำตอบ:


392

ดูที่นี้:

>>> a = 256
>>> b = 256
>>> id(a)
9987148
>>> id(b)
9987148
>>> a = 257
>>> b = 257
>>> id(a)
11662816
>>> id(b)
11662828

นี่คือสิ่งที่ฉันพบในเอกสาร Python 2 "Plain Integer Objects" (เหมือนกันกับPython 3 ):

การใช้งานปัจจุบันเก็บอาร์เรย์ของวัตถุจำนวนเต็มสำหรับจำนวนเต็มทั้งหมดระหว่าง -5 และ 256 เมื่อคุณสร้าง int ในช่วงนั้นคุณจะได้รับการอ้างอิงกลับไปยังวัตถุที่มีอยู่จริง ดังนั้นควรเปลี่ยนค่าของ 1 ฉันสงสัยว่าพฤติกรรมของ Python ในกรณีนี้จะไม่ได้กำหนด :-)


46
ไม่มีใครรู้วิธีการเลือกช่วงที่ (-5, 256)? ฉันจะไม่แปลกใจถ้ามันเป็น (0, 255) หรือแม้กระทั่ง (-255, 255) แต่ช่วงของตัวเลข 262 เริ่มต้นที่ -5 ดูเหมือนแปลกใจโดยพลการ
Woodrow Barlow

6
@ WoodrowBarlow: -5 เป็นเพียงฮิวริสติกในการจับตัวยึดตำแหน่งเชิงลบที่เหมือนกันฉันคิดว่า 0..255 ครอบคลุมอาร์เรย์ของค่าไบต์เดียว มันเป็น 256 ที่ลึกลับ แต่ฉันคิดว่ามันเป็น (dis) รวบรวมจำนวนเต็มเป็น / จากไบต์
Davis Herring

3
จากสิ่งที่ฉันเข้าใจช่วงที่เลือกโดยดูค่าที่ใช้กันทั่วไปในหลายโครงการ (และหลายภาษา)
Tony Suffolk 66

9
ตามreddit.com/r/Python/comments/18leav/…ช่วงที่เคยเป็น [-5,100] มันถูกขยายเพื่อรวมช่วงเต็มของค่าไบต์ - บวก 256 เพราะนั่นอาจเป็นตัวเลขทั่วไป
mwfearnley

2
@ Ashwani ลองอ่านความคิดเห็นถัดจากความคิดเห็นของคุณโพสต์เมื่อสองปีก่อนและคุณจะพบคำตอบสำหรับคำถามของคุณ
jbg

116

ตัวดำเนินการ“ is” ของ Python ทำงานโดยไม่คาดหมายกับจำนวนเต็มหรือไม่

โดยสรุป - ให้ฉันเน้น: อย่าใช้isเพื่อเปรียบเทียบจำนวนเต็ม

นี่ไม่ใช่พฤติกรรมที่คุณควรคาดหวัง

ให้ใช้==และ!=เปรียบเทียบความเสมอภาคและความไม่เท่าเทียมกันตามลำดับ ตัวอย่างเช่น:

>>> a = 1000
>>> a == 1000       # Test integers like this,
True
>>> a != 5000       # or this!
True
>>> a is 1000       # Don't do this! - Don't use `is` to test integers!!
False

คำอธิบาย

หากต้องการทราบสิ่งนี้คุณต้องรู้สิ่งต่อไปนี้

ก่อนอื่นจะisทำอย่างไร? มันเป็นตัวดำเนินการเปรียบเทียบ จากเอกสาร :

โอเปอเรเตอร์isและis notทดสอบการระบุตัวตนของวัตถุ: x is yเป็นจริงถ้าและถ้า x และ y เป็นวัตถุเดียวกัน x is not yให้ค่าความจริงผกผัน

และต่อไปนี้เทียบเท่า

>>> a is b
>>> id(a) == id(b)

จากเอกสาร :

id ส่งคืน“ เอกลักษณ์” ของวัตถุ นี่คือเลขจำนวนเต็ม (หรือจำนวนเต็มแบบยาว) ซึ่งรับประกันว่าจะไม่ซ้ำกันและคงที่สำหรับวัตถุนี้ในช่วงชีวิตของมัน วัตถุสองชิ้นที่มีอายุการใช้งานไม่ทับซ้อนกันอาจมีid()ค่าเท่ากัน

โปรดทราบว่าความจริงที่ว่า id ของวัตถุใน CPython (การใช้งานอ้างอิงของ Python) คือตำแหน่งในหน่วยความจำคือรายละเอียดการใช้งาน การใช้งานอื่น ๆ ของ Python (เช่น Jython หรือ IronPython) อาจมีการใช้งานที่แตกต่างกันได้อย่างง่ายดายidได้ง่ายอาจจะมีการดำเนินงานที่แตกต่างกันสำหรับ

ดังนั้นสิ่งที่ใช้สำหรับกรณีis? PEP8 อธิบาย :

การเปรียบเทียบกับแบบซิงเกิลNoneควรทำด้วยเสมอisหรือ is notไม่ควรใช้ตัวดำเนินการที่เท่าเทียมกัน

คำถาม

คุณถามและระบุคำถามต่อไปนี้ (พร้อมรหัส):

ทำไมสิ่งต่อไปนี้ถึงทำงานโดยไม่คาดหมายใน Python

>>> a = 256
>>> b = 256
>>> a is b
True           # This is an expected result

มันไม่ได้เป็นผลลัพธ์ที่คาดหวัง ทำไมถึงเป็นเช่นนั้น มันหมายความว่าจำนวนเต็มมูลค่าที่256อ้างอิงโดยทั้งสองaและbเป็นอินสแตนซ์เดียวกันของจำนวนเต็ม จำนวนเต็มไม่เปลี่ยนแปลงใน Python ดังนั้นจึงไม่สามารถเปลี่ยนแปลงได้ สิ่งนี้ไม่ควรมีผลกระทบกับรหัสใด ๆ ไม่ควรคาดหวัง มันเป็นเพียงรายละเอียดการดำเนินการ

แต่บางทีเราควรจะดีใจที่ไม่มีอินสแตนซ์แยกใหม่ในหน่วยความจำทุกครั้งที่เราระบุค่าเท่ากับ 256

>>> a = 257
>>> b = 257
>>> a is b
False          # What happened here? Why is this False?

ดูเหมือนว่าตอนนี้เรามีจำนวนเต็มสองอินสแตนซ์แยกกันโดยมีค่าเป็น257ในหน่วยความจำ เนื่องจากจำนวนเต็มไม่เปลี่ยนรูปหน่วยความจำนี้จึงสูญเปล่า หวังว่าเราจะไม่เสียอะไรมากมาย เราอาจจะไม่ แต่พฤติกรรมนี้ไม่รับประกัน

>>> 257 is 257
True           # Yet the literal numbers compare properly

ดูเหมือนว่าการติดตั้ง Python โดยเฉพาะของคุณกำลังพยายามที่จะฉลาดและไม่ได้สร้างจำนวนเต็มที่มีคุณค่าซ้ำซ้อนในหน่วยความจำเว้นแต่ว่ามันจะต้อง ดูเหมือนว่าคุณกำลังใช้การอ้างอิงของ Python ซึ่งก็คือ CPython ดีสำหรับ CPython

มันอาจจะดีกว่านี้ถ้า CPython สามารถทำสิ่งนี้ได้ทั่วโลกถ้าทำได้อย่างถูก

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

สิ่งที่isไม่

isตรวจสอบว่าidวัตถุทั้งสองนั้นเหมือนกัน ใน CPython นั้นidเป็นตำแหน่งในหน่วยความจำ แต่อาจเป็นหมายเลขที่ไม่ซ้ำกันอื่น ๆ ในการนำไปใช้อื่น ในการย้ำรหัสนี้ด้วย:

>>> a is b

เป็นเช่นเดียวกับ

>>> id(a) == id(b)

ทำไมเราต้องการใช้isแล้ว

นี่อาจเป็นการตรวจสอบที่เร็วมากเมื่อเปรียบเทียบกับการพูดว่าการตรวจสอบว่าสตริงที่ยาวมากสองรายการมีค่าเท่ากันหรือไม่ แต่เนื่องจากมันใช้กับลักษณะเฉพาะของวัตถุเราจึงมีข้อ จำกัด ในการใช้งานสำหรับมัน ในความเป็นจริงเราส่วนใหญ่ต้องการใช้เพื่อตรวจสอบNoneซึ่งเป็น singleton (อินสแตนซ์เดียวที่มีอยู่ในที่เดียวในหน่วยความจำ) เราอาจสร้าง singletons อื่น ๆ หากมีความเป็นไปได้ที่จะทำให้พวกเขาสับสนซึ่งเราอาจตรวจสอบด้วยisแต่สิ่งเหล่านี้ค่อนข้างหายาก นี่คือตัวอย่าง (จะทำงานใน Python 2 และ 3) เช่น

SENTINEL_SINGLETON = object() # this will only be created one time.

def foo(keyword_argument=None):
    if keyword_argument is None:
        print('no argument given to foo')
    bar()
    bar(keyword_argument)
    bar('baz')

def bar(keyword_argument=SENTINEL_SINGLETON):
    # SENTINEL_SINGLETON tells us if we were not passed anything
    # as None is a legitimate potential argument we could get.
    if keyword_argument is SENTINEL_SINGLETON:
        print('no argument given to bar')
    else:
        print('argument to bar: {0}'.format(keyword_argument))

foo()

สิ่งที่พิมพ์:

no argument given to foo
no argument given to bar
argument to bar: None
argument to bar: baz

และเพื่อให้เราเห็นด้วยisและแมวมองเราสามารถที่จะแยกความแตกต่างระหว่างเวลาที่เรียกว่ามีการขัดแย้งใดและเมื่อมันถูกเรียกว่ามีbar Noneเหล่านี้เป็นหลักกรณีการใช้งานสำหรับis- ไม่ได้ใช้ในการทดสอบเพื่อความเท่าเทียมกันของจำนวนเต็มสตริง tuples สิ่งอื่น ๆ เช่นนี้หรือ


"นี่เป็นกรณีการใช้งานหลักis- ห้ามใช้เพื่อทดสอบความเท่าเทียมของจำนวนเต็ม, สตริง, สิ่งอันดับ, หรือสิ่งอื่น ๆ เช่นนี้" isแต่ผมกำลังพยายามที่จะบูรณาการกลไกของรัฐง่ายในชั้นเรียนของฉันและตั้งแต่รัฐเป็นค่าทึบแสงที่มีคุณสมบัติเฉพาะที่สังเกตได้ก็คือว่าเป็นเหมือนกันหรือแตกต่างกันก็ดูเป็นธรรมชาติมากสำหรับพวกเขาที่จะเทียบเคียงกับ ฉันวางแผนที่จะใช้สตริง interned เป็นสถานะ ฉันต้องการเลขจำนวนเต็มธรรมดา แต่น่าเสียดายที่ Python ไม่สามารถใช้เลขจำนวนเต็ม ( 0 is 0เป็นรายละเอียดการนำไปใช้)
Alexey

@Alexey ดูเหมือนว่าคุณต้องการ enums? stackoverflow.com/questions/37601644/…
Aaron Hall

บางทีขอบคุณไม่รู้จักพวกเขา นี่อาจเป็นการเพิ่มเติมที่เหมาะสมสำหรับคุณที่จะตอบ IMO
Alexey

บางทีการใช้วัตถุที่เป็นใบ้จำนวนมากเช่นทหารยามในคำตอบของคุณอาจเป็นวิธีแก้ปัญหาที่เบากว่า ...
Alexey

@Alexey enums อยู่ในไลบรารีมาตรฐานของ Python 3 และนั่นอาจกระตุ้นให้โค้ดของคุณมีความหมายมากกว่า sentinels เปลือย ๆ
Aaron Hall

60

ขึ้นอยู่กับว่าคุณกำลังดูว่า 2 สิ่งเท่ากันหรือวัตถุเดียวกัน

isตรวจสอบเพื่อดูว่าพวกเขาเป็นวัตถุเดียวกันไม่เท่ากัน int ขนาดเล็กอาจชี้ไปที่ตำแหน่งหน่วยความจำเดียวกันเพื่อประสิทธิภาพของพื้นที่

In [29]: a = 3
In [30]: b = 3
In [31]: id(a)
Out[31]: 500729144
In [32]: id(b)
Out[32]: 500729144

คุณควรใช้==เพื่อเปรียบเทียบความเท่าเทียมกันของวัตถุใด ๆ คุณสามารถระบุพฤติกรรมด้วย__eq__และและ__ne__คุณสมบัติ


ยกนิ้วให้จริงเพื่ออธิบายวิธีเปรียบเทียบวัตถุโดยพลการเช่น OP ถาม !!
Joooeey

54

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


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

ใน CPython ที่C-APIฟังก์ชั่นที่จับสร้างใหม่วัตถุint PyLong_FromLong(long v)คำอธิบายสำหรับฟังก์ชั่นนี้คือ:

การดำเนินงานในปัจจุบันช่วยให้อาร์เรย์ของวัตถุจำนวนเต็มสำหรับจำนวนเต็มทั้งหมดระหว่าง -5 และ 256 เมื่อคุณสร้าง int ในช่วงที่คุณจริงเพียงแค่ได้รับกลับอ้างอิงถึงวัตถุที่มีอยู่ ดังนั้นจึงเป็นไปได้ที่จะเปลี่ยนค่า 1 ฉันสงสัยว่าพฤติกรรมของ Python ในกรณีนี้ไม่ได้กำหนดไว้ :-)

(ตัวเอียงของฉัน)

ไม่รู้เรื่องเกี่ยวกับคุณ แต่ฉันเห็นสิ่งนี้และคิดว่า: เรามาหาแถวนั้นกัน!

หากคุณยังไม่ได้เล่นกับรหัส C ที่ใช้ CPython คุณควร ; ทุกอย่างสวยจัดและอ่านได้ สำหรับกรณีของเราเราต้องดูในObjectsไดเรกทอรีย่อยของต้นไม้หลักไดเรกทอรีรหัสแหล่งที่มา

PyLong_FromLongข้อตกลงกับวัตถุดังนั้นจึงไม่ควรจะยากที่จะสรุปว่าเราจำเป็นต้องมองภายในlong longobject.cหลังจากมองเข้าไปข้างในคุณอาจคิดว่าสิ่งต่าง ๆ วุ่นวาย ฟังก์ชั่นที่เรากำลังมองหาอยู่นั้นหนาวเหน็บที่บรรทัดที่ 230 ที่รอให้เราตรวจสอบ มันเป็นฟังก์ชั่นขนาดเล็กดังนั้นตัวหลัก (ไม่รวมการประกาศ) วางได้อย่างง่ายดายที่นี่:

PyObject *
PyLong_FromLong(long ival)
{
    // omitting declarations

    CHECK_SMALL_INT(ival);

    if (ival < 0) {
        /* negate: cant write this as abs_ival = -ival since that
           invokes undefined behaviour when ival is LONG_MIN */
        abs_ival = 0U-(unsigned long)ival;
        sign = -1;
    }
    else {
        abs_ival = (unsigned long)ival;
    }

    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SIZE(v) = sign;
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v; 
}

ตอนนี้เราไม่ใช่ C master-code-haxxorzแต่เราก็ไม่ได้โง่เราจะเห็นว่าการCHECK_SMALL_INT(ival);แอบมองเราอย่างเย้ายวน เราสามารถเข้าใจได้ว่ามันเกี่ยวข้องกับเรื่องนี้ ลองดูกัน:

#define CHECK_SMALL_INT(ival) \
    do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
        return get_small_int((sdigit)ival); \
    } while(0)

ดังนั้นมันเป็นมาโครที่เรียกใช้ฟังก์ชันget_small_intถ้าค่าivalเป็นไปตามเงื่อนไข:

if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS)

ดังนั้นสิ่งที่เป็นNSMALLNEGINTSและNSMALLPOSINTS? แมโคร! ที่นี่พวกเขาคือ :

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

ดังนั้นเงื่อนไขของเราคือการif (-5 <= ival && ival < 257)โทรget_small_intการโทร

ต่อไปเรามาดูget_small_intในความรุ่งโรจน์ของมัน (เอาล่ะเราจะดูที่ร่างกายเพราะนั่นคือสิ่งที่น่าสนใจ):

PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);

โอเคประกาศ a PyObjectยืนยันว่าเงื่อนไขก่อนหน้าถือและดำเนินการมอบหมาย:

v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];

small_intsดูเหมือนอาเรย์มากมายที่เราเคยค้นหาและมันก็เป็น! เราสามารถอ่านเอกสารคำด่าแล้วเราก็จะรู้มาตลอด! :

/* Small integers are preallocated in this array so that they
   can be shared.
   The integers that are preallocated are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

ใช่แล้วนี่คือคนของเรา เมื่อคุณต้องการสร้างใหม่intในช่วง[NSMALLNEGINTS, NSMALLPOSINTS)คุณจะได้รับการอ้างอิงกลับไปยังวัตถุที่มีอยู่แล้วที่ได้รับการจัดสรรล่วงหน้า

เนื่องจากการอ้างอิงอ้างถึงวัตถุเดียวกันออกid()โดยตรงหรือตรวจสอบตัวตนด้วยisมันจะส่งคืนสิ่งเดียวกัน

แต่พวกเขาจะจัดสรรเมื่อไหร่ ??

ในระหว่างการเริ่มต้นใน_PyLong_Init Python ยินดีที่จะใส่ในห่วงสำหรับทำเช่นนี้สำหรับคุณ:

for (ival = -NSMALLNEGINTS; ival <  NSMALLPOSINTS; ival++, v++) {

ลองดูแหล่งที่มาเพื่ออ่านเนื้อความลูป!

ฉันหวังว่าคำอธิบายของฉันทำให้คุณชัดเจนในสิ่งCตอนนี้ (ปุนตั้งใจชัดเจน)


แต่, 257 is 257 ? ว่าไง?

นี้เป็นจริงได้ง่ายขึ้นที่จะอธิบายและฉันมีความพยายามที่จะทำเช่นนั้นอยู่แล้ว ; เป็นเพราะข้อเท็จจริงที่ว่า Python จะดำเนินการคำสั่งแบบโต้ตอบนี้เป็นบล็อกเดียว:

>>> 257 is 257

ในช่วง complilation ของคำสั่งนี้ CPython จะเห็นว่าคุณมีสองตัวอักษรที่ตรงกันและจะใช้เหมือนกันที่เป็นตัวแทนของPyLongObject 257คุณสามารถดูสิ่งนี้ได้หากคุณรวบรวมและตรวจสอบเนื้อหา:

>>> codeObj = compile("257 is 257", "blah!", "exec")
>>> codeObj.co_consts
(257, None)

เมื่อ CPython ทำการดำเนินการตอนนี้มันจะโหลดวัตถุเดียวกันที่แน่นอน:

>>> import dis
>>> dis.dis(codeObj)
  1           0 LOAD_CONST               0 (257)   # dis
              3 LOAD_CONST               0 (257)   # dis again
              6 COMPARE_OP               8 (is)

ดังนั้นจะกลับมาisTrue


37

ในขณะที่คุณสามารถตรวจสอบในไฟล์ต้นฉบับintobject.c , Python แคชจำนวนเต็มขนาดเล็กเพื่อประสิทธิภาพ ทุกครั้งที่คุณสร้างการอ้างอิงถึงจำนวนเต็มขนาดเล็กคุณจะอ้างอิงจำนวนเต็มขนาดเล็กที่แคชไม่ใช่วัตถุใหม่ 257 ไม่ใช่จำนวนเต็มขนาดเล็กดังนั้นจึงคำนวณเป็นวัตถุอื่น

มันจะดีกว่าที่จะใช้==เพื่อจุดประสงค์นั้น


19

ฉันคิดว่าสมมติฐานของคุณถูกต้อง การทดสอบด้วยid(เอกลักษณ์ของวัตถุ):

In [1]: id(255)
Out[1]: 146349024

In [2]: id(255)
Out[2]: 146349024

In [3]: id(257)
Out[3]: 146802752

In [4]: id(257)
Out[4]: 148993740

In [5]: a=255

In [6]: b=255

In [7]: c=257

In [8]: d=257

In [9]: id(a), id(b), id(c), id(d)
Out[9]: (146349024, 146349024, 146783024, 146804020)

ดูเหมือนว่าตัวเลข<= 255จะถือว่าเป็นตัวอักษรและสิ่งใด ๆ ข้างต้นจะถือว่าแตกต่างกัน!


1
มันเป็นเพราะวัตถุที่เป็นตัวแทนของค่าจาก -5 ถึง +256 ถูกสร้างขึ้นในเวลาเริ่มต้น - และเพื่อให้การใช้ค่าเหล่านั้นทั้งหมดเพื่อสร้างวัตถุล่วงหน้า การอ้างอิงเกือบทั้งหมดไปยังจำนวนเต็มภายนอกช่วงนั้นสร้างวัตถุภายในใหม่ทุกครั้งที่มีการอ้างอิง ฉันคิดว่าการใช้คำตามตัวอักษรทำให้เกิดความสับสน - ตามตัวอักษรตามปกติหมายถึงค่าใด ๆ ที่พิมพ์ในโค้ด - ดังนั้นจำนวนทั้งหมดในซอร์สโค้ดจึงเป็นตัวอักษร
Tony Suffolk 66

13

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


12

มีอีกประเด็นที่ไม่ได้ชี้ให้เห็นในคำตอบที่มีอยู่ Python ได้รับอนุญาตให้รวมสองค่าที่ไม่เปลี่ยนรูปได้และค่า int ขนาดเล็กที่สร้างไว้ล่วงหน้าไม่ใช่วิธีเดียวที่จะเกิดขึ้นได้ การใช้ Python ไม่รับประกันจะทำเช่นนี้ แต่พวกมันทั้งหมดทำเพื่อมากกว่าแค่ ints ขนาดเล็ก


สำหรับสิ่งหนึ่งที่มีบางค่าก่อนสร้างอื่น ๆ เช่นที่ว่างเปล่าtuple, strและbytesและบางสายสั้น ๆ (ใน CPython 3.6 ก็ 256 ตัวเดียว Latin-1 สตริง) ตัวอย่างเช่น:

>>> a = ()
>>> b = ()
>>> a is b
True

แต่ค่าที่ไม่ได้สร้างไว้ล่วงหน้าก็สามารถเหมือนกันได้ ลองพิจารณาตัวอย่างเหล่านี้:

>>> c = 257
>>> d = 257
>>> c is d
False
>>> e, f = 258, 258
>>> e is f
True

และสิ่งนี้ไม่ จำกัด เฉพาะintค่า:

>>> g, h = 42.23e100, 42.23e100
>>> g is h
True

เห็นได้ชัดว่า CPython ไม่ได้มาพร้อมกับfloatค่าที่สร้างไว้ล่วงหน้าสำหรับ42.23e100ความคุ้มค่าแล้วเกิดอะไรขึ้นที่นี่?

คอมไพเลอร์ CPython จะผสานค่าคงที่ของชนิดที่รู้จักกันไม่เปลี่ยนรูปบางอย่างเช่นint, float, str, bytes, ในหน่วยรวบรวมเดียวกัน สำหรับโมดูลโมดูลทั้งหมดเป็นหน่วยการรวบรวม แต่ที่ล่ามแบบโต้ตอบแต่ละคำสั่งจะเป็นหน่วยการรวบรวมที่แยกต่างหาก ตั้งแต่cและdถูกกำหนดในคำสั่งแยกค่าของพวกเขาจะไม่ถูกรวมเข้าด้วยกัน ตั้งแต่eและfถูกกำหนดในคำสั่งเดียวกันค่าของพวกเขาจะถูกรวมเข้าด้วยกัน


คุณสามารถดูว่าเกิดอะไรขึ้นโดยการแยกส่วนรหัสไบต์ ลองกำหนดฟังก์ชั่นที่ทำe, f = 128, 128แล้วเรียกdis.disใช้และคุณจะเห็นว่ามีค่าคงที่เดียว(128, 128)

>>> def f(): i, j = 258, 258
>>> dis.dis(f)
  1           0 LOAD_CONST               2 ((128, 128))
              2 UNPACK_SEQUENCE          2
              4 STORE_FAST               0 (i)
              6 STORE_FAST               1 (j)
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE
>>> f.__code__.co_consts
(None, 128, (128, 128))
>>> id(f.__code__.co_consts[1], f.__code__.co_consts[2][0], f.__code__.co_consts[2][1])
4305296480, 4305296480, 4305296480

คุณอาจสังเกตเห็นว่าคอมไพเลอร์ได้จัดเก็บ128เป็นค่าคงที่แม้ว่า bytecode จะไม่ได้ใช้งานจริงซึ่งจะทำให้คุณมีความคิดว่าคอมไพเลอร์ CPython ของการปรับให้เหมาะสมทำอย่างไร ซึ่งหมายความว่าสิ่งอันดับ (ไม่ว่าง) จริง ๆ แล้วไม่ได้รวมเข้าด้วยกัน:

>>> k, l = (1, 2), (1, 2)
>>> k is l
False

ใส่ลงในฟังก์ชั่นdisนั้นและดูที่co_consts- มี a 1และ a 2สองสิ่ง(1, 2)อันดับที่ใช้ร่วมกัน1และ2แต่ไม่เหมือนกันและสิ่ง((1, 2), (1, 2))อันดับที่มีสิ่งอันดับสองเท่ากัน


มีการเพิ่มประสิทธิภาพอีกหนึ่งอย่างที่ CPython ทำได้: การใช้สตริง ไม่เหมือนกับการคอมไพล์ค่าคงที่คอมไพเลอร์ซึ่งไม่ได้ จำกัด อยู่ที่ตัวอักษรของซอร์สโค้ด:

>>> m = 'abc'
>>> n = 'abc'
>>> m is n
True

ในทางกลับกันมันถูก จำกัดstrประเภทและสายอักขระของหน่วยเก็บข้อมูลภายใน "ascii compact", "compact" หรือ "legacy ready"และในหลาย ๆ กรณีเฉพาะ "ascii compact" เท่านั้นที่จะได้รับการฝึกงาน


ไม่ว่าในกรณีใดก็ตามกฎสำหรับค่าที่จะต้องอาจจะเป็นหรือไม่สามารถแตกต่างกันไปจากการนำไปใช้งานและระหว่างเวอร์ชันของการนำไปปฏิบัติเดียวกันและระหว่างการทำงานของรหัสเดียวกันในสำเนาเดียวกันของการนำไปปฏิบัติเดียวกัน .

มันคุ้มค่าที่จะเรียนรู้กฎสำหรับ Python หนึ่งอันเพื่อความสนุกของมัน แต่มันก็ไม่คุ้มค่าที่จะไว้ใจพวกเขาในรหัสของคุณ กฎที่ปลอดภัยเพียงข้อเดียวคือ:

  • อย่าเขียนโค้ดที่สมมติว่าค่าที่เปลี่ยนรูปไม่ได้สองค่าเท่ากัน แต่สร้างแยกต่างหากนั้นเหมือนกัน (อย่าใช้x is yใช้x == y )
  • อย่าเขียนโค้ดที่สมมติว่าค่าที่เปลี่ยนรูปไม่ได้สองค่าเท่ากัน แต่สร้างแยกต่างหากนั้นแตกต่างกัน (อย่าใช้x is not yใช้x != y)

หรือกล่าวอีกนัยหนึ่งใช้isเพื่อทดสอบสำหรับเอกสารแบบซิงเกิล (เช่นNone) หรือที่สร้างขึ้นในที่เดียวในรหัส (เช่น_sentinel = object()สำนวน)


คำแนะนำที่คลุมเครือน้อยเพียง: ไม่ได้ใช้เพื่อเปรียบเทียบการใช้งานx is y x == yในทำนองเดียวกันอย่าใช้x is not yใช้x != y
smci

ดูคำถามนี้ทำไมจึงเป็นa=257; b=257หนึ่งบรรทัดa is bTrue
Joe

8

is เป็นตัวดำเนินการความเท่าเทียมกันของข้อมูลเฉพาะตัว (ทำงานเหมือนid(a) == id(b)); มันแค่ว่าตัวเลขสองตัวที่เท่ากันนั้นไม่จำเป็นต้องเป็นวัตถุเดียวกัน สำหรับเหตุผลประสิทธิภาพบางจำนวนเต็มขนาดเล็กเกิดขึ้นจะmemoizedดังนั้นพวกเขาจะมีแนวโน้มที่จะเป็นแบบเดียวกัน (นี้สามารถทำได้เนื่องจากพวกเขาจะไม่เปลี่ยนรูป)

===ในทางกลับกันผู้ประกอบการของ PHPอธิบายว่าเป็นการตรวจสอบความเสมอภาคและประเภท: x == y and type(x) == type(y)ตามความคิดเห็นของ Paulo Freitas สิ่งนี้จะเพียงพอสำหรับหมายเลขทั่วไป แต่แตกต่างจากisคลาสที่กำหนด__eq__ในลักษณะที่ไร้สาระ:

class Unequal:
    def __eq__(self, other):
        return False

เห็นได้ชัดว่า PHP ช่วยให้สิ่งเดียวกันสำหรับชั้นเรียน "ในตัว" (ซึ่งฉันหมายถึงการใช้งานที่ระดับ C ไม่ใช่ใน PHP) การใช้งานที่ไร้สาระเล็กน้อยอาจเป็นวัตถุตัวจับเวลาซึ่งมีค่าแตกต่างกันทุกครั้งที่ใช้เป็นตัวเลข ค่อนข้างว่าทำไมคุณต้องการเลียนแบบ Visual Basic Nowแทนการแสดงว่าเป็นการประเมินด้วยtime.time()ฉันไม่รู้

Greg Hewgill (OP) แสดงความคิดเห็นที่ชัดเจน "เป้าหมายของฉันคือการเปรียบเทียบเอกลักษณ์ของวัตถุมากกว่าความเท่าเทียมกันของค่ายกเว้นตัวเลขที่ฉันต้องการรักษาเอกลักษณ์ของวัตถุเหมือนกับความเท่าเทียมกันของมูลค่า"

นี่จะเป็นอีกคำตอบหนึ่งเนื่องจากเราต้องจัดหมวดหมู่สิ่งต่าง ๆ เป็นตัวเลขหรือไม่เพื่อเลือกว่าเราจะเปรียบเทียบกับ==หรือisไม่ CPythonกำหนดโปรโตคอลหมายเลขรวมถึง PyNumber_Check แต่ตัวนี้ไม่สามารถเข้าถึงได้จาก Python

เราสามารถลองใช้isinstanceกับหมายเลขทุกประเภทที่เรารู้ได้ แต่สิ่งนี้อาจไม่สมบูรณ์ โมดูล types มีรายการ StringTypes แต่ไม่มี NumberTypes ตั้งแต่ Python 2.6 คลาสตัวเลขในตัวมีคลาสพื้นฐานnumbers.Numberแต่มีปัญหาเดียวกัน:

import numpy, numbers
assert not issubclass(numpy.int16,numbers.Number)
assert issubclass(int,numbers.Number)

โดยวิธีการNumPyจะผลิตอินสแตนซ์ที่แยกจากกันของตัวเลขที่ต่ำ

ฉันไม่รู้คำตอบของคำถามนี้ ฉันคิดว่าหนึ่งในทางทฤษฎีสามารถใช้ ctypes ในการโทรPyNumber_Checkแต่แม้ฟังก์ชั่นนั้นจะถูกถกเถียงกันและมันก็ไม่ได้พกพาได้อย่างแน่นอน เราจะต้องเฉพาะเจาะจงน้อยลงเกี่ยวกับสิ่งที่เราทดสอบในตอนนี้

ในท้ายที่สุดปัญหานี้เกิดจากงูหลามไม่เดิมมีต้นไม้ชนิดกับภาคเช่นโครงการของ number?หรือของ Haskell ประเภทระดับ Num isตรวจสอบตัวตนของวัตถุไม่ใช่ค่าความเท่าเทียมกัน PHP มีประวัติศาสตร์ที่มีสีสันเช่นกันที่===เห็นได้ชัดว่าทำงานเป็นisเพียงบนวัตถุใน PHP5 แต่ไม่ PHP4 นั่นคือความเจ็บปวดที่เพิ่มขึ้นของการย้ายข้ามภาษา (รวมถึงเวอร์ชันหนึ่ง)


4

นอกจากนี้ยังเกิดขึ้นกับสตริง:

>>> s = b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

ตอนนี้ทุกอย่างดูเหมือนดี

>>> s = 'somestr'
>>> b = 'somestr'
>>> s == b, s is b, id(s), id(b)
(True, True, 4555519392, 4555519392)

คาดหวังเช่นกัน

>>> s1 = b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, True, 4555308080, 4555308080)

>>> s1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> b1 = 'somestrdaasd ad ad asd as dasddsg,dlfg ,;dflg, dfg a'
>>> s1 == b1, s1 is b1, id(s1), id(b1)
(True, False, 4555308176, 4555308272)

ตอนนี้ไม่คาดคิด


เกิดขึ้นกับสิ่งนี้ - เห็นด้วยแม้กระทั่งผู้ที่ฝืน ดังนั้นฉันจึงเล่นกับมันและมันก็แปลกกว่า - เกี่ยวข้องกับอวกาศ ตัวอย่างเช่นสตริง'xx'จะเป็นไปตามที่คาดไว้'xxx'แต่'x x'ไม่ใช่
Brian

2
นั่นเป็นเพราะดูเหมือนว่าสัญลักษณ์หากไม่มีที่ว่างในนั้น ชื่อจะถูก interned โดยอัตโนมัติดังนั้นหากมีสิ่งใดถูกตั้งชื่อไว้xxที่ใดก็ได้ในเซสชัน Python ของคุณสตริงนั้นจะถูกฝึกงานอยู่แล้ว และอาจมีฮิวริสติกที่ทำถ้ามันคล้ายกับชื่อ เช่นเดียวกับตัวเลขสิ่งนี้สามารถทำได้เพราะพวกมันไม่เปลี่ยนรูป docs.python.org/2/library/functions.html#intern guilload.com/python-string-interning
Yann Vernier

3

มีอะไรใหม่ใน Python 3.8: การเปลี่ยนแปลงพฤติกรรมของ Python :

ตอนนี้คอมไพเลอร์สร้างSyntaxWarningเมื่อตรวจสอบตัวตน ( isและ is not) ใช้กับตัวอักษรบางประเภท (เช่นสตริง, ints) สิ่งเหล่านี้มักจะสามารถทำงานได้โดยไม่ตั้งใจใน CPython แต่ไม่ได้รับการรับรองโดยข้อกำหนดภาษา คำเตือนแนะนำให้ผู้ใช้ใช้การทดสอบความเท่าเทียมกัน ( == และ!=) แทน

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