Skip to content
This page has been auto-translated and may contain errors.View in English

Classes e objetos ​

Todo tipo com o qual você já trabalhou até agora (strings, listas, dicionários) é, na verdade, uma classe. Quando você chama "hello".upper(), você está chamando um método em um objeto string. Classes permitem que você defina seus próprios tipos, com seus próprios dados e comportamentos. Uma classe Player pode guardar um nome, uma pontuação e um nível, e saber como se exibir.

Classes são o mecanismo para criar tipos definidos pelo usuário. Uma classe define um modelo: os dados que cada instância guarda (seus atributos, os valores armazenados nela) e as operações que ela suporta (seus métodos, as funções vinculadas a ela). Em vez de acompanhar valores em variáveis paralelas e passá-las para todo lado, você agrupa tudo em um único objeto com uma interface clara.

Uma instrução class cria um novo tipo (um tipo de valor personalizado, da mesma natureza que str e int) e o vincula a um nome, como qualquer outra atribuição. Você cria uma instância (um objeto concreto desse tipo) chamando a classe: Player("Alice"). Por trás dessa chamada, o Python aloca um objeto vazio e executa __init__ para preenchê-lo. O modelo é uniforme do início ao fim: uma classe é, ela mesma, um objeto que você pode armazenar em uma variável, passar para uma função ou inspecionar em tempo de execução (enquanto o programa está rodando), e é exatamente isso que torna possíveis os construtores alternativos e os registros que veremos mais adiante neste capítulo.

Modelo e instâncias ​

Uma classe é um modelo (um "molde"). Uma instância é uma coisa específica feita a partir desse molde. Você pode criar quantas instâncias precisar, cada uma com seus próprios dados, mas compartilhando os mesmos métodos definidos na classe.

Uma classe define estrutura e comportamento. Instâncias são objetos criados a partir dessa classe: cada uma carrega seus próprios dados, mas compartilha os mesmos objetos de método da classe. Criar uma instância significa chamar a classe como se fosse uma função: Dog() cria uma nova instância de Dog.

Chamar uma classe faz duas coisas, em ordem: aloca uma instância vazia e depois executa __init__ nela para preencher os dados. O objeto pronto guarda a referência à sua classe (você pode consultá-la com type(rex)), e é essa ligação que permite ao Python encontrar um método quando você chama rex.bark(): ele procura primeiro na instância, depois na classe. Na prática, isso significa que os métodos ficam em um único lugar (na classe, compartilhados), enquanto os dados ficam por instância — então mil objetos Dog custam mil conjuntos de dados, mas apenas uma cópia de cada método.

python
class Dog:
    def bark(self):
        print("Woof!")

rex = Dog()
luna = Dog()

rex.bark()    # "Woof!"
luna.bark()   # "Woof!"

Dog é a classe. rex e luna são instâncias: dois cachorros diferentes, cada um compartilhando o mesmo comportamento definido na classe.

JunoModelo e instâncias Uma classe é o molde, uma instância é uma coisa construída a partir dele. Chame a classe como se fosse uma função, Dog(), e você recebe uma instância nova. Toda instância compartilha os métodos da classe, mas mantém seus próprios dados, de modo que rex e luna podem se comportar da mesma forma sendo cachorros diferentes.
JunoModelo e instâncias Chamar Dog() cria uma instância: dados próprios, métodos compartilhados da classe. Esse agrupamento é a ideia central: valores relacionados e o código que age sobre eles viajam juntos, em vez de ficarem como variáveis soltas que você passa manualmente de um lugar para outro.
JunoModelo e instâncias Chamar uma classe aloca um objeto vazio e depois executa __init__ nele. Métodos ficam uma única vez na classe e são compartilhados, dados ficam por instância, então uma chamada de método em rex é encontrada olhando primeiro a instância e depois a classe. Essa divisão é o que mantém dez mil instâncias baratas.

__init__ e self ​

__init__ é o método que o Python chama automaticamente quando você cria uma nova instância. É ali que você configura os dados iniciais do objeto. self é a forma que um método usa para se referir à instância específica sobre a qual está operando, e é sempre o primeiro parâmetro.

