@classmethod Nedir?

Kolay gelsin.

Nesne Tabanlı Programlama (Devamı) — Yazbel Python Belgeleri burayı okuyarken @classmethod'u fark ettim. Ama tam anlayamadım. Bu @classmethod tam olarak nedir? Neden kullanırız?

Değerli bilgileriniz için şimdiden teşekkürler.

class SampleClass:
    class_attribute = []

    def __init__(self, param1, param2):
        self.param1 = param1 
        self.param2 = param2 
        self.instance_attribute = 0

    def instanceMethod(self):
        self.instance_attribute += 1 
        return self.instance_attribute
    
    @classmethod
    def classMethod(cls):
        cls.class_attribute.append(5)
        return cls.class_attribute

Bu class'ı biraz kurcalarsanız olayı anlayabilirsiniz diye düşünüyorum. Gelin birlikte kurcalayalım.


Öncelikle SampleClass isimli class'ımızın class_attribute adında bir niteliği olduğunu görüyoruz. Bu bir class attribute'tur. Bu, şu demek oluyor, bir instance'e ihtiyaç duymadan bu niteliğe erişebilirsiniz. Örnek:

class SampleClass:
    class_attribute = []

    def __init__(self, param1, param2):
        self.param1 = param1 
        self.param2 = param2 
        self.instance_attribute = 0

    def instanceMethod(self):
        self.instance_attribute += 1 
        return self.instance_attribute
    
    @classmethod
    def classMethod(cls):
        cls.class_attribute.append(5)
        return cls.class_attribute

print(SampleClass.class_attribute) # Output: []

Gördüğünüz gibi elimizde bu class'ın bir instance'i olmadan class_attribute'a erişebiliyoruz. Sebep? Çünkü bu bir class attribute'u, bir class instance'nin attribute'u değil.
Mesela benzer şekilde SampleClass.instance_attribute'a erişemezsiniz çünkü bunun için self'e, yani bir instance'e ihtiyacınız var: self ile erişebildiğiniz şeylere ancak bir instance'iniz varken erişebilirsiniz çünkü self zaten instance'in kendisidir.


def __init__(self, param1, param2):
    self.param1 = param1 
    self.param2 = param2 
    self.instance_attribute = 0

Burada üç tane instance attribute'u tanımlanmış, self ile tanımlanmış olmaları dikkatinizi çeksin. Bu attribute'lara ancak self ile, yani bir instance yardımı ile erişebilirsiniz.
Mesela

class SampleClass:
    class_attribute = []

    def __init__(self, param1, param2):
        self.param1 = param1 
        self.param2 = param2 
        self.instance_attribute = 0

    def instanceMethod(self):
        self.instance_attribute += 1 
        return self.instance_attribute
    
    @classmethod
    def classMethod(cls):
        cls.class_attribute.append(5)
        return cls.class_attribute

# Works properly
instance = SampleClass("param1", "param2")
print(instance.param1)

# AttributeError: type object 'SampleClass' has no attribute 'param1'
print(SampleClass.param1) 

def instanceMethod(self):
    self.instance_attribute += 1 
    return self.instance_attribute

Burada bir metot tanımlıyoruz fakat self parametresi alması, bir instance attribute'a erişmesi dikkatinizi çeksin ki bu bir instance method idir. Dolayısıyla bu metodu kullanabilmek için de bir objeye ihtiyaç var, bir instance yaratılmalı.

class SampleClass:
    class_attribute = []

    def __init__(self, param1, param2):
        self.param1 = param1 
        self.param2 = param2 
        self.instance_attribute = 0

    def instanceMethod(self):
        self.instance_attribute += 1 
        return self.instance_attribute
    
    @classmethod
    def classMethod(cls):
        cls.class_attribute.append(5)
        return cls.class_attribute

# Works properly
instance = SampleClass("param1", "param2")
print(instance.instanceMethod())

# TypeError: SampleClass.instanceMethod() missing 1 required positional argument: 'self'
print(SampleClass.instanceMethod()) # self parametresi bir argüman bekliyor

@classmethod
  def classMethod(cls):
     cls.class_attribute.append(5)
     return cls.class_attribute

İşte burası. self'in ve instance'in üzerine bu kadar gitmemin ardından burada self'in olmaması göze batıyor olmalı. Burada bir @classmethod decorator function'ının kullanılması, metodun self yerine cls parametresi alması, bir class_attribute'una erişilmesi dikkatinizi çekmiştir ki bu metot bir class method idir. Yani bu, şu demek, bu metot self ile tanımlanan niteliklere erişemez çünkü bunların bir instance'e ihtiyacı vardır fakat class attribute'lara erişebilir çünkü bunların bir instance'e ihtiyacı yoktur.

