Python'un iç sınıflarının amacı nedir?


100

Python'un iç / iç içe geçmiş sınıfları kafamı karıştırıyor. Onlar olmadan başarılamayacak bir şey var mı? Eğer öyleyse, o şey nedir?

Yanıtlar:


89

Http://www.geekinterview.com/question_details/64739 adresinden alıntılanmıştır :

İç sınıfın avantajları:

  • Sınıfların mantıksal gruplandırılması : Bir sınıf yalnızca bir başka sınıf için yararlıysa, onu o sınıfa gömmek ve ikisini bir arada tutmak mantıklıdır. Bu tür "yardımcı sınıfların" iç içe yerleştirilmesi, paketlerini daha akıcı hale getirir.
  • Arttırılmış kapsülleme : B'nin, aksi takdirde özel ilan edilecek olan A'nın üyelerine erişime ihtiyaç duyduğu iki üst düzey A ve B sınıfını düşünün. B sınıfını AA sınıfının içinde gizleyerek üyeleri özel ilan edilebilir ve B bunlara erişebilir. Ayrıca B'nin kendisi de dış dünyadan gizlenebilir.
  • Daha okunaklı, bakımı kolay kod : Üst düzey sınıflar içinde küçük sınıfları yerleştirmek, kodu kullanıldığı yere daha yakın yerleştirir.

Ana avantaj organizasyondur. İç sınıfları ile gerçekleştirilebilir şey olabilir onlar olmadan gerçekleştirilebilir.


50
Kuşatma argümanı elbette Python için geçerli değildir.
bobince

31
İlk nokta Python için de geçerli değil. Bir modül dosyasında istediğiniz kadar sınıf tanımlayabilirsiniz, böylece onları bir arada tutabilirsiniz ve paket organizasyonu da etkilenmez. Son nokta çok öznel ve bunun geçerli olduğuna inanmıyorum. Kısacası, bu cevapta Python'da iç sınıfların kullanımını destekleyen herhangi bir argüman bulamıyorum.
Chris Arndt

17
Yine de bunlar, programlamada iç sınıfların kullanılmasının nedenleridir. Sadece rakip bir cevabı düşürmeye çalışıyorsun. Bu adamın verdiği cevap sağlam.
İnversus

16
@Inversus: Katılmıyorum. Bu bir cevap değil, başka birinin farklı bir dil (yani Java) hakkındaki cevabından genişletilmiş bir alıntıdır . Olumsuz oy verildi ve umarım diğerleri de aynısını yapar.
Kevin

5
Bu yanıta katılıyorum ve itirazlara katılmıyorum. Yuvalanmış sınıflar Java'nın iç sınıfları olmasa da kullanışlıdırlar. İç içe geçmiş bir sınıfın amacı organizasyondur. Etkili olarak, bir sınıfı diğerinin ad alanının altına koyuyorsunuz. Bunu yapmak için mantıklı mantıklı, bu ise Pythonic: "İsim alanları harika bir fikir korna biri - en fazla olanların yapalım!". Örneğin, DataLoaderbir CacheMissistisna atabilen bir sınıf düşünün . İstisnayı ana sınıfın altına yerleştirmek, DataLoader.CacheMissyalnızca içe aktarabileceğiniz DataLoaderancak yine de istisnayı kullanabileceğiniz anlamına gelir .
cbarrick

50

Onlar olmadan başarılamayacak bir şey var mı?

Hayır. Sınıfı normal olarak en üst düzeyde tanımlamaya ve sonra ona bir referansı dış sınıfa kopyalamaya kesinlikle eşdeğerdirler.

İç içe geçmiş sınıflara 'izin verilmesinin' özel bir nedeni olduğunu sanmıyorum, bunlara açıkça 'izin vermemek' için özel bir anlam ifade etmiyor.