__init__ inicializa uma instância recém-alocada. self é um nome convencional para o primeiro parâmetro de todo método de instância; o Python passa a instância automaticamente quando você chama alice.display(). Atributos definidos em self dentro de __init__ são atributos de instância: cada instância tem sua própria cópia.

__init__ roda sobre a instância já alocada e a configura. self não é uma palavra-chave, é o primeiro parâmetro, e o Python o preenche para você: quando você chama alice.display(), o Python passa alice como self nos bastidores. Definir self.attr = value grava naquela única instância, então dois objetos Player nunca interferem nos dados um do outro. Uma coisa que surpreende quem vem de linguagens mais rígidas: o Python permite adicionar atributos em qualquer lugar, não só em __init__, então um erro de digitação como self.scor = 0 cria um novo atributo em vez de gerar um erro. Defina todo atributo que a instância vai usar dentro de __init__, mesmo que seja com None, para que a estrutura do objeto fique declarada em um só lugar e um erro de digitação em outro ponto salte aos olhos.

python
class Player:
    def __init__(self, name, score=0):
        self.name = name
        self.score = score

    def add_points(self, points):
        self.score += points

    def display(self):
        print(f"{self.name}: {self.score} points")

alice = Player("Alice")
bob = Player("Bob", score=50)

alice.add_points(30)
alice.display()   # "Alice: 30 points"
bob.display()     # "Bob: 50 points"

self.name e self.score são atributos de instância: pertencem ao objeto específico, não à classe em si. Cada instância de Player tem seu próprio name e score.

Juno__init__ e self__init__ roda no momento em que você cria uma instância, então é ali que você define os dados iniciais com self.name = value. self é a instância com a qual o Python está trabalhando, e é sempre o primeiro parâmetro de um método, entregue a você automaticamente. Você nunca passa self manualmente quando chama alice.display().
Juno__init__ e self__init__ configura uma instância nova, e tudo que você armazena em self ali é um atributo de instância, uma cópia por objeto. self é o primeiro parâmetro de todo método de instância; o Python passa a instância para você quando você escreve alice.display(), você só o declara na definição.
Juno__init__ e selfself é a instância, passada para você em toda chamada, e escritas em self.attr caem naquele único objeto. O Python não vai te impedir de adicionar atributos fora de __init__, então um self.scor mal digitado silenciosamente cria um novo atributo. Declare todo atributo dentro de __init__, mesmo como None, e os erros de digitação não terão onde se esconder.

Métodos ​

Qualquer função definida dentro de uma classe é um método. Métodos de instância sempre têm self como primeiro parâmetro; o Python o passa automaticamente. Métodos podem ler e alterar os dados da instância através de self.

Métodos de instância são funções comuns armazenadas na classe. Quando você acessa instance.method, o Python devolve um método vinculado (bound method): a mesma função com a instância já conectada como self, então você só precisa passar os argumentos restantes. Retornar self de um método permite encadeamento: obj.scale(2).rotate(90).

Um método é uma única função na classe, compartilhada por todas as instâncias. Acessar c.scale não devolve a função crua, devolve um método vinculado (bound method): a mesma função com c já conectado como self, e é assim que self é passado sem que você precise fazer isso. Dois padrões valem a pena no código real. Retornar self de um método que altera o estado dá a você uma interface fluente (chamadas encadeadas como c.scale(2).scale(0.5)); reserve isso para objetos no estilo builder onde o encadeamento se lê bem, não para tudo, já que um método que retorna self e um que retorna um novo valor parecem idênticos no ponto de chamada. E, como um método vinculado é um valor de primeira classe, você pode guardar um (handler = obj.save) e chamá-lo depois, o que é a base de callbacks e manipuladores de eventos.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

    def scale(self, factor):
        self.radius *= factor
        return self    # returning self allows chaining: c.scale(2).scale(0.5)