class SampleClass:
    class_attribute = []

    def __init__(self, param1, param2):
        self.param1 = param1 
        self.param2 = param2 
        self.instance_attribute = 0

    def instanceMethod(self):
        self.instance_attribute += 1 
        return self.instance_attribute
    
    @classmethod
    def classMethod(cls):
        cls.class_attribute.append(5)
        return cls.class_attribute

print(SampleClass.classMethod()) # Output: [5]

Yani bir class yapısında


Turuncu ile belirttiğim, bir instance'e ihtiyaç duymadan erişebileceğiniz bu yerler birbiriyle yakından ilişkilidir.


Umarım anlatabilmişimdir. Size tavsiyem aynı kod örneği üzerinden kurcalama yapmaya devam etmeniz. Ne durumda hangi niteliğe nasıl ve ne zaman erişebiliyorum vb. sorular sorarak ihtiyacınız olan kısımları print ederek kendi sorularınıza cevap bulabilirsiniz.

8 Beğeni

Teşekkür ederim <3 Kolay gelsin.

1 Beğeni

Python’da bir sınıfta bir metot yazmak üzereyken 3 seçeneğiniz vardır:

  • düz metot: hem örneğe (instance) (ve dolayısıyla) hem de sınıfa (class; type(self)'tir) erişebilir
  • sınıf metodu: yalnız sınıfa erişebilir
  • statik metot: ne örneğe ne sınıfa erişir.

Sınıfın içerisinde 1-girintiyle yazdığınız alelade her metot düz metottur. Nesne tabanlı programlamanın sunduğu, geniş çerçevede bahsedilen "nitelik ve metotlar"daki metotlar bunlardır. Madem hem örneğe hem de sınıfa erişilebiliyor bunlar üzerinden, diğer ikisinin yaptığı şeyleri de bu düz metotları kullanarak gerçekleştiremez miyiz? Gerçekleştirebiliriz evet. Ama niyeti ortaya koyma, okunabilirlik, performans gibi nedenlerden dolayı bunu yapmamalıyız, zaten o ikisinin varoluş amaçları da bu kaygıları taşımaktadır.

Peki ilk soru şu: nasıl oluyor da bir sınıf metodu bir anda örneğe erişemez oluyor? İşte o, sizin bahsettiğiniz classmethod fonksiyonu (aslında sınıf ama olsun) ile metot dekore edilerek (@) sağlanıyor. Benzer durum staticmethod için de geçerlidir.

Asıl soru: durup dururken neden sınıf metodu yazıyoruz? Sınıf metodları yaygın olarak alternatif constructor görevi görürler, “factory” fonksiyonlarıdırlar. Mesela şöyle bir sınıf olsun:

class Ulke:
    def __init__(self, isim, nufus, iller):
        self.isim  = isim
        self.nufus = nufus
        self.iller = iller

   def il_basina_nufus(self):
       return self.nufus / len(self.iller)

Bir iller listesi alıyoruz, bir isim string’i ve nufus tamsayısı ile nesneyi oluşturuyoruz. Normal durumlar, normal metotlar. Bu sınıfı piyasa sürdükten sonra, fazla sayıda ile sahip olan ülkelerin oluşturulmasında, uzun bir iller listesinin paslanmasının pratik olmadığı tarafınıza bildiriliyor. Diyorlar ki, ya bizim ülkenin listesini bir dosyada tutuyorum da satır satır, senin sınıfa onu göndersem de oradan illeri çekse olmaz mı?

Siz de tamam diyorsunuz ve düşünüyorsunuz. Hmm… __init__'e bir argüman daha ekleyeyim, illerin_dosya_yolu, opsiyonel olsun; onu pasladılarsa oradan okurum. Tabii bu durumda iller'i de opsiyonel yapmam gerekir çünkü illerin_dosya_yolunu veren iller'i paslamayacaktır. Ha tabii şimdi bir de ikisini de (veya hiçbirini!) paslayanları uyarmak gerekir… :d E yapalım bari:

class Ulke:
    def __init__(self, isim, nufus, iller=None, illerin_dosya_yolu=None):
        # validasyon...
        if iller is not None and illerin_dosya_yolu is not None:
            raise ValueError("hem `iller` hem de `dosya_yolu` mu? Yapmayın...")
        if iller is None and illerin_dosya_yolu is None:
            raise ValueError("ne `iller` ne de `dosya_yolu` mu? Yapmayın...")

        # illeri dosyadan okuyoruz eğer dosya yolu verildiyse
        if illerin_dosya_yolu is not None:
            with open(illerin_dosya_yolu) as fh:
                iller = [*map(str.strip, fh)]
 
        self.isim  = isim
        self.nufus = nufus
        self.iller = iller

   def il_basina_nufus(self):
       return self.nufus / len(self.iller)

Yani…Gideri var gibi ama daha iyisini yapabiliriz. İşte sınıf metodları burada imdadımıza koşuyor bu alternatif constructor olmalarıyla; bir factory fonksiyonu olarak kullanacağız. Gelenek de bu metotların ismini from ile başlatmaktır. Yani “şuradan bir nesne oluştur”. Bizim örnekte adını from_file koyabiliriz. Türkçe isterseniz dosyadan olabilir:

class Ulke:
    # eski haline geri döndü
    def __init__(self, isim, nufus, iller):
        self.isim  = isim
        self.nufus = nufus
        self.iller = iller

   # bu zaten düz metot geldi düz metot gidiyor
   def il_basina_nufus(self):
       return self.nufus / len(self.iller)

   # bu yeni! alternatif constructor.
   @classmethod
   def dosyadan(cls, isim, nufus, illerin_dosya_yolu):
       # dosyadan illeri alıverelim
       with open(illerin_dosya_yolu) as fh:
           iller = [*map(str.strip, fh)]
      
       # şimdi "asıl" constructor'u çağrırız `cls` üzerinden
       return cls(isim, nufus, iller)

Kod sadeleşti, ayrı olması gereken şeyler ayrıldı, hakimiyet daha da kolay hale geldi, ekstra validasyonlara, acaba hangisini paslamışlar if’lerine gerek kalmadı. Artık kullanıcılar 2 şekilde Ulke nesneleri oluşturma özgürlüğündeler:

# Direkt liste paslıyoruz `iller` için
# ve `Ulke(...)` diye ilklendiriyoruz
monaco = Ulke(isim="Monaco",
              nufus=39_244,
              iller=["Fontvieille", "Monaco-Ville", "La Condamine", "Monte Carlo"])

# Dosya yolu veriyoruz `iller` için
# ve `Ulke.dosyadan(...)` diye ilklendiriyoruz
fas = Ulke.dosyadan(isim="Fas",
                    nufus=37_112_080,
                    illerin_dosya_yolu="fas_in_illeri.txt")

Buna bir örneği de Python’daki dict sınıfında görürüz. dict.fromkeys metodu vardır; elinizde şimdilik sadece anahtarlar varsa (veya tüm anahtarları aynı değere göndermek istiyorsanız) bu alternatif sözlük oluşturan (fabrika) sınıf metodunu kullanırsınız dict(...) / {...} yerine.

Son olarak da sınıf metodunun ilk parametresi olan, sınıfı temsil eden cls'ye değinelim. İlk parametreyi artık self yerine cls diye adlandırıyoruz (artık self'e yani örneğe erişimimiz yok (ihtiyacımız da yok), gelenek gereği de cls diyoruz). Peki ona ne gerek var? Yukarıda return Ulke(isim, nufus, iller) yazsaydık return cls(isim, nufus, iller) yerine olmaz mıydı? Olurdu evet, çalışırdı. cls(...) şeklinde çağırmak inheritance’a saygı duymak içindir. Olur da Ulke'nin bir alt sınıfı (Ozerk_Ulke gibi) da bu sınıf metodundan nemalanmak isterse, mesela Ozerk_Ulke.dosyadan(...) şeklinde, eğer siz return Ulke(isim, nufus, iller) yazdıysanız, geriye düz Ulke nesnesi dönecektir! Ama istenen Ozerk_Ulke sınıfından bir nesne dönmesidir; cls işte bu işe yarar, yaptığımız o çağrıda o noktada cls == Ozerk_Ulke olacaktır ve Ozerk_Ulke nesnesi elde ederiz geriye. (Yan not: inheritance girdiğinde işin içine, **kwargs eklemek gerekir sınıf metoduna ki alt sınıfın üst sınıfta olmayan niteliklerinin (mesela burada bagli_olunan_ulke niteliği olabilir) ilklendirilmesine ket vurulmasın; sonra cls(..., **kwargs) denilir.)

Statik metot da ne örneğe ne sınıfa ihtiyaç duyuyordu, onun da kullanım alanları vardır daha az sık olsa da; sınıf metodu, statik metot ve sınıflarla ilgili bazı diğer bilgiler adına şu videoyu izleyebilirsiniz.

8 Beğeni

Proje klasorumu kurcaladim, guzel ornek cikar mi diye (find ~/proj -name "*.py" -not -path "*venv*" -exec grep -HA5 classmethod '{}' \;):

class Config(object):
	@classmethod
	def from_config(cls, config):
		qc = cls()

		qc.hosts = list(map(lambda host_port: (host_port[0], int(host_port[1])), map(lambda h: h.split(':'), config.get('hosts').split())))
		qc.username = config.get('username')
		qc.password = config.get('password')
		qc.vhost = config.get('vhost')
		[...]

	def __init__(self):
		self.hosts = []
		self.username = None
		self.password = None
		self.vhost = '/'
		[...]

staticmethod daha az, cunku neden duz fonksiyon yazmak varken sinifin icine koyayim?

Sinifa gobekten bagli ama instance kullanmiyor, veya bulundugu yerin modul yerine sinif ici olmasi daha mantikliysa:

5 Beğeni