Dış / sahip nesnesinin yaşam döngüsü içinde var olan ve her zaman dış sınıfın bir örneğine (Java'nın yaptığı gibi iç sınıflara) bir referansı olan bir sınıf arıyorsanız, Python'un iç içe geçmiş sınıfları o şey değildir. Ama bunun gibi bir şeyi hackleyebilirsin :

import weakref, new

class innerclass(object):
    """Descriptor for making inner classes.

    Adds a property 'owner' to the inner class, pointing to the outer
    owner instance.
    """

    # Use a weakref dict to memoise previous results so that
    # instance.Inner() always returns the same inner classobj.
    #
    def __init__(self, inner):
        self.inner= inner
        self.instances= weakref.WeakKeyDictionary()

    # Not thread-safe - consider adding a lock.
    #
    def __get__(self, instance, _):
        if instance is None:
            return self.inner
        if instance not in self.instances:
            self.instances[instance]= new.classobj(
                self.inner.__name__, (self.inner,), {'owner': instance}
            )
        return self.instances[instance]


# Using an inner class
#
class Outer(object):
    @innerclass
    class Inner(object):
        def __repr__(self):
            return '<%s.%s inner object of %r>' % (
                self.owner.__class__.__name__,
                self.__class__.__name__,
                self.owner
            )

>>> o1= Outer()
>>> o2= Outer()
>>> i1= o1.Inner()
>>> i1
<Outer.Inner inner object of <__main__.Outer object at 0x7fb2cd62de90>>
>>> isinstance(i1, Outer.Inner)
True
>>> isinstance(i1, o1.Inner)
True
>>> isinstance(i1, o2.Inner)
False

(Bu, Python 2.6 ve 3.0'da yeni olan sınıf dekoratörlerini kullanır. Aksi takdirde, sınıf tanımından sonra "İç = iç sınıf (İç)" demeniz gerekir.)


5
Çağrısı olduğunu kullanımın söz o tipik iç iç sınıfı belirlenerek Python ele alınabilir (örneklerini dış sınıfın örnekleri ile bir ilişki yani Java vari iç sınıflar) yöntemlerine onlar görecek - Dış sınıfın dış en selfgerekli fazladan bir şey yapmadan (sadece genellikle iç 's koyardı farklı bir tanımlayıcı kullanmak self; gibi innerselfve bu aracılığıyla dış örneği erişmek mümkün olacaktır).
Evgeni Sergeev

WeakKeyDictionaryBu örnekte a kullanmak , aslında anahtarların gereksiz yere toplanmasına izin vermez, çünkü değerler, owneröznitelikleri aracılığıyla ilgili anahtarlara güçlü bir şekilde başvurur.
Kritzefitz

36

Bunu anlayabilmek için başınızı sarmalamanız gereken bir şey var. Çoğu dilde, sınıf tanımları derleyiciye yönelik yönergelerdir. Yani sınıf, program çalıştırılmadan önce oluşturulur. Python'da tüm ifadeler çalıştırılabilir. Bu şu anlama gelir:

class foo(object):
    pass

tıpkı bunun gibi çalışma zamanında yürütülen bir ifadedir:

x = y + z

Bu, yalnızca diğer sınıflar içinde sınıflar oluşturmakla kalmayıp, istediğiniz yerde sınıflar da oluşturabileceğiniz anlamına gelir. Bu kodu düşünün:

def foo():
    class bar(object):
        ...
    z = bar()

Dolayısıyla, "iç sınıf" fikri aslında bir dil yapısı değildir; bir programcı yapısıdır. Guido'nun burada nasıl ortaya çıktığına dair çok iyi bir özeti var . Ama esasen, temel fikir bunun dilin gramerini basitleştirmesidir.


16

Sınıflar içinde yuvalama sınıfları:

  • İç içe geçmiş sınıflar sınıf tanımını şişirerek neler olup bittiğini görmeyi zorlaştırır.

  • İç içe geçmiş sınıflar, testi daha zor hale getirecek birleştirme oluşturabilir.

  • Python'da, Java'nın aksine, bir dosyaya / modüle birden fazla sınıf koyabilirsiniz, böylece sınıf yine de üst düzey sınıfa yakın kalır ve hatta sınıf adının önüne "_" eklenebilir, bu da başkalarının olmaması gerektiğini belirtmeye yardımcı olabilir. onu kullanarak.

Yuvalanmış sınıfların yararlı olabileceği yer, işlevlerin içindedir

def some_func(a, b, c):
   class SomeClass(a):
      def some_method(self):
         return b
   SomeClass.__doc__ = c
   return SomeClass

Sınıf, C ++ 'da şablon meta programlaması gibi dinamik bir sınıf oluşturmanıza olanak tanıyan işlevdeki değerleri yakalar


7

İç içe geçmiş sınıflara karşı argümanları anlıyorum, ancak bazı durumlarda bunları kullanmak için bir durum var. Çift bağlantılı bir liste sınıfı oluşturduğumu ve düğümleri korumak için bir düğüm sınıfı oluşturmam gerektiğini hayal edin. İki seçeneğim var, DoublyLinkedList sınıfının içinde Node sınıfı oluşturmak veya DoublyLinkedList sınıfının dışında Node sınıfını oluşturmak. Bu durumda ilk seçeneği tercih ederim, çünkü Node sınıfı yalnızca DoublyLinkedList sınıfı içinde anlamlıdır. Gizleme / kapsülleme avantajı olmasa da, Node sınıfının DoublyLinkedList sınıfının bir parçası olduğunu söyleyebilmenin bir gruplama yararı vardır.


5
Bu, aynı Nodesınıfın, sizin de oluşturabileceğiniz diğer bağlantılı liste sınıfı türleri için yararlı olmadığını varsayarsak doğrudur , bu durumda muhtemelen dışarıda olmalıdır.
Acumenus

Başka bir deyişle Node, ad alanı altındadır DoublyLinkedListve böyle olmak mantıklıdır. Bu ise Pythonic: "İsim alanları harika bir fikir korna biri - edelim bunlardan daha yap!"
cbarrick

@cbarrick: "Bunlardan daha fazlasını" yapmak onları iç içe geçirmek hakkında hiçbir şey söylemiyor.
Ethan Furman

4

Onlar olmadan başarılamayacak bir şey var mı? Eğer öyleyse, o şey nedir?

Şey var kolayca olmadan yapılamaz : ilgili sınıfların miras .

İşte ilgili sınıflarla minimalist bir örnek Ave B:

class A(object):
    class B(object):
        def __init__(self, parent):
            self.parent = parent

    def make_B(self):
        return self.B(self)


class AA(A):  # Inheritance
    class B(A.B):  # Inheritance, same class name
        pass

Bu kod, oldukça makul ve öngörülebilir bir davranışa yol açar:

>>> type(A().make_B())
<class '__main__.A.B'>
>>> type(A().make_B().parent)
<class '__main__.A'>
>>> type(AA().make_B())
<class '__main__.AA.B'>
>>> type(AA().make_B().parent)
<class '__main__.AA'>

En Büst düzey bir sınıf olsaydınız self.B(), yöntemde make_Byazamazdınız, yalnızca yazardınız B()ve bu nedenle yeterli sınıflara dinamik bağlanmayı kaybedersiniz .

Bu yapıda, sınıf Agövdesinde asla sınıfa atıfta bulunmamalısınız B. Bu, parentniteliği sınıfta tanıtmanın motivasyonudur B.

Tabii ki, bu dinamik bağlama , sınıfların sıkıcı ve hataya açık bir enstrümantasyonu pahasına, iç sınıf olmadan yeniden oluşturulabilir .


1

Bunu için kullandığım ana kullanım durumu, küçük modüllerin çoğalmasını önlemek ve ayrı modüllere ihtiyaç duyulmadığında ad alanı kirliliğini önlemektir. Var olan bir sınıfı genişletiyorsam, ancak bu sınıfın her zaman ona bağlanması gereken başka bir alt sınıfa başvurması gerekir. Örneğin, utils.pyiçinde pek çok yardımcı sınıf içeren, mutlaka birbirine bağlı olmayan bir modülüm olabilir, ancak bu yardımcı sınıfların bazıları için birleştirmeyi güçlendirmek istiyorum . Örneğin, https://stackoverflow.com/a/8274307/2718295'i uyguladığımda

: utils.py:

import json, decimal

class Helper1(object):
    pass

class Helper2(object):
    pass

# Here is the notorious JSONEncoder extension to serialize Decimals to JSON floats
class DecimalJSONEncoder(json.JSONEncoder):

    class _repr_decimal(float): # Because float.__repr__ cannot be monkey patched
        def __init__(self, obj):
            self._obj = obj
        def __repr__(self):
            return '{:f}'.format(self._obj)

    def default(self, obj): # override JSONEncoder.default
        if isinstance(obj, decimal.Decimal):
            return self._repr_decimal(obj)
        # else
        super(self.__class__, self).default(obj)
        # could also have inherited from object and used return json.JSONEncoder.default(self, obj) 

O zaman şunları yapabiliriz:

>>> from utils import DecimalJSONEncoder
>>> import json, decimal
>>> json.dumps({'key1': decimal.Decimal('1.12345678901234'), 
... 'key2':'strKey2Value'}, cls=DecimalJSONEncoder)
{"key2": "key2_value", "key_1": 1.12345678901234}

Elbette, kalıtımdan json.JSONEnocdertamamen kaçınabilir ve varsayılanı () geçersiz kılabilirdik:

:

import decimal, json

class Helper1(object):
    pass

def json_encoder_decimal(obj):
    class _repr_decimal(float):
        ...

    if isinstance(obj, decimal.Decimal):
        return _repr_decimal(obj)

    return json.JSONEncoder(obj)


>>> json.dumps({'key1': decimal.Decimal('1.12345678901234')}, default=json_decimal_encoder)
'{"key1": 1.12345678901234}'

Ancak bazen sadece kongre utilsiçin, genişletilebilirlik sınıflarından oluşmak istersiniz .

İşte başka bir kullanım durumu: OuterClass'ımdaki değişkenler için çağırmak zorunda kalmadan bir fabrika istiyorum copy:

class OuterClass(object):

    class DTemplate(dict):
        def __init__(self):
            self.update({'key1': [1,2,3],
                'key2': {'subkey': [4,5,6]})


    def __init__(self):
        self.outerclass_dict = {
            'outerkey1': self.DTemplate(),
            'outerkey2': self.DTemplate()}



obj = OuterClass()
obj.outerclass_dict['outerkey1']['key2']['subkey'].append(4)
assert obj.outerclass_dict['outerkey2']['key2']['subkey'] == [4,5,6]

Bu modeli, @staticmethodnormalde bir fabrika işlevi için kullanacağınız dekoratör yerine tercih ederim .


1

1. İşlevsel olarak eşdeğer iki yol

Daha önce gösterilen iki yol, işlevsel olarak aynıdır. Bununla birlikte, bazı ince farklılıklar vardır ve birini diğerine tercih etmek istediğiniz durumlar vardır.

Yol 1: İç içe geçmiş sınıf tanımı
(= "İç içe sınıf")

class MyOuter1:
    class Inner:
        def show(self, msg):
            print(msg)

Yol 2: Modül düzeyinde İç sınıf Dış sınıfa eklenmiş
(= "Başvurulan iç sınıfa")

class _InnerClass:
    def show(self, msg):
        print(msg)

class MyOuter2:
    Inner = _InnerClass

Alt çizgi, PEP8'i takip etmek için kullanılır "dahili arayüzler (paketler, modüller, sınıflar, işlevler, öznitelikler veya diğer adlar) - tek bir alt çizgi ile ön ek olmalıdır."

2. Benzerlikler

Aşağıdaki kod parçacığı, "İç içe geçmiş sınıf" ile "Başvurulan iç sınıf" arasındaki işlevsel benzerlikleri gösterir; Bir iç sınıf örneğinin türünü kod kontrol ederken aynı şekilde davranırlar. Söylemeye gerek yok, aynı m.inner.anymethod()şekilde davranırdı m1vem2

m1 = MyOuter1()
m2 = MyOuter2()

innercls1 = getattr(m1, 'Inner', None)
innercls2 = getattr(m2, 'Inner', None)

isinstance(innercls1(), MyOuter1.Inner)
# True

isinstance(innercls2(), MyOuter2.Inner)
# True

type(innercls1()) == mypackage.outer1.MyOuter1.Inner
# True (when part of mypackage)

type(innercls2()) == mypackage.outer2.MyOuter2.Inner
# True (when part of mypackage)

3. Farklılıklar

"İç içe sınıf" ve "Başvurulan iç sınıf" arasındaki farklar aşağıda listelenmiştir. Büyük değiller ama bazen bunlardan birini veya diğerini seçmek istersiniz.

3.1 Kod Kapsülleme

"İç içe sınıflar" ile kodu "Başvurulan iç sınıfa" göre daha iyi kapsüllemek mümkündür. Modül ad alanındaki bir sınıf, global bir değişkendir. Yuvalanmış sınıfların amacı, modüldeki dağınıklığı azaltmak ve iç sınıfı dış sınıfa yerleştirmektir.

Hiç kimse * kullanmıyorken from packagename import *, düşük miktarda modül seviyesi değişkenleri, örneğin kod tamamlama / intellisense ile bir IDE kullanırken iyi olabilir.

* Değil mi?

3.2 Kodun okunabilirliği

Django belgeleri , model meta verileri için iç sınıf Meta'yı kullanma talimatı verir . Çerçeve kullanıcılarına class Foo(models.Model)iç ile bir iç yazmaları talimatını vermek biraz daha net * class Meta;

class Ox(models.Model):
    horn_length = models.IntegerField()

    class Meta:
        ordering = ["horn_length"]
        verbose_name_plural = "oxen"

"a yazın class _Meta, sonra a class Foo(models.Model)ile yazın Meta = _Meta" yerine;

class _Meta:
    ordering = ["horn_length"]
    verbose_name_plural = "oxen"

class Ox(models.Model):
    Meta = _Meta
    horn_length = models.IntegerField()
  • "İç içe sınıf" yaklaşımı ile kod , yuvalanmış bir madde işareti listesi okunabilir , ancak "Başvurulan iç sınıf" yöntemiyle _Meta, "alt öğelerini" (öznitelikleri) görmek için tanımını görmek için geri kaydırmak gerekir .

  • "Başvurulan iç sınıf" yöntemi, kod iç içe geçme düzeyiniz büyürse veya başka bir nedenle satırlar uzunsa daha okunabilir olabilir.

* Elbette bir zevk meselesi

3.3 Biraz farklı hata mesajları

Bu büyük bir mesele değil, sadece bütünlük için: İç sınıf için var olmayan özniteliğe erişirken, biraz farklı istisnalar görürüz. Bölüm 2'de verilen örneğe devam edersek:

innercls1.foo()
# AttributeError: type object 'Inner' has no attribute 'foo'

innercls2.foo()
# AttributeError: type object '_InnerClass' has no attribute 'foo'

Bunun nedeni type, iç sınıfların s'lerinin

type(innercls1())
#mypackage.outer1.MyOuter1.Inner

type(innercls2())
#mypackage.outer2._InnerClass

0

def test_something():% 100 test kapsamına yaklaşmak için (örneğin, bazı yöntemleri geçersiz kılarak çok nadiren tetiklenen günlük ifadelerini test etmek), unittest fonksiyonları içinde (yani içeride ) kasıtlı olarak hatalı alt sınıflar oluşturmak için Python'un iç sınıflarını kullandım .

Geçmişe bakıldığında, Ed'in cevabına benziyor https://stackoverflow.com/a/722036/1101109

Bu tür iç sınıflar kapsam dışına çıkmalı ve bunlara yapılan tüm referanslar kaldırıldıktan sonra çöp toplamaya hazır olmalıdır . Örneğin, aşağıdaki inner.pydosyayı alın:

class A(object):
    pass

def scope():
    class Buggy(A):
        """Do tests or something"""
    assert isinstance(Buggy(), A)

OSX Python 2.7.6 altında aşağıdaki ilginç sonuçları alıyorum:

>>> from inner import A, scope
>>> A.__subclasses__()
[]
>>> scope()
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A, scope
>>> from inner import A
>>> A.__subclasses__()
[<class 'inner.Buggy'>]
>>> del A
>>> import gc
>>> gc.collect()
0
>>> gc.collect()  # Yes I needed to call the gc twice, seems reproducible
3
>>> from inner import A
>>> A.__subclasses__()
[]

İpucu - Devam etmeyin ve bunu, buggy sınıflarıma yönelik diğer (önbelleğe alınmış?) Referansları saklıyor gibi görünen Django modelleriyle yapmayı denemeyin.

Bu nedenle, genel olarak,% 100 test kapsamına gerçekten değer vermedikçe ve diğer yöntemleri kullanamıyorsanız, bu tür bir amaç için iç sınıfları kullanmanızı tavsiye etmem. Yine de __subclasses__(), kullanırsanız, bazen iç sınıflar tarafından kirletilebileceğinin farkında olmanın güzel olduğunu düşünüyorum . Her iki durumda da bu kadarını takip ettiyseniz, bence bu noktada Python'a, özel dunderscores'e ve hepsine oldukça derinden giriyoruz.


3
Bunların hepsi alt sınıflarla ilgili değil, iç sınıflarla ilgili değil mi ?? A
klaas

Yukarıdaki olayda Buggy A'dan miras alır. Yani alt sınıf bunu gösterir. Ayrıca yerleşik işlev issubclass ()
klaas

Teşekkürler @klaas, .__subclasses__()Python'da işler kapsam dışına çıktığında iç sınıfların çöp toplayıcıyla nasıl etkileşime girdiğini anlamak için kullandığımın daha açık hale getirilebileceğini düşünüyorum . Bu, görsel olarak gönderiye hakim gibi görünüyor, bu nedenle ilk 1-3 paragraf biraz daha genişlemeyi hak ediyor.
pzrq
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.