c = Circle(5)
print(c.area())    # 78.53975
c.scale(2)
print(c.area())    # 314.159
JunoMétodos Um método é uma função definida dentro de uma classe, e seu primeiro parâmetro é sempre self, a instância sobre a qual ele está trabalhando. O Python passa self para você, então você chama c.area() sem nada extra. Através de self, um método lê e altera os dados daquele próprio objeto.
JunoMétodos Métodos são funções comuns na classe; acessá-las através de uma instância vincula self automaticamente, então você passa só o resto. Retorne self quando quiser encadeamento como c.scale(2).scale(0.5). Caso contrário, retorne o valor que quem chamou está esperando.
JunoMétodosobj.method te dá um método vinculado, a função com obj conectado como self, e é por isso que você nunca o passa. Retornar self te dá encadeamento fluente, vale a pena em builders, confuso se todo método fizer isso. E, como um método vinculado é um valor, handler = obj.save guarda ele para um callback depois.

Variáveis de classe vs. variáveis de instância ​

Variáveis definidas diretamente na classe (não dentro de __init__) são variáveis de classe. Todas as instâncias compartilham a mesma variável de classe. Variáveis definidas em self dentro de __init__ são variáveis de instância, únicas para cada objeto.

Uma variável de classe é armazenada uma única vez na classe e compartilhada por todas as instâncias. Uma variável de instância é armazenada no objeto individual. Quando você lê self.attr, o Python verifica primeiro a instância, depois a classe. Quando você escreve self.attr = value, isso sempre cria ou atualiza a cópia própria da instância, ocultando a variável de classe apenas para aquele objeto.

Ler self.attr verifica primeiro a instância, depois a classe, então uma variável de classe funciona como um valor padrão compartilhado até que alguma instância escreva seu próprio valor e o sobreponha. A armadilha que costuma pegar em produção é uma variável de classe mutável (uma que você pode alterar no lugar, como uma lista ou dict). Atribuir self.x = ... cria uma cópia privada, mas self.x.append(...) não: isso alcança o único objeto compartilhado e todas as instâncias veem a mudança. Se você quer uma lista nova por instância, construa-a em __init__ com self.history = [], nunca como history = [] no nível da classe. Reserve variáveis de classe para coisas que realmente são compartilhadas, e mantenha-as imutáveis: constantes, padrões, contadores.

python
class Player:
    max_lives = 3    # class variable, same for every Player

    def __init__(self, name):
        self.name = name   # instance variable, unique to each Player
        self.lives = Player.max_lives

    def die(self):
        self.lives -= 1

alice = Player("Alice")
bob = Player("Bob")

Player.max_lives = 5    # change for all current and future instances

Use variáveis de classe para valores compartilhados entre todas as instâncias: constantes, contadores, padrões. Use variáveis de instância para dados que diferem por objeto.

JunoVariáveis de classe vs. variáveis de instância Uma variável definida diretamente na classe é compartilhada por todas as instâncias; uma variável definida em self dentro de __init__ pertence só àquele objeto. Escrever self.attr = value sempre cria ou atualiza a cópia própria da instância. Então use variáveis de classe quando um valor é igual para todos, variáveis de instância quando ele varia por objeto.
JunoVariáveis de classe vs. variáveis de instância Ler self.attr verifica a instância, depois a classe, então uma variável de classe é um padrão compartilhado. Escrever self.attr = value sempre cai na instância e oculta a de classe para aquele objeto. Mantenha variáveis de classe para constantes e padrões, o estado de cada objeto vai em self.
JunoVariáveis de classe vs. variáveis de instância O bug silencioso é uma variável de classe mutável: self.x = ... cria uma cópia privada, mas self.x.append(...) altera o único objeto compartilhado, então todas as instâncias veem isso. Quer uma lista por instância, construa-a em __init__ com self.history = [], nunca history = [] no nível da classe.

__str__ e __repr__ ​

__str__ controla o que print() e f-strings mostram para o seu objeto. __repr__ controla a visão de desenvolvedor mostrada no console e usada para depuração. Sempre defina __repr__. Defina __str__ quando quiser uma exibição limpa e voltada para o usuário, separada da visão de depuração.

__str__ é chamado por str(), print() e pelas f-strings do capítulo sobre saída, o texto voltado para o usuário. __repr__ é chamado por repr() e é mostrado quando um objeto é exibido no console, a visão de desenvolvedor. Se apenas __repr__ estiver definido, o Python o usa para ambos. A convenção: __repr__ retorna uma string que parece o código para recriar o objeto; __str__ retorna um resumo legível.

Esses são dois métodos dunder (abreviação de double-underscore: métodos nomeados com __ no início e no fim, que o Python chama para você no momento certo). print(obj) e str(obj) chamam __str__, recorrendo a __repr__ quando não há __str__. repr(obj), e um objeto exibido puro no console, chamam __repr__. Então __repr__ é o que sempre tem uma função a cumprir, e é por isso que a regra é: sempre defina __repr__, adicione __str__ só quando o texto voltado para o usuário precisar ser diferente do texto de depuração. Faça __repr__ parecer a chamada do construtor que criou o objeto (Player(name='Alice', score=87)) e use !r nos campos, para que as strings mantenham suas aspas e um espaço ou quebra de linha perdidos apareçam em vez de ficarem escondidos. Um bom __repr__ é o que torna uma linha de log ou um stack trace legível às 2 da manhã.

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def __str__(self):
        return f"{self.name} ({self.score} pts)"

    def __repr__(self):
        return f"Player(name={self.name!r}, score={self.score})"

alice = Player("Alice", 87)
print(alice)        # "Alice (87 pts)"   (uses __str__)
repr(alice)         # "Player(name='Alice', score=87)"  (uses __repr__)

Sempre defina __repr__. Defina __str__ quando quiser uma representação limpa voltada para o usuário, separada da visão de depuração. Se apenas __repr__ estiver definido, o Python o usa para ambos.

Juno__str__ e __repr____str__ é o que print() e f-strings mostram, a versão amigável. __repr__ é a visão de desenvolvedor que você vê no console. Sempre escreva __repr__; é ele que cumpre uma função mesmo quando você se esquece do __str__. Adicione __str__ só quando o texto voltado para o usuário precisar ser diferente.
Juno__str__ e __repr____str__ é a versão legível para print() e f-strings; __repr__ é a versão de depuração, e o Python recorre a ela quando não há __str__. Sempre defina __repr__, faça-o parecer a chamada que criou o objeto, e isso te poupa trabalho depois quando um objeto aparecer em um log.
Juno__str__ e __repr____repr__ sempre tem uma função a cumprir, __str__ só quando a visão do usuário precisar ser diferente, então defina __repr__ primeiro e sempre. Molde-o como o construtor e coloque !r nos campos, para que um espaço ou quebra de linha perdidos apareçam em vez de se esconderem. Esse único hábito é o que torna um stack trace às 2 da manhã legível.

Convenção de privacidade ​

O Python não tem variáveis realmente privadas, mas um único underscore no início de um nome (_balance) é uma convenção que sinaliza "isso é interno, não use diretamente de fora da classe". Não é imposto pela linguagem; é uma comunicação para outros desenvolvedores.

Um único underscore (_attr) é uma convenção que sinaliza uso interno. O Python não a impõe, mas todos os linters, IDEs e desenvolvedores a respeitam. Um duplo underscore (__attr) dispara o name mangling (ofuscação de nome): o Python o reescreve para _ClassName__attr, o que evita colisões acidentais em subclasses. Não é privacidade real; é um mecanismo para evitar colisões.

Um único underscore é só convenção: nada o impõe, mas linters (ferramentas que analisam seu código em busca de estilo e possíveis bugs), editores e revisores todos leem _balance como "interno, não toque de fora". Um duplo underscore dispara o name mangling: o Python reescreve __attr para _ClassName__attr, para que uma subclasse que defina seu próprio __attr não colida acidentalmente com o da classe-mãe. É uma proteção contra colisões, não privacidade: o nome ofuscado ainda é acessível de fora, se alguém insistir. Use isso apenas em uma classe-base pensada para ser amplamente estendida por subclasses, onde você precisa de um atributo que as subclasses não possam pisar; no código comum, um único underscore é a escolha certa por padrão, e o duplo underscore geralmente só cria confusão. Se você quer impedir totalmente que atributos arbitrários sejam definidos (aquele self.scor mal digitado de antes), defina __slots__ com a lista fixa de nomes de atributos: isso diz ao Python que esses são os únicos atributos que uma instância pode ter, então qualquer outra atribuição gera um erro em vez de passar silenciosamente, e como bônus reduz o consumo de memória por instância.

python
class BankAccount:
    def __init__(self, balance):
        self._balance = balance    # _ means "hands off"

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount

    def balance(self):
        return self._balance

Um duplo underscore (__name) dispara o name mangling; o Python renomeia o atributo para _ClassName__name para evitar conflitos em subclasses. É raramente necessário. O underscore único é a convenção na maior parte do código.

JunoConvenção de privacidade O Python não tem variáveis realmente privadas, mas um underscore no início (_balance) é o sinal combinado para "interno, não toque nisso de fora". Nada te impede de acessar, é uma mensagem para outros desenvolvedores, incluindo você no futuro. O duplo underscore é uma ferramenta mais rara para evitar conflitos de nomes em subclasses; o underscore único é o que você vai usar no dia a dia.
JunoConvenção de privacidade Um único underscore significa "interno", e toda ferramenta e revisor respeita isso mesmo que o Python não o imponha. Um duplo underscore dispara o name mangling para _ClassName__attr, o que protege contra conflitos de nome em subclasses, não contra acesso. Use o underscore único por padrão; o duplo é raro.
JunoConvenção de privacidade Underscore único é convenção, duplo underscore ofusca para _ClassName__attr para evitar conflitos em subclasses, nenhum dos dois é privacidade real. Reserve __attr para classes-base amplamente estendidas por subclasses; em outros casos, só cria confusão. Quer proibir atributos soltos e pegar o self.scor mal digitado, defina __slots__ com a lista fixa.

Herança ​

Uma classe pode herdar de outra classe, recebendo automaticamente todos os seus atributos e métodos. Você pode então sobrescrever métodos específicos na subclasse para mudar seu comportamento. Isso permite reaproveitar uma base comum e especializar onde for necessário.

A herança modela uma relação "é-um": um Dog é um Animal. A subclasse recebe todos os métodos e atributos da classe-mãe e pode sobrescrever qualquer um deles definindo sua própria versão. Quando você chama um método, o Python procura primeiro na subclasse, depois sobe para a classe-mãe, então um método não sobrescrito é herdado automaticamente. Esse caminho de busca é a ordem de resolução de métodos (MRO), a lista ordenada de classes que o Python percorre.

Quando você chama obj.method, o Python busca nas classes em uma ordem fixa, o MRO (ordem de resolução de métodos, method resolution order: a lista achatada de uma classe e seus ancestrais, que pode ser lida com Dog.__mro__), e usa a primeira correspondência encontrada. Herança simples é direta, subclasse e depois a classe-mãe. O motivo de o MRO existir é a herança múltipla e o caso do diamante (duas classes-mãe que herdam de um mesmo ancestral comum): o Python ordena as classes de modo que o ancestral apareça uma única vez, depois de ambas as classes-mãe, e resolve as chamadas de forma determinística. Na prática: prefira hierarquias rasas e herança simples, porque árvores profundas ou com herança múltipla dificultam entender "qual método realmente rodou". Quando você misturar várias classes-base, roteie cada __init__ através de super() (próxima seção), para que cada um rode exatamente uma vez, seguindo essa ordem.

python
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        return "..."

class Dog(Animal):
    def speak(self):
        return f"{self.name} says Woof!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

pets = [Dog("Rex"), Cat("Luna"), Dog("Max")]
for pet in pets:
    print(pet.speak())

Dog e Cat herdam __init__ de Animal, então não precisam do seu próprio. Eles sobrescrevem speak() com seu comportamento específico.

JunoHerança Uma subclasse herda tudo o que a classe-mãe tem, e depois sobrescreve só os métodos que quer mudar. Tudo que você não sobrescreve é herdado da classe-mãe de graça, e esse é o ponto central: escreva o comportamento compartilhado uma vez, especialize onde ele muda. Dog e Cat reaproveitam o __init__ de Animal e só redefinem speak().
JunoHerança Herança é "é-um": a subclasse recebe os métodos da classe-mãe e sobrescreve o que precisa, e chamadas não sobrescritas são herdadas normalmente. O Python encontra um método percorrendo o MRO, subclasse primeiro, depois subindo até a classe-mãe. Reaproveite a base compartilhada, redefina só o que muda.
JunoHerança O Python resolve um método percorrendo o MRO e usando a primeira correspondência, e é isso que torna o caso do diamante determinístico. Mantenha hierarquias rasas e aposte na herança simples; árvores profundas ou com herança múltipla transformam "qual método rodou" em um jogo de adivinhação. Quando precisar misturar classes-base, encadeie todo __init__ através de super().

super() ​

super() chama um método da classe-mãe. Use-o quando quiser estender o comportamento da classe-mãe em vez de substituí-lo completamente: chame o __init__ da classe-mãe para executar sua configuração, e depois adicione o que sua subclasse precisar por cima.

super() retorna um objeto proxy que delega chamadas de método para a próxima classe no MRO. Sempre chame super().__init__() a partir do __init__ de uma subclasse quando a classe-mãe tiver um. Pular isso significa que o código de configuração da classe-mãe não roda, o que pode deixar o objeto em um estado inconsistente.

super() não significa "minha classe-mãe direta", significa "a próxima classe seguindo o MRO" (a ordem de busca da seção anterior). Em uma subclasse simples, essa próxima classe é a classe-mãe, então a distinção parece acadêmica, mas é exatamente isso que faz a herança múltipla funcionar: se toda classe na árvore chama super().__init__(...), cada __init__ roda uma vez, na ordem do MRO, sem que nenhuma classe fixe o nome de uma classe-mãe diretamente no código. É por isso que você escreve super().__init__() e não Animal.__init__(self): nomear a classe-mãe diretamente quebra a cadeia no momento em que outra classe é misturada, e pode fazer uma base compartilhada rodar duas vezes. O super() puro, sem argumentos, é a forma moderna; o mais antigo super(Dog, self) é a mesma coisa escrita de forma explícita, que você ainda vai encontrar em código que suporta versões mais antigas.

python
class Animal:
    def __init__(self, name, sound):
        self.name = name
        self.sound = sound

class Dog(Animal):
    def __init__(self, name):
        super().__init__(name, "Woof")   # call Animal.__init__
        self.tricks = []                  # add something extra

    def learn(self, trick):
        self.tricks.append(trick)

rex = Dog("Rex")
rex.learn("sit")
print(rex.tricks)   # ["sit"]

Sempre chame super().__init__() quando sua subclasse tiver seu próprio __init__ e a classe-mãe também tiver.

Junosuper()super() alcança a classe-mãe, então super().__init__() executa a configuração da classe-mãe antes de você adicionar a sua própria. Use isso quando sua subclasse escrever seu próprio __init__ e a classe-mãe também tiver um. Pule isso e a configuração da classe-mãe nunca roda, deixando o objeto pela metade.
Junosuper()super() delega para a próxima classe acima, então chame super().__init__() a partir do __init__ de uma subclasse sempre que a classe-mãe tiver um. Esqueça isso e a configuração da classe-mãe é pulada, o que pode deixar o objeto em um estado inconsistente. É a linha que conecta sua subclasse à classe-mãe que ela estende.
Junosuper()super() significa "próxima classe no MRO", não "minha classe-mãe", e é por isso que fixar Animal.__init__(self) no código quebra a cadeia quando outra classe é misturada. Se todo __init__ chama super(), cada um roda uma vez, em ordem. super() puro é a forma moderna; super(Dog, self) é a mesma coisa escrita de forma explícita.

Métodos de classe e métodos estáticos ​

@classmethod cria um método que recebe a própria classe em vez de uma instância. É útil para construtores alternativos: criar uma instância a partir de uma string, um arquivo, ou outro formato. @staticmethod é uma função comum que vive dentro da classe por motivos de organização; não recebe nem a instância nem a classe.

@classmethod recebe cls (a classe) como primeiro argumento, não uma instância. O uso principal é construtores alternativos que criam instâncias a partir de formatos de entrada diferentes. @staticmethod é uma função comum organizada dentro do namespace da classe; não tem acesso nem à classe nem à instância. Use @classmethod para construtores, @staticmethod para funções utilitárias logicamente ligadas à classe.

@classmethod recebe a classe como primeiro argumento (cls) em vez de uma instância, e o detalhe que faz valer a diferença é que cls é a classe sobre a qual o método foi chamado, não aquela em que ele foi definido. Então, se Player.from_string constrói com cls(...) e uma subclasse ProPlayer chama ProPlayer.from_string(...), o resultado é um ProPlayer, não um Player. É exatamente por isso que construtores alternativos usam @classmethod em vez de fixar o nome da classe diretamente: eles continuam funcionando sob herança. @staticmethod não recebe nem a instância nem a classe; é uma função comum guardada dentro da classe por organização, quando a lógica pertence à classe mas não precisa de nenhum de seus dados. Use um classmethod quando estiver criando uma instância a partir de outro formato, um staticmethod para um auxiliar relacionado, e um método normal para qualquer coisa que toque nos dados de um objeto.

python
class Player:
    def __init__(self, name, score):
        self.name = name
        self.score = score

    @classmethod
    def from_string(cls, data):
        name, score = data.split(",")
        return cls(name, int(score))

alice = Player.from_string("Alice,87")
python
class Player:
    @staticmethod
    def is_valid_name(name):
        return name.isalpha() and len(name) >= 2

Player.is_valid_name("Alice")   # True
Player.is_valid_name("A1")      # False

Use @classmethod para construtores alternativos. Use @staticmethod para funções utilitárias que logicamente pertencem à classe, mas não precisam de dados de instância nem de classe.

JunoMétodos de classe e métodos estáticos@classmethod te entrega a classe em vez de uma instância, o que o torna a escolha certa para construtores alternativos: montar um Player a partir de uma string, um arquivo, qualquer formato que você tiver. @staticmethod é uma função comum guardada dentro da classe por organização; não recebe nem a classe nem a instância. Métodos comuns tocam nos dados de um objeto, esses dois não.
JunoMétodos de classe e métodos estáticos@classmethod recebe cls, a própria classe, então use isso para construtores alternativos que retornam cls(...). @staticmethod não recebe nada implícito, é um auxiliar organizado dentro do namespace da classe. Método normal para dados por objeto, classmethod para construir instâncias, staticmethod para um utilitário relacionado.
JunoMétodos de classe e métodos estáticos O ponto de um construtor via classmethod é que cls é sempre quem o chamou, então ProPlayer.from_string(...) constrói um ProPlayer, não um Player. Fixe o nome da classe no código e você perde isso. Classmethod para construir, staticmethod para um auxiliar relacionado que não precisa de dados, método normal para tudo que toca a instância.

@property ​

@property permite acessar um método como se fosse um atributo, sem precisar de parênteses. Use isso para valores que são calculados a partir de outros atributos e que parecem naturais de ler como um simples acesso a atributo.

@property transforma um método em um atributo somente leitura. O método roda quando o atributo é acessado. Isso é útil para valores calculados derivados de dados armazenados, e para adicionar validação ao acesso de atributos sem mudar a interface pública. Um @name.setter correspondente torna o atributo gravável.

@property transforma um método em algo que você lê como um atributo: c.area, sem parênteses, roda o método a cada vez. Adicione @area.setter e ele se torna gravável também, com seu código rodando na entrada, que é onde entra a validação (rejeitar um raio negativo antes que ele seja armazenado). O real valor disso é que não muda nada para quem chama: você pode começar com um atributo simples self.radius e depois promovê-lo a uma property com validação sem tocar em uma única linha que o lê ou grava. É por isso que o Python não tem toda a cerimônia de getters e setters: você expõe atributos simples e só recorre a uma property quando um deles precisa de cálculo ou proteção. Duas cautelas para produção: o acesso parece gratuito, mas roda código, então mantenha-o leve (guarde em cache um resultado caro em vez de recalculá-lo a cada leitura), e uma property que silenciosamente faz um trabalho pesado ou levanta um erro surpreende quem supunha que era um campo simples.

python
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        return 3.14159 * self.radius ** 2

    @property
    def diameter(self):
        return self.radius * 2

c = Circle(5)
print(c.area)      # 78.53975 (looks like an attribute, runs like a method)
print(c.diameter)  # 10

Properties são úteis para valores calculados: coisas derivadas de outros atributos que parecem naturais de acessar sem ().

Juno@property@property permite ler um método como um atributo, sem parênteses: c.area em vez de c.area(). Se encaixa em valores calculados a partir de outros atributos e que parecem naturais de ler como dados simples. Nos bastidores, ainda roda seu método a cada acesso.
Juno@property@property lê um método como um atributo, e um @name.setter correspondente o torna gravável para que você possa validar na entrada. A vantagem: você pode transformar um atributo simples em uma property depois, sem mudar nenhum código de quem o chama. Use isso para valores calculados ou protegidos, não como hábito em todo campo.
Juno@property O motivo para gostar de @property: um atributo simples pode se tornar calculado ou validado depois, sem nenhuma mudança para quem o chama, então você evita getters e setters até realmente precisar de um. O acesso parece gratuito, mas roda código, então mantenha-o leve, guarde em cache os caros, e não escONDA trabalho pesado ou um erro atrás do que parece um campo simples.

Na prática ​

Uma classe Player com atributos de instância, métodos, uma @property e __str__:

python
class Player:
    max_lives = 3

    def __init__(self, name: str):
        self.name = name
        self.score = 0
        self.lives = Player.max_lives

    def earn_points(self, amount: int) -> None:
        self.score += amount

    def take_hit(self) -> bool:
        self.lives -= 1
        return self.lives > 0

    @property
    def is_alive(self) -> bool:
        return self.lives > 0

    def __str__(self) -> str:
        return f"{self.name} | Score: {self.score} | Lives: {self.lives}"

alice = Player("Alice")
alice.earn_points(50)
alice.take_hit()
print(alice)            # "Alice | Score: 50 | Lives: 2"
print(alice.is_alive)   # True

Uma classe User que usa um atributo privado com um getter @property, um método deactivate e um serializador to_dict:

python
class User:
    def __init__(self, user_id: int, username: str, email: str):
        self.id = user_id
        self.username = username
        self.email = email
        self._active = True

    @property
    def active(self) -> bool:
        return self._active

    def deactivate(self) -> None:
        self._active = False

    def to_dict(self) -> dict:
        return {
            "id": self.id,
            "username": self.username,
            "email": self.email,
            "active": self._active,
        }

    def __repr__(self) -> str:
        return f"User(id={self.id}, username={self.username!r})"

alice = User(1, "alice", "[email protected]")
print(alice.to_dict())
alice.deactivate()
print(alice.active)   # False

Uma classe DataSplit que encapsula o fracionamento entre treino e validação por trás de properties, com __repr__ para uma saída de depuração limpa:

python
class DataSplit:
    def __init__(self, data: list, train_ratio: float = 0.8):
        split = int(len(data) * train_ratio)
        self._train = data[:split]
        self._val = data[split:]

    @property
    def train(self) -> list:
        return self._train

    @property
    def val(self) -> list:
        return self._val

    @property
    def sizes(self) -> tuple[int, int]:
        return len(self._train), len(self._val)

    def __repr__(self) -> str:
        return f"DataSplit(train={len(self._train)}, val={len(self._val)})"

data = list(range(100))
split = DataSplit(data, train_ratio=0.8)
print(split)         # DataSplit(train=80, val=20)
print(split.sizes)   # (80, 20)

O prefixo underscore em _train e _val sinaliza que quem chamar deveria passar pelas properties em vez de alterar diretamente as listas brutas. O Python não vai impor isso, mas isso estabelece um contrato claro.